From f106d62cbbeedba4c168cd915847fa77e004d9c2 Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Wed, 30 Oct 2024 16:58:04 -0400 Subject: [PATCH 1/4] Added password field (which is not being checked at the moment) and made other fields pointers to be able to check for their presence --- internal/account/account.go | 20 +++++++++++++ internal/account/update_account.go | 48 ++++++++++++++++-------------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/internal/account/account.go b/internal/account/account.go index 8340692..aa76c3b 100644 --- a/internal/account/account.go +++ b/internal/account/account.go @@ -30,3 +30,23 @@ type Account struct { // ExperienceLevel represents the player's experience level (0=beginner, 1=easy, 2=medium, 3=hard, 4=very hard, 5=impossible) ExperienceLevel ExperienceLevel `json:"experience_level"` } + +// AccountParam represents a player account with non-required fields. +// +// @Description Structure for representing a player account with non-required fields. +type AccountParam struct { + // Name is the name of the player. + Name *string `json:"name,omitempty"` + + // Info contains additional information about the player. + Info *string `json:"info,omitempty"` + + // Location indicates the player's real-life location. + Location *string `json:"location,omitempty"` + + // Email is the email address of the player. + Email *string `json:"email,omitempty"` + + // ExperienceLevel represents the player's experience level (0=beginner, 1=easy, 2=medium, 3=hard, 4=very hard, 5=impossible) + ExperienceLevel *ExperienceLevel `json:"experience_level,omitempty"` +} diff --git a/internal/account/update_account.go b/internal/account/update_account.go index 9cd81ba..81d5f14 100644 --- a/internal/account/update_account.go +++ b/internal/account/update_account.go @@ -13,18 +13,12 @@ import ( // // @Description Structure for the account update request payload. type UpdateAccountArgs struct { + // The account to create. + Account *AccountParam `json:"account"` + // The password for the account to be created + Password *string `json:"password"` // The account ID for the account that will be updated. - AccountId int64 `json:"account_id"` - // The new name for the account. - Name *string `json:"name,omitempty"` - // The new info for the account. - Info *string `json:"info,omitempty"` - // The new location for the account. - Location *string `json:"location,omitempty"` - // The new email for the account. - Email *string `json:"email,omitempty"` - // The new experience level for the account. - ExperienceLevel *int `json:"experience_level,omitempty"` + AccountId *int64 `json:"account_id"` } // UpdateAccount updates an account by the account ID. @@ -56,11 +50,21 @@ func UpdateAccount(w http.ResponseWriter, r *http.Request, db *sql.DB) error { return errors.New("an error occurred while decoding the request body:" + err.Error()) } - if args.AccountId == 0 { + if args.AccountId == nil { w.WriteHeader(http.StatusBadRequest) return errors.New("account_id must be specified") } + if args.Password == nil { + w.WriteHeader(http.StatusBadRequest) + return errors.New("the password for the account must be specified") + } + + if args.Account == nil { + w.WriteHeader(http.StatusBadRequest) + return errors.New("account must be specified") + } + // Use reflection to check if at least one field other than AccountId is set v := reflect.ValueOf(args) numFields := v.NumField() @@ -83,29 +87,29 @@ func UpdateAccount(w http.ResponseWriter, r *http.Request, db *sql.DB) error { params := []interface{}{} paramIndex := 1 - if args.Name != nil { + if args.Account.Name != nil { query += fmt.Sprintf("name = $%d, ", paramIndex) - params = append(params, *args.Name) + params = append(params, args.Account.Name) paramIndex++ } - if args.Info != nil { + if args.Account.Info != nil { query += fmt.Sprintf("info = $%d, ", paramIndex) - params = append(params, *args.Info) + params = append(params, args.Account.Info) paramIndex++ } - if args.Location != nil { + if args.Account.Location != nil { query += fmt.Sprintf("location = $%d, ", paramIndex) - params = append(params, *args.Location) + params = append(params, args.Account.Location) paramIndex++ } - if args.Email != nil { + if args.Account.Email != nil { query += fmt.Sprintf("email = $%d, ", paramIndex) - params = append(params, *args.Email) + params = append(params, args.Account.Email) paramIndex++ } - if args.ExperienceLevel != nil { + if args.Account.ExperienceLevel != nil { query += fmt.Sprintf("experience_level = $%d, ", paramIndex) - params = append(params, *args.ExperienceLevel) + params = append(params, args.Account.ExperienceLevel) paramIndex++ } From 9f5ed89f3982600bbcf25d58777083bf2300edbb Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Wed, 30 Oct 2024 16:58:08 -0400 Subject: [PATCH 2/4] Doc update --- docs/docs.go | 58 ++++++++++++++++++++++++++++++++--------------- docs/swagger.json | 58 ++++++++++++++++++++++++++++++++--------------- docs/swagger.yaml | 41 +++++++++++++++++++++------------ 3 files changed, 107 insertions(+), 50 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index f205561..14f641b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -257,6 +257,36 @@ const docTemplate = `{ } } }, + "account.AccountParam": { + "description": "Structure for representing a player account with non-required fields.", + "type": "object", + "properties": { + "email": { + "description": "Email is the email address of the player.", + "type": "string" + }, + "experience_level": { + "description": "ExperienceLevel represents the player's experience level (0=beginner, 1=easy, 2=medium, 3=hard, 4=very hard, 5=impossible)", + "allOf": [ + { + "$ref": "#/definitions/account.ExperienceLevel" + } + ] + }, + "info": { + "description": "Info contains additional information about the player.", + "type": "string" + }, + "location": { + "description": "Location indicates the player's real-life location.", + "type": "string" + }, + "name": { + "description": "Name is the name of the player.", + "type": "string" + } + } + }, "account.CreateAccountArgs": { "description": "Structure for the account creation request payload.", "type": "object", @@ -308,28 +338,20 @@ const docTemplate = `{ "description": "Structure for the account update request payload.", "type": "object", "properties": { + "account": { + "description": "The account to create.", + "allOf": [ + { + "$ref": "#/definitions/account.AccountParam" + } + ] + }, "account_id": { "description": "The account ID for the account that will be updated.", "type": "integer" }, - "email": { - "description": "The new email for the account.", - "type": "string" - }, - "experience_level": { - "description": "The new experience level for the account.", - "type": "integer" - }, - "info": { - "description": "The new info for the account.", - "type": "string" - }, - "location": { - "description": "The new location for the account.", - "type": "string" - }, - "name": { - "description": "The new name for the account.", + "password": { + "description": "The password for the account to be created", "type": "string" } } diff --git a/docs/swagger.json b/docs/swagger.json index d3400bc..06a074a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -248,6 +248,36 @@ } } }, + "account.AccountParam": { + "description": "Structure for representing a player account with non-required fields.", + "type": "object", + "properties": { + "email": { + "description": "Email is the email address of the player.", + "type": "string" + }, + "experience_level": { + "description": "ExperienceLevel represents the player's experience level (0=beginner, 1=easy, 2=medium, 3=hard, 4=very hard, 5=impossible)", + "allOf": [ + { + "$ref": "#/definitions/account.ExperienceLevel" + } + ] + }, + "info": { + "description": "Info contains additional information about the player.", + "type": "string" + }, + "location": { + "description": "Location indicates the player's real-life location.", + "type": "string" + }, + "name": { + "description": "Name is the name of the player.", + "type": "string" + } + } + }, "account.CreateAccountArgs": { "description": "Structure for the account creation request payload.", "type": "object", @@ -299,28 +329,20 @@ "description": "Structure for the account update request payload.", "type": "object", "properties": { + "account": { + "description": "The account to create.", + "allOf": [ + { + "$ref": "#/definitions/account.AccountParam" + } + ] + }, "account_id": { "description": "The account ID for the account that will be updated.", "type": "integer" }, - "email": { - "description": "The new email for the account.", - "type": "string" - }, - "experience_level": { - "description": "The new experience level for the account.", - "type": "integer" - }, - "info": { - "description": "The new info for the account.", - "type": "string" - }, - "location": { - "description": "The new location for the account.", - "type": "string" - }, - "name": { - "description": "The new name for the account.", + "password": { + "description": "The password for the account to be created", "type": "string" } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a8b0184..8c33adb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -20,6 +20,27 @@ definitions: description: Name is the name of the player. type: string type: object + account.AccountParam: + description: Structure for representing a player account with non-required fields. + properties: + email: + description: Email is the email address of the player. + type: string + experience_level: + allOf: + - $ref: '#/definitions/account.ExperienceLevel' + description: ExperienceLevel represents the player's experience level (0=beginner, + 1=easy, 2=medium, 3=hard, 4=very hard, 5=impossible) + info: + description: Info contains additional information about the player. + type: string + location: + description: Location indicates the player's real-life location. + type: string + name: + description: Name is the name of the player. + type: string + type: object account.CreateAccountArgs: description: Structure for the account creation request payload. properties: @@ -57,23 +78,15 @@ definitions: account.UpdateAccountArgs: description: Structure for the account update request payload. properties: + account: + allOf: + - $ref: '#/definitions/account.AccountParam' + description: The account to create. account_id: description: The account ID for the account that will be updated. type: integer - email: - description: The new email for the account. - type: string - experience_level: - description: The new experience level for the account. - type: integer - info: - description: The new info for the account. - type: string - location: - description: The new location for the account. - type: string - name: - description: The new name for the account. + password: + description: The password for the account to be created type: string type: object game.CreateGameArgs: From a9c12613d2ebd062d8bed752f44891d14e8a7057 Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Thu, 31 Oct 2024 13:33:50 -0400 Subject: [PATCH 3/4] Seem to have fixed error with comparing passwords (and only allowed changes with password being the same) --- .vscode/settings.json | 1 + internal/account/create_account.go | 9 +++++---- internal/account/update_account.go | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b5b1ea..79548fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "gamemode", "gofmt", "Gopls", + "Hasher", "healthcheck", "healthgrpc", "Ninjaboy", diff --git a/internal/account/create_account.go b/internal/account/create_account.go index 59941f4..5010fdb 100644 --- a/internal/account/create_account.go +++ b/internal/account/create_account.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/rand" "database/sql" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -11,8 +12,6 @@ import ( "net/http" "net/mail" - "encoding/hex" - argon2 "golang.org/x/crypto/argon2" ) @@ -60,6 +59,8 @@ func isEmailValid(email string, db *sql.DB) (bool, error) { return true, nil } +var Hasher = NewArgon2idHash(1, 32, 64*1024, 32, 256) + // CreateAccount handles the creation of a new account. // // @Summary Create a new account @@ -120,7 +121,7 @@ func CreateAccount(w http.ResponseWriter, r *http.Request, db *sql.DB) error { return errors.New("the provided email is not valid") } - hashSalt, err := NewArgon2idHash(1, 32, 64*1024, 32, 256).GenerateHash([]byte(account.Password), nil) + hashSalt, err := Hasher.GenerateHash([]byte(account.Password), nil) if err != nil { log.Println("error saving a password: ", err.Error()) return errors.New("an error occurred while saving the password. Please try again later") @@ -229,7 +230,7 @@ func (a *Argon2idHash) Compare(hash, salt, password []byte) error { } func storeHashAndSalt(hashSalt *HashSalt, accountEmail string, db *sql.DB) error { - result, err := db.Query("INSERT INTO passwords (account_email, hash, salt) VALUES ($1, $2, $3)", accountEmail, hex.EncodeToString(hashSalt.hash), hex.EncodeToString(hashSalt.salt)) + result, err := db.Query("INSERT INTO passwords (account_email, hash, salt) VALUES ($1, $2, $3)", accountEmail, base64.StdEncoding.EncodeToString(hashSalt.hash), base64.StdEncoding.EncodeToString(hashSalt.salt)) if err != nil { return errors.New("an error occurred while inserting a hash-salt pair into the database: " + err.Error()) } diff --git a/internal/account/update_account.go b/internal/account/update_account.go index 81d5f14..12aa77c 100644 --- a/internal/account/update_account.go +++ b/internal/account/update_account.go @@ -2,6 +2,7 @@ package account import ( "database/sql" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -65,6 +66,31 @@ func UpdateAccount(w http.ResponseWriter, r *http.Request, db *sql.DB) error { return errors.New("account must be specified") } + // Get the current password hash and salt from the database + var storedHash, storedSalt string + err = db.QueryRow("SELECT hash, salt FROM passwords WHERE id = $1", args.AccountId).Scan(&storedHash, &storedSalt) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return fmt.Errorf("error retrieving account credentials: %v", err) + } + + storedHashBytes, err := base64.StdEncoding.DecodeString(storedHash) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return fmt.Errorf("error decoding stored hash: %v", err) + } + storedSaltBytes, err := base64.StdEncoding.DecodeString(storedSalt) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return fmt.Errorf("error decoding stored salt: %v", err) + } + + err = Hasher.Compare(storedHashBytes, storedSaltBytes, []byte(*args.Password)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return fmt.Errorf("error comparing passwords: %v", err) + } + // Use reflection to check if at least one field other than AccountId is set v := reflect.ValueOf(args) numFields := v.NumField() From 8083b31347a016b01fa7bc7c056838b393666b65 Mon Sep 17 00:00:00 2001 From: Justin Farrell Date: Thu, 31 Oct 2024 13:37:19 -0400 Subject: [PATCH 4/4] Adjusted README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63fdc10..0631968 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ You absolutely can, so long as you follow the license in `LICENSE.md`. I am real - [x] Accounts can be updated - [ ] Accounts can be deleted - [ ] Passwords can be reset -- [ ] Passwords can be compared to find if passwords are correct +- [x] Passwords can be compared to find if passwords are correct - [ ] Accounts can be logged into (and will provide a valid JWT token / session for future calls) [I need to research JWTs vs session-based auth for this task] - [ ] All account endpoints are rate-limited appropriately