veza/veza-backend-api/internal/core/auth/service.go

1023 lines
38 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
package auth
import (
"context"
"errors"
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 10:14:38 +00:00
"fmt"
"os"
2025-12-03 19:29:37 +00:00
"strings"
"time"
"veza-backend-api/internal/models"
2025-12-16 18:34:08 +00:00
"veza-backend-api/internal/monitoring"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/services" // Added import for services
2025-12-16 18:34:08 +00:00
"github.com/google/uuid"
2025-12-03 19:29:37 +00:00
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"veza-backend-api/internal/validators" // Import the validators package
)
type AuthService struct {
db *gorm.DB
logger *zap.Logger
JWTService services.JWTServiceInterface
emailVerificationService services.EmailVerificationServiceInterface
refreshTokenService services.RefreshTokenServiceInterface
passwordResetService services.PasswordResetServiceInterface
2025-12-03 19:29:37 +00:00
emailValidator *validators.EmailValidator
passwordValidator *validators.PasswordValidator
passwordService services.PasswordServiceInterface
emailService services.EmailServiceInterface
jobWorker services.JobWorkerInterface
accountLockoutService *services.AccountLockoutService
refreshLock *services.RefreshLock // Redis lock for concurrent refresh token requests
2025-12-03 19:29:37 +00:00
}
func NewAuthService(
db *gorm.DB,
emailValidator *validators.EmailValidator,
passwordValidator *validators.PasswordValidator,
passwordService services.PasswordServiceInterface,
jwtService services.JWTServiceInterface,
refreshTokenService services.RefreshTokenServiceInterface,
emailVerificationService services.EmailVerificationServiceInterface,
passwordResetService services.PasswordResetServiceInterface,
emailService services.EmailServiceInterface,
jobWorker services.JobWorkerInterface,
refreshLock *services.RefreshLock,
2025-12-03 19:29:37 +00:00
logger *zap.Logger,
) *AuthService {
return &AuthService{
db: db,
logger: logger,
JWTService: jwtService,
emailVerificationService: emailVerificationService,
refreshTokenService: refreshTokenService,
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 10:14:38 +00:00
passwordResetService: passwordResetService,
2025-12-03 19:29:37 +00:00
emailValidator: emailValidator,
passwordValidator: passwordValidator,
passwordService: passwordService,
emailService: emailService,
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 10:14:38 +00:00
jobWorker: jobWorker,
accountLockoutService: nil,
refreshLock: refreshLock,
2025-12-03 19:29:37 +00:00
}
}
// SetAccountLockoutService définit le service de verrouillage de compte
// BE-SEC-007: Implement account lockout after failed login attempts
func (s *AuthService) SetAccountLockoutService(lockoutService *services.AccountLockoutService) {
s.accountLockoutService = lockoutService
}
2026-02-07 19:36:48 +00:00
// UnlockAccount déverrouille un compte (appelé par l'admin). No-op si le service de lockout est nil.
func (s *AuthService) UnlockAccount(ctx context.Context, email string) error {
if s.accountLockoutService == nil {
return nil
}
return s.accountLockoutService.UnlockAccount(ctx, email)
}
2025-12-03 19:29:37 +00:00
// GetUserByUsername récupère un utilisateur par son nom d'utilisateur
func (s *AuthService) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
var user models.User
if err := s.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
// FIX #10: Logger l'erreur avec contexte
s.logger.Error("Failed to get user by username",
zap.String("username", username),
zap.Error(err),
)
2025-12-03 19:29:37 +00:00
return nil, err
}
return &user, nil
}
// Refresh est un alias pour RefreshToken
func (s *AuthService) Refresh(ctx context.Context, refreshToken string) (*models.TokenPair, error) {
return s.RefreshToken(ctx, refreshToken)
}
func (s *AuthService) Register(ctx context.Context, email, username, password string) (*models.User, *models.TokenPair, error) {
// FIX #5: Remplacer fmt.Print* par logs structurés
s.logger.Debug("Registration started",
zap.String("email", email),
zap.String("username", username),
)
defer func() {
if r := recover(); r != nil {
s.logger.Error("PANIC in Register", zap.Any("panic", r))
panic(r) // Re-panic pour que le middleware Recovery le capture
}
}()
2025-12-03 19:29:37 +00:00
// Valider l'email
s.logger.Debug("Validating email", zap.String("email", email))
if err := s.emailValidator.Validate(email); err != nil {
s.logger.Warn("Registration failed: invalid email", zap.String("email", email), zap.Error(err))
// Utiliser le sentinel error pour que IsInvalidEmail() le détecte
if strings.Contains(err.Error(), "already exists") {
return nil, nil, services.ErrUserAlreadyExists
}
return nil, nil, fmt.Errorf("%w: %v", services.ErrInvalidEmail, err)
}
2025-12-03 19:29:37 +00:00
// Vérifier si le username existe déjà
s.logger.Debug("Checking username uniqueness", zap.String("username", username))
var usernameCount int64
if err := s.db.WithContext(ctx).Model(&models.User{}).Where("LOWER(username) = LOWER(?)", username).Count(&usernameCount).Error; err != nil {
s.logger.Error("Failed to check username uniqueness", zap.String("username", username), zap.Error(err))
return nil, nil, fmt.Errorf("failed to check username: %w", err)
}
if usernameCount > 0 {
s.logger.Warn("Registration failed: username already exists", zap.String("username", username))
return nil, nil, services.ErrUserAlreadyExists
}
2025-12-03 19:29:37 +00:00
// Valider le mot de passe
s.logger.Debug("Validating password", zap.String("email", email))
2025-12-03 19:29:37 +00:00
passwordStrength, err := s.passwordValidator.Validate(password)
if err != nil {
s.logger.Warn("Registration failed: weak password", zap.String("email", email), zap.Error(err))
return nil, nil, fmt.Errorf("%w: %v", services.ErrWeakPassword, err)
}
if !passwordStrength.Valid {
s.logger.Warn("Registration failed: weak password", zap.String("email", email), zap.Any("details", passwordStrength.Details))
details := strings.Join(passwordStrength.Details, ", ")
return nil, nil, fmt.Errorf("%w: %s", services.ErrWeakPassword, details)
}
2025-12-03 19:29:37 +00:00
// Hacher le mot de passe
s.logger.Debug("Hashing password", zap.String("email", email))
fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files CRITICAL fixes: - Race condition (TOCTOU) in payout/refund with SELECT FOR UPDATE (CRITICAL-001/002) - IDOR on analytics endpoint — ownership check enforced (CRITICAL-003) - CSWSH on all WebSocket endpoints — origin whitelist (CRITICAL-004) - Mass assignment on user self-update — strip privileged fields (CRITICAL-005) HIGH fixes: - Path traversal in marketplace upload — UUID filenames (HIGH-001) - IP spoofing — use Gin trusted proxy c.ClientIP() (HIGH-002) - Popularity metrics (followers, likes) set to json:"-" (HIGH-003) - bcrypt cost hardened to 12 everywhere (HIGH-004) - Refresh token lock made mandatory (HIGH-005) - Stream token replay prevention with access_count (HIGH-006) - Subscription trial race condition fixed (HIGH-007) - License download expiration check (HIGH-008) - Webhook amount validation (HIGH-009) - pprof endpoint removed from production (HIGH-010) MEDIUM fixes: - WebSocket message size limit 64KB (MEDIUM-010) - HSTS header in nginx production (MEDIUM-001) - CORS origin restricted in nginx-rtmp (MEDIUM-002) - Docker alpine pinned to 3.21 (MEDIUM-003/004) - Redis authentication enforced (MEDIUM-005) - GDPR account deletion expanded (MEDIUM-006) - .gitignore hardened (MEDIUM-007) LOW/INFO fixes: - GitHub Actions SHA pinning on all workflows (LOW-001) - .env.example security documentation (INFO-001) - Production CORS set to HTTPS (LOW-002) All tests pass. Go and Rust compile clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:44:46 +00:00
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12 /* SECURITY(REM-016): Explicit cost 12, aligned with password_service.go */)
if err != nil {
s.logger.Error("Failed to hash password", zap.Error(err))
return nil, nil, err
}
2025-12-03 19:29:37 +00:00
// Générer un slug unique à partir du username
// Le slug doit être unique, donc on vérifie et on ajoute un suffixe si nécessaire
s.logger.Debug("Generating slug", zap.String("username", username))
baseSlug := strings.ToLower(username)
slug := baseSlug
counter := 1
for {
var count int64
err := s.db.WithContext(ctx).Model(&models.User{}).Where("slug = ?", slug).Count(&count).Error
if err != nil {
s.logger.Error("Failed to check slug uniqueness", zap.String("slug", slug), zap.Error(err))
return nil, nil, err
}
if count == 0 {
break
}
slug = fmt.Sprintf("%s%d", baseSlug, counter)
counter++
if counter > 1000 {
// Fallback: utiliser un timestamp si trop de collisions
slug = fmt.Sprintf("user_%d", time.Now().Unix())
break
}
}
s.logger.Debug("Slug generated", zap.String("slug", slug), zap.String("username", username))
2025-12-03 19:29:37 +00:00
// Créer l'utilisateur dans la base de données
// IMPORTANT: Initialiser explicitement tous les champs NOT NULL pour éviter les erreurs de contrainte
s.logger.Debug("Creating user object", zap.String("email", email), zap.String("username", username))
now := time.Now()
2025-12-03 19:29:37 +00:00
user := &models.User{
ID: uuid.New(), // Générer un nouvel UUID
Email: email,
Username: username,
Slug: slug,
2025-12-03 19:29:37 +00:00
PasswordHash: string(hashedPassword),
Role: "user", // Valeur par défaut (doit correspondre à l'ENUM PostgreSQL)
IsActive: true, // Valeur par défaut
IsVerified: true, // MVP: Auto-verify email pour permettre login immédiat
IsBanned: false, // Valeur par défaut (required NOT NULL field)
TokenVersion: 0, // Valeur par défaut (required NOT NULL field)
LoginCount: 0, // Valeur par défaut (required NOT NULL field)
CreatedAt: now, // Explicitement défini pour éviter les problèmes GORM
UpdatedAt: now, // Explicitement défini pour éviter les problèmes GORM
2025-12-03 19:29:37 +00:00
}
s.logger.Debug("User object created",
zap.String("user_id", user.ID.String()),
zap.String("email", user.Email),
zap.String("username", user.Username),
zap.String("role", user.Role),
zap.Bool("is_banned", user.IsBanned),
zap.Int("login_count", user.LoginCount),
)
// Log les valeurs avant insertion pour diagnostic
s.logger.Info("Creating user with values",
zap.String("email", email),
zap.String("username", username),
zap.Bool("is_banned", user.IsBanned),
zap.Int("login_count", user.LoginCount),
zap.Int("token_version", user.TokenVersion),
zap.Bool("is_active", user.IsActive),
zap.Bool("is_verified", user.IsVerified),
zap.String("role", user.Role),
zap.String("slug", user.Slug),
zap.String("user_id", user.ID.String()),
)
// Utiliser GORM Create - GORM gère automatiquement les placeholders PostgreSQL
// Tous les champs NOT NULL sont maintenant explicitement initialisés
// IMPORTANT: Utiliser Omit pour exclure les relations qui pourraient causer des problèmes
s.logger.Debug("Inserting user in database",
zap.String("user_id", user.ID.String()),
zap.String("email", user.Email),
zap.String("username", user.Username),
zap.String("role", user.Role),
zap.Bool("is_active", user.IsActive),
zap.Bool("is_verified", user.IsVerified),
zap.Bool("is_banned", user.IsBanned),
zap.Int("token_version", user.TokenVersion),
zap.Int("login_count", user.LoginCount),
)
result := s.db.WithContext(ctx).Omit("Roles", "TrackLikes").Create(user)
if result.Error != nil {
// Log l'erreur complète pour diagnostic
err := result.Error
errMsg := err.Error()
errType := fmt.Sprintf("%T", err)
s.logger.Error("Failed to create user in database - FULL ERROR DETAILS",
zap.Error(err),
zap.String("error_type", errType),
zap.String("error_string", errMsg),
zap.String("email", email),
zap.String("username", username),
zap.String("slug", slug),
zap.String("role", user.Role),
zap.String("user_id", user.ID.String()),
zap.Any("user_fields", map[string]interface{}{
"is_banned": user.IsBanned,
"login_count": user.LoginCount,
"token_version": user.TokenVersion,
"is_active": user.IsActive,
"is_verified": user.IsVerified,
"role": user.Role,
"slug": user.Slug,
}),
)
2025-12-03 19:29:37 +00:00
// Vérifier les erreurs PostgreSQL spécifiques
// Contrainte CHECK
if strings.Contains(errMsg, "violates check constraint") {
if strings.Contains(errMsg, "chk_users_username_format") {
s.logger.Warn("Registration failed: username format invalid", zap.String("username", username))
return nil, nil, errors.New("username format invalid: must be 3-30 alphanumeric characters")
}
if strings.Contains(errMsg, "chk_users_email_format") {
s.logger.Warn("Registration failed: email format invalid", zap.String("email", email))
return nil, nil, errors.New("email format invalid")
}
// Autre contrainte CHECK
s.logger.Warn("Registration failed: check constraint violation", zap.Error(err))
return nil, nil, fmt.Errorf("validation failed: %w", err)
}
// Type ENUM manquant ou valeur invalide
if strings.Contains(errMsg, "does not exist") && strings.Contains(errMsg, "user_role") {
s.logger.Error("Registration failed: user_role enum missing from database")
return nil, nil, fmt.Errorf("database schema error: user_role enum missing - run migrations")
}
// Erreur de valeur ENUM invalide
if strings.Contains(errMsg, "invalid input value for enum") || strings.Contains(errMsg, "invalid input syntax for type user_role") {
s.logger.Error("Registration failed: invalid role value for enum",
zap.String("role", user.Role),
zap.Error(err))
return nil, nil, fmt.Errorf("invalid role value '%s' for enum user_role: %w", user.Role, err)
}
// Timeout
if strings.Contains(errMsg, "context deadline exceeded") || strings.Contains(errMsg, "timeout") {
s.logger.Warn("Registration failed: database operation timed out")
return nil, nil, fmt.Errorf("database operation timed out: %w", err)
}
// PostgreSQL error code 23505 is unique_violation
// We check for specific constraint names if possible, or fallback to generic "duplicate"
if strings.Contains(errMsg, "users_email_key") || strings.Contains(errMsg, "idx_users_email") {
2025-12-03 19:29:37 +00:00
s.logger.Warn("Registration failed: email already exists", zap.String("email", email))
return nil, nil, services.ErrUserAlreadyExists
2025-12-03 19:29:37 +00:00
}
if strings.Contains(errMsg, "users_username_key") || strings.Contains(errMsg, "idx_users_username") {
s.logger.Warn("Registration failed: username already exists", zap.String("username", username))
return nil, nil, services.ErrUserAlreadyExists
}
if strings.Contains(errMsg, "users_slug_key") || strings.Contains(errMsg, "idx_users_slug") {
s.logger.Warn("Registration failed: slug collision", zap.String("slug", user.Slug))
return nil, nil, errors.New("username unavailable (slug collision)")
}
2025-12-13 02:34:34 +00:00
// Fallback for generic unique constraint
if strings.Contains(errMsg, "unique constraint") || strings.Contains(errMsg, "duplicate key") {
s.logger.Warn("Registration failed: unique constraint violation", zap.Error(err))
return nil, nil, services.ErrUserAlreadyExists
}
2025-12-13 02:34:34 +00:00
// Pour toutes les autres erreurs, retourner l'erreur originale avec contexte
// IMPORTANT: Inclure l'erreur complète pour diagnostic
s.logger.Error("Registration failed: unknown database error",
zap.Error(err),
zap.String("error_type", errType),
zap.String("error_string", errMsg),
)
return nil, nil, fmt.Errorf("database error [%s]: %w", errType, err)
2025-12-03 19:29:37 +00:00
}
s.logger.Debug("User inserted successfully",
zap.String("user_id", user.ID.String()),
zap.Int64("rows_affected", result.RowsAffected),
)
2025-12-03 19:29:37 +00:00
// C1-07: Auto-init storage quota for new users
quota := &models.StorageQuota{
UserID: user.ID,
MaxBytes: 5 * 1024 * 1024 * 1024, // 5GB
UsedBytes: 0,
}
if err := s.db.WithContext(ctx).Create(quota).Error; err != nil {
s.logger.Warn("Failed to init storage quota",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
}
// Générer le token de vérification d'email (non-bloquant)
// Si la génération échoue, on continue quand même avec l'inscription
// L'utilisateur pourra demander un nouveau token plus tard
if s.emailVerificationService != nil {
token, err := s.emailVerificationService.GenerateToken()
if err != nil {
s.logger.Warn("Failed to generate email verification token (non-blocking)", zap.Error(err))
} else {
// Stocker le token
if err := s.emailVerificationService.StoreToken(user.ID, user.Email, token); err != nil {
s.logger.Warn("Failed to store email verification token (non-blocking)", zap.Error(err))
} else {
// Envoyer l'email de vérification (simulation pour l'instant)
s.logger.Info("Sending verification email",
zap.String("email", user.Email),
zap.String("token", token),
zap.String("user_id", user.ID.String()))
}
}
} else {
s.logger.Warn("Email verification service not available - skipping token generation")
2025-12-03 19:29:37 +00:00
}
s.logger.Info("User registered successfully", zap.String("user_id", user.ID.String()))
s.logger.Debug("Generating tokens", zap.String("user_id", user.ID.String()))
2025-12-16 18:34:08 +00:00
// MVP: Générer les tokens JWT pour permettre l'authentification immédiate
if s.JWTService == nil {
s.logger.Error("JWTService is nil - cannot generate tokens")
return nil, nil, fmt.Errorf("JWT service not available")
}
accessToken, err := s.JWTService.GenerateAccessToken(user)
if err != nil {
s.logger.Error("Failed to generate access token after registration", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to generate access token: %w", err)
}
s.logger.Debug("Access token generated", zap.String("user_id", user.ID.String()))
refreshToken, err := s.JWTService.GenerateRefreshToken(user)
if err != nil {
s.logger.Error("Failed to generate refresh token after registration", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
s.logger.Debug("Refresh token generated", zap.String("user_id", user.ID.String()))
// Stocker le refresh token en base
s.logger.Debug("Storing refresh token", zap.String("user_id", user.ID.String()))
refreshTokenTTL := s.JWTService.GetConfig().RefreshTokenTTL
if s.refreshTokenService != nil {
if err := s.refreshTokenService.Store(user.ID, refreshToken, refreshTokenTTL); err != nil {
s.logger.Error("Failed to store refresh token after registration", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to store refresh token: %w", err)
}
s.logger.Debug("Refresh token stored", zap.String("user_id", user.ID.String()))
} else {
s.logger.Warn("Refresh token service not available - skipping token storage", zap.String("user_id", user.ID.String()))
s.logger.Warn("Refresh token service not available - skipping token storage")
}
2025-12-16 18:34:08 +00:00
// MOD-P2-003: Enregistrer la métrique business
monitoring.RecordUserRegistered()
tokenPair := &models.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int(s.JWTService.GetConfig().AccessTokenTTL.Seconds()),
}
s.logger.Info("Registration completed successfully",
zap.String("user_id", user.ID.String()),
zap.String("email", email),
zap.String("username", username),
)
return user, tokenPair, nil
2025-12-03 19:29:37 +00:00
}
func (s *AuthService) Login(ctx context.Context, email, password string, rememberMe bool) (*models.User, *models.TokenPair, error) {
s.logger.Info("Attempting login", zap.String("email", email))
// BE-SEC-007: Check if account is locked
if s.accountLockoutService != nil {
locked, lockedUntil, err := s.accountLockoutService.IsAccountLocked(ctx, email)
if err != nil {
s.logger.Warn("Failed to check account lockout status",
zap.String("email", email),
zap.Error(err))
// Fail-secure: treat as locked when check fails (e.g. Redis down)
return nil, nil, errors.New("account verification temporarily unavailable. Please try again later.")
}
if locked {
if lockedUntil != nil {
s.logger.Warn("Login blocked: account is locked",
zap.String("email", email),
zap.Time("locked_until", *lockedUntil))
} else {
s.logger.Warn("Login blocked: account is locked", zap.String("email", email))
}
return nil, nil, errors.New("account is locked due to too many failed login attempts. Please try again later.")
}
}
2025-12-03 19:29:37 +00:00
var user models.User
if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
s.logger.Warn("Login failed: user not found", zap.String("email", email))
// BE-SEC-007: Record failed attempt even if user not found (to prevent email enumeration)
if s.accountLockoutService != nil {
if err := s.accountLockoutService.RecordFailedAttempt(ctx, email); err != nil {
s.logger.Warn("Failed to record failed login attempt",
zap.String("email", email),
zap.Error(err))
}
}
2025-12-03 19:29:37 +00:00
return nil, nil, errors.New("invalid credentials")
}
s.logger.Error("Database error during login", zap.Error(err), zap.String("email", email))
2025-12-03 19:29:37 +00:00
return nil, nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
s.logger.Warn("Login failed: invalid password", zap.String("email", email))
// BE-SEC-007: Record failed attempt
if s.accountLockoutService != nil {
if err := s.accountLockoutService.RecordFailedAttempt(ctx, email); err != nil {
s.logger.Warn("Failed to record failed login attempt",
zap.String("email", email),
zap.Error(err))
}
}
2025-12-03 19:29:37 +00:00
return nil, nil, errors.New("invalid credentials")
}
if !user.IsVerified {
s.logger.Warn("Login failed: email not verified", zap.String("email", email))
// BE-SEC-007: Record failed attempt (email not verified is also a failed attempt)
if s.accountLockoutService != nil {
if err := s.accountLockoutService.RecordFailedAttempt(ctx, email); err != nil {
s.logger.Warn("Failed to record failed login attempt",
zap.String("email", email),
zap.Error(err))
}
}
2025-12-03 19:29:37 +00:00
return nil, nil, errors.New("email not verified")
}
// BE-SEC-007: Record successful login (reset failed attempts counter)
if s.accountLockoutService != nil {
if err := s.accountLockoutService.RecordSuccessfulLogin(ctx, email); err != nil {
s.logger.Warn("Failed to record successful login",
zap.String("email", email),
zap.Error(err))
// Non-critique, on continue
}
}
2025-12-03 19:29:37 +00:00
// Générer les tokens JWT
accessToken, err := s.JWTService.GenerateAccessToken(&user)
if err != nil {
s.logger.Error("Failed to generate access token", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to generate access token: %w", err)
2025-12-03 19:29:37 +00:00
}
refreshTokenTTL := s.JWTService.GetConfig().RefreshTokenTTL
2025-12-03 19:29:37 +00:00
if rememberMe {
refreshTokenTTL = s.JWTService.GetConfig().RememberMeRefreshTokenTTL // Assurez-vous que ce champ existe dans models.JWTConfig
2025-12-03 19:29:37 +00:00
}
refreshToken, err := s.JWTService.GenerateRefreshToken(&user)
if err != nil {
s.logger.Error("Failed to generate refresh token", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to generate refresh token: %w", err)
2025-12-03 19:29:37 +00:00
}
// Stocker le refresh token en base
if err := s.refreshTokenService.Store(user.ID, refreshToken, refreshTokenTTL); err != nil {
s.logger.Error("Failed to store refresh token", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to store refresh token: %w", err)
2025-12-03 19:29:37 +00:00
}
s.logger.Info("User logged in successfully", zap.String("user_id", user.ID.String()))
return &user, &models.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int(s.JWTService.GetConfig().AccessTokenTTL.Seconds()),
2025-12-03 19:29:37 +00:00
}, nil
}
// LoginWith2FA validates email/password, verifies the TOTP code, then generates and returns tokens.
// Used when the user has already been told requires_2fa from POST /auth/login and completes 2FA via POST /auth/login/2fa.
func (s *AuthService) LoginWith2FA(ctx context.Context, email, password, code string, rememberMe bool, twoFactorService *services.TwoFactorService) (*models.User, *models.TokenPair, error) {
s.logger.Info("Attempting login with 2FA", zap.String("email", email))
if s.accountLockoutService != nil {
locked, lockedUntil, err := s.accountLockoutService.IsAccountLocked(ctx, email)
if err != nil {
s.logger.Warn("Failed to check account lockout status", zap.String("email", email), zap.Error(err))
} else if locked {
if lockedUntil != nil {
remaining := time.Until(*lockedUntil)
s.logger.Warn("Login blocked: account is locked", zap.String("email", email), zap.Time("locked_until", *lockedUntil), zap.Duration("remaining", remaining))
return nil, nil, fmt.Errorf("account is locked. Please try again after %v", remaining.Round(time.Minute))
}
return nil, nil, errors.New("account is locked due to too many failed login attempts")
}
}
var user models.User
if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
s.logger.Warn("LoginWith2FA failed: user not found", zap.String("email", email))
if s.accountLockoutService != nil {
_ = s.accountLockoutService.RecordFailedAttempt(ctx, email)
}
return nil, nil, errors.New("invalid credentials")
}
s.logger.Error("Database error during login with 2FA", zap.Error(err), zap.String("email", email))
return nil, nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
s.logger.Warn("LoginWith2FA failed: invalid password", zap.String("email", email))
if s.accountLockoutService != nil {
_ = s.accountLockoutService.RecordFailedAttempt(ctx, email)
}
return nil, nil, errors.New("invalid credentials")
}
if !user.IsVerified {
s.logger.Warn("LoginWith2FA failed: email not verified", zap.String("email", email))
if s.accountLockoutService != nil {
_ = s.accountLockoutService.RecordFailedAttempt(ctx, email)
}
return nil, nil, errors.New("email not verified")
}
if twoFactorService == nil {
return nil, nil, fmt.Errorf("2FA service not available")
}
enabled, err := twoFactorService.GetTwoFactorStatus(ctx, user.ID)
if err != nil {
s.logger.Warn("Failed to get 2FA status", zap.String("user_id", user.ID.String()), zap.Error(err))
return nil, nil, fmt.Errorf("failed to verify 2FA: %w", err)
}
if !enabled {
return nil, nil, errors.New("2FA not enabled for this account")
}
valid, err := twoFactorService.VerifyTwoFactor(ctx, user.ID, code)
if err != nil {
s.logger.Warn("2FA verification error", zap.String("user_id", user.ID.String()), zap.Error(err))
return nil, nil, fmt.Errorf("2FA verification failed: %w", err)
}
if !valid {
s.logger.Warn("Invalid 2FA code", zap.String("user_id", user.ID.String()))
if s.accountLockoutService != nil {
_ = s.accountLockoutService.RecordFailedAttempt(ctx, email)
}
return nil, nil, errors.New("invalid 2FA code")
}
if s.accountLockoutService != nil {
_ = s.accountLockoutService.RecordSuccessfulLogin(ctx, email)
}
accessToken, err := s.JWTService.GenerateAccessToken(&user)
if err != nil {
s.logger.Error("Failed to generate access token for 2FA login", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to generate access token: %w", err)
}
refreshTokenTTL := s.JWTService.GetConfig().RefreshTokenTTL
if rememberMe {
refreshTokenTTL = s.JWTService.GetConfig().RememberMeRefreshTokenTTL
}
refreshToken, err := s.JWTService.GenerateRefreshToken(&user)
if err != nil {
s.logger.Error("Failed to generate refresh token for 2FA login", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
if err := s.refreshTokenService.Store(user.ID, refreshToken, refreshTokenTTL); err != nil {
s.logger.Error("Failed to store refresh token for 2FA login", zap.Error(err), zap.String("user_id", user.ID.String()))
return nil, nil, fmt.Errorf("failed to store refresh token: %w", err)
}
s.logger.Info("User logged in successfully with 2FA", zap.String("user_id", user.ID.String()))
return &user, &models.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int(s.JWTService.GetConfig().AccessTokenTTL.Seconds()),
}, nil
}
2025-12-03 19:29:37 +00:00
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*models.TokenPair, error) {
claims, err := s.JWTService.ValidateToken(refreshToken)
if err != nil {
s.logger.Warn("Invalid refresh token format", zap.Error(err))
return nil, errors.New("invalid refresh token")
}
if !claims.IsRefresh {
s.logger.Warn("Token is not a refresh token")
return nil, errors.New("invalid token type")
}
fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files CRITICAL fixes: - Race condition (TOCTOU) in payout/refund with SELECT FOR UPDATE (CRITICAL-001/002) - IDOR on analytics endpoint — ownership check enforced (CRITICAL-003) - CSWSH on all WebSocket endpoints — origin whitelist (CRITICAL-004) - Mass assignment on user self-update — strip privileged fields (CRITICAL-005) HIGH fixes: - Path traversal in marketplace upload — UUID filenames (HIGH-001) - IP spoofing — use Gin trusted proxy c.ClientIP() (HIGH-002) - Popularity metrics (followers, likes) set to json:"-" (HIGH-003) - bcrypt cost hardened to 12 everywhere (HIGH-004) - Refresh token lock made mandatory (HIGH-005) - Stream token replay prevention with access_count (HIGH-006) - Subscription trial race condition fixed (HIGH-007) - License download expiration check (HIGH-008) - Webhook amount validation (HIGH-009) - pprof endpoint removed from production (HIGH-010) MEDIUM fixes: - WebSocket message size limit 64KB (MEDIUM-010) - HSTS header in nginx production (MEDIUM-001) - CORS origin restricted in nginx-rtmp (MEDIUM-002) - Docker alpine pinned to 3.21 (MEDIUM-003/004) - Redis authentication enforced (MEDIUM-005) - GDPR account deletion expanded (MEDIUM-006) - .gitignore hardened (MEDIUM-007) LOW/INFO fixes: - GitHub Actions SHA pinning on all workflows (LOW-001) - .env.example security documentation (INFO-001) - Production CORS set to HTTPS (LOW-002) All tests pass. Go and Rust compile clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:44:46 +00:00
// SECURITY(REM-010): Lock is mandatory — if Redis is down, reject refresh to prevent TOCTOU race.
if s.refreshLock == nil {
s.logger.Error("Refresh lock not configured — rejecting refresh for safety")
return nil, errors.New("refresh service unavailable")
}
acquired, release := s.refreshLock.AcquireRefreshLock(ctx, claims.UserID, refreshToken)
if !acquired {
s.logger.Warn("Concurrent refresh attempt blocked by lock")
return nil, errors.New("refresh already in progress")
}
fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files CRITICAL fixes: - Race condition (TOCTOU) in payout/refund with SELECT FOR UPDATE (CRITICAL-001/002) - IDOR on analytics endpoint — ownership check enforced (CRITICAL-003) - CSWSH on all WebSocket endpoints — origin whitelist (CRITICAL-004) - Mass assignment on user self-update — strip privileged fields (CRITICAL-005) HIGH fixes: - Path traversal in marketplace upload — UUID filenames (HIGH-001) - IP spoofing — use Gin trusted proxy c.ClientIP() (HIGH-002) - Popularity metrics (followers, likes) set to json:"-" (HIGH-003) - bcrypt cost hardened to 12 everywhere (HIGH-004) - Refresh token lock made mandatory (HIGH-005) - Stream token replay prevention with access_count (HIGH-006) - Subscription trial race condition fixed (HIGH-007) - License download expiration check (HIGH-008) - Webhook amount validation (HIGH-009) - pprof endpoint removed from production (HIGH-010) MEDIUM fixes: - WebSocket message size limit 64KB (MEDIUM-010) - HSTS header in nginx production (MEDIUM-001) - CORS origin restricted in nginx-rtmp (MEDIUM-002) - Docker alpine pinned to 3.21 (MEDIUM-003/004) - Redis authentication enforced (MEDIUM-005) - GDPR account deletion expanded (MEDIUM-006) - .gitignore hardened (MEDIUM-007) LOW/INFO fixes: - GitHub Actions SHA pinning on all workflows (LOW-001) - .env.example security documentation (INFO-001) - Production CORS set to HTTPS (LOW-002) All tests pass. Go and Rust compile clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:44:46 +00:00
defer release()
2025-12-03 19:29:37 +00:00
if err := s.refreshTokenService.Validate(claims.UserID, refreshToken); err != nil {
s.logger.Warn("Refresh token invalid or revoked", zap.Error(err))
return nil, errors.New("invalid or revoked refresh token")
}
var user models.User
if err := s.db.WithContext(ctx).First(&user, claims.UserID).Error; err != nil {
s.logger.Error("User not found for refresh token", zap.Error(err))
return nil, errors.New("user not found")
}
newAccessToken, err := s.JWTService.GenerateAccessToken(&user)
if err != nil {
s.logger.Error("Failed to generate new access token", zap.Error(err))
return nil, err
}
newRefreshToken, err := s.JWTService.GenerateRefreshToken(&user)
if err != nil {
s.logger.Error("Failed to generate new refresh token", zap.Error(err))
return nil, err
}
if err := s.refreshTokenService.Rotate(user.ID, refreshToken, newRefreshToken, s.JWTService.GetConfig().RefreshTokenTTL); err != nil {
2025-12-03 19:29:37 +00:00
s.logger.Error("Failed to rotate refresh token", zap.Error(err))
return nil, err
}
return &models.TokenPair{
AccessToken: newAccessToken,
RefreshToken: newRefreshToken,
ExpiresIn: int(s.JWTService.GetConfig().AccessTokenTTL.Seconds()),
2025-12-03 19:29:37 +00:00
}, nil
}
func (s *AuthService) VerifyEmail(ctx context.Context, token string) error {
userID, err := s.emailVerificationService.VerifyToken(token)
if err != nil {
s.logger.Warn("Email verification failed", zap.Error(err))
return err
}
if err := s.db.WithContext(ctx).Model(&models.User{}).Where("id = ?", userID).Update("is_verified", true).Error; err != nil {
s.logger.Error("Failed to update user verification status", zap.Error(err))
return err
}
if err := s.emailVerificationService.InvalidateOldTokens(userID); err != nil {
s.logger.Warn("Failed to invalidate old verification tokens", zap.Error(err))
}
s.logger.Info("Email verified successfully", zap.String("user_id", userID.String()))
return nil
}
func (s *AuthService) ResendVerificationEmail(ctx context.Context, email string) error {
var user models.User
if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil
}
return err
}
if user.IsVerified {
return errors.New("email already verified")
}
if err := s.emailVerificationService.InvalidateOldTokens(user.ID); err != nil {
s.logger.Error("Failed to invalidate old tokens", zap.Error(err))
}
token, err := s.emailVerificationService.GenerateToken()
if err != nil {
return err
}
if err := s.emailVerificationService.StoreToken(user.ID, user.Email, token); err != nil {
2025-12-03 19:29:37 +00:00
return err
}
s.logger.Info("Resending verification email",
zap.String("email", user.Email),
zap.String("token", token),
zap.String("user_id", user.ID.String()))
return nil
}
func (s *AuthService) Logout(ctx context.Context, userID uuid.UUID, refreshToken string) error {
// Valider le refresh token
claims, err := s.JWTService.ValidateToken(refreshToken)
if err != nil {
s.logger.Warn("Invalid refresh token during logout", zap.Error(err), zap.String("user_id", userID.String()))
return nil // Ne pas retourner d'erreur pour ne pas bloquer le logout côté UI
}
if claims.UserID != userID {
s.logger.Warn("User ID mismatch for logout request", zap.String("requested_user_id", userID.String()), zap.String("token_user_id", claims.UserID.String()))
return errors.New("user ID mismatch")
}
if err := s.refreshTokenService.Revoke(claims.UserID, refreshToken); err != nil {
s.logger.Error("Failed to revoke refresh token during logout", zap.Error(err), zap.String("user_id", userID.String()))
return err
}
s.logger.Info("User logged out successfully", zap.String("user_id", userID.String()))
return nil
}
func (s *AuthService) InvalidateAllUserSessions(ctx context.Context, userID uuid.UUID, sessionService interface {
RevokeAllUserSessions(ctx context.Context, userID uuid.UUID) (int64, error)
}) error {
if err := s.refreshTokenService.RevokeAll(userID); err != nil {
s.logger.Error("Failed to revoke all refresh tokens", zap.Error(err))
return err
}
if sessionService != nil {
count, err := sessionService.RevokeAllUserSessions(ctx, userID)
if err != nil {
s.logger.Error("Failed to revoke user sessions", zap.Error(err))
} else {
s.logger.Info("Revoked user sessions", zap.Int64("count", count), zap.String("user_id", userID.String()))
}
}
s.logger.Info("All user sessions invalidated", zap.String("user_id", userID.String()))
return nil
}
// MIGRATION UUID: userID migré vers uuid.UUID
func (s *AuthService) AdminVerifyUser(ctx context.Context, userID uuid.UUID) error {
result := s.db.WithContext(ctx).Model(&models.User{}).Where("id = ?", userID).Update("is_verified", true)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("user not found")
}
_ = s.emailVerificationService.InvalidateOldTokens(userID)
s.logger.Info("User verified by admin", zap.String("user_id", userID.String()))
return nil
}
// MIGRATION UUID: userID migré vers uuid.UUID
func (s *AuthService) AdminBlockUser(ctx context.Context, userID uuid.UUID) error {
if err := s.refreshTokenService.RevokeAll(userID); err != nil {
return err
}
s.logger.Info("User blocked by admin", zap.String("user_id", userID.String()))
return nil
}
func (s *AuthService) RequestPasswordReset(ctx context.Context, email string) error {
var user models.User
if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound {
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 10:14:38 +00:00
// Return nil to prevent email enumeration - always return success
2025-12-03 19:29:37 +00:00
return nil
}
return err
}
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 10:14:38 +00:00
// Invalidate old tokens for this user
if err := s.passwordResetService.InvalidateOldTokens(user.ID); err != nil {
s.logger.Warn("Failed to invalidate old password reset tokens",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
// Continue anyway, not critical
}
// Generate new reset token
token, err := s.passwordResetService.GenerateToken()
2025-12-03 19:29:37 +00:00
if err != nil {
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 10:14:38 +00:00
s.logger.Error("Failed to generate password reset token",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
return fmt.Errorf("failed to generate reset token: %w", err)
}
// Store token in database
if err := s.passwordResetService.StoreToken(user.ID, token); err != nil {
s.logger.Error("Failed to store password reset token",
zap.String("user_id", user.ID.String()),
zap.Error(err),
)
return fmt.Errorf("failed to store reset token: %w", err)
}
// Send password reset email via job worker (asynchrone)
if s.jobWorker != nil {
// Construire l'URL de reset
baseURL := os.Getenv("FRONTEND_URL")
if baseURL == "" {
appDomain := os.Getenv("APP_DOMAIN")
if appDomain == "" {
appDomain = "veza.fr"
}
baseURL = "http://" + appDomain + ":5173"
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 10:14:38 +00:00
}
resetURL := fmt.Sprintf("%s/reset-password?token=%s", baseURL, token)
// Préparer les données du template
templateData := map[string]interface{}{
"Username": user.Username,
"ResetURL": resetURL,
}
// Enqueue le job d'email avec template
s.jobWorker.EnqueueEmailJobWithTemplate(
user.Email,
"Reset your Veza password",
"password_reset",
templateData,
)
s.logger.Info("Password reset email job enqueued",
zap.String("user_id", user.ID.String()),
zap.String("email", user.Email),
)
} else {
// Fallback sur l'ancien système si job worker non disponible
s.logger.Warn("Job worker not available, using direct email service")
if err := s.emailService.SendPasswordResetEmail(user.ID, user.Email, token); err != nil {
s.logger.Error("Failed to send password reset email",
zap.String("user_id", user.ID.String()),
zap.String("email", user.Email),
zap.Error(err),
)
}
2025-12-03 19:29:37 +00:00
}
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 10:14:38 +00:00
s.logger.Info("Password reset requested successfully",
zap.String("email", email),
zap.String("user_id", user.ID.String()),
zap.String("token_preview", token[:min(len(token), 8)]+"..."),
)
2025-12-03 19:29:37 +00:00
return nil
}
func (s *AuthService) ResetPassword(ctx context.Context, token, newPassword string) error {
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 10:14:38 +00:00
// Verify the reset token
userID, err := s.passwordResetService.VerifyToken(token)
2025-12-03 19:29:37 +00:00
if err != nil {
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 10:14:38 +00:00
s.logger.Warn("Password reset token verification failed",
zap.String("token_preview", token[:min(len(token), 8)]+"..."),
zap.Error(err),
)
return fmt.Errorf("invalid or expired token: %w", err)
}
// Validate password strength
if err := s.passwordService.ValidatePassword(newPassword); err != nil {
s.logger.Warn("Password validation failed during reset",
zap.String("user_id", userID.String()),
zap.Error(err),
)
return fmt.Errorf("invalid password: %w", err)
}
// Update password using PasswordService
if err := s.passwordService.UpdatePassword(userID, newPassword); err != nil {
s.logger.Error("Failed to update password during reset",
zap.String("user_id", userID.String()),
zap.Error(err),
)
return fmt.Errorf("failed to update password: %w", err)
}
// Mark token as used
if err := s.passwordResetService.MarkTokenAsUsed(token); err != nil {
// Log but don't fail - password is already updated
s.logger.Warn("Failed to mark password reset token as used",
zap.String("user_id", userID.String()),
zap.String("token_preview", token[:min(len(token), 8)]+"..."),
zap.Error(err),
)
}
// Invalidate all user sessions (revoke refresh tokens)
if err := s.refreshTokenService.RevokeAll(userID); err != nil {
s.logger.Warn("Failed to revoke refresh tokens after password reset",
zap.String("user_id", userID.String()),
zap.Error(err),
)
// Don't fail - password is already updated
2025-12-03 19:29:37 +00:00
}
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 10:14:38 +00:00
s.logger.Info("Password reset completed successfully",
zap.String("user_id", userID.String()),
)
2025-12-03 19:29:37 +00:00
return nil
}
// MIGRATION UUID: userID migré vers uuid.UUID
func (s *AuthService) ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error {
var user models.User
if err := s.db.WithContext(ctx).First(&user, userID).Error; err != nil {
return err
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)); err != nil {
return errors.New("invalid current password")
}
fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files CRITICAL fixes: - Race condition (TOCTOU) in payout/refund with SELECT FOR UPDATE (CRITICAL-001/002) - IDOR on analytics endpoint — ownership check enforced (CRITICAL-003) - CSWSH on all WebSocket endpoints — origin whitelist (CRITICAL-004) - Mass assignment on user self-update — strip privileged fields (CRITICAL-005) HIGH fixes: - Path traversal in marketplace upload — UUID filenames (HIGH-001) - IP spoofing — use Gin trusted proxy c.ClientIP() (HIGH-002) - Popularity metrics (followers, likes) set to json:"-" (HIGH-003) - bcrypt cost hardened to 12 everywhere (HIGH-004) - Refresh token lock made mandatory (HIGH-005) - Stream token replay prevention with access_count (HIGH-006) - Subscription trial race condition fixed (HIGH-007) - License download expiration check (HIGH-008) - Webhook amount validation (HIGH-009) - pprof endpoint removed from production (HIGH-010) MEDIUM fixes: - WebSocket message size limit 64KB (MEDIUM-010) - HSTS header in nginx production (MEDIUM-001) - CORS origin restricted in nginx-rtmp (MEDIUM-002) - Docker alpine pinned to 3.21 (MEDIUM-003/004) - Redis authentication enforced (MEDIUM-005) - GDPR account deletion expanded (MEDIUM-006) - .gitignore hardened (MEDIUM-007) LOW/INFO fixes: - GitHub Actions SHA pinning on all workflows (LOW-001) - .env.example security documentation (INFO-001) - Production CORS set to HTTPS (LOW-002) All tests pass. Go and Rust compile clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:44:46 +00:00
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), 12 /* SECURITY(REM-016): Explicit cost 12, aligned with password_service.go */)
2025-12-03 19:29:37 +00:00
if err != nil {
return err
}
if err := s.db.WithContext(ctx).Model(&user).Update("password_hash", string(hashedPassword)).Error; err != nil {
return err
}
if err := s.refreshTokenService.RevokeAll(userID); err != nil {
s.logger.Warn("Failed to revoke refresh tokens after password change", zap.Error(err))
}
s.logger.Info("Password changed successfully", zap.String("user_id", userID.String()))
return nil
}
func (s *AuthService) ValidateAccessToken(tokenString string) (*models.CustomClaims, error) {
return s.JWTService.ValidateToken(tokenString)
}
func (s *AuthService) UpdateLastLogin(ctx context.Context, userID uuid.UUID) error {
return s.db.WithContext(ctx).Model(&models.User{}).
Where("id = ?", userID).
Update("last_login_at", time.Now()).Error
}
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 10:14:38 +00:00
// min returns the minimum of two integers (helper function)
func min(a, b int) int {
if a < b {
return a
}
return b
}