veza/veza-backend-api/internal/handlers/password_reset_handler.go
okinrev b7955a680c P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

185 lines
5.7 KiB
Go

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"
"go.uber.org/zap"
)
// RequestPasswordResetRequest represents a request to reset password
// T0193: Request structure for password reset endpoint
type RequestPasswordResetRequest struct {
Email string `json:"email" binding:"required,email"`
}
// RequestPasswordReset handles password reset request
// T0193: Creates endpoint POST /api/v1/auth/password/reset-request
func RequestPasswordReset(
passwordResetService *services.PasswordResetService,
passwordService *services.PasswordService,
emailService *services.EmailService,
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)
c.JSON(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),
)
}
// Always return generic success message for security
c.JSON(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
type ResetPasswordRequest struct {
Token string `json:"token" binding:"required"`
NewPassword string `json:"new_password" binding:"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
func ResetPassword(
passwordResetService *services.PasswordResetService,
passwordService *services.PasswordService,
authService *auth.AuthService, // Changed to *auth.AuthService
sessionService *services.SessionService,
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),
)
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()),
)
c.JSON(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
}