package auth import ( "context" "errors" "strings" "time" "shared-album/internal/httpx" "shared-album/internal/models" ) type TokenPair struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` ExpiresIn int64 `json:"expires_in"` } type Service struct { store Repository tm *TokenManager } func NewService(store Repository, tm *TokenManager) *Service { return &Service{store: store, tm: tm} } func (s *Service) Register(ctx context.Context, email, password, name string) (*models.User, *TokenPair, error) { email = strings.TrimSpace(strings.ToLower(email)) name = strings.TrimSpace(name) if email == "" || !strings.Contains(email, "@") { return nil, nil, httpx.BadRequest("invalid_email", "valid email is required") } if len(password) < 8 { return nil, nil, httpx.BadRequest("weak_password", "password must be at least 8 characters") } if name == "" { return nil, nil, httpx.BadRequest("invalid_name", "name is required") } if _, err := s.store.GetUserByEmail(ctx, email); err == nil { return nil, nil, httpx.Conflict("email_taken", "email is already registered") } else if !errors.Is(err, ErrNotFound) { return nil, nil, err } hash, err := HashPassword(password) if err != nil { return nil, nil, err } u := &models.User{ ID: NewID(), Email: email, PasswordHash: hash, Name: name, CreatedAt: time.Now(), } if err := s.store.CreateUser(ctx, u); err != nil { return nil, nil, err } pair, err := s.issuePair(ctx, u) if err != nil { return nil, nil, err } return u, pair, nil } func (s *Service) Login(ctx context.Context, email, password string) (*models.User, *TokenPair, error) { email = strings.TrimSpace(strings.ToLower(email)) u, err := s.store.GetUserByEmail(ctx, email) if err != nil { if errors.Is(err, ErrNotFound) { return nil, nil, httpx.Unauthorized("invalid_credentials", "invalid email or password") } return nil, nil, err } if !CheckPassword(u.PasswordHash, password) { return nil, nil, httpx.Unauthorized("invalid_credentials", "invalid email or password") } pair, err := s.issuePair(ctx, u) if err != nil { return nil, nil, err } return u, pair, nil } func (s *Service) Refresh(ctx context.Context, refreshToken string) (*TokenPair, error) { hash := HashRefreshToken(refreshToken) t, err := s.store.GetRefreshToken(ctx, hash) if err != nil { if errors.Is(err, ErrNotFound) { return nil, httpx.Unauthorized("invalid_refresh_token", "refresh token is invalid or expired") } return nil, err } if t.RevokedAt != nil || time.Now().After(t.ExpiresAt) { return nil, httpx.Unauthorized("invalid_refresh_token", "refresh token is invalid or expired") } u, err := s.store.GetUserByID(ctx, t.UserID) if err != nil { return nil, httpx.Unauthorized("invalid_refresh_token", "refresh token is invalid or expired") } if err := s.store.RevokeRefreshToken(ctx, t.ID); err != nil { return nil, err } return s.issuePair(ctx, u) } func (s *Service) Logout(ctx context.Context, refreshToken string) error { hash := HashRefreshToken(refreshToken) t, err := s.store.GetRefreshToken(ctx, hash) if err != nil { if errors.Is(err, ErrNotFound) { return nil } return err } return s.store.RevokeRefreshToken(ctx, t.ID) } func (s *Service) issuePair(ctx context.Context, u *models.User) (*TokenPair, error) { access, err := s.tm.GenerateAccess(u.ID, u.Email) if err != nil { return nil, err } raw, hash, err := NewRefreshToken() if err != nil { return nil, err } rt := &models.RefreshToken{ ID: NewID(), UserID: u.ID, TokenHash: hash, ExpiresAt: time.Now().Add(s.tm.RefreshTTL()), } if err := s.store.CreateRefreshToken(ctx, rt); err != nil { return nil, err } return &TokenPair{ AccessToken: access, RefreshToken: raw, TokenType: "Bearer", ExpiresIn: int64(s.tm.AccessTTL().Seconds()), }, nil }