221 lines
9.7 KiB
Python
221 lines
9.7 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:
|
|
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:
|
|
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"})
|