From 0a00a5ee2b9249ee9b2f6244e062ee9cf1e4067b Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Mon, 23 Jun 2025 16:23:46 +0300 Subject: [PATCH] feat: registrationBegin method without email; fix: missing sqlc query parameter name; feat: util for generating security codes; feat: enums package --- backend/internal/database/models.go | 52 +++++------ backend/internal/database/query.sql.go | 120 ++++++++++++------------- backend/internal/services/auth.go | 37 +++++++- backend/internal/utils/enums/enums.go | 8 ++ backend/internal/utils/securityCode.go | 26 ++++++ sqlc/query.sql | 4 +- sqlc/sqlc.yaml | 2 +- 7 files changed, 155 insertions(+), 94 deletions(-) create mode 100644 backend/internal/utils/enums/enums.go create mode 100644 backend/internal/utils/securityCode.go diff --git a/backend/internal/database/models.go b/backend/internal/database/models.go index 609d5b3..48d0f2d 100644 --- a/backend/internal/database/models.go +++ b/backend/internal/database/models.go @@ -12,11 +12,11 @@ type BannedUser struct { ID int64 UserID int64 Date pgtype.Timestamp - Reason pgtype.Text + Reason *string ExpiresAt pgtype.Timestamp - BannedBy pgtype.Text - Pardoned pgtype.Bool - PardonedBy pgtype.Text + BannedBy *string + Pardoned *bool + PardonedBy *string } type ConfirmationCode struct { @@ -25,17 +25,17 @@ type ConfirmationCode struct { CodeType int32 CodeHash string ExpiresAt pgtype.Timestamp - Used pgtype.Bool - Deleted pgtype.Bool + Used *bool + Deleted *bool } type LoginInformation struct { ID int64 UserID int64 - Email pgtype.Text + Email *string PasswordHash string - TotpEncrypted pgtype.Text - Email2faEnabled pgtype.Bool + TotpEncrypted *string + Email2faEnabled *bool PasswordChangeDate pgtype.Timestamp } @@ -43,41 +43,41 @@ type Profile struct { ID int64 UserID int64 Name string - Bio pgtype.Text - AvatarUrl pgtype.Text + Bio *string + AvatarUrl *string Birthday pgtype.Timestamp - Color pgtype.Text - ColorGrad pgtype.Text + Color *string + ColorGrad *string } type ProfileSetting struct { ID int64 ProfileID int64 - HideFulfilled pgtype.Bool - HideProfileDetails pgtype.Bool - HideForUnauthenticated pgtype.Bool - HideBirthday pgtype.Bool - HideDates pgtype.Bool - Captcha pgtype.Bool - FollowersOnlyInteraction pgtype.Bool + HideFulfilled *bool + HideProfileDetails *bool + HideForUnauthenticated *bool + HideBirthday *bool + HideDates *bool + Captcha *bool + FollowersOnlyInteraction *bool } type Session struct { ID int64 UserID int64 Guid pgtype.UUID - Name pgtype.Text - Platform pgtype.Text - LatestIp pgtype.Text + Name *string + Platform *string + LatestIp *string LoginTime pgtype.Timestamp LastSeenDate pgtype.Timestamp - Terminated pgtype.Bool + Terminated *bool } type User struct { ID int64 Username string - Verified pgtype.Bool + Verified *bool RegistrationDate pgtype.Timestamp - Deleted pgtype.Bool + Deleted *bool } diff --git a/backend/internal/database/query.sql.go b/backend/internal/database/query.sql.go index 953ac5f..59c27d2 100644 --- a/backend/internal/database/query.sql.go +++ b/backend/internal/database/query.sql.go @@ -19,8 +19,8 @@ VALUES ( $1, $2, $3, $4) RETURNING id, user_id, date, reason, expires_at, banned type CreateBannedUserParams struct { UserID int64 ExpiresAt pgtype.Timestamp - Reason pgtype.Text - BannedBy pgtype.Text + Reason *string + BannedBy *string } func (q *Queries) CreateBannedUser(ctx context.Context, arg CreateBannedUserParams) (BannedUser, error) { @@ -45,24 +45,18 @@ func (q *Queries) CreateBannedUser(ctx context.Context, arg CreateBannedUserPara } const createConfirmationCode = `-- name: CreateConfirmationCode :one -INSERT INTO confirmation_codes(user_id, code_type, code_hash, expires_at) -VALUES ($1, $2, crypt($3, gen_salt('bf')), $4) RETURNING id, user_id, code_type, code_hash, expires_at, used, deleted +INSERT INTO confirmation_codes(user_id, code_type, code_hash) +VALUES ($1, $2, crypt($3::text, gen_salt('bf'))) RETURNING id, user_id, code_type, code_hash, expires_at, used, deleted ` type CreateConfirmationCodeParams struct { - UserID int64 - CodeType int32 - Crypt string - ExpiresAt pgtype.Timestamp + UserID int64 + CodeType int32 + Code string } func (q *Queries) CreateConfirmationCode(ctx context.Context, arg CreateConfirmationCodeParams) (ConfirmationCode, error) { - row := q.db.QueryRow(ctx, createConfirmationCode, - arg.UserID, - arg.CodeType, - arg.Crypt, - arg.ExpiresAt, - ) + row := q.db.QueryRow(ctx, createConfirmationCode, arg.UserID, arg.CodeType, arg.Code) var i ConfirmationCode err := row.Scan( &i.ID, @@ -83,7 +77,7 @@ VALUES ( $1, $2, crypt($3::text, gen_salt('bf')) ) RETURNING id, user_id, email, type CreateLoginInformationParams struct { UserID int64 - Email pgtype.Text + Email *string Password string } @@ -110,11 +104,11 @@ VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, user_id, name, bio, avatar_url type CreateProfileParams struct { UserID int64 Name string - Bio pgtype.Text + Bio *string Birthday pgtype.Timestamp - AvatarUrl pgtype.Text - Color pgtype.Text - ColorGrad pgtype.Text + AvatarUrl *string + Color *string + ColorGrad *string } func (q *Queries) CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error) { @@ -170,9 +164,9 @@ VALUES ($1, $2, $3, $4) RETURNING id, user_id, guid, name, platform, latest_ip, type CreateSessionParams struct { UserID int64 - Name pgtype.Text - Platform pgtype.Text - LatestIp pgtype.Text + Name *string + Platform *string + LatestIp *string } func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { @@ -331,18 +325,18 @@ WHERE users.username = $1 AND ($2 IS FALSE OR profile_settings.hide_for_unauthen type GetProfileByUsernameRestrictedParams struct { Username string - Column2 pgtype.Bool + Column2 *bool } type GetProfileByUsernameRestrictedRow struct { Username string Name string Birthday pgtype.Timestamp - Bio pgtype.Text - AvatarUrl pgtype.Text - Color pgtype.Text - ColorGrad pgtype.Text - HideProfileDetails pgtype.Bool + Bio *string + AvatarUrl *string + Color *string + ColorGrad *string + HideProfileDetails *bool } func (q *Queries) GetProfileByUsernameRestricted(ctx context.Context, arg GetProfileByUsernameRestrictedParams) (GetProfileByUsernameRestrictedRow, error) { @@ -405,17 +399,17 @@ LIMIT 20 OFFSET 20 * $1 ` type GetProfilesRestrictedParams struct { - Column1 pgtype.Int4 - Column2 pgtype.Bool + Column1 *int32 + Column2 *bool } type GetProfilesRestrictedRow struct { Username string Name string - AvatarUrl pgtype.Text - Color pgtype.Text - ColorGrad pgtype.Text - HideProfileDetails pgtype.Bool + AvatarUrl *string + Color *string + ColorGrad *string + HideProfileDetails *bool } func (q *Queries) GetProfilesRestricted(ctx context.Context, arg GetProfilesRestrictedParams) ([]GetProfilesRestrictedRow, error) { @@ -558,7 +552,7 @@ type GetUserByLoginCredentialsRow struct { ID int64 Username string PasswordHash string - TotpEncrypted pgtype.Text + TotpEncrypted *string } func (q *Queries) GetUserByLoginCredentials(ctx context.Context, arg GetUserByLoginCredentialsParams) (GetUserByLoginCredentialsRow, error) { @@ -654,11 +648,11 @@ WHERE id = $1 type UpdateBannedUserParams struct { ID int64 - Reason pgtype.Text + Reason *string ExpiresAt pgtype.Timestamp - BannedBy pgtype.Text - Pardoned pgtype.Bool - PardonedBy pgtype.Text + BannedBy *string + Pardoned *bool + PardonedBy *string } func (q *Queries) UpdateBannedUser(ctx context.Context, arg UpdateBannedUserParams) error { @@ -681,8 +675,8 @@ WHERE id = $1 type UpdateConfirmationCodeParams struct { ID int64 - Used pgtype.Bool - Deleted pgtype.Bool + Used *bool + Deleted *bool } func (q *Queries) UpdateConfirmationCode(ctx context.Context, arg UpdateConfirmationCodeParams) error { @@ -699,10 +693,10 @@ WHERE users.username = $1 AND login_informations.user_id = users.id type UpdateLoginInformationByUsernameParams struct { Username string - Email pgtype.Text + Email *string Password string - TotpEncrypted pgtype.Text - Email2faEnabled pgtype.Bool + TotpEncrypted *string + Email2faEnabled *bool PasswordChangeDate pgtype.Timestamp } @@ -728,11 +722,11 @@ WHERE username = $1 type UpdateProfileByUsernameParams struct { Username string Name string - Bio pgtype.Text + Bio *string Birthday pgtype.Timestamp - AvatarUrl pgtype.Text - Color pgtype.Text - ColorGrad pgtype.Text + AvatarUrl *string + Color *string + ColorGrad *string } func (q *Queries) UpdateProfileByUsername(ctx context.Context, arg UpdateProfileByUsernameParams) error { @@ -763,13 +757,13 @@ WHERE id = $1 type UpdateProfileSettingsParams struct { ID int64 - HideFulfilled pgtype.Bool - HideProfileDetails pgtype.Bool - HideForUnauthenticated pgtype.Bool - HideBirthday pgtype.Bool - HideDates pgtype.Bool - Captcha pgtype.Bool - FollowersOnlyInteraction pgtype.Bool + HideFulfilled *bool + HideProfileDetails *bool + HideForUnauthenticated *bool + HideBirthday *bool + HideDates *bool + Captcha *bool + FollowersOnlyInteraction *bool } func (q *Queries) UpdateProfileSettings(ctx context.Context, arg UpdateProfileSettingsParams) error { @@ -794,12 +788,12 @@ WHERE id = $1 type UpdateSessionParams struct { ID int64 - Name pgtype.Text - Platform pgtype.Text - LatestIp pgtype.Text + Name *string + Platform *string + LatestIp *string LoginTime pgtype.Timestamp LastSeenDate pgtype.Timestamp - Terminated pgtype.Bool + Terminated *bool } func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) error { @@ -823,8 +817,8 @@ WHERE id = $1 type UpdateUserParams struct { ID int64 - Verified pgtype.Bool - Deleted pgtype.Bool + Verified *bool + Deleted *bool } func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error { @@ -840,8 +834,8 @@ WHERE username = $1 type UpdateUserByUsernameParams struct { Username string - Verified pgtype.Bool - Deleted pgtype.Bool + Verified *bool + Deleted *bool } func (q *Queries) UpdateUserByUsername(ctx context.Context, arg UpdateUserByUsernameParams) error { diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go index a91db04..bec05fd 100644 --- a/backend/internal/services/auth.go +++ b/backend/internal/services/auth.go @@ -5,6 +5,7 @@ import ( errs "easywish/internal/errors" "easywish/internal/models" "easywish/internal/utils" + "easywish/internal/utils/enums" "go.uber.org/zap" ) @@ -27,18 +28,50 @@ func NewAuthService(_log *zap.Logger, _dbctx database.DbContext) AuthService { func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) { + var user database.User + var generatedCode string + helper, db, _ := database.NewDbHelperTransaction(a.dbctx) + defer helper.Rollback() - user, err := db.TXQueries.CreateUser(db.CTX, request.Username) // TODO: validation + var err error - if err != nil { + if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil { // TODO: validation a.log.Error("Failed to add user to database", zap.Error(err)) return false, errs.ErrServerError } + + a.log.Info("Registraion of a new user", zap.String("username", user.Username), zap.Int64("id", user.ID)) + + if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{ + UserID: user.ID, + Email: request.Email, + Password: request.Password, // Hashed in database + }); err != nil { + a.log.Error("Failed to add login information for user to database", zap.Error(err)) + return false, errs.ErrServerError + } + + if generatedCode, err = utils.GenerateSecure6DigitNumber(); err != nil { + a.log.Error("Failed to generate a registration code", zap.Error(err)) + return false, errs.ErrServerError + } + + if _, err = db.TXQueries.CreateConfirmationCode(db.CTX, database.CreateConfirmationCodeParams{ + UserID: user.ID, + CodeType: int32(enums.RegistrationCodeType), + Code: generatedCode, // Hashed in database + }); err != nil { + a.log.Error("Failed to add registration code to database", zap.Error(err)) + return false, errs.ErrServerError + } + a.log.Info("Registered a new user", zap.String("username", user.Username)) helper.Commit() + a.log.Debug("Declated registration code for a new user", zap.String("username", user.Username), zap.String("code", generatedCode)) + // TODO: Send verification email return true, nil diff --git a/backend/internal/utils/enums/enums.go b/backend/internal/utils/enums/enums.go new file mode 100644 index 0000000..1861b2e --- /dev/null +++ b/backend/internal/utils/enums/enums.go @@ -0,0 +1,8 @@ +package enums + +type ConfirmationCodeType int32 + +const ( + RegistrationCodeType ConfirmationCodeType = iota + PasswordResetCodeType +) diff --git a/backend/internal/utils/securityCode.go b/backend/internal/utils/securityCode.go new file mode 100644 index 0000000..41a99e8 --- /dev/null +++ b/backend/internal/utils/securityCode.go @@ -0,0 +1,26 @@ +package utils + +import ( + "crypto/rand" + "fmt" + "io" +) + +func GenerateSecure6DigitNumber() (string, error) { + // Generate a random number between 0 and 999999 (inclusive) + // This ensures we get a 6-digit number, including those starting with 0 + max := 1000000 // Upper bound (exclusive) + b := make([]byte, 4) // A 4-byte slice is sufficient for a 32-bit integer + + if _, err := io.ReadFull(rand.Reader, b); err != nil { + return "", fmt.Errorf("failed to read random bytes: %w", err) + } + + // Convert bytes to an integer + // We use a simple modulo operation to get a number within our desired range. + // While this introduces a slight bias for very large ranges, for 1,000,000 + // it's negligible and simpler than more complex methods like rejection sampling. + num := int(uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])) % max + + return fmt.Sprintf("%06d", num), nil +} diff --git a/sqlc/query.sql b/sqlc/query.sql index 87fb266..f51def8 100644 --- a/sqlc/query.sql +++ b/sqlc/query.sql @@ -94,8 +94,8 @@ WHERE users.username = $1; --: Confirmation Code Object {{{ ;-- name: CreateConfirmationCode :one -INSERT INTO confirmation_codes(user_id, code_type, code_hash, expires_at) -VALUES ($1, $2, crypt($3, gen_salt('bf')), $4) RETURNING *; +INSERT INTO confirmation_codes(user_id, code_type, code_hash) +VALUES ($1, $2, crypt(@code::text, gen_salt('bf'))) RETURNING *; ;-- name: GetConfirmationCodeByCode :one SELECT * FROM confirmation_codes diff --git a/sqlc/sqlc.yaml b/sqlc/sqlc.yaml index eee19b1..a107d06 100644 --- a/sqlc/sqlc.yaml +++ b/sqlc/sqlc.yaml @@ -8,7 +8,7 @@ sql: out: "../backend/internal/database" sql_package: "pgx/v5" emit_prepared_queries: true - emit_interface: false + emit_pointers_for_null_types: true database: # managed: true uri: "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"