- rename Go module, binary, systemd unit, PG_DATABASE default, frontend package and docs from shared-album to galo - add theme-aware halo emblem and favicon (new ui-branding capability) - add make build-release (versioned, stripped, static binary) and run the binary from the repo root
148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"galo/internal/httpx"
|
|
"galo/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
|
|
}
|