refactor(server): encapsulate service initialization in ServerBuilder

Move all service creation (I18n, Email, MagicLink, Session, Config)
from main.go into ServerBuilder.Build(), making main.go minimal and
self-contained.

- ServerBuilder now only requires WithDB() and WithTenantProvider()
- AuthProvider and Authorizer have sensible CE defaults
- Rename DynamicAuthProvider → auth.Provider for simplicity
- Remove unused With* methods for internal services
This commit is contained in:
Benjamin
2026-01-16 14:43:58 +01:00
parent b0ef28b0ae
commit 493d915fa7
5 changed files with 159 additions and 283 deletions

View File

@@ -8,20 +8,14 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/btouchard/ackify-ce/backend/internal/application/services"
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/auth"
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/database"
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/email"
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/i18n"
"github.com/btouchard/ackify-ce/backend/internal/infrastructure/tenant"
"github.com/btouchard/ackify-ce/backend/pkg/config"
"github.com/btouchard/ackify-ce/backend/pkg/logger"
"github.com/btouchard/ackify-ce/backend/pkg/web"
webauth "github.com/btouchard/ackify-ce/backend/pkg/web/auth"
)
// Build-time variables set via ldflags
@@ -59,78 +53,12 @@ func main() {
log.Fatalf("failed to initialize tenant provider: %v", err)
}
// Create repositories needed for auth
oauthSessionRepo := database.NewOAuthSessionRepository(db, tenantProvider)
configRepo := database.NewConfigRepository(db, tenantProvider)
magicLinkRepo := database.NewMagicLinkRepository(db)
// Create ConfigService (needed for dynamic auth config)
encryptionKey := cfg.OAuth.CookieSecret
configService := services.NewConfigService(configRepo, cfg, encryptionKey)
// Initialize config from DB or ENV
err = tenant.WithTenantContextFromProvider(ctx, db, tenantProvider, func(txCtx context.Context) error {
return configService.Initialize(txCtx)
})
if err != nil {
logger.Logger.Warn("Failed to initialize config service, using ENV config", "error", err)
}
// Create i18n service
i18nService, err := i18n.NewI18n(getLocalesDir())
if err != nil {
log.Fatalf("Failed to initialize i18n: %v", err)
}
// Create email renderer and sender if SMTP is configured
var emailSender email.Sender
var emailRenderer *email.Renderer
if cfg.Mail.Host != "" {
emailRenderer = email.NewRenderer(getTemplatesDir(), cfg.App.BaseURL, cfg.App.Organisation,
cfg.Mail.FromName, cfg.Mail.From, cfg.Mail.DefaultLocale, i18nService)
emailSender = email.NewSMTPSender(cfg.Mail, emailRenderer)
}
// Create MagicLinkService
magicLinkService := services.NewMagicLinkService(services.MagicLinkServiceConfig{
Repository: magicLinkRepo,
EmailSender: emailSender,
I18n: i18nService,
BaseURL: cfg.App.BaseURL,
AppName: cfg.App.Organisation,
RateLimitPerEmail: cfg.Auth.MagicLinkRateLimitEmail,
RateLimitPerIP: cfg.Auth.MagicLinkRateLimitIP,
})
// Create a SessionService (always needed for session management)
sessionService := auth.NewSessionService(auth.SessionServiceConfig{
CookieSecret: cfg.OAuth.CookieSecret,
SecureCookies: cfg.App.SecureCookies,
SessionRepo: oauthSessionRepo,
})
// Create DynamicAuthProvider (unified auth for OIDC + MagicLink)
authProvider := webauth.NewDynamicAuthProvider(webauth.DynamicAuthProviderConfig{
ConfigProvider: configService,
SessionService: sessionService,
MagicLinkService: magicLinkService,
BaseURL: cfg.App.BaseURL,
})
// Create authorizer
authorizer := webauth.NewSimpleAuthorizer(cfg.App.AdminEmails, cfg.App.OnlyAdminCanCreate)
// === Build Server ===
// All services (I18n, Email, MagicLink, Config, Session) and
// default providers (DynamicAuthProvider, SimpleAuthorizer) are created internally.
server, err := web.NewServerBuilder(cfg, frontend, Version).
WithDB(db).
WithTenantProvider(tenantProvider).
WithAuthProvider(authProvider).
WithAuthorizer(authorizer).
WithConfigService(configService).
WithI18nService(i18nService).
WithEmailSender(emailSender).
WithEmailRenderer(emailRenderer).
WithMagicLinkService(magicLinkService).
Build(ctx)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
@@ -158,49 +86,3 @@ func main() {
log.Println("Community Edition server exited")
}
func getTemplatesDir() string {
if envDir := os.Getenv("ACKIFY_TEMPLATES_DIR"); envDir != "" {
return envDir
}
if execPath, err := os.Executable(); err == nil {
execDir := filepath.Dir(execPath)
defaultDir := filepath.Join(execDir, "templates")
if _, err := os.Stat(defaultDir); err == nil {
return defaultDir
}
}
possiblePaths := []string{"templates", "./templates"}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return "templates"
}
func getLocalesDir() string {
if envDir := os.Getenv("ACKIFY_LOCALES_DIR"); envDir != "" {
return envDir
}
if execPath, err := os.Executable(); err == nil {
execDir := filepath.Dir(execPath)
defaultDir := filepath.Join(execDir, "locales")
if _, err := os.Stat(defaultDir); err == nil {
return defaultDir
}
}
possiblePaths := []string{"locales", "./locales"}
for _, path := range possiblePaths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return "locales"
}

