mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
feat: first commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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) {}
|
||||
Reference in New Issue
Block a user