// Copyright (c) 2025 Nikolai Papin // // This file is part of Easywish // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package services import ( "context" "easywish/config" "easywish/internal/database" "easywish/internal/dto" errs "easywish/internal/errors" "easywish/internal/models" "easywish/internal/utils" "easywish/internal/utils/enums" "errors" "fmt" "time" "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/jackc/pgerrcode" "github.com/jackc/pgx/v5" "go.uber.org/zap" ) type AuthService interface { RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) RegistrationComplete(cinfo dto.ClientInfo, request models.RegistrationCompleteRequest) (*models.RegistrationCompleteResponse, error) Login(cinfo dto.ClientInfo, request models.LoginRequest) (*models.LoginResponse, error) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) PasswordResetBegin(request models.PasswordResetBeginRequest) (bool, error) PasswordResetComplete(request models.PasswordResetCompleteRequest) (*models.PasswordResetCompleteResponse, error) ChangePassword(request models.ChangePasswordRequest, cinfo dto.ClientInfo) (bool, error) ValidateToken(token string, tokenType enums.JwtTokenType) (*dto.SessionInfo, error) } type authServiceImpl struct { log *zap.Logger dbctx database.DbContext redis *redis.Client smtp SmtpService } func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.Client, _smtp SmtpService) AuthService { authService := &authServiceImpl{log: _log, dbctx: _dbctx, redis: _redis, smtp: _smtp} // Cache terminated sessions // FIXME: review possible RAM overflow db := database.NewDbHelper(_dbctx) guids, err := db.Queries.GetUnexpiredTerminatedSessionsGuids(db.CTX) if err != nil { panic("Failed to load terminated sessions' GUIDs") } ctx := context.TODO() // FIXME: review possible problems due to a large pipeline request pipe := _redis.Pipeline() for _, guid := range guids { if err := pipe.Set(ctx, fmt.Sprintf("session::%s::is_terminated", guid), true, 0).Err(); err != nil { panic("Failed to cache terminated session: " + err.Error()) } } if _, err := pipe.Exec(ctx); err != nil { panic("Failed to execute redis pipeline request for caching terminated sessions: " + err.Error()) } _log.Info("Cached terminated sessions' GUIDs in Redis", zap.Int("amount", len(guids))) return authService } func (a *authServiceImpl) terminateAllSessionsForUser(ctx context.Context, username string, queries *database.Queries) error { sessionGuids, err := queries.TerminateAllSessionsForUserByUsername(ctx, username); if err != nil { a.log.Error( "Failed to terminate older sessions for user trying to log in", zap.String("username", username), zap.Error(err)) return err } pipe := a.redis.Pipeline() for _, guid := range sessionGuids { pipe.Set(ctx, fmt.Sprintf("session::%s::is_terminated", guid), true, time.Duration(8 * time.Hour)) // XXX: magic number } if _, err := pipe.Exec(ctx); err != nil { a.log.Error( "Failed to cache terminated sessions", zap.Error(err)) return err } return nil } func (a *authServiceImpl) registerSession(ctx context.Context, userID int64, cinfo dto.ClientInfo, queries *database.Queries) (*database.Session, error) { session, err := queries.CreateSession(ctx, database.CreateSessionParams{ UserID: userID, Name: utils.NewPointer(cinfo.UserAgent), Platform: utils.NewPointer(cinfo.UserAgent), LatestIp: utils.NewPointer(cinfo.IP), }); if err != nil { a.log.Error( "Failed to add session to database", zap.Error(err)) return nil, err } a.log.Info( "Registered a new user session", zap.String("username", cinfo.Username), zap.String("session", cinfo.Session)) return &session, nil } func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) { var occupationStatus database.CheckUserRegistrationAvailabilityRow var user database.User var generatedCode string var generatedCodeHash string var passwordHash string var err error helper, db, err := database.NewDbHelperTransaction(a.dbctx) if err != nil { a.log.Error( "Failed to open a transaction", zap.Error(err)) return false, errs.ErrServerError } defer helper.RollbackOnError(err) isInProgress, err := a.redis.Get( context.TODO(), fmt.Sprintf("email::%s::registration_in_progress", request.Email), ).Bool(); if err != nil { if err != redis.Nil { a.log.Error( "Failed to look up cached registration_in_progress state of email as part of registration procedure", zap.String("email", request.Email), zap.Error(err)) return false, errs.ErrServerError } isInProgress = false } else if isInProgress { a.log.Warn( "Attempted to begin registration on email that is in progress of registration or on cooldown", zap.String("email", request.Email)) return false, errs.ErrTooManyRequests } if occupationStatus, err = db.TXQueries.CheckUserRegistrationAvailability(db.CTX, database.CheckUserRegistrationAvailabilityParams{ Email: request.Email, Username: request.Username, }); err != nil { a.log.Error( "Failed to check credentials availability for registration", zap.String("username", request.Username), zap.String("email", request.Email), zap.Error(err)) return false, errs.ErrServerError } if occupationStatus.UsernameBusy { a.log.Warn( "Attempted registration for a taken username", zap.String("email", request.Email), zap.String("username", request.Username)) return false, errs.ErrUsernameTaken } else if occupationStatus.EmailBusy { // Falsely confirm in order to avoid disclosing registered email addresses if err := a.redis.Set( context.TODO(), fmt.Sprintf("email::%s::registration_in_progress", request.Email), true, time.Duration(10*time.Minute), // XXX: magic number ).Err(); err != nil { a.log.Error( "Failed to falsely set cache registration_in_progress state for email as a measure to prevent email enumeration", zap.String("email", request.Email), zap.Error(err)) return false, errs.ErrServerError } a.log.Warn( "Attempted registration for a taken email", zap.String("email", request.Email), zap.String("username", request.Username)) return true, nil } else { if _, err := db.TXQueries.DeleteUnverifiedAccountsHavingUsernameOrEmail(db.CTX, database.DeleteUnverifiedAccountsHavingUsernameOrEmailParams{ Username: request.Username, Email: request.Email, }); err != nil { a.log.Error( "Failed to purge unverified accounts as part of registration", zap.String("email", request.Email), zap.String("username", request.Username), zap.Error(err)) return false, errs.ErrServerError } } if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil { a.log.Error("Failed to add user to database", zap.Error(err)) return false, errs.ErrServerError } if passwordHash, err = utils.HashPassword(request.Password); err != nil { a.log.Error("Error on hashing password", zap.Error(err)) return false, errs.ErrServerError } if _, err = db.TXQueries.CreateLoginInformation(db.CTX, database.CreateLoginInformationParams{ UserID: user.ID, Email: utils.NewPointer(request.Email), PasswordHash: passwordHash, // Hashed in database }); err != nil { if errs.MatchPgError(err, pgerrcode.UniqueViolation) { // Since we've already checked for username previously, only email is left return false, errs.ErrEmailTaken } 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 generatedCodeHash, err = utils.HashPassword(generatedCode); err != nil { a.log.Error("Failed to hash generated verification code", zap.Error(err)) return false, errs.ErrServerError } if _, err = db.TXQueries.CreateConfirmationCode(db.CTX, database.CreateConfirmationCodeParams{ UserID: user.ID, CodeType: int32(enums.RegistrationCodeType), CodeHash: generatedCodeHash, // 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), zap.Int64("id", user.ID)) if config.GetConfig().SmtpEnabled { if err := a.smtp.SendEmail( request.Email, "Easywish", fmt.Sprintf("Your registration code is %s", generatedCode), ); err != nil { a.log.Error( "Failed to send registration email", zap.String("email", request.Email), zap.String("username", request.Username), zap.Error(err)) return false, errs.ErrServerError } } else { a.log.Debug( "Declared registration code for a new user. Enable SMTP in the config to disable this message", zap.String("username", user.Username), zap.String("code", generatedCode)) } if err := a.redis.Set( context.TODO(), fmt.Sprintf("email::%s::registration_in_progress", request.Email), true, time.Duration(10*time.Minute), // XXX: magic number ).Err(); err != nil { a.log.Error( "Failed to cache registration_in_progress state for email", zap.String("email", request.Email), zap.Error(err)) return false, errs.ErrServerError } if err = helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction", zap.Error(err)) return false, errs.ErrServerError } return true, nil } func (a *authServiceImpl) RegistrationComplete(cinfo dto.ClientInfo, request models.RegistrationCompleteRequest) (*models.RegistrationCompleteResponse, error) { var user database.User var profile database.Profile var confirmationCode database.ConfirmationCode var accessToken, refreshToken string var err error helper, db, err := database.NewDbHelperTransaction(a.dbctx) if err != nil { a.log.Error( "Failed to open a transaction", zap.Error(err)) return nil, errs.ErrServerError } defer helper.RollbackOnError(err) user, err = db.TXQueries.GetUserByUsername(db.CTX, request.Username) if err != nil { if errors.Is(err, pgx.ErrNoRows) { a.log.Warn( "Could not find user attempting to complete registration with given username", zap.String("username", request.Username), zap.Error(err)) return nil, errs.ErrUserNotFound } a.log.Error( "Failed to get user", zap.String("username", request.Username), zap.Error(err)) return nil, errs.ErrServerError } confirmationCode, err = db.TXQueries.GetValidConfirmationCodeByCode(db.CTX, database.GetValidConfirmationCodeByCodeParams{ UserID: user.ID, CodeType: int32(enums.RegistrationCodeType), Code: request.VerificationCode, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { a.log.Warn( "User supplied unexistent confirmation code for completing registration", zap.String("username", user.Username), zap.String("code", request.VerificationCode), zap.Error(err)) return nil, errs.ErrForbidden } a.log.Error( "Failed to acquire specified registration code", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{ ID: confirmationCode.ID, Used: utils.NewPointer(true), }) if err != nil { a.log.Error( "Failed to update the user's registration code used state", zap.String("username", user.Username), zap.Int64("confirmation_code_id", confirmationCode.ID), zap.Error(err)) return nil, errs.ErrServerError } err = db.TXQueries.UpdateUser(db.CTX, database.UpdateUserParams{ ID: user.ID, Verified: utils.NewPointer(true), }) if err != nil { a.log.Error("Failed to update verified status for user", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } profile, err = db.TXQueries.CreateProfile(db.CTX, database.CreateProfileParams{ UserID: user.ID, Name: request.Name, }) if err != nil { a.log.Error("Failed to create profile for user", zap.String("username", user.Username), ) return nil, errs.ErrServerError } _, err = db.TXQueries.CreateProfileSettings(db.CTX, profile.ID) if err != nil { a.log.Error("Failed to create profile settings for user", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } session, err := a.registerSession(context.TODO(), user.ID, cinfo, &db.TXQueries); if err != nil { a.log.Error("", zap.Error(err)) return nil, errs.ErrServerError } // TODO: get user role accessToken, refreshToken, err = utils.GenerateTokens(user.Username, session.Guid.String(), enums.UserRole) if err != nil { a.log.Error( "Failed to create tokens for newly registered user, rolling back registration", zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } if err = helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction", zap.Error(err)) return nil, errs.ErrServerError } a.log.Info( "User verified registration", zap.String("username", request.Username)) response := models.RegistrationCompleteResponse{Tokens: models.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, }} return &response, nil } // TODO: totp func (a *authServiceImpl) Login(cinfo dto.ClientInfo, request models.LoginRequest) (*models.LoginResponse, error) { var userRow database.GetValidUserByLoginCredentialsRow var err error helper, db, err := database.NewDbHelperTransaction(a.dbctx) if err != nil { a.log.Error( "Failed to open a transaction", zap.Error(err)) return nil, errs.ErrServerError } defer helper.RollbackOnError(err) userRow, err = db.TXQueries.GetValidUserByLoginCredentials(db.CTX, database.GetValidUserByLoginCredentialsParams{ Username: request.Username, Password: request.Password, }) if err != nil { a.log.Warn( "Failed login attempt", zap.Error(err)) var returnedError error switch err { case pgx.ErrNoRows: returnedError = errs.ErrForbidden default: returnedError = errs.ErrServerError } return nil, returnedError } // Until release 4, only 1 session at a time is supported err = a.terminateAllSessionsForUser(context.TODO(), request.Username, &db.TXQueries); if err != nil { a.log.Error( "Failed to terminate user's sessions during login", zap.Error(err)) return nil, errs.ErrServerError } session, err := a.registerSession(context.TODO(), userRow.ID, cinfo, &db.TXQueries); if err != nil { a.log.Error("", zap.Error(err)) return nil, errs.ErrServerError } // TODO: get user role accessToken, refreshToken, err := utils.GenerateTokens(userRow.Username, session.Guid.String(), enums.UserRole) if err != nil { a.log.Error( "Failed to generate tokens for a new login", zap.String("username", userRow.Username), zap.Error(err)) return nil, errs.ErrServerError } if err = helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction", zap.Error(err)) return nil, errs.ErrServerError } response := models.LoginResponse{Tokens: models.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, }} return &response, nil } func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) { sessionInfo, err := a.ValidateToken(request.RefreshToken, enums.JwtRefreshTokenType) if err != nil { if utils.ErrorIsOneOf( err, errs.ErrInvalidToken, errs.ErrTokenExpired, errs.ErrWrongTokenType, errs.ErrSessionNotFound, errs.ErrSessionTerminated, ) { return nil, err } else { a.log.Error( "Encountered an unexpected error while validating token", zap.Error(err)) return nil, errs.ErrServerError } } accessToken, refreshToken, err := utils.GenerateTokens( sessionInfo.Username, sessionInfo.Session, sessionInfo.Role, ) if err != nil { a.log.Error( "Failed to generate tokens for user during refresh", zap.String("username", sessionInfo.Username), zap.String("session", sessionInfo.Session), zap.Error(err)) return nil, errs.ErrServerError } response := models.RefreshResponse{ Tokens: models.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, }, } return &response, nil } func (a *authServiceImpl) ValidateToken(jwtToken string, tokenType enums.JwtTokenType) (*dto.SessionInfo, error) { var err error token, err := jwt.ParseWithClaims( jwtToken, &dto.UserClaims{}, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(config.GetConfig().JwtSecret), nil }, ) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return nil, errs.ErrTokenExpired } return nil, errs.ErrInvalidToken } claims, ok := token.Claims.(*dto.UserClaims) if ok && token.Valid { if claims.Type != tokenType { return nil, errs.ErrWrongTokenType } ctx := context.TODO() isTerminated, redisErr := a.redis.Get(ctx, fmt.Sprintf("session::%s::is_terminated", claims.Session)).Bool() if redisErr != nil && redisErr != redis.Nil { a.log.Error( "Failed to lookup cache to check whether session is not terminated", zap.Error(redisErr)) return nil, redisErr } // Cache if nil if redisErr == redis.Nil { db := database.NewDbHelper(a.dbctx) session, err := db.Queries.GetSessionByGuid(db.CTX, claims.Session) if err != nil { if errors.Is(err, pgx.ErrNoRows) { a.log.Warn( "Session does not exist or was deleted", zap.String("session", claims.Session)) return nil, errs.ErrSessionNotFound } a.log.Error( "Failed to lookup session in database", zap.String("session", claims.Session), zap.Error(err)) return nil, err } if err := a.redis.Set( ctx, fmt.Sprintf("session::%s::is_terminated", claims.Session), *session.Terminated, time.Duration(8*time.Hour), // XXX: magic number ).Err(); err != nil { a.log.Error( "Failed to cache session's is_terminated state", zap.String("session", claims.Session), zap.Error(err)) // c.AbortWithStatus(http.StatusInternalServerError) return nil, err } isTerminated = *session.Terminated } if isTerminated { return nil, errs.ErrSessionTerminated } } sessionInfo := dto.SessionInfo{ Username: claims.Username, Session: claims.Session, Role: claims.Role, } return &sessionInfo, nil } func (a *authServiceImpl) PasswordResetBegin(request models.PasswordResetBeginRequest) (bool, error) { var user database.User var generatedCode, hashedCode string var err error helper, db, err := database.NewDbHelperTransaction(a.dbctx) if err != nil { a.log.Error( "Failed to open a transaction", zap.Error(err)) return false, errs.ErrServerError } defer helper.RollbackOnError(err) ctx := context.TODO() cooldownTimeUnix, redisErr := a.redis.Get(ctx, fmt.Sprintf("email::%s::reset_cooldown", request.Email)).Int64() if redisErr != nil && redisErr != redis.Nil { a.log.Error( "Failed to get reset_cooldown state for user", zap.String("email", request.Email), zap.Error(redisErr)) return false, errs.ErrServerError } if time.Now().Unix() < cooldownTimeUnix { a.log.Warn( "Attempted to request a new password reset code for email on active reset cooldown", zap.String("email", request.Email)) return false, errs.ErrTooManyRequests } if user, err = db.TXQueries.GetUserByEmail(db.CTX, request.Email); err != nil { if errors.Is(err, pgx.ErrNoRows) { // Enable cooldown for the email despite that account does not exist err := a.redis.Set( ctx, fmt.Sprintf("email::%s::reset_cooldown", request.Email), time.Now().Add(10*time.Minute), time.Duration(10*time.Minute), ).Err() if err != nil { a.log.Error( "Failed to set reset cooldown for email", zap.Error(err)) return false, err } a.log.Warn( "Requested password reset email for unexistent user", zap.String("email", request.Email)) return true, nil } a.log.Error( "Failed to retrieve user from database", zap.String("email", request.Email), zap.Error(err)) return false, errs.ErrServerError } generatedCode = uuid.New().String() if hashedCode, err = utils.HashPassword(generatedCode); err != nil { a.log.Error( "Failed to hash password reset code for user", zap.String("username", user.Username), zap.Error(err)) return false, errs.ErrServerError } if _, err = db.TXQueries.CreateConfirmationCode(db.CTX, database.CreateConfirmationCodeParams{ UserID: user.ID, CodeType: int32(enums.PasswordResetCodeType), CodeHash: hashedCode, }); err != nil { a.log.Error( "Failed to save user password reset code to the database", zap.String("username", user.Username), zap.Error(err)) } err = a.redis.Set( ctx, fmt.Sprintf("email::%s::reset_cooldown", request.Email), time.Now().Add(10*time.Minute), time.Duration(10*time.Minute), ).Err() if err != nil { a.log.Error( "Failed to set reset cooldown for email. Cancelling password reset", zap.Error(err)) return false, err } if err = helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction", zap.Error(err)) return false, errs.ErrServerError } return true, nil } func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetCompleteRequest) (*models.PasswordResetCompleteResponse, error) { var resetCode database.ConfirmationCode var user database.User var session database.Session var hashedPassword, accessToken, refreshToken string var err error helper, db, err := database.NewDbHelperTransaction(a.dbctx) if err != nil { a.log.Error( "Failed to open a transaction", zap.Error(err)) return nil, errs.ErrServerError } if user, err = db.TXQueries.GetUserByEmail(db.CTX, request.Email); err != nil { if errors.Is(err, pgx.ErrNoRows) { a.log.Warn( "Attempted to complete password reset for unregistered email", zap.String("email", request.Email), zap.Error(err)) return nil, errs.ErrForbidden } a.log.Error( "Failed to look up user of email while trying to complete password reset", zap.String("email", request.Email), zap.Error(err)) return nil, errs.ErrServerError } if resetCode, err = db.TXQueries.GetValidConfirmationCodeByCode(db.CTX, database.GetValidConfirmationCodeByCodeParams{ UserID: user.ID, CodeType: int32(enums.PasswordResetCodeType), Code: request.VerificationCode, }); err != nil { if errors.Is(err, pgx.ErrNoRows) { a.log.Warn( "Attempted to reset password for user using incorrect confirmation code", zap.String("email", request.Email), zap.String("username", user.Username), zap.String("provided_code", request.VerificationCode), zap.Error(err)) return nil, errs.ErrForbidden } } if err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{ ID: resetCode.ID, Used: utils.NewPointer(true), }); err != nil { a.log.Error( "Failed to invalidate password reset code upon use", zap.String("username", user.Username), zap.String("email", request.Email), zap.Int64("code_id", resetCode.ID), zap.Error(err)) return nil, errs.ErrServerError } if hashedPassword, err = utils.HashPassword(request.NewPassword); err != nil { a.log.Error( "Failed to hash new password as part of user password reset", zap.String("email", request.Email), zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } if err = db.TXQueries.UpdateLoginInformationByUsername(db.CTX, database.UpdateLoginInformationByUsernameParams{ Username: user.Username, PasswordHash: hashedPassword, }); err != nil { a.log.Error( "Failed to save new password to database as part of user password reset", zap.String("username", user.Username), zap.String("email", request.Email), zap.Error(err)) } if request.LogOutSessions { err = a.terminateAllSessionsForUser(context.TODO(), user.Username, &db.TXQueries); if err != nil { a.log.Error( "Failed to terminate user's sessions during login", zap.Error(err)) return nil, errs.ErrServerError } } session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{ UserID: user.ID, Name: utils.NewPointer("First device"), Platform: utils.NewPointer("Unknown"), LatestIp: utils.NewPointer("Unknown"), }); if err != nil { a.log.Error( "Failed to create new session for user as part of user password reset", zap.String("email", request.Email), zap.String("username", user.Username), zap.Error(err)) } // TODO: get user role if accessToken, refreshToken, err = utils.GenerateTokens(user.Username, session.Guid.String(), enums.UserRole); err != nil { a.log.Error( "Failed to generate tokens as part of user password reset", zap.String("email", request.Email), zap.String("username", user.Username), zap.Error(err)) return nil, errs.ErrServerError } response := models.PasswordResetCompleteResponse{ Tokens: models.Tokens{ AccessToken: accessToken, RefreshToken: refreshToken, }, } if err = helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction", zap.Error(err)) return nil, errs.ErrServerError } return &response, nil } func (a *authServiceImpl) ChangePassword(request models.ChangePasswordRequest, uinfo dto.ClientInfo) (bool, error) { var err error helper, db, err := database.NewDbHelperTransaction(a.dbctx); if err != nil { a.log.Error( "Failed to open a transaction", zap.Error(err)) return false, errs.ErrServerError } defer helper.RollbackOnError(err) linfo, err := db.TXQueries.GetLoginInformationByUsername(db.CTX, uinfo.Username); if err != nil { a.log.Error( "Failed to get user login information", zap.Error(err)) return false, errs.ErrServerError } if !utils.CheckPasswordHash(request.OldPassword, linfo.PasswordHash) { a.log.Warn( "Provided invalid old password while changing password", zap.String("username", uinfo.Username)) return false, errs.ErrForbidden } newPasswordHash, err := utils.HashPassword(request.NewPassword); if err != nil { a.log.Error( "Failed to hash new password while changing password", zap.String("username", uinfo.Username), zap.Error(err)) return false, errs.ErrServerError } err = db.TXlessQueries.UpdateLoginInformationByUsername(db.CTX, database.UpdateLoginInformationByUsernameParams{ Username: uinfo.Username, PasswordHash: newPasswordHash, }); if err != nil { a.log.Error( "Failed to save new password into database", zap.String("username", uinfo.Username), zap.Error(err)) return false, errs.ErrServerError } if err := helper.Commit(); err != nil { a.log.Error( "Failed to commit transaction", zap.Error(err)) return false, errs.ErrServerError } return true, nil }