View File

@@ -34,7 +34,6 @@ var (
ErrInvalidCategory = errors.New("invalid configuration category")
)
// configRepository defines the interface for config storage
type configRepository interface {
GetByCategory(ctx context.Context, category models.ConfigCategory) (*models.TenantConfig, error)
GetAll(ctx context.Context) ([]*models.TenantConfig, error)

View File

@@ -22,34 +22,32 @@ import (
"github.com/btouchard/ackify-ce/backend/pkg/types"
)
// ConfigProvider provides dynamic configuration for auth.
type ConfigProvider interface {
type configProvider interface {
GetConfig() *models.MutableConfig
}
// MagicLinkService defines magic link operations.
type MagicLinkService interface {
type magicLinkService interface {
RequestMagicLink(ctx context.Context, email, redirectTo, ip, userAgent, locale string) error
VerifyMagicLink(ctx context.Context, token, ip, userAgent string) (*models.MagicLinkToken, error)
VerifyReminderAuthToken(ctx context.Context, token, ip, userAgent string) (*models.MagicLinkToken, error)
CreateReminderAuthToken(ctx context.Context, email, docID string) (string, error)
}
// DynamicAuthProviderConfig holds configuration for creating a DynamicAuthProvider.
type DynamicAuthProviderConfig struct {
ConfigProvider ConfigProvider
// ProviderConfig holds a configuration for creating a Provider.
type ProviderConfig struct {
ConfigProvider configProvider
SessionService *infraAuth.SessionService
MagicLinkService MagicLinkService
MagicLinkService magicLinkService
BaseURL string
}
// DynamicAuthProvider implements providers.AuthProvider with dynamic config.
// It reads OIDC/MagicLink configuration from ConfigProvider on each call,
// Provider implements providers.AuthProvider with dynamic config.
// It reads OIDC/MagicLink configuration from configProvider on each call,
// supporting hot-reload of authentication settings.
type DynamicAuthProvider struct {
configProvider ConfigProvider
type Provider struct {
configProvider configProvider
sessionService *infraAuth.SessionService
magicLinkService MagicLinkService
magicLinkService magicLinkService
baseURL string
// Cache for oauth2.Config to avoid recreating on every request
@@ -59,9 +57,9 @@ type DynamicAuthProvider struct {
cachedOIDCCfg models.OIDCConfig
}
// NewDynamicAuthProvider creates a new dynamic auth provider.
func NewDynamicAuthProvider(cfg DynamicAuthProviderConfig) *DynamicAuthProvider {
return &DynamicAuthProvider{
// NewAuthProvider creates a new dynamic auth provider.
func NewAuthProvider(cfg ProviderConfig) *Provider {
return &Provider{
configProvider: cfg.ConfigProvider,
sessionService: cfg.SessionService,
magicLinkService: cfg.MagicLinkService,
@@ -71,30 +69,30 @@ func NewDynamicAuthProvider(cfg DynamicAuthProviderConfig) *DynamicAuthProvider
// === Session Management ===
func (p *DynamicAuthProvider) GetCurrentUser(r *http.Request) (*types.User, error) {
func (p *Provider) GetCurrentUser(r *http.Request) (*types.User, error) {
return p.sessionService.GetUser(r)
}
func (p *DynamicAuthProvider) SetCurrentUser(w http.ResponseWriter, r *http.Request, user *types.User) error {
func (p *Provider) SetCurrentUser(w http.ResponseWriter, r *http.Request, user *types.User) error {
return p.sessionService.SetUser(w, r, user)
}
func (p *DynamicAuthProvider) Logout(w http.ResponseWriter, r *http.Request) {
func (p *Provider) Logout(w http.ResponseWriter, r *http.Request) {
p.sessionService.Logout(w, r)
}
func (p *DynamicAuthProvider) IsConfigured() bool {
func (p *Provider) IsConfigured() bool {
return p.IsOIDCEnabled() || p.IsMagicLinkEnabled()
}
// === OIDC Authentication ===
func (p *DynamicAuthProvider) IsOIDCEnabled() bool {
func (p *Provider) IsOIDCEnabled() bool {
cfg := p.configProvider.GetConfig()
return cfg.OIDC.Enabled && cfg.OIDC.ClientID != "" && cfg.OIDC.ClientSecret != ""
}
func (p *DynamicAuthProvider) StartOIDC(w http.ResponseWriter, r *http.Request, nextURL string) string {
func (p *Provider) StartOIDC(w http.ResponseWriter, r *http.Request, nextURL string) string {
if !p.IsOIDCEnabled() {
logger.Logger.Error("StartOIDC called but OIDC is not enabled")
return ""
@@ -144,7 +142,7 @@ func (p *DynamicAuthProvider) StartOIDC(w http.ResponseWriter, r *http.Request,
oauth2.SetAuthURLParam("code_challenge_method", "S256"))
}
func (p *DynamicAuthProvider) startOIDCWithoutPKCE(w http.ResponseWriter, r *http.Request, nextURL string, oauthConfig *oauth2.Config) string {
func (p *Provider) startOIDCWithoutPKCE(w http.ResponseWriter, r *http.Request, nextURL string, oauthConfig *oauth2.Config) string {
randPart := securecookie.GenerateRandomKey(20)
token := base64.RawURLEncoding.EncodeToString(randPart)
state := token + ":" + base64.RawURLEncoding.EncodeToString([]byte(nextURL))
@@ -165,7 +163,7 @@ func (p *DynamicAuthProvider) startOIDCWithoutPKCE(w http.ResponseWriter, r *htt
return oauthConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("prompt", promptParam))
}
func (p *DynamicAuthProvider) VerifyOIDCState(w http.ResponseWriter, r *http.Request, stateToken string) bool {
func (p *Provider) VerifyOIDCState(w http.ResponseWriter, r *http.Request, stateToken string) bool {
session, _ := p.sessionService.GetSession(r)
stored, _ := session.Values["oauth_state"].(string)
@@ -182,7 +180,7 @@ func (p *DynamicAuthProvider) VerifyOIDCState(w http.ResponseWriter, r *http.Req
return false
}
func (p *DynamicAuthProvider) HandleOIDCCallback(ctx context.Context, w http.ResponseWriter, r *http.Request, code, state string) (*types.User, string, error) {
func (p *Provider) HandleOIDCCallback(ctx context.Context, w http.ResponseWriter, r *http.Request, code, state string) (*types.User, string, error) {
if !p.IsOIDCEnabled() {
return nil, "/", fmt.Errorf("OIDC is not enabled")
}
@@ -252,7 +250,7 @@ func (p *DynamicAuthProvider) HandleOIDCCallback(ctx context.Context, w http.Res
return user, nextURL, nil
}
func (p *DynamicAuthProvider) GetOIDCLogoutURL() string {
func (p *Provider) GetOIDCLogoutURL() string {
cfg := p.configProvider.GetConfig()
if cfg.OIDC.LogoutURL == "" {
return ""
@@ -260,7 +258,7 @@ func (p *DynamicAuthProvider) GetOIDCLogoutURL() string {
return cfg.OIDC.LogoutURL + "?continue=" + p.baseURL
}
func (p *DynamicAuthProvider) IsAllowedDomain(email string) bool {
func (p *Provider) IsAllowedDomain(email string) bool {
cfg := p.configProvider.GetConfig()
if cfg.OIDC.AllowedDomain == "" {
return true
@@ -276,12 +274,12 @@ func (p *DynamicAuthProvider) IsAllowedDomain(email string) bool {
// === MagicLink Authentication ===
func (p *DynamicAuthProvider) IsMagicLinkEnabled() bool {
func (p *Provider) IsMagicLinkEnabled() bool {
cfg := p.configProvider.GetConfig()
return cfg.MagicLink.Enabled && cfg.SMTP.Host != ""
}
func (p *DynamicAuthProvider) RequestMagicLink(ctx context.Context, email, redirectTo, ip, userAgent, locale string) error {
func (p *Provider) RequestMagicLink(ctx context.Context, email, redirectTo, ip, userAgent, locale string) error {
if !p.IsMagicLinkEnabled() {
return fmt.Errorf("MagicLink is not enabled")
}
@@ -291,7 +289,7 @@ func (p *DynamicAuthProvider) RequestMagicLink(ctx context.Context, email, redir
return p.magicLinkService.RequestMagicLink(ctx, email, redirectTo, ip, userAgent, locale)
}
func (p *DynamicAuthProvider) VerifyMagicLink(ctx context.Context, token, ip, userAgent string) (*providers.MagicLinkResult, error) {
func (p *Provider) VerifyMagicLink(ctx context.Context, token, ip, userAgent string) (*providers.MagicLinkResult, error) {
if p.magicLinkService == nil {
return nil, fmt.Errorf("MagicLink service not configured")
}
@@ -308,7 +306,7 @@ func (p *DynamicAuthProvider) VerifyMagicLink(ctx context.Context, token, ip, us
}, nil
}
func (p *DynamicAuthProvider) VerifyReminderAuthToken(ctx context.Context, token, ip, userAgent string) (*providers.MagicLinkResult, error) {
func (p *Provider) VerifyReminderAuthToken(ctx context.Context, token, ip, userAgent string) (*providers.MagicLinkResult, error) {
if p.magicLinkService == nil {
return nil, fmt.Errorf("MagicLink service not configured")
}
@@ -325,7 +323,7 @@ func (p *DynamicAuthProvider) VerifyReminderAuthToken(ctx context.Context, token
}, nil
}
func (p *DynamicAuthProvider) CreateReminderAuthToken(ctx context.Context, email, docID string) (string, error) {
func (p *Provider) CreateReminderAuthToken(ctx context.Context, email, docID string) (string, error) {
if p.magicLinkService == nil {
return "", fmt.Errorf("MagicLink service not configured")
}
@@ -334,7 +332,7 @@ func (p *DynamicAuthProvider) CreateReminderAuthToken(ctx context.Context, email
// === Internal helpers ===
func (p *DynamicAuthProvider) getOAuthConfig() *oauth2.Config {
func (p *Provider) getOAuthConfig() *oauth2.Config {
cfg := p.configProvider.GetConfig()
p.mu.RLock()
@@ -372,14 +370,14 @@ func (p *DynamicAuthProvider) getOAuthConfig() *oauth2.Config {
return p.cachedOAuthCfg
}
func (p *DynamicAuthProvider) configMatches(cfg models.OIDCConfig) bool {
func (p *Provider) configMatches(cfg models.OIDCConfig) bool {
return p.cachedOIDCCfg.ClientID == cfg.ClientID &&
p.cachedOIDCCfg.ClientSecret == cfg.ClientSecret &&
p.cachedOIDCCfg.AuthURL == cfg.AuthURL &&
p.cachedOIDCCfg.TokenURL == cfg.TokenURL
}
func (p *DynamicAuthProvider) parseUserInfo(resp *http.Response) (*types.User, error) {
func (p *Provider) parseUserInfo(resp *http.Response) (*types.User, error) {
var rawUser map[string]interface{}
body, err := io.ReadAll(resp.Body)
if err != nil {
@@ -436,4 +434,4 @@ func subtleConstantTimeCompare(a, b string) bool {
}
// Compile-time interface check
var _ providers.AuthProvider = (*DynamicAuthProvider)(nil)
var _ providers.AuthProvider = (*Provider)(nil)

View File

@@ -5,7 +5,7 @@ import (
"context"
"strings"
"github.com/btouchard/ackify-ce/backend/pkg/web"
"github.com/btouchard/ackify-ce/backend/pkg/providers"
)
// SimpleAuthorizer is an authorization implementation based on a list of admin emails.
@@ -30,13 +30,13 @@ func NewSimpleAuthorizer(adminEmails []string, onlyAdminCanCreate bool) *SimpleA
}
}
// IsAdmin implements web.Authorizer.
// IsAdmin implements providers.Authorizer.
func (a *SimpleAuthorizer) IsAdmin(_ context.Context, userEmail string) bool {
normalized := strings.ToLower(strings.TrimSpace(userEmail))
return a.adminEmails[normalized]
}
// CanCreateDocument implements web.Authorizer.
// CanCreateDocument implements providers.Authorizer.
func (a *SimpleAuthorizer) CanCreateDocument(ctx context.Context, userEmail string) bool {
if !a.onlyAdminCanCreate {
return true
@@ -45,4 +45,4 @@ func (a *SimpleAuthorizer) CanCreateDocument(ctx context.Context, userEmail stri
}
// Compile-time interface check.
var _ web.Authorizer = (*SimpleAuthorizer)(nil)
var _ providers.Authorizer = (*SimpleAuthorizer)(nil)

View File

@@ -28,6 +28,7 @@ import (
"github.com/btouchard/ackify-ce/backend/pkg/crypto"
"github.com/btouchard/ackify-ce/backend/pkg/logger"
"github.com/btouchard/ackify-ce/backend/pkg/storage"
webauth "github.com/btouchard/ackify-ce/backend/pkg/web/auth"
sdk "github.com/btouchard/shm/sdk/golang"
)
@@ -52,8 +53,10 @@ type Server struct {
}
// ServerBuilder allows dependency injection for extensibility.
// AuthProvider and Authorizer are REQUIRED and must be provided.
// QuotaEnforcer and AuditLogger have sensible defaults for CE.
// DB and TenantProvider are REQUIRED.
// AuthProvider and Authorizer have sensible CE defaults (AuthProvider, SimpleAuthorizer).
// QuotaEnforcer and AuditLogger have sensible CE defaults (NoLimit, LogOnly).
// All technical services (I18n, Email, MagicLink, Reminder, Config) are created internally.
type ServerBuilder struct {
cfg *config.Config
frontend embed.FS
@@ -62,21 +65,22 @@ type ServerBuilder struct {
// Core infrastructure (required)
db *sql.DB
tenantProvider tenant.Provider
signer *crypto.Ed25519Signer
// Capability providers (auth and authorizer are REQUIRED)
// Capability providers (all have CE defaults)
authProvider AuthProvider
authorizer Authorizer
quotaEnforcer QuotaEnforcer
auditLogger AuditLogger
// Optional infrastructure
// Internal infrastructure (created by Build)
signer *crypto.Ed25519Signer
i18nService *i18n.I18n
emailSender email.Sender
emailRenderer *email.Renderer
storageProvider storage.Provider
sessionService *auth.SessionService
// Core services (created internally or injected)
// Internal services (created by Build)
magicLinkService *services.MagicLinkService
signatureService *services.SignatureService
documentService *services.DocumentService
@@ -95,44 +99,18 @@ func NewServerBuilder(cfg *config.Config, frontend embed.FS, version string) *Se
}
}
// WithDB injects a database connection.
// WithDB injects a database connection (REQUIRED).
func (b *ServerBuilder) WithDB(db *sql.DB) *ServerBuilder {
b.db = db
return b
}
// WithTenantProvider injects a tenant provider.
// WithTenantProvider injects a tenant provider (REQUIRED).
func (b *ServerBuilder) WithTenantProvider(tp tenant.Provider) *ServerBuilder {
b.tenantProvider = tp
return b
}
// WithSigner injects a cryptographic signer.
func (b *ServerBuilder) WithSigner(signer *crypto.Ed25519Signer) *ServerBuilder {
b.signer = signer
return b
}
// WithI18nService injects an i18n service.
func (b *ServerBuilder) WithI18nService(i18n *i18n.I18n) *ServerBuilder {
b.i18nService = i18n
return b
}
// WithEmailSender injects an email sender.
func (b *ServerBuilder) WithEmailSender(sender email.Sender) *ServerBuilder {
b.emailSender = sender
return b
}
// WithEmailRenderer injects an email renderer.
func (b *ServerBuilder) WithEmailRenderer(renderer *email.Renderer) *ServerBuilder {
b.emailRenderer = renderer
return b
}
// === Capability Providers ===
// WithAuthProvider injects an authentication provider (REQUIRED).
func (b *ServerBuilder) WithAuthProvider(provider AuthProvider) *ServerBuilder {
b.authProvider = provider
@@ -157,62 +135,31 @@ func (b *ServerBuilder) WithAuditLogger(logger AuditLogger) *ServerBuilder {
return b
}
// WithMagicLinkService injects a magic link service.
func (b *ServerBuilder) WithMagicLinkService(service *services.MagicLinkService) *ServerBuilder {
b.magicLinkService = service
return b
}
// WithSignatureService injects a signature service.
func (b *ServerBuilder) WithSignatureService(service *services.SignatureService) *ServerBuilder {
b.signatureService = service
return b
}
// WithDocumentService injects a document service.
func (b *ServerBuilder) WithDocumentService(service *services.DocumentService) *ServerBuilder {
b.documentService = service
return b
}
// WithAdminService injects an admin service.
func (b *ServerBuilder) WithAdminService(service *services.AdminService) *ServerBuilder {
b.adminService = service
return b
}
// WithWebhookService injects a webhook service.
func (b *ServerBuilder) WithWebhookService(service *services.WebhookService) *ServerBuilder {
b.webhookService = service
return b
}
// WithReminderService injects a reminder service.
func (b *ServerBuilder) WithReminderService(service *services.ReminderAsyncService) *ServerBuilder {
b.reminderService = service
return b
}
// WithConfigService injects a configuration service.
func (b *ServerBuilder) WithConfigService(service *services.ConfigService) *ServerBuilder {
b.configService = service
return b
}
// Build constructs the server with all dependencies.
func (b *ServerBuilder) Build(ctx context.Context) (*Server, error) {
if err := b.validateProviders(); err != nil {
return nil, err
}
b.setDefaultProviders()
if err := b.initializeInfrastructure(); err != nil {
return nil, err
}
repos := b.createRepositories()
// Initialize services that depend on repos
if err := b.initializeConfigService(ctx, repos); err != nil {
return nil, err
}
b.initializeMagicLinkService(repos)
b.initializeSessionService(repos)
// Now we can set default providers (they depend on services above)
b.setDefaultProviders()
b.initializeCoreServices(repos)
b.initializeReminderService(repos)
if err := b.initializeTelemetry(ctx); err != nil {
return nil, err
}
@@ -227,10 +174,7 @@ func (b *ServerBuilder) Build(ctx context.Context) (*Server, error) {
return nil, err
}
b.initializeCoreServices(repos)
b.initializeConfigService(ctx, repos)
magicLinkWorker := b.initializeMagicLinkCleanupWorker(ctx)
b.initializeReminderService(repos)
sessionWorker, err := b.initializeSessionWorker(ctx, repos)
if err != nil {
@@ -263,17 +207,29 @@ func (b *ServerBuilder) Build(ctx context.Context) (*Server, error) {
// validateProviders checks that required providers are set.
func (b *ServerBuilder) validateProviders() error {
if b.authProvider == nil {
return errors.New("authProvider is required: use WithAuthProvider()")
if b.db == nil {
return errors.New("database is required: use WithDB()")
}
if b.authorizer == nil {
return errors.New("authorizer is required: use WithAuthorizer()")
if b.tenantProvider == nil {
return errors.New("tenantProvider is required: use WithTenantProvider()")
}
return nil
}
// setDefaultProviders sets default implementations for optional providers.
// Must be called AFTER initializeConfigService, initializeMagicLinkService, and initializeSessionService.
func (b *ServerBuilder) setDefaultProviders() {
if b.authProvider == nil {
b.authProvider = webauth.NewAuthProvider(webauth.ProviderConfig{
ConfigProvider: b.configService,
SessionService: b.sessionService,
MagicLinkService: b.magicLinkService,
BaseURL: b.cfg.App.BaseURL,
})
}
if b.authorizer == nil {
b.authorizer = webauth.NewSimpleAuthorizer(b.cfg.App.AdminEmails, b.cfg.App.OnlyAdminCanCreate)
}
if b.quotaEnforcer == nil {
b.quotaEnforcer = NewNoLimitQuotaEnforcer()
}
@@ -282,19 +238,38 @@ func (b *ServerBuilder) setDefaultProviders() {
}
}
// initializeInfrastructure initializes signer and storage provider.
// i18n and emailSender are expected to be injected from main.go.
// initializeInfrastructure initializes signer, i18n, email and storage.
func (b *ServerBuilder) initializeInfrastructure() error {
var err error
if b.signer == nil {
b.signer, err = crypto.NewEd25519Signer()
if err != nil {
return fmt.Errorf("failed to initialize signer: %w", err)
}
// Signer
b.signer, err = crypto.NewEd25519Signer()
if err != nil {
return fmt.Errorf("failed to initialize signer: %w", err)
}
if b.storageProvider == nil && b.cfg.Storage.IsEnabled() {
// I18n
b.i18nService, err = i18n.NewI18n(getLocalesDir())
if err != nil {
return fmt.Errorf("failed to initialize i18n: %w", err)
}
// Email (only if SMTP is configured)
if b.cfg.Mail.Host != "" {
b.emailRenderer = email.NewRenderer(
getTemplatesDir(),
b.cfg.App.BaseURL,
b.cfg.App.Organisation,
b.cfg.Mail.FromName,
b.cfg.Mail.From,
b.cfg.Mail.DefaultLocale,
b.i18nService,
)
b.emailSender = email.NewSMTPSender(b.cfg.Mail, b.emailRenderer)
}
// Storage
if b.cfg.Storage.IsEnabled() {
provider, err := storage.NewProvider(b.cfg.Storage)
if err != nil {
return fmt.Errorf("failed to initialize storage provider: %w", err)
@@ -319,6 +294,7 @@ type repositories struct {
webhookDelivery *database.WebhookDeliveryRepository
oauthSession *database.OAuthSessionRepository
config *database.ConfigRepository
magicLink services.MagicLinkRepository
}
// createRepositories creates all repository instances.
@@ -333,6 +309,7 @@ func (b *ServerBuilder) createRepositories() *repositories {
webhookDelivery: database.NewWebhookDeliveryRepository(b.db, b.tenantProvider),
oauthSession: database.NewOAuthSessionRepository(b.db, b.tenantProvider),
config: database.NewConfigRepository(b.db, b.tenantProvider),
magicLink: database.NewMagicLinkRepository(b.db),
}
}
@@ -395,29 +372,51 @@ func (b *ServerBuilder) initializeEmailWorker(ctx context.Context, repos *reposi
// initializeCoreServices initializes signature, document, admin, and webhook services.
func (b *ServerBuilder) initializeCoreServices(repos *repositories) {
if b.signatureService == nil {
b.signatureService = services.NewSignatureService(repos.signature, repos.document, b.signer)
b.signatureService.SetChecksumConfig(&b.cfg.Checksum)
}
if b.documentService == nil {
b.documentService = services.NewDocumentService(repos.document, repos.expectedSigner, &b.cfg.Checksum)
}
if b.adminService == nil {
b.adminService = services.NewAdminService(repos.document, repos.expectedSigner)
}
if b.webhookService == nil {
b.webhookService = services.NewWebhookService(repos.webhook, repos.webhookDelivery)
}
b.signatureService = services.NewSignatureService(repos.signature, repos.document, b.signer)
b.signatureService.SetChecksumConfig(&b.cfg.Checksum)
b.documentService = services.NewDocumentService(repos.document, repos.expectedSigner, &b.cfg.Checksum)
b.adminService = services.NewAdminService(repos.document, repos.expectedSigner)
b.webhookService = services.NewWebhookService(repos.webhook, repos.webhookDelivery)
}
// initializeConfigService is a no-op when configService is injected from main.go.
// Kept for API compatibility.
func (b *ServerBuilder) initializeConfigService(_ context.Context, _ *repositories) {
// configService is expected to be injected and initialized from main.go
// initializeConfigService creates and initializes the configuration service.
func (b *ServerBuilder) initializeConfigService(ctx context.Context, repos *repositories) error {
encryptionKey := b.cfg.OAuth.CookieSecret
b.configService = services.NewConfigService(repos.config, b.cfg, encryptionKey)
// Initialize config from DB or ENV
err := tenant.WithTenantContextFromProvider(ctx, b.db, b.tenantProvider, func(txCtx context.Context) error {
return b.configService.Initialize(txCtx)
})
if err != nil {
logger.Logger.Warn("Failed to initialize config service, using ENV config", "error", err)
}
return nil
}
// initializeMagicLinkService creates the magic link service.
func (b *ServerBuilder) initializeMagicLinkService(repos *repositories) {
b.magicLinkService = services.NewMagicLinkService(services.MagicLinkServiceConfig{
Repository: repos.magicLink,
EmailSender: b.emailSender,
I18n: b.i18nService,
BaseURL: b.cfg.App.BaseURL,
AppName: b.cfg.App.Organisation,
RateLimitPerEmail: b.cfg.Auth.MagicLinkRateLimitEmail,
RateLimitPerIP: b.cfg.Auth.MagicLinkRateLimitIP,
})
}
// initializeSessionService creates the session service for auth.
func (b *ServerBuilder) initializeSessionService(repos *repositories) {
b.sessionService = auth.NewSessionService(auth.SessionServiceConfig{
CookieSecret: b.cfg.OAuth.CookieSecret,
SecureCookies: b.cfg.App.SecureCookies,
SessionRepo: repos.oauthSession,
})
}
// initializeMagicLinkCleanupWorker starts the cleanup worker for expired magic link tokens.
// magicLinkService is expected to be injected from main.go.
func (b *ServerBuilder) initializeMagicLinkCleanupWorker(ctx context.Context) *workers.MagicLinkCleanupWorker {
magicLinkWorker := workers.NewMagicLinkCleanupWorker(b.magicLinkService, 1*time.Hour, b.db, b.tenantProvider)
go magicLinkWorker.Start(ctx)
@@ -426,16 +425,14 @@ func (b *ServerBuilder) initializeMagicLinkCleanupWorker(ctx context.Context) *w
// initializeReminderService initializes reminder service.
func (b *ServerBuilder) initializeReminderService(repos *repositories) {
if b.reminderService == nil {
b.reminderService = services.NewReminderAsyncService(
repos.expectedSigner,
repos.reminder,
repos.emailQueue,
b.magicLinkService,
b.i18nService,
b.cfg.App.BaseURL,
)
}
b.reminderService = services.NewReminderAsyncService(
repos.expectedSigner,
repos.reminder,
repos.emailQueue,
b.magicLinkService,
b.i18nService,
b.cfg.App.BaseURL,
)
}
// initializeSessionWorker initializes OAuth session cleanup worker.
@@ -465,7 +462,7 @@ func (b *ServerBuilder) buildRouter(repos *repositories, whPublisher *services.W
DB: b.db,
TenantProvider: b.tenantProvider,
// Capability providers (AuthProvider handles OIDC + MagicLink dynamically)
// Capability providers (Provider handles OIDC + MagicLink dynamically)
AuthProvider: b.authProvider,
Authorizer: b.authorizer,
SignatureService: b.signatureService,