bstein-dev-home/backend/atlas_portal/routes/access_request_status.py

229 lines
10 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from flask import jsonify, redirect, request
def register_access_request_status(app, deps) -> None:
"""Register access request status routes."""
@app.route("/api/access/request/status", methods=["POST"])
def request_access_status() -> Any:
"""Return current provisioning and onboarding status for a request.
WHY: this endpoint is polled by the public flow, so it also advances
safe automatic transitions before rendering the latest state.
"""
if not deps.settings.ACCESS_REQUEST_ENABLED:
return jsonify({"error": "request access disabled"}), 503
if not deps.configured():
return jsonify({"error": "server not deps.configured"}), 503
ip = deps._client_ip()
if not deps.rate_limit_allow(
ip,
key="access_request_status",
limit=deps.settings.ACCESS_REQUEST_STATUS_RATE_LIMIT,
window_sec=deps.settings.ACCESS_REQUEST_STATUS_RATE_WINDOW_SEC,
):
return jsonify({"error": "rate limited"}), 429
payload = request.get_json(silent=True) or {}
code = (payload.get("request_code") or payload.get("code") or "").strip()
reveal_initial_password = bool(
payload.get("reveal_initial_password") or payload.get("reveal_password")
)
if not code:
return jsonify({"error": "request_code is required"}), 400
# Additional per-code limiter to avoid global NAT rate-limit blowups.
if not deps.rate_limit_allow(
f"{ip}:{code}",
key="access_request_status_code",
limit=max(20, deps.settings.ACCESS_REQUEST_STATUS_RATE_LIMIT),
window_sec=deps.settings.ACCESS_REQUEST_STATUS_RATE_WINDOW_SEC,
):
return jsonify({"error": "rate limited"}), 429
try:
with deps.connect() as conn:
row = conn.execute(
"""
SELECT status,
username,
initial_password,
initial_password_revealed_at,
email_verified_at
FROM access_requests
WHERE request_code = %s
""",
(code,),
).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
current_status = deps._normalize_status(row.get("status") or "")
if current_status == "accounts_building" and not deps.ariadne_client.enabled():
try:
deps.provision_access_request(code)
except Exception:
pass
row = conn.execute(
"""
SELECT status,
username,
initial_password,
initial_password_revealed_at,
email_verified_at
FROM access_requests
WHERE request_code = %s
""",
(code,),
).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
status = deps._advance_status(conn, code, row.get("username") or "", row.get("status") or "")
response: dict[str, Any] = {
"ok": True,
"status": status,
"username": row.get("username") or "",
"email_verified": bool(row.get("email_verified_at")),
}
task_rows = conn.execute(
"""
SELECT task, status, detail, updated_at
FROM access_request_tasks
WHERE request_code = %s
ORDER BY task
""",
(code,),
).fetchall()
if task_rows:
tasks: list[dict[str, Any]] = []
blocked = False
for task_row in task_rows:
task_name = task_row.get("task") if isinstance(task_row, dict) else None
task_status = task_row.get("status") if isinstance(task_row, dict) else None
detail = task_row.get("detail") if isinstance(task_row, dict) else None
updated_at = task_row.get("updated_at") if isinstance(task_row, dict) else None
if isinstance(task_status, str) and task_status == "error":
blocked = True
task_payload: dict[str, Any] = {
"task": task_name or "",
"status": task_status or "",
}
if isinstance(detail, str) and detail:
task_payload["detail"] = detail
if isinstance(updated_at, datetime):
task_payload["updated_at"] = updated_at.astimezone(timezone.utc).isoformat()
tasks.append(task_payload)
response["tasks"] = tasks
response["automation_complete"] = deps.provision_tasks_complete(conn, code)
response["blocked"] = blocked
if status in {"awaiting_onboarding", "ready"}:
revealed_at = row.get("initial_password_revealed_at")
if isinstance(revealed_at, datetime):
response["initial_password_revealed_at"] = revealed_at.astimezone(timezone.utc).isoformat()
if reveal_initial_password:
password = row.get("initial_password")
if isinstance(password, str) and password and revealed_at is None:
response["initial_password"] = password
conn.execute(
"UPDATE access_requests SET initial_password_revealed_at = NOW() WHERE request_code = %s AND initial_password_revealed_at IS NULL",
(code,),
)
if status in {"awaiting_onboarding", "ready"}:
response["onboarding_url"] = f"/onboarding?code={code}"
if status in {"awaiting_onboarding", "ready"}:
response["onboarding"] = deps._onboarding_payload(conn, code, row.get("username") or "")
return jsonify(response)
except Exception:
return jsonify({"error": "failed to load status"}), 502
@app.route("/api/access/request/retry", methods=["POST"])
def request_access_retry() -> Any:
"""Retry failed provisioning tasks for an access request."""
if not deps.settings.ACCESS_REQUEST_ENABLED:
return jsonify({"error": "request access disabled"}), 503
if not deps.configured():
return jsonify({"error": "server not deps.configured"}), 503
ip = deps._client_ip()
if not deps.rate_limit_allow(
ip,
key="access_request_retry",
limit=deps.settings.ACCESS_REQUEST_STATUS_RATE_LIMIT,
window_sec=deps.settings.ACCESS_REQUEST_STATUS_RATE_WINDOW_SEC,
):
return jsonify({"error": "rate limited"}), 429
payload = request.get_json(silent=True) or {}
code = (payload.get("request_code") or payload.get("code") or "").strip()
tasks = payload.get("tasks")
task_list = [task for task in tasks if isinstance(task, str) and task.strip()] if isinstance(tasks, list) else []
if not code:
return jsonify({"error": "request_code is required"}), 400
if deps.ariadne_client.enabled():
retry_payload = {"tasks": task_list} if task_list else None
return deps.ariadne_client.proxy(
"POST",
f"/api/access/requests/{code}/retry",
payload=retry_payload,
)
try:
with deps.connect() as conn:
row = conn.execute(
"SELECT status FROM access_requests WHERE request_code = %s",
(code,),
).fetchone()
if not row:
return jsonify({"error": "not found"}), 404
status = row.get("status") or ""
if status not in {"accounts_building", "approved"}:
return jsonify({"error": "request not retryable"}), 409
conn.execute(
"UPDATE access_requests SET provision_attempted_at = NULL WHERE request_code = %s",
(code,),
)
if task_list:
conn.execute(
"""
UPDATE access_request_tasks
SET status = 'pending',
detail = 'retry requested',
updated_at = NOW()
WHERE request_code = %s
AND task = ANY(%s::text[])
AND status = 'error'
""",
(code, task_list),
)
else:
conn.execute(
"""
UPDATE access_request_tasks
SET status = 'pending',
detail = 'retry requested',
updated_at = NOW()
WHERE request_code = %s AND status = 'error'
""",
(code,),
)
except Exception:
return jsonify({"error": "failed to retry request"}), 502
try:
deps.provision_access_request(code)
except Exception:
pass
return jsonify({"ok": True, "status": "accounts_building"})