"""The signed-in user's own account (milestone 490): the time zone every page writes its times in, and their email (milestone 495). Any role reaches it, for themselves only.""" from __future__ import annotations from quart import Blueprint, abort, current_app, redirect, render_template, request, session from steward.auth.middleware import require_role from steward.core import formatting from steward.models.users import User, UserRole account_bp = Blueprint("account", __name__, url_prefix="/account") _SOURCES = {"local": "a Steward password", "ldap": "LDAP", "oidc": "single sign-on"} async def _me(db) -> User: # A share link opens widgets read-only; it has no account. user_id = session.get("user_id") if not user_id: abort(403) user = await db.get(User, user_id) if user is None: abort(403) return user @account_bp.get("/") @require_role(UserRole.viewer) async def account(error: str | None = None, status: int = 200): from steward.core.mail import role_defaults from steward.core.settings import get_all_settings async with current_app.db_sessionmaker() as db: user = await _me(db) defaults = role_defaults(user, await get_all_settings(db)) tz = formatting.zone(user.time_zone) zone_name = user.time_zone or "UTC" return await render_template( "account/index.html", user=user, source=_SOURCES.get(user.auth_source, user.auth_source), zone_name=zone_name, zone_title=zone_name.split("/", 1)[-1].replace("_", " "), zone_label=formatting.tz_label(tz), zones=formatting.zones_by_region(), defaults=defaults, error=error), status @account_bp.post("/") @require_role(UserRole.viewer) async def account_save(): """The page's one form: your time zone, your address and whether you're emailed alerts and the weekly report (milestone 495). Nothing is saved unless all of it is valid.""" from sqlalchemy.exc import IntegrityError from steward.core.audit import log_audit from steward.core.mail import apply_email_form form = await request.form name = (form.get("time_zone") or "").strip() if name not in formatting.zone_names(): return await account(f"{name or 'That'} is not a time zone Steward knows.", 400) try: async with current_app.db_sessionmaker() as db: async with db.begin(): user = await _me(db) changes, error = apply_email_form(user, form) if error: raise ValueError(error) before = user.time_zone user.time_zone = name except ValueError as exc: return await account(str(exc), 400) except IntegrityError: return await account("Another account already uses that address.", 400) session["tz"] = name if before != name: await log_audit(current_app, user.id, user.username, "account.time_zone", detail={"time_zone": name, "was": before or "UTC"}) if changes: await log_audit(current_app, user.id, user.username, "account.email", detail=changes) return redirect("/account/") @account_bp.post("/time-zone/adopt") @require_role(UserRole.viewer) async def adopt_time_zone(): """The browser's zone, saved for an account that has none yet: base.html sends it once, on the first page after a sign-in without one. A zone set on the Account page is never replaced. Answers whether it was saved.""" name = ((await request.get_json(silent=True)) or {}).get("time_zone") or "" if name not in formatting.zone_names(): return {"adopted": False}, 400 async with current_app.db_sessionmaker() as db: async with db.begin(): user = await _me(db) if user.time_zone is not None: return {"adopted": False} user.time_zone = name session["tz"] = name return {"adopted": True}