|
@@ -9,6 +9,7 @@ import (
|
|
|
"time"
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
|
+ "github.com/photoplaces/backend/internal/log"
|
|
|
"github.com/photoplaces/backend/internal/models"
|
|
"github.com/photoplaces/backend/internal/models"
|
|
|
"github.com/photoplaces/backend/internal/repository"
|
|
"github.com/photoplaces/backend/internal/repository"
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"golang.org/x/crypto/bcrypt"
|
|
@@ -57,13 +58,17 @@ type RegisterInput struct {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
|
|
func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
|
|
|
|
|
+ logger := log.FromContext(ctx)
|
|
|
|
|
+
|
|
|
existing, _ := s.userRepo.GetByEmail(ctx, input.Email)
|
|
existing, _ := s.userRepo.GetByEmail(ctx, input.Email)
|
|
|
if existing != nil {
|
|
if existing != nil {
|
|
|
|
|
+ logger.WarnContext(ctx, "registration attempt with existing email", slog.String("email", input.Email))
|
|
|
return nil, "", ErrEmailExists
|
|
return nil, "", ErrEmailExists
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "password hashing failed", log.WithError(err))
|
|
|
return nil, "", fmt.Errorf("hash password: %w", err)
|
|
return nil, "", fmt.Errorf("hash password: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -75,9 +80,12 @@ func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthR
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "user creation failed", log.WithError(err), slog.String("email", input.Email))
|
|
|
return nil, "", fmt.Errorf("create user: %w", err)
|
|
return nil, "", fmt.Errorf("create user: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ logger.InfoContext(ctx, "user registered", slog.String("user_id", user.ID), slog.String("email", user.Email), slog.String("role", user.Role))
|
|
|
|
|
+
|
|
|
return s.generateTokens(ctx, user)
|
|
return s.generateTokens(ctx, user)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -87,22 +95,29 @@ type LoginInput struct {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *AuthService) Login(ctx context.Context, input LoginInput) (*AuthResult, string, error) {
|
|
func (s *AuthService) Login(ctx context.Context, input LoginInput) (*AuthResult, string, error) {
|
|
|
|
|
+ logger := log.FromContext(ctx)
|
|
|
|
|
+
|
|
|
user, err := s.userRepo.GetByEmail(ctx, input.Email)
|
|
user, err := s.userRepo.GetByEmail(ctx, input.Email)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "user lookup failed", log.WithError(err), slog.String("email", input.Email))
|
|
|
return nil, "", fmt.Errorf("get user: %w", err)
|
|
return nil, "", fmt.Errorf("get user: %w", err)
|
|
|
}
|
|
}
|
|
|
if user == nil {
|
|
if user == nil {
|
|
|
|
|
+ logger.WarnContext(ctx, "login attempt for non-existent user", slog.String("email", input.Email))
|
|
|
return nil, "", ErrInvalidCreds
|
|
return nil, "", ErrInvalidCreds
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if user.Status == "banned" {
|
|
if user.Status == "banned" {
|
|
|
|
|
+ logger.WarnContext(ctx, "login attempt for banned user", slog.String("user_id", user.ID))
|
|
|
return nil, "", ErrUserBanned
|
|
return nil, "", ErrUserBanned
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)); err != nil {
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)); err != nil {
|
|
|
|
|
+ logger.WarnContext(ctx, "invalid password attempt", slog.String("user_id", user.ID))
|
|
|
return nil, "", ErrInvalidCreds
|
|
return nil, "", ErrInvalidCreds
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ logger.InfoContext(ctx, "user logged in", slog.String("user_id", user.ID), slog.String("role", user.Role))
|
|
|
return s.generateTokens(ctx, user)
|
|
return s.generateTokens(ctx, user)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -159,48 +174,65 @@ func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *AuthService) RefreshSession(ctx context.Context, plainRefreshToken string) (*AuthResult, string, error) {
|
|
func (s *AuthService) RefreshSession(ctx context.Context, plainRefreshToken string) (*AuthResult, string, error) {
|
|
|
|
|
+ logger := log.FromContext(ctx)
|
|
|
|
|
+
|
|
|
storedToken, err := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
|
|
storedToken, err := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "refresh token lookup failed", log.WithError(err))
|
|
|
return nil, "", fmt.Errorf("lookup refresh token: %w", err)
|
|
return nil, "", fmt.Errorf("lookup refresh token: %w", err)
|
|
|
}
|
|
}
|
|
|
if storedToken == nil {
|
|
if storedToken == nil {
|
|
|
|
|
+ logger.WarnContext(ctx, "refresh attempt with invalid/expired token")
|
|
|
return nil, "", ErrInvalidToken
|
|
return nil, "", ErrInvalidToken
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
user, err := s.userRepo.GetByID(ctx, storedToken.UserID)
|
|
user, err := s.userRepo.GetByID(ctx, storedToken.UserID)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "user lookup failed during refresh", log.WithError(err), slog.String("user_id", storedToken.UserID))
|
|
|
return nil, "", fmt.Errorf("get user: %w", err)
|
|
return nil, "", fmt.Errorf("get user: %w", err)
|
|
|
}
|
|
}
|
|
|
if user == nil {
|
|
if user == nil {
|
|
|
|
|
+ logger.WarnContext(ctx, "refresh attempt for deleted user", slog.String("user_id", storedToken.UserID))
|
|
|
return nil, "", ErrInvalidToken
|
|
return nil, "", ErrInvalidToken
|
|
|
}
|
|
}
|
|
|
if user.Status == "banned" {
|
|
if user.Status == "banned" {
|
|
|
|
|
+ logger.WarnContext(ctx, "refresh attempt for banned user", slog.String("user_id", user.ID))
|
|
|
return nil, "", ErrUserBanned
|
|
return nil, "", ErrUserBanned
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if err := s.refreshTokenRepo.Delete(ctx, storedToken.TokenHash); err != nil {
|
|
if err := s.refreshTokenRepo.Delete(ctx, storedToken.TokenHash); err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "failed to delete old refresh token", log.WithError(err))
|
|
|
return nil, "", fmt.Errorf("delete old refresh token: %w", err)
|
|
return nil, "", fmt.Errorf("delete old refresh token: %w", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ logger.InfoContext(ctx, "token refreshed", slog.String("user_id", user.ID))
|
|
|
return s.generateTokens(ctx, user)
|
|
return s.generateTokens(ctx, user)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *AuthService) RevokeSession(ctx context.Context, plainRefreshToken string) error {
|
|
func (s *AuthService) RevokeSession(ctx context.Context, plainRefreshToken string) error {
|
|
|
|
|
+ logger := log.FromContext(ctx)
|
|
|
|
|
+
|
|
|
storedToken, err := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
|
|
storedToken, err := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
|
|
+ logger.ErrorContext(ctx, "revoke: refresh token lookup failed", log.WithError(err))
|
|
|
return fmt.Errorf("lookup refresh token: %w", err)
|
|
return fmt.Errorf("lookup refresh token: %w", err)
|
|
|
}
|
|
}
|
|
|
if storedToken == nil {
|
|
if storedToken == nil {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
+ logger.InfoContext(ctx, "session revoked", slog.String("user_id", storedToken.UserID))
|
|
|
return s.refreshTokenRepo.Revoke(ctx, storedToken.TokenHash)
|
|
return s.refreshTokenRepo.Revoke(ctx, storedToken.TokenHash)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *AuthService) RevokeAllSessions(ctx context.Context, userID string) error {
|
|
func (s *AuthService) RevokeAllSessions(ctx context.Context, userID string) error {
|
|
|
|
|
+ logger := log.FromContext(ctx)
|
|
|
|
|
+ logger.InfoContext(ctx, "revoking all sessions for user", slog.String("user_id", userID))
|
|
|
return s.refreshTokenRepo.RevokeAllForUser(ctx, userID)
|
|
return s.refreshTokenRepo.RevokeAllForUser(ctx, userID)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *AuthService) CleanupExpiredTokens(ctx context.Context) error {
|
|
func (s *AuthService) CleanupExpiredTokens(ctx context.Context) error {
|
|
|
|
|
+ logger := log.FromContext(ctx)
|
|
|
|
|
+ logger.InfoContext(ctx, "cleaning up expired refresh tokens")
|
|
|
return s.refreshTokenRepo.CleanupExpired(ctx)
|
|
return s.refreshTokenRepo.CleanupExpired(ctx)
|
|
|
}
|
|
}
|
|
|
|
|
|