defmodule ToweropsWeb.Api.MobileAuthController do @moduledoc """ API controller for mobile app authentication. Handles QR code-based authentication flow: 1. Web app creates QR token (handled by LiveView) 2. Mobile app scans QR code and calls verify_qr_token 3. Mobile app calls complete_qr_login to get session token """ use ToweropsWeb, :controller import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1] alias Towerops.MobileSessions @doc """ POST /api/v1/mobile/auth/qr/verify Verifies a QR login token is valid (not expired, not used). Request body: { "token": "base64-encoded-token" } Response: { "valid": true, "user_email": "user@example.com" } """ def verify_qr_token(conn, %{"token" => token}) do case MobileSessions.get_qr_login_token(token) do nil -> conn |> put_status(:unauthorized) |> json(%{valid: false, error: "Invalid or expired token"}) _qr_token -> # Don't leak the associated user's email here. Anyone who guesses or # captures a QR token would otherwise be able to enumerate emails by # calling /verify. The email is only meaningful at /complete (which # consumes the token and creates a session). json(conn, %{valid: true}) end end def verify_qr_token(conn, _params) do conn |> put_status(:bad_request) |> json(%{error: "Missing token parameter"}) end @doc """ POST /api/v1/mobile/auth/qr/complete Completes QR login by creating a mobile session. Request body: { "token": "base64-encoded-qr-token", "device_name": "iPhone 15 Pro", "device_os": "iOS 17.2", "app_version": "1.0.0" } Response: { "session_token": "long-lived-session-token", "expires_at": "2026-04-15T19:44:25Z", "user": { "id": "uuid", "email": "user@example.com" } } """ def complete_qr_login(conn, params) do token = params["token"] device_name = params["device_name"] device_os = params["device_os"] app_version = params["app_version"] push_token = params["push_token"] push_platform = params["push_platform"] if token do device_attrs = %{ device_name: device_name, device_os: device_os, app_version: app_version, push_token: push_token, push_platform: push_platform } case MobileSessions.complete_qr_login(token, device_attrs) do {:ok, session} -> session = Towerops.Repo.preload(session, :user) json(conn, %{ session_token: session.raw_token, expires_at: session.expires_at, user: %{ id: session.user.id, email: session.user.email } }) {:error, :invalid_token} -> conn |> put_status(:unauthorized) |> json(%{error: "Invalid or expired token"}) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> json(%{error: "Failed to create session", details: translate_errors(changeset)}) end else conn |> put_status(:bad_request) |> json(%{error: "Missing token parameter"}) end end @doc """ GET /api/v1/mobile/auth/session Returns current session info (requires mobile auth). Response: { "id": "session-uuid", "device_name": "iPhone 15 Pro", "device_os": "iOS 17.2", "app_version": "1.0.0", "last_used_at": "2026-01-15T19:44:25Z", "expires_at": "2026-04-15T19:44:25Z" } """ def get_session(conn, _params) do session = conn.assigns.current_mobile_session json(conn, %{ id: session.id, device_name: session.device_name, device_os: session.device_os, app_version: session.app_version, last_used_at: session.last_used_at, expires_at: session.expires_at }) end @doc """ DELETE /api/v1/mobile/auth/session Revokes the current mobile session (logout). Response: { "success": true } """ def revoke_session(conn, _params) do session = conn.assigns.current_mobile_session case MobileSessions.revoke_session(session.id) do {:ok, _} -> json(conn, %{success: true}) {:error, _} -> conn |> put_status(:internal_server_error) |> json(%{error: "Failed to revoke session"}) end end end