chore(v2): restructure monorepo to src/ layout with uv
Aligns the repo with the python-project-spec-design.md template chosen for V2.0.0. Big move, no logic changes. The 3 pre-existing test failures (test_recipes::test_update_recipe, test_recipes:: test_recipe_versioning, test_tasks::test_reorder_tasks, plus the client test_save_measurement_proxy) survive unchanged. Layout changes - server/ -> src/backend/ - server/middleware/ -> src/backend/api/middleware/ - server/routers/ -> src/backend/api/routers/ - server/models/ -> src/backend/models/orm/ - server/schemas/ -> src/backend/models/api/ - server/uploads/ -> uploads/ (project root, mounted volume) - server/tests/ -> src/backend/tests/ - client/ -> src/frontend/flask_app/ (Flask kept; React deroga is documented in CLAUDE.md, justified by tablet UX, USB caliper/barcode workflow and Fabric.js integration) Tooling - pyproject.toml: monorepo with [project] core deps and optional-dependencies server / client / dev. Replaces both server/requirements.txt and client/requirements.txt. - uv.lock + .python-version (3.11) committed for reproducible builds. - Dockerfile (root, backend) and Dockerfile.frontend rewritten to use uv sync --frozen --no-dev --extra server|client; legacy Dockerfiles preserved as Dockerfile.legacy for reference but excluded from build context via .dockerignore. - docker-compose.dev.yml + docker-compose.yml: build context now ".", dockerfile pointing to the root files. Code adjustments forced by the move - Every "from config|database|models|schemas|services|routers|middleware import ..." rewritten to its src.backend.* equivalent (50+ files including indented inline imports inside test bodies). - src/backend/migrations/env.py: insert project root into sys.path so alembic can resolve src.backend.* imports regardless of cwd. - src/backend/config.py: env_file ../../.env (was ../.env), upload_path resolves project root via parents[2]. - src/backend/tests/conftest.py + tests: import ... from src.backend.* instead of bare names; old per-directory pytest.ini files removed in favor of root pyproject.toml [tool.pytest.ini_options]. - .gitignore: uploads/ at root, src/frontend/flask_app/static/css/ tailwind.css path; .dockerignore tightened. - CLAUDE.md: rewrote sections "Layout del repository", "Comandi di Sviluppo", "Database & Migrations", "Test", "i18n", and all path references throughout the architecture sections. Verified - uv lock resolves 77 packages; uv sync --extra server --extra client --extra dev installs cleanly. - uv run pytest: 171 passed, 4 pre-existing failures. - uv run alembic -c src/backend/migrations/alembic.ini check loads config and metadata (errors only on the absent local MySQL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""Authentication blueprint - login, logout, profile."""
|
||||
from functools import wraps
|
||||
|
||||
from flask import Blueprint, abort, flash, redirect, render_template, request, session, url_for
|
||||
from flask_babel import gettext as _
|
||||
|
||||
from services.api_client import api_client
|
||||
|
||||
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to require login for protected routes."""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get("api_key"):
|
||||
flash(_("Effettua il login per continuare"), "warning")
|
||||
return redirect(url_for("auth.login"))
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
|
||||
def role_required(*roles):
|
||||
"""Decorator to require one of the specified roles.
|
||||
|
||||
Usage:
|
||||
@role_required("MeasurementTec", "Metrologist")
|
||||
def my_view(): ...
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
user = session.get("user", {})
|
||||
user_roles = user.get("roles", [])
|
||||
if not any(r in user_roles for r in roles):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
return decorator
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
"""Login page and form handler."""
|
||||
# Se già loggato, redirect a home
|
||||
if session.get("api_key"):
|
||||
try:
|
||||
return redirect(url_for("measure.select_recipe"))
|
||||
except Exception:
|
||||
return redirect(url_for("auth.profile"))
|
||||
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
|
||||
if not username or not password:
|
||||
flash(_("Inserisci username e password"), "error")
|
||||
return render_template("auth/login.html")
|
||||
|
||||
try:
|
||||
response = api_client.post("/api/auth/login", data={"username": username, "password": password})
|
||||
|
||||
if response.get("error"):
|
||||
flash(response.get("detail", _("Credenziali non valide")), "error")
|
||||
return render_template("auth/login.html")
|
||||
|
||||
# Salva in sessione
|
||||
session["api_key"] = response["api_key"]
|
||||
session["user"] = response["user"]
|
||||
session["user_id"] = response["user"]["id"]
|
||||
|
||||
# Applica preferenze utente
|
||||
user = response["user"]
|
||||
if user.get("language_pref"):
|
||||
session["language"] = user["language_pref"]
|
||||
if user.get("theme_pref"):
|
||||
session["theme"] = user["theme_pref"]
|
||||
|
||||
# Carica logo aziendale dalle impostazioni di sistema
|
||||
settings_resp = api_client.get("/api/settings")
|
||||
if not settings_resp.get("error"):
|
||||
logo_path = settings_resp.get("company_logo_path")
|
||||
if logo_path:
|
||||
session["company_logo"] = logo_path
|
||||
|
||||
flash(_("Benvenuto, %(name)s!", name=user.get("display_name", username)), "success")
|
||||
|
||||
# Redirect dopo login
|
||||
try:
|
||||
return redirect(url_for("measure.select_recipe"))
|
||||
except Exception:
|
||||
return redirect(url_for("auth.profile"))
|
||||
|
||||
except Exception as e:
|
||||
flash(_("Errore di connessione al server: %(error)s", error=str(e)), "error")
|
||||
return render_template("auth/login.html")
|
||||
|
||||
return render_template("auth/login.html")
|
||||
|
||||
|
||||
@auth_bp.route("/logout", methods=["GET", "POST"])
|
||||
def logout():
|
||||
"""Clear session and redirect to login."""
|
||||
# Invalida il token sul server
|
||||
if session.get("api_key"):
|
||||
try:
|
||||
api_client.post("/api/auth/logout")
|
||||
except Exception:
|
||||
pass # Ignora errori durante logout
|
||||
|
||||
session.clear()
|
||||
flash(_("Logout effettuato"), "info")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@auth_bp.route("/profile", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def profile():
|
||||
"""User profile page - change display name, language, theme."""
|
||||
if request.method == "POST":
|
||||
display_name = request.form.get("display_name", "").strip()
|
||||
language_pref = request.form.get("language_pref", "").strip()
|
||||
theme_pref = request.form.get("theme_pref", "").strip()
|
||||
|
||||
try:
|
||||
data = {}
|
||||
if display_name:
|
||||
data["display_name"] = display_name
|
||||
if language_pref:
|
||||
data["language_pref"] = language_pref
|
||||
if theme_pref:
|
||||
data["theme_pref"] = theme_pref
|
||||
|
||||
response = api_client.put("/api/auth/me", data=data)
|
||||
|
||||
if response.get("error"):
|
||||
flash(response.get("detail", _("Errore durante l'aggiornamento del profilo")), "error")
|
||||
else:
|
||||
# Aggiorna sessione
|
||||
session["user"] = response
|
||||
if language_pref:
|
||||
session["language"] = language_pref
|
||||
if theme_pref:
|
||||
session["theme"] = theme_pref
|
||||
|
||||
flash(_("Profilo aggiornato con successo"), "success")
|
||||
return redirect(url_for("auth.profile"))
|
||||
|
||||
except Exception as e:
|
||||
flash(_("Errore di connessione al server: %(error)s", error=str(e)), "error")
|
||||
|
||||
# GET - carica dati profilo
|
||||
user_data = api_client.get("/api/auth/me")
|
||||
if user_data.get("error"):
|
||||
flash(_("Errore nel caricamento del profilo: %(error)s", error=user_data.get("detail", "")), "error")
|
||||
user_data = session.get("user", {})
|
||||
else:
|
||||
session["user"] = user_data
|
||||
|
||||
return render_template("auth/profile.html", user=user_data)
|
||||
Reference in New Issue
Block a user