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.
104 lines
3.5 KiB
Go
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),
|
|
)
|
|
}
|