mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
121 lines
3.0 KiB
Go
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) {}
|