mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-19 07:28:46 +07:00
feat: first commit
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// Package di wires concrete adapters into the application core. Each command
|
||||
// builds only what it needs (the frontend command touches no infrastructure).
|
||||
package di
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/snip/backend/internal/adapter/identity"
|
||||
"github.com/snip/backend/internal/adapter/memory"
|
||||
"github.com/snip/backend/internal/adapter/postgres"
|
||||
"github.com/snip/backend/internal/adapter/rediscache"
|
||||
"github.com/snip/backend/internal/adapter/security"
|
||||
"github.com/snip/backend/internal/config"
|
||||
"github.com/snip/backend/internal/httpx/api"
|
||||
"github.com/snip/backend/internal/httpx/frontend"
|
||||
"github.com/snip/backend/internal/httpx/redirect"
|
||||
"github.com/snip/backend/internal/port"
|
||||
"github.com/snip/backend/internal/service"
|
||||
)
|
||||
|
||||
// Container holds config and constructs per-command applications.
|
||||
type Container struct {
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
func New(cfg config.Config) *Container { return &Container{cfg: cfg} }
|
||||
|
||||
// App is a runnable handler plus its resource closers.
|
||||
type App struct {
|
||||
Handler http.Handler
|
||||
closers []func()
|
||||
}
|
||||
|
||||
func (a *App) Close() {
|
||||
for i := len(a.closers) - 1; i >= 0; i-- {
|
||||
a.closers[i]()
|
||||
}
|
||||
}
|
||||
|
||||
type infra struct {
|
||||
links port.LinkRepository
|
||||
users port.UserRepository
|
||||
cache port.LinkCache
|
||||
closers []func()
|
||||
}
|
||||
|
||||
// buildInfra picks the storage adapters based on STORE.
|
||||
func (c *Container) buildInfra(ctx context.Context) (*infra, error) {
|
||||
if c.cfg.Store == "memory" {
|
||||
return &infra{links: memory.NewLinkRepo(), users: memory.NewUserRepo(), cache: memory.NewCache()}, nil
|
||||
}
|
||||
pool, err := postgres.Connect(ctx, c.cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache, err := rediscache.New(ctx, c.cfg.RedisAddr, c.cfg.RedisPassword, c.cfg.RedisDB)
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &infra{
|
||||
links: postgres.NewLinkRepo(pool),
|
||||
users: postgres.NewUserRepo(pool),
|
||||
cache: cache,
|
||||
closers: []func(){pool.Close, func() { _ = cache.Close() }},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildFrontend constructs the static SPA server (no DB/Redis).
|
||||
func (c *Container) BuildFrontend() *App {
|
||||
return &App{Handler: frontend.New(c.cfg.FrontendDist).Handler()}
|
||||
}
|
||||
|
||||
// BuildAPI constructs the /api/v1 server. OIDC provider discovery runs in the
|
||||
// background so the HTTP server (and its health endpoint) starts immediately.
|
||||
// Providers become available once discovery completes; until then the /auth/*
|
||||
// endpoints return an empty provider list and login buttons are hidden.
|
||||
func (c *Container) BuildAPI(ctx context.Context) (*App, error) {
|
||||
inf, err := c.buildInfra(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasher := security.NewBcryptHasher()
|
||||
sessions := security.NewHMACSessions(c.cfg.SessionSecret)
|
||||
linkSvc := service.NewLinkService(inf.links, inf.cache, hasher)
|
||||
authSvc := service.NewAuthService(nil, inf.users, sessions) // providers injected below
|
||||
h := api.New(linkSvc, authSvc, sessions, c.cfg).Handler()
|
||||
|
||||
// Discover OIDC providers in the background; inject them once ready.
|
||||
go func() {
|
||||
providers := c.buildProviders(ctx)
|
||||
authSvc.SetProviders(providers)
|
||||
log.Printf("di: %d identity provider(s) ready", len(providers))
|
||||
}()
|
||||
|
||||
return &App{Handler: h, closers: inf.closers}, nil
|
||||
}
|
||||
|
||||
// BuildRedirect constructs the redirect server and starts the click flusher.
|
||||
func (c *Container) BuildRedirect(ctx context.Context) (*App, error) {
|
||||
inf, err := c.buildInfra(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasher := security.NewBcryptHasher()
|
||||
recorder := service.NewAsyncClickRecorder(inf.links, c.cfg.ClickFlushInterval)
|
||||
recorder.Start(ctx)
|
||||
redirectSvc := service.NewRedirectService(inf.links, inf.cache, hasher, recorder)
|
||||
h := redirect.New(redirectSvc, c.cfg.ShortDomain, c.cfg.PublicURL).Handler()
|
||||
closers := append([]func(){recorder.Stop}, inf.closers...)
|
||||
return &App{Handler: h, closers: closers}, nil
|
||||
}
|
||||
|
||||
// buildProviders configures the available login methods. Each is optional: if
|
||||
// its credentials are absent (or discovery fails) the button simply won't show.
|
||||
func (c *Container) buildProviders(ctx context.Context) []port.IdentityProvider {
|
||||
var providers []port.IdentityProvider
|
||||
callback := func(name string) string {
|
||||
return c.cfg.PublicURL + "/api/v1/auth/" + name + "/callback"
|
||||
}
|
||||
|
||||
if c.cfg.GoogleClientID != "" && c.cfg.GoogleClientSecret != "" {
|
||||
if p := dialProvider(ctx, "google", identity.GoogleIssuer,
|
||||
c.cfg.GoogleClientID, c.cfg.GoogleClientSecret, callback("google")); p != nil {
|
||||
providers = append(providers, p)
|
||||
}
|
||||
}
|
||||
if c.cfg.OIDCIssuer != "" && c.cfg.OIDCClientID != "" {
|
||||
if p := dialProvider(ctx, "oidc", c.cfg.OIDCIssuer,
|
||||
c.cfg.OIDCClientID, c.cfg.OIDCClientSecret, callback("oidc")); p != nil {
|
||||
providers = append(providers, p)
|
||||
}
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
// dialProvider performs OIDC discovery with a short retry, so the api command
|
||||
// can start alongside a still-booting (mock) provider instead of silently
|
||||
// disabling login on a transient connection error.
|
||||
func dialProvider(ctx context.Context, name, issuer, clientID, secret, redirect string) port.IdentityProvider {
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= 15; attempt++ {
|
||||
p, err := identity.NewOIDCProvider(ctx, name, issuer, clientID, secret, redirect, nil)
|
||||
if err == nil {
|
||||
log.Printf("di: %s provider ready (issuer %s)", name, issuer)
|
||||
return p
|
||||
}
|
||||
lastErr = err
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}
|
||||
log.Printf("di: %s provider disabled after retries: %v", name, lastErr)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user