mitbringsl/backend/internal/config/config.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

101 lines
3.8 KiB
Go

// Package config holds all runtime configuration for the backend.
// Values are parsed from environment variables via struct tags.
package config
import (
"fmt"
"time"
"github.com/caarlos0/env/v11"
)
// Config is the single source of runtime configuration.
type Config struct {
// HTTPAddr is the address the HTTP server listens on, e.g. ":8080".
HTTPAddr string `env:"HTTP_ADDR" envDefault:":8080"`
// AppEnv: "development" or "production".
AppEnv string `env:"APP_ENV" envDefault:"development"`
// LogLevel: debug | info | warn | error.
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
// PublicBaseURL is the externally reachable URL (scheme + host), no trailing slash.
// Used e.g. for OIDC redirect URIs. Example: "https://mitbringsl.example.com".
PublicBaseURL string `env:"PUBLIC_BASE_URL" envDefault:"http://localhost:8080"`
// Database
DatabaseURL string `env:"DATABASE_URL,required"`
DBMaxOpenConns int32 `env:"DB_MAX_OPEN_CONNS" envDefault:"25"`
DBMaxIdleConns int32 `env:"DB_MAX_IDLE_CONNS" envDefault:"5"`
DBMaxLifetime time.Duration `env:"DB_MAX_CONN_LIFETIME" envDefault:"30m"`
// Session token config (opaque tokens issued by this backend after login).
SessionTokenBytes int `env:"SESSION_TOKEN_BYTES" envDefault:"32"` // 256-bit
SessionTokenTTL time.Duration `env:"SESSION_TOKEN_TTL" envDefault:"720h"` // 30 days
SessionCookieName string `env:"SESSION_COOKIE_NAME" envDefault:"mitbringsl_session"`
// AuthPasswordEnabled controls whether email/password registration and
// login are offered. Set to false to enforce OIDC-only login.
AuthPasswordEnabled bool `env:"AUTH_PASSWORD_ENABLED" envDefault:"true"`
// OIDC providers. Both optional; enable per provider.
GoogleOIDC GoogleOIDCConfig
GenericOIDC GenericOIDCConfig
// CORS allowed origins (comma-separated). Empty = no CORS headers.
CORSAllowedOrigins []string `env:"CORS_ALLOWED_ORIGINS" envSeparator:","`
}
// GoogleOIDCConfig for "Sign in with Google".
type GoogleOIDCConfig struct {
Enabled bool `env:"OIDC_GOOGLE_ENABLED" envDefault:"false"`
ClientID string `env:"OIDC_GOOGLE_CLIENT_ID"` // the audience the verifier accepts
Issuer string `env:"OIDC_GOOGLE_ISSUER" envDefault:"https://accounts.google.com"`
}
// GenericOIDCConfig for any standards-compliant OIDC IdP (Keycloak, Authentik, ...).
type GenericOIDCConfig struct {
Enabled bool `env:"OIDC_GENERIC_ENABLED" envDefault:"false"`
Issuer string `env:"OIDC_GENERIC_ISSUER"`
ClientID string `env:"OIDC_GENERIC_CLIENT_ID"`
// DisplayName is shown to users in the app, e.g. "Authentik" or "Keycloak".
// Defaults to "OIDC" when empty.
DisplayName string `env:"OIDC_GENERIC_DISPLAY_NAME" envDefault:"OIDC"`
}
// Load reads configuration from environment variables and validates basic invariants.
func Load() (*Config, error) {
var cfg Config
if err := env.Parse(&cfg); err != nil {
return nil, fmt.Errorf("parse env: %w", err)
}
if err := cfg.validate(); err != nil {
return nil, err
}
return &cfg, nil
}
// validate enforces invariants that env parsing alone can't express.
func (c *Config) validate() error {
if c.GoogleOIDC.Enabled {
if c.GoogleOIDC.ClientID == "" {
return fmt.Errorf("OIDC_GOOGLE_ENABLED=true requires OIDC_GOOGLE_CLIENT_ID")
}
if c.GoogleOIDC.Issuer == "" {
return fmt.Errorf("OIDC_GOOGLE_ENABLED=true requires OIDC_GOOGLE_ISSUER")
}
}
if c.GenericOIDC.Enabled {
if c.GenericOIDC.ClientID == "" {
return fmt.Errorf("OIDC_GENERIC_ENABLED=true requires OIDC_GENERIC_CLIENT_ID")
}
if c.GenericOIDC.Issuer == "" {
return fmt.Errorf("OIDC_GENERIC_ENABLED=true requires OIDC_GENERIC_ISSUER")
}
}
return nil
}
// IsProduction reports whether the app runs in production mode.
func (c *Config) IsProduction() bool { return c.AppEnv == "production" }