Compare commits
1 Commits
main
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06843cd28 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ opensavecloudserver
|
||||
storage/
|
||||
.idea/
|
||||
build
|
||||
*.log
|
||||
*.log
|
||||
id_rsa*
|
||||
@@ -1,29 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"opensavecloudserver/database"
|
||||
"opensavecloudserver/upload"
|
||||
)
|
||||
|
||||
// RemoveUser rome the user from the db and all his datas
|
||||
func RemoveUser(user *database.User) error {
|
||||
if err := database.RemoveAllUserGameEntries(user); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := upload.RemoveFolders(user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return database.RemoveUser(user)
|
||||
}
|
||||
|
||||
func SetAdmin(user *database.User) error {
|
||||
user.Role = database.AdminRole
|
||||
user.IsAdmin = true
|
||||
return database.SaveUser(user)
|
||||
}
|
||||
|
||||
func RemoveAdminRole(user *database.User) error {
|
||||
user.Role = database.UserRole
|
||||
user.IsAdmin = false
|
||||
return database.SaveUser(user)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
"opensavecloudserver/database"
|
||||
)
|
||||
|
||||
var secret []byte
|
||||
|
||||
type AccessToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type Registration struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
secret = make([]byte, 512)
|
||||
_, err := rand.Read(secret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Connect(username, password string) (*AccessToken, error) {
|
||||
user, err := database.UserByUsername(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(password)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := token(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccessToken{
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ParseToken(token string) (int, error) {
|
||||
var claims jwt.MapClaims
|
||||
_, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return secret, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if userId, ok := claims["sub"]; ok {
|
||||
return int(userId.(float64)), nil
|
||||
}
|
||||
return 0, errors.New("this token does not have a userId in it")
|
||||
}
|
||||
|
||||
func Register(user *Registration) error {
|
||||
_, err := database.UserByUsername(user.Username)
|
||||
if err == nil {
|
||||
return errors.New("this username already exist")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return database.AddUser(user.Username, hash)
|
||||
}
|
||||
|
||||
func token(userId int) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
|
||||
"sub": userId,
|
||||
})
|
||||
return token.SignedString(secret)
|
||||
}
|
||||
16
build.sh
16
build.sh
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
platforms=("windows/amd64" "linux/amd64" "linux/arm64" "linux/arm")
|
||||
platforms=("windows/amd64" "linux/amd64" "linux/arm64")
|
||||
|
||||
if [[ -d "./build" ]]
|
||||
then
|
||||
@@ -21,15 +21,11 @@ do
|
||||
go generate
|
||||
env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=1 go build -o $output_name -a
|
||||
else
|
||||
if [ $GOARCH = "arm" ]; then
|
||||
env GOARM=7 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
|
||||
else
|
||||
if [ $GOARCH = "amd64" ]; then
|
||||
env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
|
||||
else
|
||||
env GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
|
||||
fi
|
||||
fi
|
||||
if [ $GOARCH = "amd64" ]; then
|
||||
env GOAMD64=v3 GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
|
||||
else
|
||||
env GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=0 go build -o $output_name -a
|
||||
fi
|
||||
fi
|
||||
|
||||
done
|
||||
15
common.go
15
common.go
@@ -3,24 +3,17 @@ package main
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"opensavecloudserver/config"
|
||||
"opensavecloudserver/database"
|
||||
"os"
|
||||
)
|
||||
|
||||
func InitCommon() {
|
||||
func initLogger() (err error) {
|
||||
f, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
err = f.Close()
|
||||
}(f)
|
||||
log.SetOutput(io.MultiWriter(os.Stdout, f))
|
||||
|
||||
config.Init()
|
||||
database.Init()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
---
|
||||
server:
|
||||
port: 8080
|
||||
ping: no
|
||||
compress: no
|
||||
database:
|
||||
host: localhost
|
||||
password: root
|
||||
port: 3306
|
||||
username: root
|
||||
url: "postgres://username:password@localhost:5432/database_name"
|
||||
features:
|
||||
allow_register: false
|
||||
allow_register: no
|
||||
password_hash_cost: 16
|
||||
path:
|
||||
cache: "/var/osc/cache"
|
||||
|
||||
109
config/config.go
109
config/config.go
@@ -1,84 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
Server ServerConfiguration `yaml:"server"`
|
||||
Database DatabaseConfiguration `yaml:"database"`
|
||||
Features FeaturesConfiguration `yaml:"features"`
|
||||
Path PathConfiguration `yaml:"path"`
|
||||
}
|
||||
type (
|
||||
Configuration struct {
|
||||
Server ServerConfiguration `yaml:"server"`
|
||||
Database DatabaseConfiguration `yaml:"database"`
|
||||
Features FeaturesConfiguration `yaml:"features"`
|
||||
Path PathConfiguration `yaml:"path"`
|
||||
}
|
||||
|
||||
type PathConfiguration struct {
|
||||
Cache string `yaml:"cache"`
|
||||
Storage string `yaml:"storage"`
|
||||
}
|
||||
PathConfiguration struct {
|
||||
Cache string `yaml:"cache"`
|
||||
Storage string `yaml:"storage"`
|
||||
RSAKey string `yaml:"rsa_key"`
|
||||
}
|
||||
|
||||
type ServerConfiguration struct {
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
ServerConfiguration struct {
|
||||
Port int `yaml:"port"`
|
||||
PingEndPoint bool `yaml:"ping"`
|
||||
Compress bool `yaml:"compress"`
|
||||
}
|
||||
|
||||
type DatabaseConfiguration struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Username string `yaml:"username"`
|
||||
Password *string `yaml:"password"`
|
||||
}
|
||||
DatabaseConfiguration struct {
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
type FeaturesConfiguration struct {
|
||||
AllowRegister bool `yaml:"allow_register"`
|
||||
PasswordHashCost *int `yaml:"password_hash_cost"`
|
||||
}
|
||||
FeaturesConfiguration struct {
|
||||
AllowRegister bool `yaml:"allow_register"`
|
||||
PasswordHashCost *int `yaml:"password_hash_cost"`
|
||||
}
|
||||
)
|
||||
|
||||
var currentConfig *Configuration
|
||||
|
||||
func Init() {
|
||||
path := flag.String("config", "./config.yml", "Set the configuration file path")
|
||||
flag.Parse()
|
||||
configYamlContent, err := os.ReadFile(*path)
|
||||
func Load(path string) (Configuration, error) {
|
||||
configYamlContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return Configuration{}, err
|
||||
}
|
||||
err = yaml.Unmarshal(configYamlContent, ¤tConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("error: %s", err)
|
||||
var config Configuration
|
||||
if err := yaml.Unmarshal(configYamlContent, &config); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
checkConfig()
|
||||
if err := checkConfig(config); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func checkConfig() {
|
||||
if currentConfig.Features.PasswordHashCost == nil {
|
||||
currentConfig.Features.PasswordHashCost = new(int)
|
||||
*currentConfig.Features.PasswordHashCost = bcrypt.DefaultCost
|
||||
} else if *currentConfig.Features.PasswordHashCost < bcrypt.MinCost && *currentConfig.Features.PasswordHashCost > bcrypt.MaxCost {
|
||||
log.Fatalf("password_hash_cost is not on the supported range (%d < x < %d)", bcrypt.MinCost, bcrypt.MaxCost)
|
||||
func checkConfig(c Configuration) error {
|
||||
if c.Features.PasswordHashCost == nil {
|
||||
c.Features.PasswordHashCost = new(int)
|
||||
*c.Features.PasswordHashCost = bcrypt.DefaultCost
|
||||
} else if *c.Features.PasswordHashCost < bcrypt.MinCost && *c.Features.PasswordHashCost > bcrypt.MaxCost {
|
||||
return fmt.Errorf("password_hash_cost is not on the supported range (%d < x < %d)", bcrypt.MinCost, bcrypt.MaxCost)
|
||||
}
|
||||
if _, err := os.Stat(currentConfig.Path.Storage); err != nil {
|
||||
log.Fatal(err)
|
||||
if _, err := os.Stat(c.Path.Storage); err != nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := os.Stat(currentConfig.Path.Cache); err != nil {
|
||||
log.Fatal(err)
|
||||
if _, err := os.Stat(c.Path.Cache); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func Database() *DatabaseConfiguration {
|
||||
return ¤tConfig.Database
|
||||
}
|
||||
|
||||
func Features() *FeaturesConfiguration {
|
||||
return ¤tConfig.Features
|
||||
}
|
||||
|
||||
func Path() *PathConfiguration {
|
||||
return ¤tConfig.Path
|
||||
}
|
||||
|
||||
func Server() *ServerConfiguration {
|
||||
return ¤tConfig.Server
|
||||
return nil
|
||||
}
|
||||
|
||||
118
config/security/security.go
Normal file
118
config/security/security.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPrivateKeyName = "id_rsa"
|
||||
defaultPublicKeyName = "id_rsa.pub"
|
||||
)
|
||||
|
||||
// GenerateNewRSAKey generates a new RSA key, writes it to a file and return the private key
|
||||
func GenerateNewRSAKey(savePath string, bitSize uint16) (*rsa.PrivateKey, error) {
|
||||
pkPath := path.Join(savePath, defaultPrivateKeyName)
|
||||
pubPath := path.Join(savePath, defaultPublicKeyName)
|
||||
|
||||
privateKey, err := generatePrivateKey(int(bitSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKeyBytes, err := generatePublicKey(&privateKey.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privateKeyBytes := encodePrivateKeyToPEM(privateKey)
|
||||
|
||||
if err := writeKeyToFile(privateKeyBytes, pkPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := writeKeyToFile(publicKeyBytes, pubPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func LoadPrivateKey(folderPath string) (*rsa.PrivateKey, error) {
|
||||
pkPath := path.Join(folderPath, defaultPrivateKeyName)
|
||||
f, err := os.ReadFile(pkPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(f)
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func generatePrivateKey(bitSize int) (*rsa.PrivateKey, error) {
|
||||
// Private Key generation
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, bitSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate Private Key
|
||||
err = privateKey.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("Private Key generated")
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
|
||||
func encodePrivateKeyToPEM(privateKey *rsa.PrivateKey) []byte {
|
||||
// Get ASN.1 DER format
|
||||
privDER := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
|
||||
// pem.Block
|
||||
privBlock := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Headers: nil,
|
||||
Bytes: privDER,
|
||||
}
|
||||
|
||||
// Private key in PEM format
|
||||
privatePEM := pem.EncodeToMemory(&privBlock)
|
||||
|
||||
return privatePEM
|
||||
}
|
||||
|
||||
// generatePublicKey take a rsa.PublicKey and return bytes suitable for writing to .pub file
|
||||
// returns in the format "ssh-rsa ..."
|
||||
func generatePublicKey(privatekey *rsa.PublicKey) ([]byte, error) {
|
||||
publicRsaKey, err := ssh.NewPublicKey(privatekey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKeyBytes := ssh.MarshalAuthorizedKey(publicRsaKey)
|
||||
|
||||
log.Println("Public key generated")
|
||||
return pubKeyBytes, nil
|
||||
}
|
||||
|
||||
// writePemToFile writes keys to a file
|
||||
func writeKeyToFile(keyBytes []byte, saveFileTo string) error {
|
||||
err := os.WriteFile(saveFileTo, keyBytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Key saved to: %s", saveFileTo)
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package constant
|
||||
|
||||
const Version = "1.0.0"
|
||||
const Version = "2.0.0"
|
||||
|
||||
const ApiVersion = 1
|
||||
const ApiVersion = 2
|
||||
|
||||
12
data/datasource/datasource.go
Normal file
12
data/datasource/datasource.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package datasource
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type (
|
||||
Datasource interface {
|
||||
Connect(dsn string) error
|
||||
DB() *gorm.DB
|
||||
}
|
||||
)
|
||||
40
data/datasource/pgsql/models/game/game.go
Normal file
40
data/datasource/pgsql/models/game/game.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"opensavecloudserver/data/datasource"
|
||||
"opensavecloudserver/data/repository/game"
|
||||
"opensavecloudserver/data/repository/user"
|
||||
)
|
||||
|
||||
type (
|
||||
GameDatasource struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
)
|
||||
|
||||
func (g *GameDatasource) GameMetadataByID(ID game.ID) (game.GameMetadata, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (g *GameDatasource) CreateGameEntry(game game.NewGameEntry) (game.GameMetadata, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (g *GameDatasource) GameSavesHistory(gameID game.ID) ([]game.GameSaveVersion, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (g *GameDatasource) UserGamesByUserID(userID user.ID) ([]game.GameMetadata, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func NewGameDatasource(dts datasource.Datasource) game.GameRepository {
|
||||
g := new(GameDatasource)
|
||||
g.db = dts.DB()
|
||||
return g
|
||||
}
|
||||
31
data/datasource/pgsql/models/user/user.go
Normal file
31
data/datasource/pgsql/models/user/user.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"opensavecloudserver/data/datasource"
|
||||
"opensavecloudserver/data/repository/user"
|
||||
)
|
||||
|
||||
type (
|
||||
UserDatasource struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
)
|
||||
|
||||
func NewUserDatasource(dts datasource.Datasource) user.UserRepository {
|
||||
repo := new(UserDatasource)
|
||||
repo.db = dts.DB()
|
||||
return repo
|
||||
}
|
||||
|
||||
func (ud *UserDatasource) UserByID(ID user.ID) (user.User, error) {
|
||||
return nil, user.ErrUserNotFound
|
||||
}
|
||||
|
||||
func (ud *UserDatasource) UserByUsername(username string) (user.User, error) {
|
||||
return nil, user.ErrUserNotFound
|
||||
}
|
||||
|
||||
func (ud *UserDatasource) CreateUser(u user.NewUserTemplate) (user.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
30
data/datasource/pgsql/pgsql.go
Normal file
30
data/datasource/pgsql/pgsql.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"opensavecloudserver/data/datasource"
|
||||
)
|
||||
|
||||
type (
|
||||
DatabaseDatasource struct {
|
||||
conn *gorm.DB
|
||||
}
|
||||
)
|
||||
|
||||
func NewDatabaseDatasource() datasource.Datasource {
|
||||
return new(DatabaseDatasource)
|
||||
}
|
||||
|
||||
func (dd *DatabaseDatasource) Connect(dsn string) error {
|
||||
conn, err := gorm.Open(postgres.Open(dsn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dd.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dd *DatabaseDatasource) DB() *gorm.DB {
|
||||
return dd.conn
|
||||
}
|
||||
40
data/repository/game/game.go
Normal file
40
data/repository/game/game.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"opensavecloudserver/data/repository/user"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
GameRepository interface {
|
||||
GameMetadataByID(ID ID) (GameMetadata, error)
|
||||
CreateGameEntry(game NewGameEntry) (GameMetadata, error)
|
||||
GameSavesHistory(gameID ID) ([]GameSaveVersion, error)
|
||||
UserGamesByUserID(userID user.ID) ([]GameMetadata, error)
|
||||
}
|
||||
|
||||
GameMetadata interface {
|
||||
ID() uuid.UUID
|
||||
Name() string
|
||||
Path() string
|
||||
Revision() string
|
||||
}
|
||||
|
||||
GameSaveVersion interface {
|
||||
ID() string
|
||||
Date() time.Time
|
||||
}
|
||||
|
||||
NewGameEntry interface {
|
||||
Path() string
|
||||
Name() string
|
||||
}
|
||||
|
||||
ID uuid.UUID
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGameNotFound = errors.New("game not found")
|
||||
)
|
||||
36
data/repository/user/user.go
Normal file
36
data/repository/user/user.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type (
|
||||
UserRepository interface {
|
||||
UserByID(ID ID) (User, error)
|
||||
UserByUsername(username string) (User, error)
|
||||
CreateUser(user NewUserTemplate) (User, error)
|
||||
}
|
||||
|
||||
User interface {
|
||||
ID() uuid.UUID
|
||||
Username() string
|
||||
DisplayName() string
|
||||
Roles() []Role
|
||||
SetPassword() string
|
||||
CheckPassword(password string) bool
|
||||
}
|
||||
|
||||
NewUserTemplate interface {
|
||||
Username() string
|
||||
Password() string
|
||||
DisplayName() string
|
||||
}
|
||||
|
||||
ID uuid.UUID
|
||||
Role string
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
)
|
||||
@@ -1,207 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"log"
|
||||
"opensavecloudserver/config"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
const AdminRole string = "admin"
|
||||
const UserRole string = "user"
|
||||
|
||||
func Init() {
|
||||
dbConfig := config.Database()
|
||||
var err error
|
||||
connectionString := ""
|
||||
if dbConfig.Password != nil {
|
||||
connectionString = fmt.Sprintf("%s:%s@tcp(%s:%d)/osc?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
dbConfig.Username,
|
||||
*dbConfig.Password,
|
||||
dbConfig.Host,
|
||||
dbConfig.Port)
|
||||
} else {
|
||||
connectionString = fmt.Sprintf("%s@tcp(%s:%d)/osc?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
dbConfig.Username,
|
||||
dbConfig.Host,
|
||||
dbConfig.Port)
|
||||
}
|
||||
db, err = gorm.Open(mysql.Open(connectionString), &gorm.Config{
|
||||
Logger: logger.New(
|
||||
log.New(os.Stdout, "", log.LstdFlags), // io writer
|
||||
logger.Config{
|
||||
SlowThreshold: time.Second, // Slow SQL threshold
|
||||
LogLevel: logger.Error, // Log level
|
||||
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
|
||||
Colorful: true, // Enable color
|
||||
},
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func AllUsers() ([]*User, error) {
|
||||
var users []*User
|
||||
err := db.Model(User{}).Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.Role == AdminRole {
|
||||
user.IsAdmin = true
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UserByUsername get a user by the username
|
||||
func UserByUsername(username string) (*User, error) {
|
||||
var user *User
|
||||
err := db.Model(User{}).Where(User{Username: username}).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Role == AdminRole {
|
||||
user.IsAdmin = true
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func ChangeUsername(userId int, newUsername string) error {
|
||||
user, err := UserById(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Username = newUsername
|
||||
return db.Save(user).Error
|
||||
}
|
||||
|
||||
// UserById get a user
|
||||
func UserById(userId int) (*User, error) {
|
||||
var user *User
|
||||
err := db.Model(User{}).Where(userId).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Role == AdminRole {
|
||||
user.IsAdmin = true
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// AddUser register a user
|
||||
func AddUser(username string, password []byte) error {
|
||||
user := &User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
return db.Save(user).Error
|
||||
}
|
||||
|
||||
func SaveUser(user *User) error {
|
||||
return db.Save(user).Error
|
||||
}
|
||||
|
||||
func RemoveUser(user *User) error {
|
||||
return db.Delete(User{}, user.ID).Error
|
||||
}
|
||||
|
||||
func RemoveAllUserGameEntries(user *User) error {
|
||||
return db.Delete(Game{}, Game{UserId: user.ID}).Error
|
||||
}
|
||||
|
||||
func RemoveGame(game *Game) error {
|
||||
return db.Delete(Game{}, Game{UserId: game.UserId, ID: game.ID}).Error
|
||||
}
|
||||
|
||||
// AddAdmin register a user and set his role to admin
|
||||
/*func AddAdmin(username string, password []byte) error {
|
||||
user := &User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
Role: AdminRole,
|
||||
}
|
||||
return db.Save(user).Error
|
||||
}*/
|
||||
|
||||
// GameInfoById return information of a game
|
||||
func GameInfoById(userId, gameId int) (*Game, error) {
|
||||
var game *Game
|
||||
err := db.Model(Game{}).Where(Game{ID: gameId, UserId: userId}).First(&game).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return game, nil
|
||||
}
|
||||
|
||||
// GameInfosByUserId get all saved games for a user
|
||||
func GameInfosByUserId(userId int) ([]*Game, error) {
|
||||
var games []*Game
|
||||
err := db.Model(Game{}).Where(Game{UserId: userId}).Find(&games).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return games, nil
|
||||
}
|
||||
|
||||
// CreateGame create an entry for a new game save, do this only for create a new entry
|
||||
func CreateGame(userId int, name string) (*Game, error) {
|
||||
gameUUID := uuid.New()
|
||||
game := &Game{
|
||||
Name: name,
|
||||
Revision: 0,
|
||||
PathStorage: gameUUID.String() + ".bin",
|
||||
UserId: userId,
|
||||
Available: false,
|
||||
}
|
||||
if err := db.Save(&game).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return game, nil
|
||||
}
|
||||
|
||||
func UpdateGameRevision(game *Game, hash string) error {
|
||||
game.Revision += 1
|
||||
if game.Hash == nil {
|
||||
game.Hash = new(string)
|
||||
}
|
||||
*game.Hash = hash
|
||||
game.Available = true
|
||||
if game.LastUpdate == nil {
|
||||
game.LastUpdate = new(time.Time)
|
||||
}
|
||||
*game.LastUpdate = time.Now()
|
||||
err := db.Save(game).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword change the password of the user, the param 'password' must be the clear password
|
||||
func ChangePassword(userId int, password []byte) error {
|
||||
user, err := UserById(userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword(password, *config.Features().PasswordHashCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = hashedPassword
|
||||
err = db.Save(user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package database
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Password []byte `json:"-"`
|
||||
ID int `json:"id"`
|
||||
IsAdmin bool `json:"is_admin" gorm:"-:all"`
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
Name string `json:"name"`
|
||||
PathStorage string `json:"-"`
|
||||
ID int `json:"id"`
|
||||
Revision int `json:"rev"`
|
||||
UserId int `json:"-"`
|
||||
Available bool `json:"available"`
|
||||
Hash *string `json:"hash"`
|
||||
LastUpdate *time.Time `json:"last_update"`
|
||||
}
|
||||
55
go.mod
55
go.mod
@@ -1,29 +1,50 @@
|
||||
module opensavecloudserver
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/getlantern/systray v1.2.1
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/google/uuid v1.3.0
|
||||
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gorm.io/driver/mysql v1.3.3
|
||||
gorm.io/gorm v1.23.5
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9
|
||||
golang.org/x/crypto v0.8.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.5.2
|
||||
gorm.io/gorm v1.25.0
|
||||
tawesoft.co.uk/go/dialog v0.0.0-20201103210221-4175697d086f
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
|
||||
github.com/getlantern/errors v1.0.3 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af // indirect
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.3.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.4 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.4 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
)
|
||||
|
||||
186
go.sum
186
go.sum
@@ -1,51 +1,183 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE=
|
||||
github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af h1:cvD5qCZpH/Q32Ae0i1W1lRkVuM21czEZaJpTuRiJjc4=
|
||||
github.com/getlantern/golog v0.0.0-20230206140254-6d0a2e0f79af/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7 h1:Od0xvR4iK3gZwhkIbxnHw4Teusv+n5G/F9dW7x+C2f0=
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.1 h1:udsC2k98v2hN359VTFShuQW6GGprRprw6kD6539JikI=
|
||||
github.com/getlantern/systray v1.2.1/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
||||
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
|
||||
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
|
||||
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8=
|
||||
github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=
|
||||
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
|
||||
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
|
||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
||||
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
|
||||
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
|
||||
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
|
||||
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM=
|
||||
gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
||||
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
||||
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
||||
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
tawesoft.co.uk/go/dialog v0.0.0-20201103210221-4175697d086f h1:9LAzHynM3Jq3FZxP6rhoK3l96fSN+S47KABTOzE6gf4=
|
||||
tawesoft.co.uk/go/dialog v0.0.0-20201103210221-4175697d086f/go.mod h1:h1pWmQQT/jWrxa6VT3M7lFUif/2F0+WVISAi447Punc=
|
||||
|
||||
73
main.go
73
main.go
@@ -1,17 +1,82 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"opensavecloudserver/config"
|
||||
"opensavecloudserver/config/security"
|
||||
"opensavecloudserver/constant"
|
||||
"opensavecloudserver/data/datasource/pgsql"
|
||||
"opensavecloudserver/data/datasource/pgsql/models/game"
|
||||
"opensavecloudserver/data/datasource/pgsql/models/user"
|
||||
"opensavecloudserver/server"
|
||||
"opensavecloudserver/server/authentication/impl"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("Open Save Cloud (Server) %s (%s %s)\n", constant.Version, runtime.GOOS, runtime.GOARCH)
|
||||
InitCommon()
|
||||
server.Serve()
|
||||
fmt.Printf("Open Save Cloud (Server) v%s %s/%s\n", constant.Version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
_, err := fmt.Fprintln(os.Stderr, "the server has encountered an error and must stop: ", err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := initLogger(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path := flag.String("config", "./config.yml", "Set the configuration file path")
|
||||
generateRSAKey := flag.Bool("new-rsa-key", false, "Generate a new RSA key. This key will be written to the rsa_key path set in the configuration file")
|
||||
flag.Parse()
|
||||
appConfiguration, err := config.Load(*path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var pk *rsa.PrivateKey
|
||||
if *generateRSAKey {
|
||||
pk, err = security.GenerateNewRSAKey(appConfiguration.Path.RSAKey, 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
pk, err = security.LoadPrivateKey(appConfiguration.Path.RSAKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
dao := pgsql.NewDatabaseDatasource()
|
||||
err = dao.Connect(appConfiguration.Database.URL)
|
||||
if err != nil {
|
||||
log.Fatal("failed to connect to the datasource: ", err)
|
||||
}
|
||||
|
||||
userRepo := user.NewUserDatasource(dao)
|
||||
jwtAuthenticator, err := impl.NewJWTAuthenticator(pk, userRepo)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
deps := server.DatasourceDependencies{
|
||||
UserRepository: userRepo,
|
||||
GameRepository: game.NewGameDatasource(dao),
|
||||
Authenticator: jwtAuthenticator,
|
||||
}
|
||||
|
||||
appServer := server.NewServer(appConfiguration, deps)
|
||||
log.Println("the server is up and ready to listen on", appServer.Server.Addr)
|
||||
err = appServer.Server.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ package main
|
||||
import (
|
||||
_ "embed"
|
||||
"github.com/getlantern/systray"
|
||||
"opensavecloudserver/constant"
|
||||
"opensavecloudserver/server"
|
||||
"os"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tawesoft.co.uk/go/dialog"
|
||||
)
|
||||
|
||||
//go:generate go-winres make
|
||||
@@ -16,32 +17,50 @@ import (
|
||||
var icon []byte
|
||||
|
||||
func main() {
|
||||
go func() {
|
||||
InitCommon()
|
||||
server.Serve()
|
||||
}()
|
||||
path := flag.String("config", "./config.yml", "Set the configuration file path")
|
||||
flag.Parse()
|
||||
appConfiguration, err := config.Load(*path)
|
||||
if err != nil {
|
||||
dialog.Alert("An error occured while starting the server: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := initLogger(); err != nil {
|
||||
dialog.Alert("An error occured while starting the server: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
appServer := server.NewServer(appConfiguration)
|
||||
go func(s *http.Server) {
|
||||
err := s.ListenAndServe()
|
||||
defer systray.Quit()
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}(appServer.Server)
|
||||
startWindowsApp(appServer.Server)
|
||||
}
|
||||
|
||||
func startWindowsApp(appServer *http.Server) {
|
||||
onReady := func() {
|
||||
systray.SetIcon(icon)
|
||||
systray.SetTitle("Open Save Cloud Server")
|
||||
systray.SetTooltip("The server is up and ready")
|
||||
systray.AddMenuItem("Open Save Cloud "+constant.Version, "").Disable()
|
||||
systray.AddMenuItem("Running on "+appServer.Addr, "").Disable()
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Shutdown", "Quit the server")
|
||||
for {
|
||||
select {
|
||||
case <-mQuit.ClickedCh:
|
||||
func(s *http.Server) {
|
||||
mQuit.Disable()
|
||||
systray.SetTooltip("Shutting down the server...")
|
||||
s.Shutdown()
|
||||
}(appServer)
|
||||
}
|
||||
}
|
||||
}
|
||||
onExit := func() {}
|
||||
systray.Run(onReady, onExit)
|
||||
}
|
||||
|
||||
func onReady() {
|
||||
systray.SetIcon(icon)
|
||||
systray.SetTitle("Open Save Cloud Server")
|
||||
systray.SetTooltip("Open Save Cloud Server")
|
||||
systray.AddMenuItem("Open Save Cloud", "").Disable()
|
||||
systray.AddMenuItem(constant.Version, "").Disable()
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quit", "Quit the server")
|
||||
select {
|
||||
case <-mQuit.ClickedCh:
|
||||
quit()
|
||||
}
|
||||
}
|
||||
|
||||
func quit() {
|
||||
systray.Quit()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func onExit() {
|
||||
systray.Quit()
|
||||
}
|
||||
|
||||
215
server/admin.go
215
server/admin.go
@@ -1,220 +1,33 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"opensavecloudserver/admin"
|
||||
"opensavecloudserver/authentication"
|
||||
"opensavecloudserver/database"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UpdateUsername struct {
|
||||
Id int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
func (s *HTTPServer) createUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
userInfo := new(authentication.Registration)
|
||||
err = json.Unmarshal(body, userInfo)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = authentication.Register(userInfo)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
user, err := database.UserByUsername(userInfo.Username)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(user, w, r)
|
||||
func (s *HTTPServer) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
queryId := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
user, err := database.UserById(id)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = admin.RemoveUser(user)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(user, w, r)
|
||||
func (s *HTTPServer) listAllServerUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func AllUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := database.AllUsers()
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(users, w, r)
|
||||
func (s *HTTPServer) userHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func User(w http.ResponseWriter, r *http.Request) {
|
||||
queryId := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
user, err := database.UserById(id)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(user, w, r)
|
||||
func (s *HTTPServer) updateUserRoleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func SetAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
queryId := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
user, err := database.UserById(id)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = admin.SetAdmin(user)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(user, w, r)
|
||||
func (s *HTTPServer) updateUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func SetNotAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
queryId := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
user, err := database.UserById(id)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = admin.RemoveAdminRole(user)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(user, w, r)
|
||||
}
|
||||
|
||||
func ChangeUserPassword(w http.ResponseWriter, r *http.Request) {
|
||||
queryId := chi.URLParam(r, "id")
|
||||
userId, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
newPassword := new(NewPassword)
|
||||
err = json.Unmarshal(body, newPassword)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if newPassword.Password != newPassword.VerifyPassword {
|
||||
badRequest("password are not the same", w, r)
|
||||
return
|
||||
}
|
||||
err = database.ChangePassword(userId, []byte(newPassword.Password))
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
payload := &successMessage{
|
||||
Message: "Password changed",
|
||||
Timestamp: time.Now(),
|
||||
Status: 200,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
}
|
||||
|
||||
func ChangeUsername(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
newUserInfo := new(UpdateUsername)
|
||||
err = json.Unmarshal(body, newUserInfo)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if len(newUserInfo.Username) < 3 {
|
||||
badRequest("username need at least 3 characters", w, r)
|
||||
return
|
||||
}
|
||||
_, err = database.UserByUsername(newUserInfo.Username)
|
||||
if err == nil {
|
||||
badRequest("username already exist", w, r)
|
||||
return
|
||||
}
|
||||
err = database.ChangeUsername(newUserInfo.Id, newUserInfo.Username)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
payload := &successMessage{
|
||||
Message: "Username changed",
|
||||
Timestamp: time.Now(),
|
||||
Status: 200,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
func (s *HTTPServer) updateUsernameHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@@ -3,94 +3,88 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"opensavecloudserver/authentication"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Credential struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
type (
|
||||
userLogin struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
userRegistration struct {
|
||||
UserUsername string `json:"username"`
|
||||
UserPassword string `json:"password"`
|
||||
UserDisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
userPresenter struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
jwtPresenter struct {
|
||||
Token []byte `json:"token"`
|
||||
}
|
||||
)
|
||||
|
||||
func (ur userRegistration) Username() string {
|
||||
return ur.UserUsername
|
||||
}
|
||||
|
||||
type TokenValidation struct {
|
||||
Valid bool `json:"valid"`
|
||||
func (ur userRegistration) Password() string {
|
||||
return ur.UserPassword
|
||||
}
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
func (ur userRegistration) DisplayName() string {
|
||||
return ur.UserDisplayName
|
||||
}
|
||||
|
||||
func (s *HTTPServer) loginUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
credential := new(Credential)
|
||||
err = json.Unmarshal(body, credential)
|
||||
|
||||
var ul userLogin
|
||||
err = json.Unmarshal(body, &ul)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
token, err := authentication.Connect(credential.Username, credential.Password)
|
||||
|
||||
jwt, err := s.deps.Authenticator.Authenticate(ul.Username, ul.Password)
|
||||
if err != nil {
|
||||
unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
ok(token, w, r)
|
||||
|
||||
payload := jwtPresenter{
|
||||
Token: jwt,
|
||||
}
|
||||
ok(payload, w)
|
||||
}
|
||||
|
||||
func Register(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *HTTPServer) registerUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
registration := new(authentication.Registration)
|
||||
err = json.Unmarshal(body, registration)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = authentication.Register(registration)
|
||||
if err != nil {
|
||||
badRequest(err.Error(), w, r)
|
||||
return
|
||||
}
|
||||
payload := successMessage{
|
||||
Message: "You are now registered",
|
||||
Timestamp: time.Now(),
|
||||
Status: 200,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
}
|
||||
|
||||
func CheckToken(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
var ur userRegistration
|
||||
err = json.Unmarshal(body, &ur)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
credential := new(authentication.AccessToken)
|
||||
err = json.Unmarshal(body, credential)
|
||||
|
||||
u, err := s.deps.UserRepository.CreateUser(ur)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
panic(err)
|
||||
}
|
||||
_, err = authentication.ParseToken(credential.Token)
|
||||
if err != nil {
|
||||
payload := TokenValidation{
|
||||
Valid: false,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
return
|
||||
payload := userPresenter{
|
||||
ID: u.ID().String(),
|
||||
Username: u.Username(),
|
||||
DisplayName: u.DisplayName(),
|
||||
}
|
||||
payload := TokenValidation{
|
||||
Valid: true,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
ok(payload, w)
|
||||
}
|
||||
|
||||
25
server/authentication/authentication.go
Normal file
25
server/authentication/authentication.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"opensavecloudserver/data/repository/user"
|
||||
)
|
||||
|
||||
type (
|
||||
Authenticator interface {
|
||||
Authenticate(username, password string) ([]byte, error)
|
||||
Validate(token string) (Session, error)
|
||||
}
|
||||
|
||||
Session interface {
|
||||
UserID() user.ID
|
||||
Scopes() []Scope
|
||||
Roles() []user.Role
|
||||
}
|
||||
|
||||
Scope string
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadPassword = errors.New("failed to verify password")
|
||||
)
|
||||
128
server/authentication/impl/impl.go
Normal file
128
server/authentication/impl/impl.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package impl
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"opensavecloudserver/data/repository/user"
|
||||
"opensavecloudserver/server/authentication"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
JWTAuthenticator struct {
|
||||
userRepo user.UserRepository
|
||||
key jwk.Key
|
||||
pubKey jwk.Key
|
||||
}
|
||||
|
||||
JWTSession struct {
|
||||
id user.ID
|
||||
scopes []authentication.Scope
|
||||
roles []user.Role
|
||||
}
|
||||
|
||||
claimKey string
|
||||
)
|
||||
|
||||
func (J JWTSession) UserID() user.ID {
|
||||
return J.id
|
||||
}
|
||||
|
||||
func (J JWTSession) Scopes() []authentication.Scope {
|
||||
return J.scopes
|
||||
}
|
||||
|
||||
func (J JWTSession) Roles() []user.Role {
|
||||
return J.roles
|
||||
}
|
||||
|
||||
const (
|
||||
userScopesClaimKey claimKey = "user.scopes"
|
||||
userRolesClaimKey claimKey = "user.roles"
|
||||
|
||||
issuerName string = "oscs"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrScopesNotFound = errors.New("scopes not found in JWT")
|
||||
ErrRolesNotFound = errors.New("roles not found in JWT")
|
||||
)
|
||||
|
||||
func (J *JWTAuthenticator) Authenticate(username, password string) ([]byte, error) {
|
||||
u, err := J.userRepo.UserByUsername(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.CheckPassword(password) {
|
||||
// Build a JWT!
|
||||
tok, err := jwt.NewBuilder().
|
||||
Issuer(issuerName).
|
||||
IssuedAt(time.Now()).
|
||||
Subject(u.ID().String()).
|
||||
Claim(string(userScopesClaimKey), "").
|
||||
Claim(string(userRolesClaimKey), u.Roles()).
|
||||
Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sign a JWT!
|
||||
return jwt.Sign(tok, jwt.WithKey(jwa.RS256, J.key))
|
||||
}
|
||||
return nil, authentication.ErrBadPassword
|
||||
}
|
||||
|
||||
func (J *JWTAuthenticator) Validate(token string) (authentication.Session, error) {
|
||||
verifiedToken, err := jwt.Parse([]byte(token), jwt.WithKey(jwa.RS256, J.pubKey), jwt.WithValidate(true), jwt.WithIssuer(issuerName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := uuid.Parse(verifiedToken.Subject())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value, ok := verifiedToken.Get(string(userScopesClaimKey))
|
||||
if !ok {
|
||||
return nil, ErrScopesNotFound
|
||||
}
|
||||
scopes, ok := value.([]authentication.Scope)
|
||||
if !ok {
|
||||
return nil, ErrScopesNotFound
|
||||
}
|
||||
value, ok = verifiedToken.Get(string(userRolesClaimKey))
|
||||
if !ok {
|
||||
return nil, ErrRolesNotFound
|
||||
}
|
||||
roles, ok := value.([]user.Role)
|
||||
if !ok {
|
||||
return nil, ErrRolesNotFound
|
||||
}
|
||||
return JWTSession{
|
||||
id: user.ID(id),
|
||||
scopes: scopes,
|
||||
roles: roles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewJWTAuthenticator(key *rsa.PrivateKey, userRepo user.UserRepository) (authentication.Authenticator, error) {
|
||||
a := new(JWTAuthenticator)
|
||||
a.userRepo = userRepo
|
||||
// Parse, serialize, slice and dice JWKs!
|
||||
privkey, err := jwk.FromRaw(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubkey, err := jwk.PublicKeyOf(privkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.key = privkey
|
||||
a.pubKey = pubkey
|
||||
return a, nil
|
||||
}
|
||||
348
server/data.go
348
server/data.go
@@ -1,348 +1,42 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"opensavecloudserver/config"
|
||||
"opensavecloudserver/database"
|
||||
"opensavecloudserver/upload"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type NewGameInfo struct {
|
||||
Name string `json:"name"`
|
||||
// createSaveEntryHandler create a game entry to the database
|
||||
func (s *HTTPServer) createSaveEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
type UploadGameInfo struct {
|
||||
GameId int `json:"game_id"`
|
||||
// saveInformationHandler get the game save information from the database
|
||||
func (s *HTTPServer) saveInformationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
type LockError struct {
|
||||
Message string `json:"message"`
|
||||
// allUserSavesInformationHandler all game saves information for a user
|
||||
func (s *HTTPServer) allUserSavesInformationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
type NewPassword struct {
|
||||
Password string `json:"password"`
|
||||
VerifyPassword string `json:"verify_password"`
|
||||
// uploadDataHandler upload the game save archive to the storage folder
|
||||
func (s *HTTPServer) uploadDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// CreateGame create a game entry to the database
|
||||
func CreateGame(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
gameInfo := new(NewGameInfo)
|
||||
err = json.Unmarshal(body, gameInfo)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
game, err := database.CreateGame(userId, gameInfo.Name)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(game, w, r)
|
||||
// downloadDataHandler send the game save archive to the client
|
||||
func (s *HTTPServer) downloadDataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// GameInfoByID get the game save information from the database
|
||||
func GameInfoByID(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
queryId := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
badRequest("Game ID missing or not an int", w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
game, err := database.GameInfoById(userId, id)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(game, w, r)
|
||||
func (s *HTTPServer) currentUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// AllGamesInformation all game saves information for a user
|
||||
func AllGamesInformation(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
games, err := database.GameInfosByUserId(userId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(games, w, r)
|
||||
func (s *HTTPServer) updateCurrentUserPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// AskForUpload check if the game save is not lock, then lock it and generate a token
|
||||
func AskForUpload(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
gameInfo := new(UploadGameInfo)
|
||||
err = json.Unmarshal(body, gameInfo)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
token, err := upload.AskForUpload(userId, gameInfo.GameId)
|
||||
if err != nil {
|
||||
ok(LockError{Message: err.Error()}, w, r)
|
||||
return
|
||||
}
|
||||
ok(token, w, r)
|
||||
}
|
||||
|
||||
// UploadSave upload the game save archive to the storage folder
|
||||
func UploadSave(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
gameId, err := gameIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer upload.UnlockGame(gameId)
|
||||
hash := r.Header.Get("X-Game-Save-Hash")
|
||||
if utf8.RuneCountInString(hash) == 0 {
|
||||
badRequest("The header X-Game-Save-Hash is missing", w, r)
|
||||
return
|
||||
}
|
||||
archiveHash := strings.ToLower(r.Header.Get("X-Hash"))
|
||||
if utf8.RuneCountInString(hash) == 0 {
|
||||
badRequest("The header X-Hash is missing", w, r)
|
||||
return
|
||||
}
|
||||
game, err := database.GameInfoById(userId, gameId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer func(file multipart.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}(file)
|
||||
err = upload.UploadToCache(file, game)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = upload.ValidateAndMove(game, archiveHash)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = database.UpdateGameRevision(game, hash)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
payload := &successMessage{
|
||||
Message: "Game uploaded",
|
||||
Timestamp: time.Now(),
|
||||
Status: 200,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
}
|
||||
|
||||
// Download send the game save archive to the client
|
||||
func Download(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
gameId, err := gameIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer upload.UnlockGame(gameId)
|
||||
game, err := database.GameInfoById(userId, gameId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
savePath := filepath.Join(config.Path().Storage, strconv.Itoa(userId), game.PathStorage)
|
||||
|
||||
if _, err := os.Stat(savePath); err == nil {
|
||||
hash, err := upload.FileHash(savePath)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
file, err := os.Open(savePath)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}(file)
|
||||
w.Header().Add("X-Hash", strings.ToUpper(hash))
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func UserInformation(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
user, err := database.UserById(userId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(user, w, r)
|
||||
}
|
||||
|
||||
func ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
newPassword := new(NewPassword)
|
||||
err = json.Unmarshal(body, newPassword)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if newPassword.Password != newPassword.VerifyPassword {
|
||||
badRequest("password are not the same", w, r)
|
||||
return
|
||||
}
|
||||
err = database.ChangePassword(userId, []byte(newPassword.Password))
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
payload := &successMessage{
|
||||
Message: "Password changed",
|
||||
Timestamp: time.Now(),
|
||||
Status: 200,
|
||||
}
|
||||
ok(payload, w, r)
|
||||
}
|
||||
|
||||
func RemoveGame(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := userIdFromContext(r.Context())
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
queryId := chi.URLParam(r, "id")
|
||||
id, err := strconv.Atoi(queryId)
|
||||
if err != nil {
|
||||
badRequest("Game ID missing or not an int", w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
game, err := database.GameInfoById(userId, id)
|
||||
if err != nil {
|
||||
notFound(err.Error(), w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = upload.RemoveGame(userId, game)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
err = database.RemoveGame(game)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
ok(game, w, r)
|
||||
func (s *HTTPServer) deleteSave(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
36
server/middleware.go
Normal file
36
server/middleware.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
// authMiddleware check the authentication token before accessing to the resource
|
||||
func (s *HTTPServer) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
}
|
||||
|
||||
// adminMiddleware check the role of the user before accessing to the resource
|
||||
func (s *HTTPServer) adminMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
}
|
||||
|
||||
// uploadMiddleware check the upload key before allowing to upload a file
|
||||
func (s *HTTPServer) uploadMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
}
|
||||
|
||||
func recoverMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -15,15 +15,9 @@ type httpError struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type successMessage struct {
|
||||
Status int `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func internalServerError(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: 500,
|
||||
Status: http.StatusInternalServerError,
|
||||
Error: "Internal Server Error",
|
||||
Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.",
|
||||
Path: r.RequestURI,
|
||||
@@ -35,7 +29,7 @@ func internalServerError(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(500)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -44,7 +38,7 @@ func internalServerError(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func notFound(message string, w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: 404,
|
||||
Status: http.StatusNotFound,
|
||||
Error: "Not Found",
|
||||
Message: message,
|
||||
Path: r.RequestURI,
|
||||
@@ -56,7 +50,28 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(404)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: http.StatusMethodNotAllowed,
|
||||
Error: "Method Not Allowed",
|
||||
Message: "The server knows the request method, but the target resource doesn't support this method",
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -65,7 +80,7 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func unauthorized(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: 401,
|
||||
Status: http.StatusUnauthorized,
|
||||
Error: "Unauthorized",
|
||||
Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.",
|
||||
Path: r.RequestURI,
|
||||
@@ -77,8 +92,8 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Header().Add("WWW-Authenticate", "Custom realm=\"Login via /api/login\"")
|
||||
w.WriteHeader(401)
|
||||
w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -87,8 +102,8 @@ func unauthorized(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: 403,
|
||||
Error: "Unauthorized",
|
||||
Status: http.StatusForbidden,
|
||||
Error: "Forbidden",
|
||||
Message: "The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.",
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
@@ -99,14 +114,14 @@ func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(403)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ok(obj interface{}, w http.ResponseWriter, _ *http.Request) {
|
||||
func ok(obj interface{}, w http.ResponseWriter) {
|
||||
payload, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
@@ -120,7 +135,7 @@ func ok(obj interface{}, w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
func badRequest(message string, w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: 400,
|
||||
Status: http.StatusBadRequest,
|
||||
Error: "Bad Request",
|
||||
Message: message,
|
||||
Path: r.RequestURI,
|
||||
@@ -132,7 +147,7 @@ func badRequest(message string, w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(400)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
|
||||
246
server/server.go
246
server/server.go
@@ -1,166 +1,138 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"log"
|
||||
"net/http"
|
||||
"opensavecloudserver/authentication"
|
||||
"opensavecloudserver/config"
|
||||
"opensavecloudserver/database"
|
||||
"opensavecloudserver/upload"
|
||||
"opensavecloudserver/data/repository/game"
|
||||
"opensavecloudserver/data/repository/user"
|
||||
"opensavecloudserver/server/authentication"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
type (
|
||||
HTTPServer struct {
|
||||
Server *http.Server
|
||||
config config.Configuration
|
||||
deps DatasourceDependencies
|
||||
}
|
||||
|
||||
const (
|
||||
UserIdKey ContextKey = "userId"
|
||||
GameIdKey ContextKey = "gameId"
|
||||
DatasourceDependencies struct {
|
||||
UserRepository user.UserRepository
|
||||
GameRepository game.GameRepository
|
||||
Authenticator authentication.Authenticator
|
||||
}
|
||||
)
|
||||
|
||||
// Serve start the http server
|
||||
func Serve() {
|
||||
// NewServer start the http server
|
||||
func NewServer(config config.Configuration, deps DatasourceDependencies) *HTTPServer {
|
||||
s := &HTTPServer{config: config, deps: deps}
|
||||
router := chi.NewRouter()
|
||||
router.NotFound(func(writer http.ResponseWriter, request *http.Request) {
|
||||
notFound("This route does not exist", writer, request)
|
||||
})
|
||||
router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) {
|
||||
methodNotAllowed(writer, request)
|
||||
})
|
||||
router.Use(middleware.Logger)
|
||||
router.Use(recovery)
|
||||
router.Route("/api", func(rApi chi.Router) {
|
||||
rApi.Route("/v1", func(r chi.Router) {
|
||||
r.Post("/login", Login)
|
||||
r.Post("/check/token", CheckToken)
|
||||
if config.Features().AllowRegister {
|
||||
r.Post("/register", Register)
|
||||
router.Use(recoverMiddleware)
|
||||
if config.Server.Compress {
|
||||
router.Use(middleware.Compress(5, "application/json", "application/octet‑stream"))
|
||||
}
|
||||
if config.Server.PingEndPoint {
|
||||
router.Use(middleware.Heartbeat("/heartbeat"))
|
||||
}
|
||||
router.Route("/api", func(routerAPI chi.Router) {
|
||||
routerAPI.Route("/v1", func(r chi.Router) {
|
||||
// Unsupported V1 because it was shitty, sorry about that
|
||||
r.HandleFunc("/*", unsupportedAPIHandler)
|
||||
})
|
||||
routerAPI.Route("/v2", func(r chi.Router) {
|
||||
// Get information about the server
|
||||
r.Get("/version", s.Information)
|
||||
// Authentication routes
|
||||
// Login and get a token
|
||||
r.Post("/login", s.loginUserHandler)
|
||||
if config.Features.AllowRegister {
|
||||
// Register a user
|
||||
r.Post("/register", s.registerUserHandler)
|
||||
}
|
||||
r.Route("/system", func(systemRouter chi.Router) {
|
||||
systemRouter.Get("/information", Information)
|
||||
})
|
||||
r.Route("/admin", func(adminRouter chi.Router) {
|
||||
adminRouter.Use(adminMiddleware)
|
||||
adminRouter.Post("/user", AddUser)
|
||||
adminRouter.Post("/user/username", ChangeUsername)
|
||||
adminRouter.Post("/user/passwd/{id}", ChangeUserPassword)
|
||||
adminRouter.Delete("/user/{id}", RemoveUser)
|
||||
adminRouter.Get("/user/{id}", User)
|
||||
adminRouter.Get("/users", AllUsers)
|
||||
adminRouter.Get("/user/role/admin/{id}", SetAdmin)
|
||||
adminRouter.Get("/user/role/user/{id}", SetNotAdmin)
|
||||
})
|
||||
// Secured routes
|
||||
r.Group(func(secureRouter chi.Router) {
|
||||
secureRouter.Use(authMiddleware)
|
||||
secureRouter.Use(s.authMiddleware)
|
||||
// Logged user routes
|
||||
secureRouter.Route("/user", func(userRouter chi.Router) {
|
||||
userRouter.Get("/information", UserInformation)
|
||||
userRouter.Post("/passwd", ChangePassword)
|
||||
// Get information about the logged user
|
||||
userRouter.Get("/", s.currentUserHandler)
|
||||
// Change the password of the current user
|
||||
userRouter.Put("/password/update", s.updateCurrentUserPasswordHandler)
|
||||
})
|
||||
secureRouter.Route("/game", func(gameRouter chi.Router) {
|
||||
gameRouter.Post("/create", CreateGame)
|
||||
gameRouter.Get("/all", AllGamesInformation)
|
||||
gameRouter.Delete("/remove/{id}", RemoveGame)
|
||||
gameRouter.Get("/info/{id}", GameInfoByID)
|
||||
gameRouter.Post("/upload/init", AskForUpload)
|
||||
// Save files routes
|
||||
secureRouter.Route("/saves", func(gameRouter chi.Router) {
|
||||
// Create a save entry
|
||||
gameRouter.Post("/create", s.createSaveEntryHandler)
|
||||
// List all available saves
|
||||
gameRouter.Get("/", s.allUserSavesInformationHandler)
|
||||
// Remove a save
|
||||
gameRouter.Delete("/{id}", s.deleteSave)
|
||||
// Get the information about a save
|
||||
gameRouter.Get("/{id}", s.saveInformationHandler)
|
||||
// Data routes
|
||||
gameRouter.Group(func(uploadRouter chi.Router) {
|
||||
uploadRouter.Use(uploadMiddleware)
|
||||
uploadRouter.Post("/upload", UploadSave)
|
||||
uploadRouter.Get("/download", Download)
|
||||
uploadRouter.Use(s.uploadMiddleware)
|
||||
// Upload data
|
||||
uploadRouter.Put("/{id}/data", s.uploadDataHandler)
|
||||
// downloadDataHandler data
|
||||
uploadRouter.Get("/{id}/data", s.downloadDataHandler)
|
||||
})
|
||||
})
|
||||
secureRouter.Route("/admin", func(adminRouter chi.Router) {
|
||||
adminRouter.Use(s.adminMiddleware)
|
||||
// Create a user
|
||||
adminRouter.Post("/user/create", s.createUserHandler)
|
||||
// Update the username of a user
|
||||
adminRouter.Post("/user/{id}/username", s.updateUsernameHandler)
|
||||
// Update the password of a user
|
||||
adminRouter.Post("/user/{id}/password/update", s.updateUserPasswordHandler)
|
||||
// Remove a user
|
||||
adminRouter.Delete("/user/{id}", s.deleteUserHandler)
|
||||
// Get information about a user
|
||||
adminRouter.Get("/user/{id}", s.userHandler)
|
||||
// List all user registered on the server
|
||||
adminRouter.Get("/users", s.listAllServerUsersHandler)
|
||||
// Update role
|
||||
adminRouter.Put("/user/{id}/role", s.updateUserRoleHandler)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
log.Println("Server is listening...")
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%d", config.Server().Port), router)
|
||||
s.Server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", config.Server.Port),
|
||||
Handler: router,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func unsupportedAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
Status: http.StatusGone,
|
||||
Error: "API Not supported anymore",
|
||||
Message: "This version of the server does not support the V1 version of the API.",
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusGone)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// authMiddleware check the authentication token before accessing to the resource
|
||||
func authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
if len(header) > 7 {
|
||||
userId, err := authentication.ParseToken(header[7:])
|
||||
if err != nil {
|
||||
unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), UserIdKey, userId)
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
unauthorized(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// adminMiddleware check the role of the user before accessing to the resource
|
||||
func adminMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
if len(header) > 7 {
|
||||
userId, err := authentication.ParseToken(header[7:])
|
||||
if err != nil {
|
||||
unauthorized(w, r)
|
||||
return
|
||||
}
|
||||
user, err := database.UserById(userId)
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
if !user.IsAdmin {
|
||||
forbidden(w, r)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), UserIdKey, userId)
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
unauthorized(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// uploadMiddleware check the upload key before allowing to upload a file
|
||||
func uploadMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("X-Upload-Key")
|
||||
if len(header) > 0 {
|
||||
if gameId, ok := upload.CheckUploadToken(header); ok {
|
||||
ctx := context.WithValue(r.Context(), GameIdKey, gameId)
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
unauthorized(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func userIdFromContext(ctx context.Context) (int, error) {
|
||||
if userId, ok := ctx.Value(UserIdKey).(int); ok {
|
||||
return userId, nil
|
||||
}
|
||||
return 0, errors.New("userId not found in context")
|
||||
}
|
||||
|
||||
func gameIdFromContext(ctx context.Context) (int, error) {
|
||||
if gameId, ok := ctx.Value(GameIdKey).(int); ok {
|
||||
return gameId, nil
|
||||
}
|
||||
return 0, errors.New("gameId not found in context")
|
||||
}
|
||||
|
||||
func recovery(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"opensavecloudserver/config"
|
||||
"opensavecloudserver/constant"
|
||||
"runtime"
|
||||
)
|
||||
@@ -10,20 +9,20 @@ import (
|
||||
type information struct {
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
Version string `json:"version"`
|
||||
ApiVersion int `json:"api_version"`
|
||||
APIVersion int `json:"api_version"`
|
||||
GoVersion string `json:"go_version"`
|
||||
OsName string `json:"os_name"`
|
||||
OsArchitecture string `json:"os_architecture"`
|
||||
OSName string `json:"os_name"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
}
|
||||
|
||||
func Information(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *HTTPServer) Information(w http.ResponseWriter, r *http.Request) {
|
||||
info := information{
|
||||
AllowRegister: config.Features().AllowRegister,
|
||||
AllowRegister: s.config.Features.AllowRegister,
|
||||
Version: constant.Version,
|
||||
ApiVersion: constant.ApiVersion,
|
||||
APIVersion: constant.ApiVersion,
|
||||
GoVersion: runtime.Version(),
|
||||
OsName: runtime.GOOS,
|
||||
OsArchitecture: runtime.GOARCH,
|
||||
OSName: runtime.GOOS,
|
||||
OSArchitecture: runtime.GOARCH,
|
||||
}
|
||||
ok(info, w, r)
|
||||
ok(info, w)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"opensavecloudserver/config"
|
||||
database2 "opensavecloudserver/data/internal/database"
|
||||
"opensavecloudserver/database"
|
||||
"os"
|
||||
"path"
|
||||
@@ -108,7 +109,7 @@ func CheckUploadToken(uploadToken string) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func UploadToCache(file multipart.File, game *database.Game) error {
|
||||
func UploadToCache(file multipart.File, game *database2.Game) error {
|
||||
filePath := path.Join(config.Path().Cache, strconv.Itoa(game.UserId))
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
err = os.Mkdir(filePath, 0766)
|
||||
@@ -133,7 +134,7 @@ func UploadToCache(file multipart.File, game *database.Game) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateAndMove(game *database.Game, hash string) error {
|
||||
func ValidateAndMove(game *database2.Game, hash string) error {
|
||||
filePath := path.Join(config.Path().Cache, strconv.Itoa(game.UserId), game.PathStorage)
|
||||
if err := checkHash(filePath, hash); err != nil {
|
||||
return err
|
||||
@@ -155,7 +156,7 @@ func checkHash(path, hash string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveToStorage(cachePath string, game *database.Game) error {
|
||||
func moveToStorage(cachePath string, game *database2.Game) error {
|
||||
filePath := path.Join(config.Path().Storage, strconv.Itoa(game.UserId))
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
err = os.Mkdir(filePath, 0766)
|
||||
@@ -200,7 +201,7 @@ func RemoveFolders(userId int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveGame(userId int, game *database.Game) error {
|
||||
func RemoveGame(userId int, game *database2.Game) error {
|
||||
filePath := path.Join(config.Path().Storage, strconv.Itoa(userId), game.PathStorage)
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user