Mirrors the dashboard's row update: "Sync now" on the account page refreshes the status block in place and shows a toast, instead of navigating to a separate result page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
162 lines
6.3 KiB
Python
162 lines
6.3 KiB
Python
from fastapi import APIRouter, Form, HTTPException, Request, status
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
from app.auth.account import authenticate_self_service
|
|
from app.auth.admin import require_self_service
|
|
from app.auth.csrf import ensure_csrf_token, validate_csrf
|
|
from app.db.repositories import ActivityRepository, SyncRunRepository, UserRepository
|
|
from app.security.credentials import CredentialCipher
|
|
from app.sync.manager import SyncAlreadyRunning
|
|
from app.web.operations import _outcome_toast, _toast_html
|
|
from app.web.routes import templates
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _cipher(request: Request) -> CredentialCipher:
|
|
return CredentialCipher(request.app.state.settings.credential_encryption_key)
|
|
|
|
|
|
def _require_non_empty(value: str, field_name: str) -> None:
|
|
if not value.strip():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"{field_name} must not be empty",
|
|
)
|
|
|
|
|
|
@router.get("/account-login", response_class=HTMLResponse)
|
|
def account_login_page(request: Request):
|
|
return templates.TemplateResponse(request, "account_login.html", {"csrf_token": ensure_csrf_token(request)})
|
|
|
|
|
|
@router.post("/account-login")
|
|
def account_login(
|
|
request: Request,
|
|
csrf_token: str = Form(...),
|
|
email: str = Form(...),
|
|
password: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
cipher = _cipher(request)
|
|
with request.app.state.session_factory() as session:
|
|
user = authenticate_self_service(session, cipher, email, password)
|
|
if user is None:
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"account_login.html",
|
|
{"csrf_token": ensure_csrf_token(request), "error": "Invalid email or password"},
|
|
status_code=401,
|
|
)
|
|
request.session["self_service_user_id"] = user.id
|
|
return RedirectResponse("/account", status_code=303)
|
|
|
|
|
|
@router.post("/account-logout")
|
|
def account_logout(request: Request, csrf_token: str = Form(...)):
|
|
validate_csrf(request, csrf_token)
|
|
request.session.pop("self_service_user_id", None)
|
|
return RedirectResponse("/account-login", status_code=303)
|
|
|
|
|
|
def _get_own_user_or_reauth(request: Request, repository: UserRepository, user_id: int):
|
|
user = repository.get(user_id)
|
|
if user is None:
|
|
request.session.pop("self_service_user_id", None)
|
|
raise HTTPException(status_code=status.HTTP_303_SEE_OTHER, headers={"Location": "/account-login"})
|
|
return user
|
|
|
|
|
|
@router.get("/account", response_class=HTMLResponse)
|
|
def account_detail(request: Request):
|
|
user_id = require_self_service(request)
|
|
with request.app.state.session_factory() as session:
|
|
user = _get_own_user_or_reauth(request, UserRepository(session), user_id)
|
|
activities = ActivityRepository(session).list_pending_for_user(user_id)
|
|
recent_runs = SyncRunRepository(session).list_recent_for_user(user_id, limit=10)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"account/detail.html",
|
|
{
|
|
"csrf_token": ensure_csrf_token(request),
|
|
"user": user,
|
|
"activities": activities,
|
|
"recent_runs": recent_runs,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/account/sync", response_class=HTMLResponse)
|
|
async def account_sync(request: Request, csrf_token: str = Form(...)):
|
|
user_id = require_self_service(request)
|
|
validate_csrf(request, csrf_token)
|
|
try:
|
|
outcome = await request.app.state.sync_manager.sync_user(user_id)
|
|
with request.app.state.session_factory() as session:
|
|
user = UserRepository(session).get(user_id)
|
|
label = user.name if user is not None else "Account"
|
|
message, level = _outcome_toast(outcome, label)
|
|
except SyncAlreadyRunning:
|
|
with request.app.state.session_factory() as session:
|
|
user = UserRepository(session).get(user_id)
|
|
label = user.name if user is not None else "Account"
|
|
message, level = f"{label}: sync already running", "info"
|
|
status_html = (
|
|
templates.get_template("fragments/account_status.html").render(user=user) if user is not None else ""
|
|
)
|
|
return HTMLResponse(status_html + _toast_html(message, level))
|
|
|
|
|
|
@router.get("/account/edit", response_class=HTMLResponse)
|
|
def account_edit_page(request: Request):
|
|
user_id = require_self_service(request)
|
|
cipher = _cipher(request)
|
|
with request.app.state.session_factory() as session:
|
|
user = _get_own_user_or_reauth(request, UserRepository(session), user_id)
|
|
return templates.TemplateResponse(
|
|
request,
|
|
"account/form.html",
|
|
{
|
|
"csrf_token": ensure_csrf_token(request),
|
|
"user": user,
|
|
"mywhoosh_email": cipher.decrypt(user.mywhoosh_email_enc),
|
|
"garmin_email": cipher.decrypt(user.garmin_email_enc),
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/account/edit")
|
|
def account_update(
|
|
request: Request,
|
|
csrf_token: str = Form(...),
|
|
mywhoosh_email: str = Form(""),
|
|
mywhoosh_password: str = Form(""),
|
|
garmin_email: str = Form(""),
|
|
garmin_password: str = Form(""),
|
|
notify_email_enabled: str | None = Form(None),
|
|
notification_email: str = Form(""),
|
|
):
|
|
user_id = require_self_service(request)
|
|
validate_csrf(request, csrf_token)
|
|
_require_non_empty(mywhoosh_email, "mywhoosh_email")
|
|
_require_non_empty(garmin_email, "garmin_email")
|
|
notify_enabled = notify_email_enabled is not None
|
|
if notify_enabled:
|
|
_require_non_empty(notification_email, "notification_email")
|
|
cipher = _cipher(request)
|
|
with request.app.state.session_factory() as session:
|
|
repository = UserRepository(session)
|
|
user = _get_own_user_or_reauth(request, repository, user_id)
|
|
values = {
|
|
"mywhoosh_email_enc": cipher.encrypt(mywhoosh_email.strip()),
|
|
"garmin_email_enc": cipher.encrypt(garmin_email.strip()),
|
|
"notify_email_enabled": notify_enabled,
|
|
"notification_email": notification_email.strip() or None,
|
|
}
|
|
if mywhoosh_password:
|
|
values["mywhoosh_password_enc"] = cipher.encrypt(mywhoosh_password)
|
|
if garmin_password:
|
|
values["garmin_password_enc"] = cipher.encrypt(garmin_password)
|
|
repository.update(user, **values)
|
|
return RedirectResponse("/account", status_code=303)
|