feat: implemented PasswordResetBegin method in auth service with cooldown for each email being stored in redis

This commit is contained in:
2025-07-12 19:32:53 +03:00
parent b91ff2c802
commit 8fa57eddb1
6 changed files with 140 additions and 10 deletions

View File

@@ -42,12 +42,12 @@ type AuthController interface {
}
type authControllerImpl struct {
authService services.AuthService
log *zap.Logger
auth services.AuthService
}
func NewAuthController(_log *zap.Logger, as services.AuthService) AuthController {
return &authControllerImpl{log: _log, authService: as}
func NewAuthController(_log *zap.Logger, _auth services.AuthService) AuthController {
return &authControllerImpl{log: _log, auth: _auth}
}
// @Summary Acquire tokens via login credentials (and 2FA code if needed)
@@ -65,7 +65,7 @@ func (a *authControllerImpl) Login(c *gin.Context) {
return
}
response, err := a.authService.Login(request.Body)
response, err := a.auth.Login(request.Body)
if err != nil {
if errors.Is(err, errs.ErrForbidden) {
@@ -134,7 +134,7 @@ func (a *authControllerImpl) RegistrationBegin(c *gin.Context) {
return
}
_, err := a.authService.RegistrationBegin(request.Body)
_, err := a.auth.RegistrationBegin(request.Body)
if err != nil {
if errors.Is(err, errs.ErrUsernameTaken) || errors.Is(err, errs.ErrEmailTaken) {
@@ -164,7 +164,7 @@ func (a *authControllerImpl) RegistrationComplete(c *gin.Context) {
return
}
response, err := a.authService.RegistrationComplete(request.Body)
response, err := a.auth.RegistrationComplete(request.Body)
if err != nil {
if errors.Is(err, errs.ErrForbidden) {

View File

@@ -25,4 +25,5 @@ var (
ErrNotImplemented = errors.New("Feature is not implemented")
ErrBadRequest = errors.New("Bad request")
ErrForbidden = errors.New("Access is denied")
ErrTooManyRequests = errors.New("Too many requests")
)

View File

@@ -18,6 +18,7 @@
package services
import (
"context"
"easywish/config"
"easywish/internal/database"
errs "easywish/internal/errors"
@@ -26,7 +27,10 @@ import (
"easywish/internal/utils/enums"
"errors"
"fmt"
"time"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5"
"go.uber.org/zap"
@@ -37,16 +41,19 @@ type AuthService interface {
RegistrationComplete(request models.RegistrationCompleteRequest) (*models.RegistrationCompleteResponse, error)
Login(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)
}
type authServiceImpl struct {
log *zap.Logger
smtp SmtpService
dbctx database.DbContext
redis *redis.Client
smtp SmtpService
}
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _smtp SmtpService) AuthService {
return &authServiceImpl{log: _log, dbctx: _dbctx, smtp: _smtp}
func NewAuthService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.Client, _smtp SmtpService) AuthService {
return &authServiceImpl{log: _log, dbctx: _dbctx, redis: _redis, smtp: _smtp}
}
func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequest) (bool, error) {
@@ -61,6 +68,9 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
var err error
// TODO: get user if it exists. If not verified and no valid code exists, delete
// and recreate
if user, err = db.TXQueries.CreateUser(db.CTX, request.Username); err != nil {
if errs.MatchPgError(err, pgerrcode.UniqueViolation) {
@@ -368,3 +378,106 @@ func (a *authServiceImpl) Login(request models.LoginRequest) (*models.LoginRespo
func (a *authServiceImpl) Refresh(request models.RefreshRequest) (*models.RefreshResponse, error) {
return nil, errs.ErrNotImplemented
}
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)
defer helper.Rollback()
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
} else if err == nil {
current_time := time.Now()
if current_time.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.TXlessQueries.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
}
helper.Commit()
return true, nil
}
func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetCompleteRequest) (*models.PasswordResetCompleteResponse, error) {
return nil, errs.ErrNotImplemented
}