Add optional Authentik (OIDC) sign-in with server-side preference sync
Signed-in users get their preferences (hidden feeds, weather location, weather toggle) stored server-side keyed by their OIDC subject and synced across devices. Anonymous visitors keep using browser localStorage, so the site stays public. First sign-in migrates existing localStorage prefs up. - config: [web.auth] section (issuer, client_id/secret, redirect, session_secret) - storage: user_preferences table + Get/PutUserPrefs - web/auth: OIDC code flow, HMAC-signed session cookie, CSRF state + nonce - web/prefs_api: GET/PUT /api/preferences (auth-gated, 64KB cap) - frontend: prefs.js sync layer seeds localStorage from server, pushes on write - header: sign-in / account control OIDC discovery is non-fatal at boot: if Authentik is down, Pete serves anonymously rather than refusing to start.
This commit is contained in:
@@ -22,10 +22,23 @@ type Config struct {
|
||||
|
||||
// WebConfig controls the read-only HTTP interface (news.parodia.dev style).
|
||||
type WebConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
ListenAddr string `toml:"listen_addr"` // e.g. ":8080" or "127.0.0.1:8080"
|
||||
SiteTitle string `toml:"site_title"` // display name in the header
|
||||
BaseURL string `toml:"base_url"` // public URL (used in metadata only)
|
||||
Enabled bool `toml:"enabled"`
|
||||
ListenAddr string `toml:"listen_addr"` // e.g. ":8080" or "127.0.0.1:8080"
|
||||
SiteTitle string `toml:"site_title"` // display name in the header
|
||||
BaseURL string `toml:"base_url"` // public URL (used in metadata only)
|
||||
Auth AuthConfig `toml:"auth"` // optional OIDC sign-in (Authentik)
|
||||
}
|
||||
|
||||
// AuthConfig wires Pete's web UI to an OIDC provider (our Authentik instance).
|
||||
// When enabled, signed-in users get their preferences stored server-side keyed
|
||||
// by the OIDC subject; anonymous visitors keep using browser localStorage.
|
||||
type AuthConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Issuer string `toml:"issuer"` // e.g. https://authentik.parodia.dev/application/o/pete/
|
||||
ClientID string `toml:"client_id"`
|
||||
ClientSecret string `toml:"client_secret"`
|
||||
RedirectURL string `toml:"redirect_url"` // e.g. https://news.parodia.dev/auth/callback
|
||||
SessionSecret string `toml:"session_secret"` // HMAC key for signing the session cookie
|
||||
}
|
||||
|
||||
type MatrixConfig struct {
|
||||
@@ -73,7 +86,7 @@ type SourceConfig struct {
|
||||
// Language, when set, drops feed items whose per-item language tag is
|
||||
// present and does not match (prefix). Useful for multilingual feeds
|
||||
// like Politico Europe that publish English + French side-by-side.
|
||||
Language string `toml:"language"`
|
||||
Language string `toml:"language"`
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
@@ -124,6 +137,16 @@ func (c *Config) validate() error {
|
||||
return fmt.Errorf("storage.db_path is required")
|
||||
}
|
||||
|
||||
if c.Web.Auth.Enabled {
|
||||
a := c.Web.Auth
|
||||
if a.Issuer == "" || a.ClientID == "" || a.ClientSecret == "" || a.RedirectURL == "" {
|
||||
return fmt.Errorf("web.auth requires issuer, client_id, client_secret, and redirect_url when enabled")
|
||||
}
|
||||
if len(a.SessionSecret) < 16 {
|
||||
return fmt.Errorf("web.auth.session_secret must be at least 16 characters")
|
||||
}
|
||||
}
|
||||
|
||||
for i, s := range c.Sources {
|
||||
if s.Name == "" {
|
||||
return fmt.Errorf("sources[%d].name is required", i)
|
||||
|
||||
Reference in New Issue
Block a user