auth.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. // Package services реализует бизнес-логику приложения (слой сервисов).
  2. // AuthService: регистрация, логин, JWT-токены (access/refresh), валидация,
  3. // ротация токенов, отзыв сессий.
  4. // PlaceService: CRUD мест, модерация, проверка владельца.
  5. package services
  6. import (
  7. "context"
  8. "crypto/rand"
  9. "encoding/base64"
  10. "errors"
  11. "fmt"
  12. "log/slog"
  13. "time"
  14. "github.com/golang-jwt/jwt/v5"
  15. "github.com/photoplaces/backend/internal/log"
  16. "github.com/photoplaces/backend/internal/models"
  17. "github.com/photoplaces/backend/internal/repository"
  18. "golang.org/x/crypto/bcrypt"
  19. )
  20. var (
  21. ErrEmailExists = errors.New("email already exists")
  22. ErrInvalidCreds = errors.New("invalid email or password")
  23. ErrUserBanned = errors.New("account is banned")
  24. ErrInvalidToken = errors.New("invalid or expired token")
  25. ErrTokenReused = errors.New("token reused, possible theft")
  26. )
  27. const (
  28. AccessTokenExpiry = 15 * time.Minute
  29. RefreshTokenExpiry = 30 * 24 * time.Hour
  30. RefreshTokenBytes = 32
  31. )
  32. type AuthService struct {
  33. userRepo *repository.UserRepo
  34. refreshTokenRepo *repository.RefreshTokenRepo
  35. jwtSecret []byte
  36. refreshSecret []byte
  37. }
  38. func NewAuthService(userRepo *repository.UserRepo, refreshTokenRepo *repository.RefreshTokenRepo, jwtSecret, refreshSecret string) *AuthService {
  39. return &AuthService{
  40. userRepo: userRepo,
  41. refreshTokenRepo: refreshTokenRepo,
  42. jwtSecret: []byte(jwtSecret),
  43. refreshSecret: []byte(refreshSecret),
  44. }
  45. }
  46. type AuthResult struct {
  47. User *models.User `json:"user"`
  48. AccessToken string `json:"access_token"`
  49. }
  50. type RegisterInput struct {
  51. Email string
  52. Password string
  53. Role string
  54. Name string
  55. }
  56. func (s *AuthService) Register(ctx context.Context, input RegisterInput) (*AuthResult, string, error) {
  57. logger := log.FromContext(ctx)
  58. existing, _ := s.userRepo.GetByEmail(ctx, input.Email)
  59. if existing != nil {
  60. logger.WarnContext(ctx, "registration attempt with existing email", slog.String("email", input.Email))
  61. return nil, "", ErrEmailExists
  62. }
  63. hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
  64. if err != nil {
  65. logger.ErrorContext(ctx, "password hashing failed", log.WithError(err))
  66. return nil, "", fmt.Errorf("hash password: %w", err)
  67. }
  68. user := &models.User{
  69. Email: input.Email,
  70. PasswordHash: string(hash),
  71. Role: input.Role,
  72. Name: strPtr(input.Name),
  73. }
  74. if err := s.userRepo.Create(ctx, user); err != nil {
  75. logger.ErrorContext(ctx, "user creation failed", log.WithError(err), slog.String("email", input.Email))
  76. return nil, "", fmt.Errorf("create user: %w", err)
  77. }
  78. logger.InfoContext(ctx, "user registered", slog.String("user_id", user.ID), slog.String("email", user.Email), slog.String("role", user.Role))
  79. return s.generateTokens(ctx, user)
  80. }
  81. type LoginInput struct {
  82. Email string
  83. Password string
  84. }
  85. func (s *AuthService) Login(ctx context.Context, input LoginInput) (*AuthResult, string, error) {
  86. logger := log.FromContext(ctx)
  87. user, err := s.userRepo.GetByEmail(ctx, input.Email)
  88. if err != nil {
  89. logger.ErrorContext(ctx, "user lookup failed", log.WithError(err), slog.String("email", input.Email))
  90. return nil, "", fmt.Errorf("get user: %w", err)
  91. }
  92. if user == nil {
  93. logger.WarnContext(ctx, "login attempt for non-existent user", slog.String("email", input.Email))
  94. return nil, "", ErrInvalidCreds
  95. }
  96. if user.Status == "banned" {
  97. logger.WarnContext(ctx, "login attempt for banned user", slog.String("user_id", user.ID))
  98. return nil, "", ErrUserBanned
  99. }
  100. if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)); err != nil {
  101. logger.WarnContext(ctx, "invalid password attempt", slog.String("user_id", user.ID))
  102. return nil, "", ErrInvalidCreds
  103. }
  104. logger.InfoContext(ctx, "user logged in", slog.String("user_id", user.ID), slog.String("role", user.Role))
  105. return s.generateTokens(ctx, user)
  106. }
  107. type TokenClaims struct {
  108. UserID string `json:"user_id"`
  109. Role string `json:"role"`
  110. jwt.RegisteredClaims
  111. }
  112. func (s *AuthService) generateTokens(ctx context.Context, user *models.User) (*AuthResult, string, error) {
  113. now := time.Now()
  114. accessClaims := TokenClaims{
  115. UserID: user.ID,
  116. Role: user.Role,
  117. RegisteredClaims: jwt.RegisteredClaims{
  118. ExpiresAt: jwt.NewNumericDate(now.Add(AccessTokenExpiry)),
  119. IssuedAt: jwt.NewNumericDate(now),
  120. },
  121. }
  122. accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.jwtSecret)
  123. if err != nil {
  124. return nil, "", fmt.Errorf("sign access token: %w", err)
  125. }
  126. refreshToken, err := generateSecureToken(RefreshTokenBytes)
  127. if err != nil {
  128. return nil, "", fmt.Errorf("generate refresh token: %w", err)
  129. }
  130. expiresAt := now.Add(RefreshTokenExpiry)
  131. if err := s.refreshTokenRepo.Create(ctx, user.ID, refreshToken, expiresAt); err != nil {
  132. return nil, "", fmt.Errorf("store refresh token: %w", err)
  133. }
  134. return &AuthResult{
  135. User: user,
  136. AccessToken: accessToken,
  137. }, refreshToken, nil
  138. }
  139. func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) {
  140. token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(t *jwt.Token) (interface{}, error) {
  141. return s.jwtSecret, nil
  142. })
  143. if err != nil {
  144. return nil, ErrInvalidToken
  145. }
  146. claims, ok := token.Claims.(*TokenClaims)
  147. if !ok || !token.Valid {
  148. return nil, ErrInvalidToken
  149. }
  150. return claims, nil
  151. }
  152. func (s *AuthService) RefreshSession(ctx context.Context, plainRefreshToken string) (*AuthResult, string, error) {
  153. logger := log.FromContext(ctx)
  154. storedToken, err := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
  155. if err != nil {
  156. logger.ErrorContext(ctx, "refresh token lookup failed", log.WithError(err))
  157. return nil, "", fmt.Errorf("lookup refresh token: %w", err)
  158. }
  159. if storedToken == nil {
  160. logger.WarnContext(ctx, "refresh attempt with invalid/expired token")
  161. return nil, "", ErrInvalidToken
  162. }
  163. user, err := s.userRepo.GetByID(ctx, storedToken.UserID)
  164. if err != nil {
  165. logger.ErrorContext(ctx, "user lookup failed during refresh", log.WithError(err), slog.String("user_id", storedToken.UserID))
  166. return nil, "", fmt.Errorf("get user: %w", err)
  167. }
  168. if user == nil {
  169. logger.WarnContext(ctx, "refresh attempt for deleted user", slog.String("user_id", storedToken.UserID))
  170. return nil, "", ErrInvalidToken
  171. }
  172. if user.Status == "banned" {
  173. logger.WarnContext(ctx, "refresh attempt for banned user", slog.String("user_id", user.ID))
  174. return nil, "", ErrUserBanned
  175. }
  176. if err := s.refreshTokenRepo.Delete(ctx, storedToken.TokenHash); err != nil {
  177. logger.ErrorContext(ctx, "failed to delete old refresh token", log.WithError(err))
  178. return nil, "", fmt.Errorf("delete old refresh token: %w", err)
  179. }
  180. logger.InfoContext(ctx, "token refreshed", slog.String("user_id", user.ID))
  181. return s.generateTokens(ctx, user)
  182. }
  183. func (s *AuthService) RevokeSession(ctx context.Context, plainRefreshToken string) error {
  184. logger := log.FromContext(ctx)
  185. storedToken, err := s.refreshTokenRepo.GetValid(ctx, plainRefreshToken)
  186. if err != nil {
  187. logger.ErrorContext(ctx, "revoke: refresh token lookup failed", log.WithError(err))
  188. return fmt.Errorf("lookup refresh token: %w", err)
  189. }
  190. if storedToken == nil {
  191. return nil
  192. }
  193. logger.InfoContext(ctx, "session revoked", slog.String("user_id", storedToken.UserID))
  194. return s.refreshTokenRepo.Revoke(ctx, storedToken.TokenHash)
  195. }
  196. func (s *AuthService) RevokeAllSessions(ctx context.Context, userID string) error {
  197. logger := log.FromContext(ctx)
  198. logger.InfoContext(ctx, "revoking all sessions for user", slog.String("user_id", userID))
  199. return s.refreshTokenRepo.RevokeAllForUser(ctx, userID)
  200. }
  201. func (s *AuthService) CleanupExpiredTokens(ctx context.Context) error {
  202. logger := log.FromContext(ctx)
  203. logger.InfoContext(ctx, "cleaning up expired refresh tokens")
  204. return s.refreshTokenRepo.CleanupExpired(ctx)
  205. }
  206. func generateSecureToken(n int) (string, error) {
  207. b := make([]byte, n)
  208. if _, err := rand.Read(b); err != nil {
  209. return "", err
  210. }
  211. return base64.URLEncoding.EncodeToString(b), nil
  212. }
  213. func strPtr(s string) *string {
  214. if s == "" {
  215. return nil
  216. }
  217. return &s
  218. }