mitbringsl/backend/internal/httpapi/api.go
Tronax 3f187f1ede
Feature: Server-driven auth method discovery + OIDC-only enforcement
Backend:
- New AUTH_PASSWORD_ENABLED flag (default true). When false, email/password
  registration and login return 403; the server enforces OIDC-only login.
- New OIDC_GENERIC_DISPLAY_NAME so the app can show 'Authentik'/'Keycloak'
  instead of a generic 'OIDC' label.
- New public endpoint GET /api/config returns which auth methods the
  server offers (password_enabled + per-provider OIDC capabilities).
  No auth required, so the login screen can query it before logging in.
- .env.example and docker-compose.yml expose the new env vars.

App:
- DTOs + MitbringslApi.getServerConfig() for /api/config.
- AuthViewModel: new 'connect' flow. The user enters the server URL,
  taps 'Verbinden', and the app fetches /api/config. The returned
  ServerAuthConfig drives which login options are shown:
    * password-only -> email/password form
    * OIDC-only     -> OIDC token form
    * both          -> toggle between the two
  If the server offers no method, a clear error is shown.
- AuthScreen: split into ConnectView (server URL) and LoginView (the
  login form matching the server's capabilities). The mode toggle only
  appears when the server offers more than one method.
2026-08-06 10:28:12 +02:00

104 lines
3.5 KiB
Go

package httpapi
import (
"net/http"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/mitbringsl/backend/internal/auth"
"github.com/mitbringsl/backend/internal/config"
"github.com/mitbringsl/backend/internal/store"
)
// API bundles all handler groups and wires the router.
type API struct {
cfg *config.Config
pool *pgxpool.Pool
health *HealthHandler
auth *AuthHandler
lists *ListHandler
ops *OpsHandler
suggest *SuggestHandler
config *ConfigHandler
}
// NewAPI constructs the API with all handler groups.
func NewAPI(cfg *config.Config, pool *pgxpool.Pool) *API {
// Auth stores
users := auth.NewUserStore(pool)
sessions := auth.NewSessionStore(pool, auth.SessionConfig{
TokenBytes: cfg.SessionTokenBytes,
TTL: cfg.SessionTokenTTL,
})
oidcSvc := auth.NewOIDCService(buildProviders(cfg))
if len(oidcSvc.Providers()) == 0 {
oidcSvc = nil
}
// Sync stores
listStore := store.NewListStore(pool)
itemStore := store.NewItemStore(pool)
opStore := store.NewOpStore(pool)
suggestStore := store.NewSuggestStore(pool)
return &API{
cfg: cfg,
pool: pool,
health: &HealthHandler{Pool: pool},
auth: NewAuthHandler(users, sessions, oidcSvc, cfg),
lists: NewListHandler(listStore, itemStore),
ops: NewOpsHandler(opStore, listStore),
suggest: NewSuggestHandler(suggestStore),
config: NewConfigHandler(cfg),
}
}
// buildProviders assembles the OIDC provider map from config. Only enabled
// providers with a non-empty issuer and client_id are included.
func buildProviders(cfg *config.Config) map[string]auth.ProviderConfig {
p := map[string]auth.ProviderConfig{}
if cfg.GoogleOIDC.Enabled && cfg.GoogleOIDC.Issuer != "" && cfg.GoogleOIDC.ClientID != "" {
p["google"] = auth.ProviderConfig{Issuer: cfg.GoogleOIDC.Issuer, ClientID: cfg.GoogleOIDC.ClientID}
}
if cfg.GenericOIDC.Enabled && cfg.GenericOIDC.Issuer != "" && cfg.GenericOIDC.ClientID != "" {
p["generic"] = auth.ProviderConfig{Issuer: cfg.GenericOIDC.Issuer, ClientID: cfg.GenericOIDC.ClientID}
}
return p
}
// Handler returns the fully wired http.Handler with all middleware applied.
func (a *API) Handler() http.Handler {
mux := http.NewServeMux()
// --- public health endpoints (no auth) ---
mux.HandleFunc("GET /healthz", a.health.Healthz)
mux.HandleFunc("GET /readyz", a.health.Readyz)
// --- public server config (auth capabilities, no auth required) ---
mux.HandleFunc("GET /api/config", a.config.Config)
// --- auth endpoints ---
mux.HandleFunc("POST /auth/register", a.auth.Register)
mux.HandleFunc("POST /auth/login", a.auth.Login)
mux.HandleFunc("POST /auth/oidc", a.auth.OIDC)
mux.HandleFunc("POST /auth/logout", a.auth.Logout)
// --- authenticated API endpoints ---
mux.Handle("GET /api/lists", a.RequireAuth(http.HandlerFunc(a.lists.List)))
mux.Handle("POST /api/lists", a.RequireAuth(http.HandlerFunc(a.lists.Create)))
mux.Handle("POST /api/lists/join", a.RequireAuth(http.HandlerFunc(a.lists.Join)))
mux.Handle("GET /api/lists/{id}", a.RequireAuth(http.HandlerFunc(a.lists.Get)))
mux.Handle("POST /api/lists/{id}/invite", a.RequireAuth(http.HandlerFunc(a.lists.Invite)))
mux.Handle("POST /api/lists/{id}/ops", a.RequireAuth(http.HandlerFunc(a.ops.Push)))
mux.Handle("GET /api/lists/{id}/ops", a.RequireAuth(http.HandlerFunc(a.ops.Pull)))
mux.Handle("GET /api/suggestions", a.RequireAuth(http.HandlerFunc(a.suggest.Search)))
return Chain(
mux,
requestIDMiddleware,
loggingMiddleware,
recoverMiddleware,
corsMiddleware(a.cfg),
)
}