feat: implemented PasswordResetBegin method in auth service with cooldown for each email being stored in redis
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user