- Add /settings page with password change form and personas list - Add /help dedicated page (replaces help modal); renders HELP.md with collapsible sections, dark theme, back link to active persona - Add 👤 account button and convert ? button to link in header - Remove help modal HTML and ~55 lines of modal JS from main app - Register settings and help routers in main.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""
|
|
Account settings router.
|
|
|
|
Routes:
|
|
GET /settings → show account settings page (requires auth)
|
|
POST /settings/password → change password
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import jwt
|
|
from fastapi import APIRouter, Form, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
|
from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password
|
|
from persona import list_user_personas
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
_STATIC = Path(__file__).parent.parent / "static"
|
|
|
|
|
|
def _get_session_user(request: Request) -> str | None:
|
|
token = request.cookies.get(COOKIE_NAME)
|
|
if not token:
|
|
return None
|
|
try:
|
|
return decode_token(token)
|
|
except jwt.InvalidTokenError:
|
|
return None
|
|
|
|
|
|
def _settings_page(username: str, personas: list[str], success: str = "", error: str = "") -> str:
|
|
html = (_STATIC / "settings.html").read_text()
|
|
html = html.replace("{{ username }}", username)
|
|
persona_items = "\n".join(
|
|
f'<li><a href="/{username}/{p}">{p}</a></li>' for p in personas
|
|
)
|
|
html = html.replace("{{ persona_items }}", persona_items or "<li><em>No personas yet.</em></li>")
|
|
back_persona = personas[0] if personas else ""
|
|
html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/")
|
|
if success:
|
|
html = html.replace("<!-- SUCCESS -->", f'<p class="success">{success}</p>')
|
|
if error:
|
|
html = html.replace("<!-- ERROR -->", f'<p class="error">{error}</p>')
|
|
return html
|
|
|
|
|
|
@router.get("/settings", include_in_schema=False)
|
|
async def settings_page(request: Request):
|
|
username = _get_session_user(request)
|
|
if not username:
|
|
return RedirectResponse("/login", status_code=302)
|
|
personas = list_user_personas(username)
|
|
return HTMLResponse(_settings_page(username, personas))
|
|
|
|
|
|
@router.post("/settings/password", include_in_schema=False)
|
|
async def change_password(
|
|
request: Request,
|
|
current_password: str = Form(...),
|
|
new_password: str = Form(...),
|
|
confirm_password: str = Form(...),
|
|
):
|
|
username = _get_session_user(request)
|
|
if not username:
|
|
return RedirectResponse("/login", status_code=302)
|
|
|
|
personas = list_user_personas(username)
|
|
|
|
if not check_credentials(username, current_password):
|
|
return HTMLResponse(_settings_page(username, personas, error="Current password is incorrect."))
|
|
|
|
if len(new_password) < 8:
|
|
return HTMLResponse(_settings_page(username, personas, error="New password must be at least 8 characters."))
|
|
|
|
if new_password != confirm_password:
|
|
return HTMLResponse(_settings_page(username, personas, error="New passwords do not match."))
|
|
|
|
set_password(username, new_password)
|
|
logger.info("password changed: %s", username)
|
|
return HTMLResponse(_settings_page(username, personas, success="Password updated successfully."))
|