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
+27 -1
View File
@@ -3,9 +3,11 @@ from quart import Blueprint, jsonify, request, session
from fabledassistant.auth import login_required, get_current_user_id
from fabledassistant.services.auth import (
authenticate,
change_password,
create_user,
get_user_by_id,
get_user_count,
is_registration_open,
)
auth_bp = Blueprint("auth", __name__, url_prefix="/api/auth")
@@ -13,6 +15,9 @@ auth_bp = Blueprint("auth", __name__, url_prefix="/api/auth")
@auth_bp.route("/register", methods=["POST"])
async def register():
if not await is_registration_open():
return jsonify({"error": "Registration is closed"}), 403
data = await request.get_json()
username = (data.get("username") or "").strip()
password = data.get("password") or ""
@@ -64,7 +69,28 @@ async def me():
return jsonify(user.to_dict())
@auth_bp.route("/password", methods=["PUT"])
@login_required
async def update_password():
data = await request.get_json()
current = data.get("current_password") or ""
new_pw = data.get("new_password") or ""
if not current:
return jsonify({"error": "Current password is required"}), 400
if len(new_pw) < 8:
return jsonify({"error": "New password must be at least 8 characters"}), 400
uid = get_current_user_id()
success = await change_password(uid, current, new_pw)
if not success:
return jsonify({"error": "Current password is incorrect"}), 403
return jsonify({"status": "ok"})
@auth_bp.route("/status", methods=["GET"])
async def status():
count = await get_user_count()
return jsonify({"has_users": count > 0})
reg_open = await is_registration_open()
return jsonify({"has_users": count > 0, "registration_open": reg_open})