glo/routes/admin.py
2026-11-03 00:22:48 +00:00

269 lines
8.0 KiB
Python

from .common import (
ROLES,
STATUSES,
now_iso,
parse_iso,
utc_now,
csrf_valid,
rate_limited,
require_moderator,
require_admin,
audit,
hash_identifier,
safe_json,
jsonify,
get_db,
db_write,
)
def register_admin_routes(app):
@app.route("/api/admin/users")
def admin_list_users():
moderator, error = require_moderator()
if error:
return error
with get_db() as db:
rows = db.execute(
"""
SELECT
u.id,
u.username,
u.role,
u.status,
u.restricted_until,
u.created_at,
u.last_login_at,
COUNT(m.id) AS message_count
FROM users AS u
LEFT JOIN messages AS m
ON m.user_id = u.id
GROUP BY u.id
ORDER BY
CASE u.role
WHEN 'admin' THEN 0
WHEN 'moderator' THEN 1
ELSE 2
END,
u.username COLLATE NOCASE
"""
).fetchall()
return jsonify(
{
"users": [
{
"id": row["id"],
"username": row["username"],
"role": row["role"],
"status": row["status"],
"restricted_until": row["restricted_until"],
"created_at": row["created_at"],
"last_login_at": row["last_login_at"],
"message_count": row["message_count"],
}
for row in rows
],
"requested_by": moderator["username"],
}
)
@app.route("/api/admin/users/<int:user_id>", methods=["PATCH"])
def admin_update_user(user_id: int):
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
admin, error = require_admin()
if error:
return error
limited = rate_limited("admin")
if limited:
return limited
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
with get_db() as db:
target = db.execute(
"""
SELECT
id,
username,
role,
status,
restricted_until
FROM users
WHERE id = ?
""",
(user_id,),
).fetchone()
if not target:
return jsonify({"error": "User not found."}), 404
if target["id"] == admin["id"]:
return jsonify({"error": "You cannot modify your own account here."}), 400
role = data.get("role", target["role"])
status = data.get("status", target["status"])
restricted_until = data.get("restricted_until", target["restricted_until"])
if role not in ROLES:
return jsonify({"error": "Invalid role."}), 400
if status not in STATUSES:
return jsonify({"error": "Invalid account status."}), 400
if status == "restricted" and restricted_until:
parsed = parse_iso(restricted_until)
if not parsed or parsed <= utc_now():
return jsonify({"error": "restricted_until must be a future ISO timestamp."}), 400
elif status != "restricted":
restricted_until = None
if target["role"] == "admin" and role != "admin":
with get_db() as db:
count = db.execute(
"""
SELECT COUNT(*) AS count
FROM users
WHERE role = 'admin'
AND status = 'active'
"""
).fetchone()["count"]
if count <= 1:
return jsonify({"error": "At least one active administrator is required."}), 400
is_admin_value = 1 if role == "admin" else 0
is_active = 1 if status in {"active", "restricted"} else 0
db_write(
"""
UPDATE users
SET
role = ?,
status = ?,
restricted_until = ?,
is_admin = ?,
is_active = ?
WHERE id = ?
""",
(role, status, restricted_until, is_admin_value, is_active, user_id),
)
if status == "blacklisted":
db_write(
"""
INSERT OR REPLACE INTO blacklist_entries (
kind,
value_hash,
reason,
created_at,
created_by_user_id
) VALUES ('user', ?, ?, ?, ?)
""",
(
hash_identifier(str(user_id)),
"Account blacklisted by administrator",
now_iso(),
admin["id"],
),
)
if target["status"] == "blacklisted" and status != "blacklisted":
db_write(
"DELETE FROM blacklist_entries WHERE kind = 'user' AND value_hash = ?",
(hash_identifier(str(user_id)),),
)
audit(
"user_updated",
admin["id"],
"user",
str(user_id),
f"role={role};status={status};restricted_until={restricted_until}",
)
return jsonify({"status": "updated"})
@app.route("/api/admin/rooms")
def admin_rooms():
moderator, error = require_moderator()
if error:
return error
with get_db() as db:
rows = db.execute(
"""
SELECT
id,
slug,
name,
description,
visibility,
created_by_user_id,
is_active
FROM rooms
WHERE is_active = 1
ORDER BY name COLLATE NOCASE
"""
).fetchall()
return jsonify(
{
"rooms": [
{
"id": row["id"],
"slug": row["slug"],
"name": row["name"],
"description": row["description"],
"visibility": row["visibility"],
}
for row in rows
]
}
)
@app.route("/api/admin/blacklist/ip", methods=["POST"])
def blacklist_ip():
if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
admin, error = require_admin()
if error:
return error
try:
data = safe_json()
except ValueError as error:
return jsonify({"error": str(error)}), 400
ip = str(data.get("ip", "")).strip()
reason = str(data.get("reason", "")).strip()[:300]
if not ip or len(ip) > 128:
return jsonify({"error": "A valid IP value is required."}), 400
db_write(
"""
INSERT OR REPLACE INTO blacklist_entries (
kind,
value_hash,
reason,
created_at,
created_by_user_id
) VALUES ('ip', ?, ?, ?, ?)
""",
(
hash_identifier(ip),
reason or "IP blacklisted by administrator",
now_iso(),
admin["id"],
),
)
audit("ip_blacklisted", admin["id"], "ip", hash_identifier(ip), reason)
return jsonify({"status": "blacklisted"})