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:
@@ -43,24 +43,28 @@ func toView(s storage.Story) StoryView {
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
SiteTitle string
|
||||
Channels []Channel
|
||||
Active string // slug of active channel, "" for landing
|
||||
Weather Weather
|
||||
Phase string // when set, forces day/dawn/dusk/night and disables the clock-driven phase JS
|
||||
AllSources template.JS // JSON array of {name, channel} for the settings panel
|
||||
SiteTitle string
|
||||
Channels []Channel
|
||||
Active string // slug of active channel, "" for landing
|
||||
Weather Weather
|
||||
Phase string // when set, forces day/dawn/dusk/night and disables the clock-driven phase JS
|
||||
AllSources template.JS // JSON array of {name, channel} for the settings panel
|
||||
AuthEnabled bool // sign-in is available
|
||||
User *SessionUser // nil for anonymous visitors
|
||||
UserPrefs template.JS // signed-in user's stored prefs blob (JSON), or "null"
|
||||
Path string // current request path, for post-login return
|
||||
}
|
||||
|
||||
type channelPage struct {
|
||||
pageData
|
||||
Channel Channel
|
||||
Stories []StoryView
|
||||
Page int
|
||||
HasPrev bool
|
||||
HasNext bool
|
||||
PrevURL string
|
||||
NextURL string
|
||||
Total int
|
||||
Channel Channel
|
||||
Stories []StoryView
|
||||
Page int
|
||||
HasPrev bool
|
||||
HasNext bool
|
||||
PrevURL string
|
||||
NextURL string
|
||||
Total int
|
||||
}
|
||||
|
||||
type indexPage struct {
|
||||
@@ -78,20 +82,32 @@ type channelStat struct {
|
||||
Total int
|
||||
}
|
||||
|
||||
func (s *Server) base() pageData {
|
||||
func (s *Server) base(r *http.Request) pageData {
|
||||
srcJSON, err := json.Marshal(s.sources)
|
||||
if err != nil {
|
||||
srcJSON = []byte("[]")
|
||||
}
|
||||
return pageData{
|
||||
SiteTitle: s.cfg.SiteTitle,
|
||||
Channels: channels,
|
||||
Weather: currentWeather(time.Now()),
|
||||
AllSources: template.JS(srcJSON),
|
||||
d := pageData{
|
||||
SiteTitle: s.cfg.SiteTitle,
|
||||
Channels: channels,
|
||||
Weather: currentWeather(time.Now()),
|
||||
AllSources: template.JS(srcJSON),
|
||||
AuthEnabled: s.auth != nil,
|
||||
UserPrefs: template.JS("null"),
|
||||
Path: r.URL.Path,
|
||||
}
|
||||
if s.auth != nil {
|
||||
if u := s.auth.userFromRequest(r); u != nil {
|
||||
d.User = u
|
||||
if blob, err := storage.GetUserPrefs(u.Sub); err == nil && blob != "" {
|
||||
d.UserPrefs = template.JS(blob)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, _ *http.Request) {
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
const (
|
||||
justPostedLimit = 6
|
||||
latestLimit = 16
|
||||
@@ -137,7 +153,7 @@ func (s *Server) handleIndex(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
data := indexPage{
|
||||
pageData: s.base(),
|
||||
pageData: s.base(r),
|
||||
JustPosted: justPosted,
|
||||
Stats: stats,
|
||||
Latest: latest,
|
||||
@@ -169,7 +185,7 @@ func (s *Server) handleChannel(w http.ResponseWriter, r *http.Request, ch Channe
|
||||
}
|
||||
total, _ := storage.CountClassifiedByChannel(ch.Slug)
|
||||
|
||||
base := s.base()
|
||||
base := s.base(r)
|
||||
base.Active = ch.Slug
|
||||
data := channelPage{
|
||||
pageData: base,
|
||||
@@ -230,7 +246,7 @@ func (s *Server) handleWeatherDemo(w http.ResponseWriter, r *http.Request) {
|
||||
intensity := pickOr(q.Get("intensity"), demoIntensities, "heavy")
|
||||
phase := pickOr(q.Get("phase"), demoPhases, "day")
|
||||
|
||||
base := s.base()
|
||||
base := s.base(r)
|
||||
base.Weather = Weather{Season: seasonForVariant(variant), Variant: variant, Intensity: intensity}
|
||||
base.Phase = phase
|
||||
|
||||
|
||||
Reference in New Issue
Block a user