Files
Snip/backend/internal/service/auth_service.go
T
2026-06-15 21:25:57 +07:00

121 lines
3.0 KiB
Go

package service
import (
"context"
"sync"
"time"
"github.com/snip/backend/internal/domain"
"github.com/snip/backend/internal/port"
)
const sessionTTL = 30 * 24 * time.Hour
// AuthService coordinates the OAuth/OIDC login dance and sessions. It supports
// any number of identity providers (Google, generic OIDC) keyed by name.
// providers may be nil on construction and injected later via SetProviders.
type AuthService struct {
mu sync.RWMutex
providers map[string]port.IdentityProvider
users port.UserRepository
sessions port.SessionManager
}
func NewAuthService(providers []port.IdentityProvider, users port.UserRepository, sessions port.SessionManager) *AuthService {
s := &AuthService{users: users, sessions: sessions}
s.setProviders(providers)
return s
}
// SetProviders replaces the provider set; safe to call from a background goroutine.
func (s *AuthService) SetProviders(providers []port.IdentityProvider) {
s.mu.Lock()
defer s.mu.Unlock()
s.setProviders(providers)
}
func (s *AuthService) setProviders(providers []port.IdentityProvider) {
m := make(map[string]port.IdentityProvider, len(providers))
for _, p := range providers {
if p != nil {
m[p.Name()] = p
}
}
s.providers = m
}
// Providers lists configured provider names (e.g. "google", "oidc").
func (s *AuthService) Providers() []string {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]string, 0, len(s.providers))
for name := range s.providers {
out = append(out, name)
}
return out
}
// AuthURL returns the provider's authorization URL for the given state.
func (s *AuthService) AuthURL(provider, state string) (string, error) {
s.mu.RLock()
p, ok := s.providers[provider]
s.mu.RUnlock()
if !ok {
return "", domain.ErrNotFound
}
return p.AuthURL(state), nil
}
// Complete exchanges an auth code, upserts the user and issues a session token.
func (s *AuthService) Complete(ctx context.Context, provider, code string) (*domain.User, string, error) {
s.mu.RLock()
p, ok := s.providers[provider]
s.mu.RUnlock()
if !ok {
return nil, "", domain.ErrNotFound
}
id, err := p.Exchange(ctx, code)
if err != nil {
return nil, "", err
}
user, err := s.users.Upsert(ctx, &domain.User{
Email: id.Email,
Name: displayName(id),
Provider: provider,
Subject: id.Subject,
})
if err != nil {
return nil, "", err
}
token, err := s.sessions.Issue(user.ID, sessionTTL)
if err != nil {
return nil, "", err
}
return user, token, nil
}
// Me resolves a session token back to a user.
func (s *AuthService) Me(ctx context.Context, token string) (*domain.User, error) {
userID, err := s.sessions.Verify(token)
if err != nil {
return nil, domain.ErrUnauthorized
}
return s.users.GetByID(ctx, userID)
}
func displayName(id *port.Identity) string {
if id.Name != "" {
return id.Name
}
if id.Email != "" {
return id.Email
}
return "there"
}
// NoopRecorder discards clicks; handy for tests and the api command (which
// doesn't serve redirects).
type NoopRecorder struct{}
func (NoopRecorder) Record(string) {}