From c06843cd2896e39a9ce372689564686432d0a516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lie=20Delhaie?= Date: Mon, 29 May 2023 17:44:50 +0200 Subject: [PATCH] Start refactoring --- .gitignore | 3 +- admin/admin.go | 29 -- authentication/authentication.go | 79 ----- build.sh | 16 +- common.go | 15 +- config.default.yml | 9 +- config/config.go | 109 +++---- config/security/security.go | 118 ++++++++ constant/constant.go | 4 +- data/datasource/datasource.go | 12 + data/datasource/pgsql/models/game/game.go | 40 +++ data/datasource/pgsql/models/user/user.go | 31 ++ data/datasource/pgsql/pgsql.go | 30 ++ data/repository/game/game.go | 40 +++ data/repository/user/user.go | 36 +++ database/database.go | 207 ------------- database/model.go | 22 -- go.mod | 55 ++-- go.sum | 186 ++++++++++-- main.go | 73 ++++- main_windows.go | 79 +++-- server/admin.go | 215 +------------ server/authentication.go | 120 ++++---- server/authentication/authentication.go | 25 ++ server/authentication/impl/impl.go | 128 ++++++++ server/data.go | 348 ++-------------------- server/middleware.go | 36 +++ server/response.go | 53 ++-- server/server.go | 246 +++++++-------- server/system.go | 19 +- upload/upload.go | 9 +- 31 files changed, 1125 insertions(+), 1267 deletions(-) delete mode 100644 admin/admin.go delete mode 100644 authentication/authentication.go create mode 100644 config/security/security.go create mode 100644 data/datasource/datasource.go create mode 100644 data/datasource/pgsql/models/game/game.go create mode 100644 data/datasource/pgsql/models/user/user.go create mode 100644 data/datasource/pgsql/pgsql.go create mode 100644 data/repository/game/game.go create mode 100644 data/repository/user/user.go delete mode 100644 database/database.go delete mode 100644 database/model.go create mode 100644 server/authentication/authentication.go create mode 100644 server/authentication/impl/impl.go create mode 100644 server/middleware.go diff --git a/.gitignore b/.gitignore index 56675cc..da1cf74 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ opensavecloudserver storage/ .idea/ build -*.log \ No newline at end of file +*.log +id_rsa* \ No newline at end of file diff --git a/admin/admin.go b/admin/admin.go deleted file mode 100644 index 5cd58ad..0000000 --- a/admin/admin.go +++ /dev/null @@ -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) -} diff --git a/authentication/authentication.go b/authentication/authentication.go deleted file mode 100644 index 2f250bd..0000000 --- a/authentication/authentication.go +++ /dev/null @@ -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) -} diff --git a/build.sh b/build.sh index 26a711b..5bd3bff 100644 --- a/build.sh +++ b/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 \ No newline at end of file diff --git a/common.go b/common.go index d460eb9..743fe0d 100644 --- a/common.go +++ b/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 } diff --git a/config.default.yml b/config.default.yml index 095edec..0364158 100644 --- a/config.default.yml +++ b/config.default.yml @@ -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" diff --git a/config/config.go b/config/config.go index 7105bb2..a9d81ac 100644 --- a/config/config.go +++ b/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 } diff --git a/config/security/security.go b/config/security/security.go new file mode 100644 index 0000000..05ded53 --- /dev/null +++ b/config/security/security.go @@ -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 +} diff --git a/constant/constant.go b/constant/constant.go index 161e1e0..19bc5af 100644 --- a/constant/constant.go +++ b/constant/constant.go @@ -1,5 +1,5 @@ package constant -const Version = "1.0.0" +const Version = "2.0.0" -const ApiVersion = 1 +const ApiVersion = 2 diff --git a/data/datasource/datasource.go b/data/datasource/datasource.go new file mode 100644 index 0000000..e9ae2bb --- /dev/null +++ b/data/datasource/datasource.go @@ -0,0 +1,12 @@ +package datasource + +import ( + "gorm.io/gorm" +) + +type ( + Datasource interface { + Connect(dsn string) error + DB() *gorm.DB + } +) diff --git a/data/datasource/pgsql/models/game/game.go b/data/datasource/pgsql/models/game/game.go new file mode 100644 index 0000000..c5999ec --- /dev/null +++ b/data/datasource/pgsql/models/game/game.go @@ -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 +} diff --git a/data/datasource/pgsql/models/user/user.go b/data/datasource/pgsql/models/user/user.go new file mode 100644 index 0000000..c342861 --- /dev/null +++ b/data/datasource/pgsql/models/user/user.go @@ -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 +} diff --git a/data/datasource/pgsql/pgsql.go b/data/datasource/pgsql/pgsql.go new file mode 100644 index 0000000..4c171b9 --- /dev/null +++ b/data/datasource/pgsql/pgsql.go @@ -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 +} diff --git a/data/repository/game/game.go b/data/repository/game/game.go new file mode 100644 index 0000000..9d530e8 --- /dev/null +++ b/data/repository/game/game.go @@ -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") +) diff --git a/data/repository/user/user.go b/data/repository/user/user.go new file mode 100644 index 0000000..f3bf904 --- /dev/null +++ b/data/repository/user/user.go @@ -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") +) diff --git a/database/database.go b/database/database.go deleted file mode 100644 index 55e19ef..0000000 --- a/database/database.go +++ /dev/null @@ -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 -} diff --git a/database/model.go b/database/model.go deleted file mode 100644 index c61924b..0000000 --- a/database/model.go +++ /dev/null @@ -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"` -} diff --git a/go.mod b/go.mod index 3f302f1..d3cc1fc 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 62d4f9c..d92e81f 100644 --- a/go.sum +++ b/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= diff --git a/main.go b/main.go index 616aa49..129154f 100644 --- a/main.go +++ b/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) + } } diff --git a/main_windows.go b/main_windows.go index afeb567..99c18ad 100644 --- a/main_windows.go +++ b/main_windows.go @@ -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() -} diff --git a/server/admin.go b/server/admin.go index d354738..4edb5d1 100644 --- a/server/admin.go +++ b/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 } diff --git a/server/authentication.go b/server/authentication.go index a564708..33e8b24 100644 --- a/server/authentication.go +++ b/server/authentication.go @@ -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) } diff --git a/server/authentication/authentication.go b/server/authentication/authentication.go new file mode 100644 index 0000000..65903db --- /dev/null +++ b/server/authentication/authentication.go @@ -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") +) diff --git a/server/authentication/impl/impl.go b/server/authentication/impl/impl.go new file mode 100644 index 0000000..dc5c1a9 --- /dev/null +++ b/server/authentication/impl/impl.go @@ -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 +} diff --git a/server/data.go b/server/data.go index 6b8d09a..a9c2698 100644 --- a/server/data.go +++ b/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 } diff --git a/server/middleware.go b/server/middleware.go new file mode 100644 index 0000000..1825850 --- /dev/null +++ b/server/middleware.go @@ -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) + }) +} diff --git a/server/response.go b/server/response.go index dfd851a..d6d91ee 100644 --- a/server/response.go +++ b/server/response.go @@ -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) diff --git a/server/server.go b/server/server.go index a7a6b19..8602142 100644 --- a/server/server.go +++ b/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) - }) -} diff --git a/server/system.go b/server/system.go index 80fb025..420c1d2 100644 --- a/server/system.go +++ b/server/system.go @@ -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) } diff --git a/upload/upload.go b/upload/upload.go index 6391882..d840d2d 100644 --- a/upload/upload.go +++ b/upload/upload.go @@ -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) }