package handlers import ( "net/http" "veza-backend-api/internal/core/auth" // Added import for authcore "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // RequestPasswordResetRequest represents a request to reset password // T0193: Request structure for password reset endpoint // MOD-P1-001: Ajout tags validate pour validation systématique type RequestPasswordResetRequest struct { Email string `json:"email" binding:"required,email" validate:"required,email"` } // RequestPasswordReset handles password reset request // T0193: Creates endpoint POST /api/v1/auth/password/reset-request // BE-SEC-013: Added audit logging for password reset requests func RequestPasswordReset( passwordResetService *services.PasswordResetService, passwordService *services.PasswordService, emailService *services.EmailService, auditService *services.AuditService, logger *zap.Logger, ) gin.HandlerFunc { return func(c *gin.Context) { commonHandler := NewCommonHandler(logger) var req RequestPasswordResetRequest if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Find user by email user, err := passwordService.GetUserByEmail(req.Email) if err != nil { // Always return success for security (prevent email enumeration) RespondSuccess(c, http.StatusOK, gin.H{"message": "If the email exists, a reset link has been sent"}) return } // Invalidate old tokens if err := passwordResetService.InvalidateOldTokens(user.ID); err != nil { logger.Error("Failed to invalidate old tokens", zap.String("user_id", user.ID.String()), zap.Error(err), ) // Continue anyway, not critical } // Generate token token, err := passwordResetService.GenerateToken() if err != nil { logger.Error("Failed to generate password reset token", zap.String("user_id", user.ID.String()), zap.Error(err), ) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"}) return } // Store token if err := passwordResetService.StoreToken(user.ID, token); err != nil { logger.Error("Failed to store password reset token", zap.String("user_id", user.ID.String()), zap.Error(err), ) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store token"}) return } // Send email if err := emailService.SendPasswordResetEmail(user.ID, user.Email, token); err != nil { // Log but don't fail - user should still get success message logger.Error("Failed to send password reset email", zap.String("user_id", user.ID.String()), zap.String("email", user.Email), zap.Error(err), ) } // BE-SEC-013: Log password reset request if auditService != nil { userID := user.ID if err := auditService.LogPasswordResetRequest(c.Request.Context(), &userID, user.Email, c.ClientIP(), c.GetHeader("User-Agent")); err != nil { logger.Warn("Failed to log password reset request", zap.String("user_id", user.ID.String()), zap.Error(err), ) } } // Always return generic success message for security RespondSuccess(c, http.StatusOK, gin.H{"message": "If the email exists, a reset link has been sent"}) } } // ResetPasswordRequest represents a request to complete password reset // T0194: Request structure for password reset completion // MOD-P1-001: Ajout tags validate pour validation systématique type ResetPasswordRequest struct { Token string `json:"token" binding:"required" validate:"required"` NewPassword string `json:"new_password" binding:"required,min=8" validate:"required,min=8"` } // ResetPassword handles password reset completion // T0194: Creates endpoint POST /api/v1/auth/password/reset // T0200: Uses AuthService.InvalidateAllUserSessions to invalidate sessions and update token_version // BE-SEC-013: Added audit logging for password reset completion func ResetPassword( passwordResetService *services.PasswordResetService, passwordService *services.PasswordService, authService *auth.AuthService, // Changed to *auth.AuthService sessionService *services.SessionService, auditService *services.AuditService, logger *zap.Logger, ) gin.HandlerFunc { return func(c *gin.Context) { commonHandler := NewCommonHandler(logger) var req ResetPasswordRequest if appErr := commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Verify token userID, err := passwordResetService.VerifyToken(req.Token) if err != nil { logger.Warn("Password reset token verification failed", zap.String("token", req.Token[:min(len(req.Token), 8)]+"..."), zap.Error(err), ) // BE-SEC-013: Log failed password reset attempt if auditService != nil { if err := auditService.LogPasswordReset(c.Request.Context(), uuid.Nil, false, c.ClientIP(), c.GetHeader("User-Agent")); err != nil { logger.Warn("Failed to log password reset failure", zap.Error(err)) } } c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired token"}) return } // Validate password strength if err := passwordService.ValidatePassword(req.NewPassword); err != nil { logger.Warn("Password validation failed", zap.String("user_id", userID.String()), zap.Error(err), ) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update password if err := passwordService.UpdatePassword(userID, req.NewPassword); err != nil { logger.Error("Failed to update password", zap.String("user_id", userID.String()), zap.Error(err), ) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update password"}) return } // Mark token as used if err := passwordResetService.MarkTokenAsUsed(req.Token); err != nil { // Log but don't fail - password is already updated logger.Warn("Failed to mark token as used", zap.String("user_id", userID.String()), zap.String("token", req.Token[:min(len(req.Token), 8)]+"..."), zap.Error(err), ) } // T0200: Invalidate all user sessions via AuthService // This updates token_version and revokes all sessions if authService != nil { err := authService.InvalidateAllUserSessions(c.Request.Context(), userID, sessionService) if err != nil { // Log but don't fail - password is already updated logger.Warn("Failed to invalidate user sessions", zap.String("user_id", userID.String()), zap.Error(err), ) } else { logger.Info("User sessions invalidated after password reset", zap.String("user_id", userID.String()), ) } } logger.Info("Password reset completed successfully", zap.String("user_id", userID.String()), ) // BE-SEC-013: Log successful password reset if auditService != nil { if err := auditService.LogPasswordReset(c.Request.Context(), userID, true, c.ClientIP(), c.GetHeader("User-Agent")); err != nil { logger.Warn("Failed to log password reset success", zap.String("user_id", userID.String()), zap.Error(err), ) } } RespondSuccess(c, http.StatusOK, gin.H{"message": "Password reset successfully"}) } } // min returns the minimum of two integers (helper function) func min(a, b int) int { if a < b { return a } return b }