Add admin tools: delete publisher and reset password

- DELETE /api/v1/admin/publishers/<id> - delete publisher with optional tool deletion
- POST /api/v1/admin/publishers/<id>/reset-password - generate temporary password

Safety features:
- Cannot delete yourself or other admins
- Delete requires typing username to confirm
- Reset password shows temp password in modal with copy button

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2026-01-13 23:56:06 -04:00
co-authored by Claude Opus 4.5
parent a4ed9ed730
commit eacfd0d74a
2 changed files with 200 additions and 1 deletions
+72
View File
@@ -2488,6 +2488,78 @@ def create_app() -> Flask:
return jsonify({"data": {"status": "updated", "publisher_id": publisher_id, "role": new_role}})
@app.route("/api/v1/admin/publishers/<int:publisher_id>", methods=["DELETE"])
@require_admin
def admin_delete_publisher(publisher_id: int) -> Response:
"""Delete a publisher and optionally their tools."""
delete_tools = request.args.get("delete_tools", "false").lower() == "true"
publisher = query_one(g.db, "SELECT * FROM publishers WHERE id = ?", [publisher_id])
if not publisher:
return error_response("PUBLISHER_NOT_FOUND", "Publisher not found", 404)
# Don't allow deleting yourself
if publisher_id == g.current_publisher["id"]:
return error_response("CANNOT_DELETE_SELF", "Cannot delete your own account", 400)
# Don't allow deleting other admins
if publisher["role"] == "admin":
return error_response("CANNOT_DELETE_ADMIN", "Cannot delete admin accounts", 400)
slug = publisher["slug"]
if delete_tools:
# Delete all tools owned by this publisher
g.db.execute("DELETE FROM tools WHERE publisher_id = ?", [publisher_id])
# Revoke all tokens
g.db.execute("UPDATE api_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE publisher_id = ?", [publisher_id])
# Delete the publisher
g.db.execute("DELETE FROM publishers WHERE id = ?", [publisher_id])
g.db.commit()
log_audit("delete_publisher", "publisher", str(publisher_id), {
"slug": slug,
"delete_tools": delete_tools,
})
return jsonify({"data": {"status": "deleted", "publisher_id": publisher_id, "slug": slug}})
@app.route("/api/v1/admin/publishers/<int:publisher_id>/reset-password", methods=["POST"])
@require_admin
def admin_reset_password(publisher_id: int) -> Response:
"""Generate a temporary password for a publisher."""
import secrets
from werkzeug.security import generate_password_hash
publisher = query_one(g.db, "SELECT * FROM publishers WHERE id = ?", [publisher_id])
if not publisher:
return error_response("PUBLISHER_NOT_FOUND", "Publisher not found", 404)
# Generate a temporary password
temp_password = secrets.token_urlsafe(12)
password_hash = generate_password_hash(temp_password)
g.db.execute(
"UPDATE publishers SET password_hash = ? WHERE id = ?",
[password_hash, publisher_id],
)
g.db.commit()
log_audit("reset_password", "publisher", str(publisher_id), {
"slug": publisher["slug"],
})
return jsonify({
"data": {
"status": "reset",
"publisher_id": publisher_id,
"slug": publisher["slug"],
"temporary_password": temp_password,
}
})
@app.route("/api/v1/admin/reports", methods=["GET"])
@require_moderator
def admin_list_reports() -> Response: