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"})