fix(theme): apply per-user theme_pref on login, persist toggle to server
alpine-init.js resolved the theme purely from localStorage, so it never reflected the logged-in user's theme_pref: switching user kept the previous user's theme because localStorage persists per-browser. - base.html injects window.__SERVER_THEME from the session theme (empty when anonymous, preserving OS-preference behaviour on the login page). - alpine-init.js now treats the server value as authoritative for logged-in users (server > localStorage > OS > light) and syncs localStorage to it. - The navbar toggle now persists the choice via POST /auth/set-theme, which updates the session and the user's theme_pref, so it survives navigation and matches on the next login. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""Authentication blueprint - login, logout, profile."""
|
||||
from functools import wraps
|
||||
|
||||
from flask import Blueprint, abort, flash, redirect, render_template, request, session, url_for
|
||||
from flask import Blueprint, abort, flash, jsonify, redirect, render_template, request, session, url_for
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from services.api_client import api_client
|
||||
@@ -133,6 +133,27 @@ def logout():
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@auth_bp.route("/set-theme", methods=["POST"])
|
||||
@login_required
|
||||
def set_theme():
|
||||
"""Persist the user's theme toggle (light/dark) to session and profile.
|
||||
|
||||
Called via AJAX from the navbar theme switch so the choice survives
|
||||
navigation and matches on the next login. Best-effort on the API side:
|
||||
the session is updated regardless so the current browsing stays correct.
|
||||
"""
|
||||
theme = (request.get_json(silent=True) or {}).get("theme", "")
|
||||
if theme not in ("light", "dark"):
|
||||
return jsonify({"error": True, "detail": "invalid theme"}), 400
|
||||
|
||||
session["theme"] = theme
|
||||
try:
|
||||
api_client.put("/api/auth/me", data={"theme_pref": theme})
|
||||
except Exception:
|
||||
pass # Non-fatal: session theme is already updated for this session.
|
||||
return jsonify({"ok": True, "theme": theme})
|
||||
|
||||
|
||||
@auth_bp.route("/profile", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def profile():
|
||||
|
||||
@@ -16,9 +16,21 @@
|
||||
|
||||
/**
|
||||
* Resolve the initial dark-mode state.
|
||||
* Priority: localStorage > system preference > light (default).
|
||||
* Priority: server (per-user theme_pref) > localStorage > system preference > light.
|
||||
*
|
||||
* The server value (window.__SERVER_THEME) is authoritative for logged-in
|
||||
* users: it reflects the theme_pref of whoever is currently authenticated,
|
||||
* so switching user actually switches theme instead of inheriting the
|
||||
* previous user's localStorage value. It is empty for anonymous pages.
|
||||
*/
|
||||
function getInitialDark() {
|
||||
var server = (typeof window !== 'undefined' && window.__SERVER_THEME) || '';
|
||||
if (server === 'dark' || server === 'light') {
|
||||
// Keep localStorage in sync so the rest of the app stays consistent.
|
||||
try { localStorage.setItem(STORAGE_KEY, server); } catch (_) {}
|
||||
return server === 'dark';
|
||||
}
|
||||
|
||||
var stored = null;
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -70,6 +82,7 @@
|
||||
toggle: function () {
|
||||
this.dark = !this.dark;
|
||||
this._apply();
|
||||
this._persist();
|
||||
},
|
||||
|
||||
set: function (dark) {
|
||||
@@ -77,6 +90,24 @@
|
||||
this._apply();
|
||||
},
|
||||
|
||||
// Persist an explicit user choice to the server (session + theme_pref),
|
||||
// so it survives navigation and matches on the next login. Best-effort.
|
||||
_persist: function () {
|
||||
try {
|
||||
var csrf = document.querySelector('meta[name=csrf-token]');
|
||||
fetch('/auth/set-theme', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': (csrf && csrf.content) || ''
|
||||
},
|
||||
body: JSON.stringify({ theme: this.dark ? 'dark' : 'light' })
|
||||
}).catch(function () {});
|
||||
} catch (_) {
|
||||
// Non-fatal: localStorage already holds the choice for this browser.
|
||||
}
|
||||
},
|
||||
|
||||
_apply: function () {
|
||||
if (this.dark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
<!-- Theme CSS Variables -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/themes.css') }}">
|
||||
|
||||
<!-- Authoritative per-user theme from the server session (empty when anonymous) -->
|
||||
<script>window.__SERVER_THEME = "{{ current_theme if current_user else '' }}";</script>
|
||||
|
||||
<!-- Alpine.js Theme Init (before Alpine loads) -->
|
||||
<script src="{{ url_for('static', filename='js/alpine-init.js') }}"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user