Add registration control, admin user management, and security hardening

- Registration auto-closes after first user; admin can toggle from /admin/users
- Admin user management view with user list and delete
- Password confirmation on registration form
- Password change in Settings (PUT /api/auth/password)
- Session cookie hardening: HttpOnly, SameSite=Lax, optional Secure flag
- Startup warning when SECRET_KEY is default
- Production deployment docs: reverse proxy, rate limiting, CSP headers
- Fix assist prompt to preserve markdown headings in target sections
- Simplify prod compose networking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 17:49:22 -05:00
parent f2496916f9
commit f77b029943
16 changed files with 809 additions and 60 deletions
+39 -10
View File
@@ -12,7 +12,7 @@
> Include file-level details in the commit body when the change is non-trivial.
## Last Updated
2026-02-11 — Phase 5.2: Tiptap Inline-Preview Editor + Layout Improvements
2026-02-12 — Phase 5.3: Registration Control, User Management, Security Hardening
## Project Overview
Fabled Assistant is a self-hosted note-taking and task-tracking application with
@@ -76,6 +76,12 @@ for AI-assisted features.
`build_context()` returns `(messages, context_meta)` tuple; metadata includes
auto-found note IDs/titles sent to frontend via SSE `context` event before streaming.
Multi-word search splits terms into per-word ILIKE with AND logic (not adjacent match).
- **Reverse proxy required for production:** The app does not terminate TLS. A
reverse proxy (Nginx/Traefik/Caddy) must sit in front of port 5000. Do not
expose the app directly to the internet.
- **Registration auto-closes:** After the first user registers (admin), registration
is closed by default. The admin can toggle it from `/admin/users`. The setting is
stored as the admin user's `registration_open` setting key.
- **No blocking long-running operations:** Any potentially slow operation (model
pulls, LLM calls, URL fetching, etc.) must never block app startup or freeze the
UI. Backend uses SSE streaming for incremental responses (chat messages and model
@@ -91,6 +97,10 @@ for AI-assisted features.
`CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, and `DO $$ BEGIN
CREATE TYPE ... EXCEPTION WHEN duplicate_object` to allow safe re-runs and
fresh database creation.
- **Session cookie security:** `HttpOnly` and `SameSite=Lax` always set. `Secure`
flag controlled by `SECURE_COOKIES` env var (enable when behind TLS).
- **Default SECRET_KEY warning:** App logs a WARNING on startup if the default
`dev-secret-change-me` key is in use.
### High-Level Component Diagram
```
@@ -199,7 +209,7 @@ fabledassistant/
│ ├── __init__.py
│ ├── app.py # Quart app factory: SPA via 404 handler, JSON 404/500 for API, request logging middleware
│ ├── auth.py # Auth decorators: login_required, admin_required, get_current_user_id
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret)
│ ├── config.py # Config from env vars + Docker secrets file support (_read_secret) + SECURE_COOKIES flag
│ ├── models/
│ │ ├── __init__.py # async_session factory, Base, imports all models
│ │ ├── user.py # User model (id, username, email, password_hash, role, created_at)
@@ -210,13 +220,13 @@ fabledassistant/
│ │ ├── __init__.py
│ │ ├── api.py # /api blueprint with /health endpoint (public)
│ │ ├── auth.py # /api/auth blueprint: register, login, logout, me, status
│ │ ├── admin.py # /api/admin blueprint: backup (user/full), restore (admin only)
│ │ ├── admin.py # /api/admin blueprint: backup, restore, user management, registration toggle (admin only)
│ │ ├── chat.py # /api/chat blueprint: conversations CRUD, SSE message streaming (all @login_required)
│ │ ├── notes.py # /api/notes CRUD + wikilinks + backlinks (all @login_required)
│ │ ├── tasks.py # /api/tasks CRUD + PATCH status (all @login_required)
│ │ └── settings.py # /api/settings GET/PUT — per-user settings (@login_required)
│ ├── services/
│ │ ├── auth.py # Auth business logic: hash_password, verify_password, create_user, authenticate, get_user_by_id
│ │ ├── auth.py # Auth business logic: hash_password, verify_password, create_user, authenticate, get_user_by_id, is_registration_open, list_users, delete_user, set_registration_open
│ │ ├── backup.py # Backup/restore: export_full_backup, export_user_backup, restore_full_backup
│ │ ├── notes.py # CRUD with user_id isolation, is_task filter, convert, backlinks, search_notes_for_context
│ │ ├── llm.py # Ollama interaction: build_context with user_id, streaming, URL fetching
@@ -268,7 +278,8 @@ fabledassistant/
│ │ └── markdownSerializer.ts # Tiptap JSON → markdown serializer: handles all StarterKit nodes + marks
│ ├── views/
│ │ ├── LoginView.vue # Login form with error display, link to register
│ │ ├── RegisterView.vue # Register form (username, password, email), first-user setup
│ │ ├── RegisterView.vue # Register form with password confirmation; shows "closed" message when registration disabled
│ │ ├── UserManagementView.vue # Admin user management: registration toggle, user list with delete
│ │ ├── ChatView.vue # Dedicated /chat page: responsive sidebar (overlay on mobile), bubble messages, note picker, context pills
│ │ ├── HomeView.vue # Landing page: recent notes + tasks + recent chats
│ │ ├── SettingsView.vue # Settings page: assistant name, model catalog, data export/restore (admin)
@@ -294,7 +305,7 @@ fabledassistant/
│ │ ├── PaginationBar.vue # Prev/next + page numbers
│ │ └── ToastNotification.vue # Fixed-position toast container with close button, warning support
│ └── router/
│ └── index.ts # Routes: /, /login, /register, /notes/*, /tasks/*, /chat, /chat/:id, /settings; beforeEach auth guard
│ └── index.ts # Routes: /, /login, /register, /notes/*, /tasks/*, /chat, /chat/:id, /settings, /admin/users; beforeEach auth guard
└── public/
```
@@ -303,13 +314,18 @@ fabledassistant/
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/health` | Health check (public) |
| GET | `/api/auth/status` | Check if any users exist (public) |
| POST | `/api/auth/register` | Register new user (first user becomes admin) |
| GET | `/api/auth/status` | Check if any users exist + registration open status (public) |
| POST | `/api/auth/register` | Register new user (first user becomes admin; returns 403 if registration closed) |
| POST | `/api/auth/login` | Login with username/password |
| POST | `/api/auth/logout` | Logout (clear session) |
| GET | `/api/auth/me` | Get current user info |
| PUT | `/api/auth/password` | Change password (body: `{current_password, new_password}`) |
| GET | `/api/admin/backup` | Export backup (`?scope=user` for own data, full requires admin) |
| POST | `/api/admin/restore` | Restore from JSON backup (admin only) |
| GET | `/api/admin/users` | List all users (admin only) |
| DELETE | `/api/admin/users/:id` | Delete a user (admin only, cannot delete self) |
| GET | `/api/admin/registration` | Get registration open/closed status (admin only) |
| PUT | `/api/admin/registration` | Toggle registration open/closed (admin only, body: `{open: bool}`) |
| GET | `/api/notes` | List notes (params: `q`, `tag`, `sort`, `order`, `limit`, `offset`; defaults to `is_task=false` — plain notes only; `?is_task=true` for tasks, `?all=true` for everything) |
| POST | `/api/notes` | Create note (body: `{title, body, status?, priority?, due_date?}` — tags auto-extracted) |
| GET | `/api/notes/tags` | List all tags from notes table (param: `q` for filter) |
@@ -583,6 +599,16 @@ When adding a new migration, follow these conventions:
- [x] **Accept trailing newline:** Accepted AI suggestions always end with a newline for clean separation
- [x] **Wider content area:** Max-width increased from 720px to 960px across all views
### Phase 5.3 — Registration Control, User Management, Security Hardening ✓
- [x] **Registration auto-closes:** After the first user registers (admin), registration is closed by default
- [x] **Registration toggle:** Admin can open/close registration from `/admin/users`
- [x] **Password confirmation:** Register form requires password confirmation with inline validation
- [x] **Admin user management:** `/admin/users` view with user list and delete (admin only)
- [x] **Session cookie hardening:** `HttpOnly`, `SameSite=Lax` always on; `Secure` via `SECURE_COOKIES` env var
- [x] **Default SECRET_KEY warning:** Logs WARNING on startup if default key is in use
- [x] **Password change:** `PUT /api/auth/password` endpoint + Settings UI section
- [x] **Production deployment docs:** README documents reverse proxy, rate limiting, CSP headers, SECRET_KEY, registration behavior
### Future / Stretch
- Tagging/labeling system with LLM-suggested tags
- Calendar/timeline view for tasks
@@ -598,7 +624,7 @@ When adding a new migration, follow these conventions:
- To reset database: `docker compose down -v && docker compose up --build`
## Current Status
**Phase:** Phase 5.2 complete. Tiptap Inline-Preview Editor + Layout Improvements.
**Phase:** Phase 5.3 complete. Registration Control, User Management, Security Hardening.
- Full note-taking and task-tracking CRUD with markdown, wikilinks, backlinks, tags
- **Tiptap WYSIWYG editor** with inline formatting preview, markdown round-trip, paste handling
- **Tag/wikilink autocomplete** via `@tiptap/suggestion` with heading disambiguation
@@ -611,7 +637,10 @@ When adding a new migration, follow these conventions:
- Context pills with promote/exclude controls; note picker in chat input
- Save assistant messages as notes (LLM-titled, chat-tagged); summarize conversations as notes
- Dedicated `/chat` page with responsive sidebar (overlay on mobile)
- Settings page: assistant name, model catalog, data export/restore (admin)
- Settings page: assistant name, model catalog, password change, data export/restore (admin)
- **Registration control**: auto-closes after first user, admin toggle, password confirmation
- **Admin user management**: `/admin/users` with user list + delete
- **Session cookie hardening**: HttpOnly, SameSite=Lax, optional Secure flag
- **App-wide layout fix**: navbar always visible, all views fit within viewport
- **Responsive design**: hamburger menu, mobile touch targets, responsive breakpoints
- **Docker Swarm production stack** with secrets, network isolation, health checks, resource limits