feat: email allowlist management in Settings + Files panel

Settings page gets an editable textarea (POST /settings/email-allowlist)
so users can view and update their per-user regex allowlist without
touching the raw JSON file.

Files panel gains a "Settings" group containing email_allowlist.json as
a raw JSON editor backup — served from home/{user}/ via files.py USER_FILES.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-29 21:56:45 -04:00
parent e0e3170de3
commit db3dd465b2
4 changed files with 85 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ import re
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from persona import persona_path, set_context, validate as validate_persona
from config import settings as _settings
router = APIRouter()
@@ -22,6 +23,9 @@ ALLOWED = {
"HELP.md",
}
# Files served from home/{user}/ instead of persona path
USER_FILES = {"email_allowlist.json"}
def _resolve(user: str, persona: str) -> None:
"""Validate and set context from query params. Raises HTTPException on bad input."""
@@ -32,7 +36,11 @@ def _resolve(user: str, persona: str) -> None:
raise HTTPException(status_code=404, detail=str(e))
def _path(filename: str):
def _path(filename: str, user: str = ""):
if filename in USER_FILES:
if not user:
raise HTTPException(status_code=400, detail="user param required for this file")
return _settings.home_root() / user / filename
if filename not in ALLOWED:
raise HTTPException(status_code=404, detail=f"File not found: {filename}")
return persona_path() / filename
@@ -55,6 +63,16 @@ async def list_files(
"size": st.st_size if st else 0,
"modified": st.st_mtime if st else None,
})
for name in sorted(USER_FILES):
p = _settings.home_root() / user / name
st = p.stat() if p.exists() else None
files.append({
"name": name,
"exists": p.exists(),
"size": st.st_size if st else 0,
"modified": st.st_mtime if st else None,
"scope": "user",
})
return {"files": files}
@@ -65,7 +83,7 @@ async def get_file(
persona: str = Query("inara"),
) -> dict:
_resolve(user, persona)
p = _path(filename)
p = _path(filename, user=user)
if not p.exists():
raise HTTPException(status_code=404, detail=f"{filename} does not exist")
return {"name": filename, "content": p.read_text()}
@@ -83,7 +101,7 @@ async def save_file(
persona: str = Query("inara"),
) -> dict:
_resolve(user, persona)
p = _path(filename)
p = _path(filename, user=user)
p.write_text(req.content)
return {"ok": True, "name": filename, "size": len(req.content)}