feat: session naming, username/persona rename, help page, contrast fixes
- Session name field: PATCH /sessions/{id} endpoint, inline rename button in UI
- Persona rename: inline ✏ toggle form in settings, POST /settings/persona/rename
- Username rename: inline form in settings, POST /settings/username (renames home dir, forces re-login)
- Help page: dedicated /help route replacing modal, collapsible sections
- Per-persona isolation: files.py and session_store.py now scope to correct user/persona
- Contrast/visibility: muted text bumped to slate-400+, session rename btn at 0.4 opacity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,11 +2,14 @@
|
||||
Account settings router.
|
||||
|
||||
Routes:
|
||||
GET /settings → show account settings page (requires auth)
|
||||
POST /settings/password → change password
|
||||
GET /settings → show account settings page (requires auth)
|
||||
POST /settings/password → change password
|
||||
POST /settings/username → rename the user account (forces re-login)
|
||||
POST /settings/persona/rename → rename a persona directory
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import jwt
|
||||
@@ -15,6 +18,9 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password
|
||||
from persona import list_user_personas
|
||||
from config import settings as app_settings
|
||||
|
||||
_SLUG_RE = re.compile(r"^[a-z_][a-z0-9_-]{0,31}$")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -36,7 +42,18 @@ def _settings_page(username: str, personas: list[str], success: str = "", error:
|
||||
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
|
||||
f'''<li>
|
||||
<a href="/{username}/{p}" class="persona-link">{p}</a>
|
||||
<button class="persona-rename-toggle" data-persona="{p}" title="Rename">✏</button>
|
||||
<form class="persona-rename-form" data-persona="{p}"
|
||||
method="POST" action="/settings/persona/rename" style="display:none">
|
||||
<input type="hidden" name="old_name" value="{p}">
|
||||
<input type="text" name="new_name" value="{p}"
|
||||
pattern="[a-z_][a-z0-9_\\-]{{0,31}}" required>
|
||||
<button type="submit">Save</button>
|
||||
<button type="button" class="persona-rename-cancel">Cancel</button>
|
||||
</form>
|
||||
</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 ""
|
||||
@@ -82,3 +99,79 @@ async def change_password(
|
||||
set_password(username, new_password)
|
||||
logger.info("password changed: %s", username)
|
||||
return HTMLResponse(_settings_page(username, personas, success="Password updated successfully."))
|
||||
|
||||
|
||||
@router.post("/settings/username", include_in_schema=False)
|
||||
async def rename_username(
|
||||
request: Request,
|
||||
new_username: str = Form(...),
|
||||
):
|
||||
username = _get_session_user(request)
|
||||
if not username:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
new_username = new_username.strip().lower()
|
||||
|
||||
if not _SLUG_RE.match(new_username):
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
error="Invalid username. Use lowercase letters, digits, _ or - only."))
|
||||
|
||||
if new_username == username:
|
||||
return RedirectResponse("/settings", status_code=302)
|
||||
|
||||
home_root = app_settings.home_root()
|
||||
old_dir = home_root / username
|
||||
new_dir = home_root / new_username
|
||||
|
||||
if new_dir.exists():
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
error=f"Username '{new_username}' is already taken."))
|
||||
|
||||
old_dir.rename(new_dir)
|
||||
logger.info("user renamed: %s → %s", username, new_username)
|
||||
|
||||
# Clear the auth cookie — old JWT now refers to a non-existent user
|
||||
resp = RedirectResponse("/login?msg=username_changed", status_code=302)
|
||||
resp.delete_cookie(COOKIE_NAME)
|
||||
return resp
|
||||
|
||||
|
||||
@router.post("/settings/persona/rename", include_in_schema=False)
|
||||
async def rename_persona(
|
||||
request: Request,
|
||||
old_name: str = Form(...),
|
||||
new_name: str = Form(...),
|
||||
):
|
||||
username = _get_session_user(request)
|
||||
if not username:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
new_name = new_name.strip().lower()
|
||||
|
||||
if not _SLUG_RE.match(new_name):
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
error="Invalid name. Use lowercase letters, digits, _ or - only."))
|
||||
|
||||
if new_name == old_name:
|
||||
return RedirectResponse("/settings", status_code=302)
|
||||
|
||||
persona_root = app_settings.home_root() / username / "persona"
|
||||
old_dir = persona_root / old_name
|
||||
new_dir = persona_root / new_name
|
||||
|
||||
if not old_dir.exists():
|
||||
return HTMLResponse(_settings_page(username, personas, error=f"Persona '{old_name}' not found."))
|
||||
|
||||
if new_dir.exists():
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
error=f"A persona named '{new_name}' already exists."))
|
||||
|
||||
old_dir.rename(new_dir)
|
||||
logger.info("persona renamed: %s/%s → %s", username, old_name, new_name)
|
||||
return RedirectResponse("/settings", status_code=302)
|
||||
|
||||
Reference in New Issue
Block a user