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
+46
View File
@@ -3,6 +3,12 @@ import json
from quart import Blueprint, Response, jsonify, request
from fabledassistant.auth import admin_required, login_required, get_current_user_id
from fabledassistant.services.auth import (
delete_user,
is_registration_open,
list_users,
set_registration_open,
)
from fabledassistant.services.backup import (
export_full_backup,
export_user_backup,
@@ -45,3 +51,43 @@ async def restore():
stats = await restore_full_backup(data)
return jsonify({"status": "ok", "stats": stats})
@admin_bp.route("/users", methods=["GET"])
@admin_required
async def get_users():
users = await list_users()
return jsonify({"users": [u.to_dict() for u in users]})
@admin_bp.route("/users/<int:user_id>", methods=["DELETE"])
@admin_required
async def remove_user(user_id: int):
current_uid = get_current_user_id()
if user_id == current_uid:
return jsonify({"error": "Cannot delete your own account"}), 400
deleted = await delete_user(user_id)
if not deleted:
return jsonify({"error": "User not found"}), 404
return jsonify({"status": "ok"})
@admin_bp.route("/registration", methods=["GET"])
@admin_required
async def get_registration():
open_status = await is_registration_open()
return jsonify({"open": open_status})
@admin_bp.route("/registration", methods=["PUT"])
@admin_required
async def toggle_registration():
data = await request.get_json()
open_val = data.get("open")
if open_val is None:
return jsonify({"error": "Missing 'open' field"}), 400
uid = get_current_user_id()
await set_registration_open(uid, bool(open_val))
return jsonify({"status": "ok", "open": bool(open_val)})