Compare commits
4 Commits
d2bf0b5828
...
5cc5123a0c
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cc5123a0c | |||
| 2eb5c51353 | |||
| b325eb3512 | |||
| a7254d8932 |
+1
-1
@@ -93,4 +93,4 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
traefik-net:
|
traefik-net:
|
||||||
external: true
|
external: true
|
||||||
name: root_default
|
name: traefik
|
||||||
|
|||||||
@@ -223,7 +223,10 @@ async def reorder_tasks(
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(RecipeTask)
|
select(RecipeTask)
|
||||||
.where(RecipeTask.id.in_(data.task_ids))
|
.where(RecipeTask.id.in_(data.task_ids))
|
||||||
.options(selectinload(RecipeTask.subtasks))
|
.options(
|
||||||
|
selectinload(RecipeTask.subtasks),
|
||||||
|
selectinload(RecipeTask.version),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
tasks_map = {t.id: t for t in result.scalars().all()}
|
tasks_map = {t.id: t for t in result.scalars().all()}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Authentication blueprint - login, logout, profile."""
|
"""Authentication blueprint - login, logout, profile."""
|
||||||
from functools import wraps
|
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 flask_babel import gettext as _
|
||||||
|
|
||||||
from services.api_client import api_client
|
from services.api_client import api_client
|
||||||
@@ -39,15 +39,32 @@ def role_required(*roles):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _post_login_landing():
|
||||||
|
"""Pick the landing endpoint based on the logged-in user's roles.
|
||||||
|
|
||||||
|
A Maker-only user must not be bounced to /measure (MeasurementTec-only),
|
||||||
|
which would raise a 403 right after login. Order of preference follows the
|
||||||
|
primary operator flow first, then editing, analysis, admin.
|
||||||
|
"""
|
||||||
|
user = session.get("user", {})
|
||||||
|
roles = user.get("roles", [])
|
||||||
|
if "MeasurementTec" in roles:
|
||||||
|
return url_for("measure.select_recipe")
|
||||||
|
if "Maker" in roles:
|
||||||
|
return url_for("maker.recipe_list")
|
||||||
|
if "Metrologist" in roles:
|
||||||
|
return url_for("statistics.dashboard")
|
||||||
|
if user.get("is_admin"):
|
||||||
|
return url_for("admin.user_list")
|
||||||
|
return url_for("auth.profile")
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
"""Login page and form handler."""
|
"""Login page and form handler."""
|
||||||
# Se già loggato, redirect a home
|
# Se già loggato, redirect alla home in base al ruolo
|
||||||
if session.get("api_key"):
|
if session.get("api_key"):
|
||||||
try:
|
return redirect(_post_login_landing())
|
||||||
return redirect(url_for("measure.select_recipe"))
|
|
||||||
except Exception:
|
|
||||||
return redirect(url_for("auth.profile"))
|
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
username = request.form.get("username", "").strip()
|
username = request.form.get("username", "").strip()
|
||||||
@@ -91,11 +108,8 @@ def login():
|
|||||||
|
|
||||||
flash(_("Benvenuto, %(name)s!", name=user.get("display_name", username)), "success")
|
flash(_("Benvenuto, %(name)s!", name=user.get("display_name", username)), "success")
|
||||||
|
|
||||||
# Redirect dopo login
|
# Redirect dopo login (in base al ruolo)
|
||||||
try:
|
return redirect(_post_login_landing())
|
||||||
return redirect(url_for("measure.select_recipe"))
|
|
||||||
except Exception:
|
|
||||||
return redirect(url_for("auth.profile"))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(_("Errore di connessione al server: %(error)s", error=str(e)), "error")
|
flash(_("Errore di connessione al server: %(error)s", error=str(e)), "error")
|
||||||
@@ -119,6 +133,27 @@ def logout():
|
|||||||
return redirect(url_for("auth.login"))
|
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"])
|
@auth_bp.route("/profile", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def profile():
|
def profile():
|
||||||
|
|||||||
@@ -16,9 +16,21 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the initial dark-mode state.
|
* 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() {
|
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;
|
var stored = null;
|
||||||
try {
|
try {
|
||||||
stored = localStorage.getItem(STORAGE_KEY);
|
stored = localStorage.getItem(STORAGE_KEY);
|
||||||
@@ -70,6 +82,7 @@
|
|||||||
toggle: function () {
|
toggle: function () {
|
||||||
this.dark = !this.dark;
|
this.dark = !this.dark;
|
||||||
this._apply();
|
this._apply();
|
||||||
|
this._persist();
|
||||||
},
|
},
|
||||||
|
|
||||||
set: function (dark) {
|
set: function (dark) {
|
||||||
@@ -77,6 +90,24 @@
|
|||||||
this._apply();
|
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 () {
|
_apply: function () {
|
||||||
if (this.dark) {
|
if (this.dark) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
<!-- Theme CSS Variables -->
|
<!-- Theme CSS Variables -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/themes.css') }}">
|
<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) -->
|
<!-- Alpine.js Theme Init (before Alpine loads) -->
|
||||||
<script src="{{ url_for('static', filename='js/alpine-init.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/alpine-init.js') }}"></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user