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.).
555 lines
19 KiB
Go
555 lines
19 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/services" // Added import for services
|
|
"veza-backend-api/internal/workers"
|
|
|
|
"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.JWTService // Changed to pointer
|
|
emailVerificationService *services.EmailVerificationService // Changed to pointer
|
|
refreshTokenService *services.RefreshTokenService // Changed to pointer
|
|
passwordResetService *services.PasswordResetService // Added for password reset
|
|
emailValidator *validators.EmailValidator
|
|
passwordValidator *validators.PasswordValidator
|
|
passwordService *services.PasswordService // Changed to pointer
|
|
emailService *services.EmailService // Changed to pointer
|
|
jobWorker *workers.JobWorker // Job worker pour envoi d'emails asynchrones
|
|
}
|
|
|
|
func NewAuthService(
|
|
db *gorm.DB,
|
|
emailValidator *validators.EmailValidator,
|
|
passwordValidator *validators.PasswordValidator,
|
|
passwordService *services.PasswordService, // Changed to pointer
|
|
jwtService *services.JWTService, // Changed to pointer
|
|
refreshTokenService *services.RefreshTokenService, // Changed to pointer
|
|
emailVerificationService *services.EmailVerificationService, // Changed to pointer
|
|
passwordResetService *services.PasswordResetService, // Added for password reset
|
|
emailService *services.EmailService, // Changed to pointer
|
|
jobWorker *workers.JobWorker, // Job worker pour emails asynchrones
|
|
logger *zap.Logger,
|
|
) *AuthService {
|
|
return &AuthService{
|
|
db: db,
|
|
logger: logger,
|
|
JWTService: jwtService,
|
|
emailVerificationService: emailVerificationService,
|
|
refreshTokenService: refreshTokenService,
|
|
passwordResetService: passwordResetService,
|
|
emailValidator: emailValidator,
|
|
passwordValidator: passwordValidator,
|
|
passwordService: passwordService,
|
|
emailService: emailService,
|
|
jobWorker: jobWorker,
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
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, password string) (*models.User, error) {
|
|
s.logger.Info("Attempting to register new user", zap.String("email", email))
|
|
|
|
// Valider l'email
|
|
if err := s.emailValidator.Validate(email); err != nil {
|
|
s.logger.Warn("Registration failed: invalid email", zap.String("email", email), zap.Error(err))
|
|
return nil, errors.New("invalid email: " + err.Error())
|
|
}
|
|
|
|
// Valider le mot de passe
|
|
passwordStrength, err := s.passwordValidator.Validate(password)
|
|
if err != nil || !passwordStrength.Valid { // Vérifiez également si la force n'est pas suffisante
|
|
s.logger.Warn("Registration failed: weak password", zap.String("email", email), zap.Error(err))
|
|
// Si l'erreur est nil mais pas valide, utilisez les détails de la force
|
|
if err == nil {
|
|
err = errors.New("weak password: " + strings.Join(passwordStrength.Details, ", "))
|
|
}
|
|
return nil, errors.New("weak password: " + err.Error())
|
|
}
|
|
|
|
// Hacher le mot de passe
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
s.logger.Error("Failed to hash password", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// Créer l'utilisateur dans la base de données
|
|
user := &models.User{
|
|
ID: uuid.New(), // Générer un nouvel UUID
|
|
Email: email,
|
|
PasswordHash: string(hashedPassword),
|
|
// Le nom d'utilisateur sera généré par défaut ou défini plus tard
|
|
// IsVerified: false par défaut
|
|
}
|
|
|
|
if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
|
|
if strings.Contains(err.Error(), "unique constraint") || strings.Contains(err.Error(), "duplicate key") {
|
|
s.logger.Warn("Registration failed: email already exists", zap.String("email", email))
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
s.logger.Error("Failed to create user in database", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// Générer le token de vérification d'email
|
|
token, err := s.emailVerificationService.GenerateToken()
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate email verification token", zap.Error(err))
|
|
return user, fmt.Errorf("failed to generate verification token: %w", err)
|
|
}
|
|
|
|
// Stocker le token
|
|
if err := s.emailVerificationService.StoreToken(user.ID, token); err != nil {
|
|
s.logger.Error("Failed to store email verification token", zap.Error(err))
|
|
return user, fmt.Errorf("failed to store verification token: %w", err)
|
|
}
|
|
|
|
// 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()))
|
|
|
|
s.logger.Info("User registered successfully", zap.String("user_id", user.ID.String()))
|
|
return user, nil
|
|
}
|
|
|
|
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))
|
|
|
|
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))
|
|
return nil, nil, errors.New("invalid credentials")
|
|
}
|
|
s.logger.Error("Database error during login", zap.Error(err))
|
|
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))
|
|
return nil, nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
if !user.IsVerified {
|
|
s.logger.Warn("Login failed: email not verified", zap.String("email", email))
|
|
return nil, nil, errors.New("email not verified")
|
|
}
|
|
|
|
// 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))
|
|
return nil, nil, err
|
|
}
|
|
|
|
refreshTokenTTL := s.JWTService.Config.RefreshTokenTTL
|
|
if rememberMe {
|
|
refreshTokenTTL = s.JWTService.Config.RememberMeRefreshTokenTTL // Assurez-vous que ce champ existe dans models.JWTConfig
|
|
}
|
|
refreshToken, err := s.JWTService.GenerateRefreshToken(&user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate refresh token", zap.Error(err))
|
|
return nil, nil, err
|
|
}
|
|
|
|
// 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))
|
|
return nil, nil, err
|
|
}
|
|
|
|
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.Config.AccessTokenTTL.Seconds()),
|
|
}, nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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.Config.RefreshTokenTTL); err != nil {
|
|
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.Config.AccessTokenTTL.Seconds()),
|
|
}, 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, token); err != nil {
|
|
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 {
|
|
// Return nil to prevent email enumeration - always return success
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 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()
|
|
if err != nil {
|
|
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 == "" {
|
|
baseURL = "http://localhost:5173"
|
|
}
|
|
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),
|
|
)
|
|
}
|
|
}
|
|
|
|
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)]+"..."),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func (s *AuthService) ResetPassword(ctx context.Context, token, newPassword string) error {
|
|
// Verify the reset token
|
|
userID, err := s.passwordResetService.VerifyToken(token)
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
s.logger.Info("Password reset completed successfully",
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
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")
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
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
|
|
}
|
|
|
|
// min returns the minimum of two integers (helper function)
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|