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:
@@ -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)})
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user