//! HTTP handlers for OAuth 2.0 Device Authorization Grant (RFC 8628). //! //! Endpoints: //! POST /api/auth/device/authorize — Client starts the device flow (public) //! GET /api/auth/device/verify — Check user_code validity (authenticated) //! POST /api/auth/device/verify — User approves/denies (authenticated) //! POST /api/auth/device/token — Client polls for tokens (public) //! GET /api/auth/device/devices — List user's authorized devices (authenticated) //! DELETE /api/auth/device/devices/{id} — Revoke a device (authenticated) use axum::{ Router, extract::{Json, Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{delete, get, post}, }; use std::sync::Arc; use uuid::Uuid; use crate::application::dtos::device_auth_dto::*; use crate::application::services::device_auth_service::DeviceAuthService; use crate::common::di::AppState; use crate::interfaces::errors::AppError; use crate::interfaces::middleware::auth::AuthUser; /// Create the device auth router. /// /// Public endpoints (no auth middleware): authorize, token /// Protected endpoints (behind auth middleware): verify (GET+POST), devices pub fn device_auth_public_routes() -> Router> { Router::new() // Client-facing endpoints (no auth needed — the client doesn't have tokens yet) .route("/authorize", post(device_authorize)) .route("/token", post(device_token)) } pub fn device_auth_protected_routes() -> Router> { Router::new() // User-facing endpoints (require valid session) .route("/verify", get(device_verify_info)) .route("/verify", post(device_verify_action)) .route("/devices", get(list_devices)) .route("/devices/{id}", delete(revoke_device)) } // ============================================================================ // POST /api/auth/device/authorize — Client initiates the device flow // ============================================================================ /// Client sends: `{ "client_name": "rclone", "scope": "webdav" }` /// Server returns: device_code, user_code, verification_uri, etc. async fn device_authorize( State(state): State>, Json(body): Json, ) -> Result { let device_service = get_device_service(&state)?; let response = device_service.initiate(body).await.map_err(|e| { tracing::error!("Device authorize failed: {}", e); AppError::from(e) })?; Ok((StatusCode::OK, Json(response))) } // ============================================================================ // POST /api/auth/device/token — Client polls for tokens // ============================================================================ /// Client sends: `{ "device_code": "...", "grant_type": "urn:ietf:params:oauth:grant-type:device_code" }` /// Returns tokens on success, or RFC 8628 error codes while pending. async fn device_token( State(state): State>, Json(body): Json, ) -> Result { let device_service = match get_device_service(&state) { Ok(svc) => svc, Err(e) => return Err(e.into_response()), }; // Validate grant_type if provided (RFC compliance) if !body.grant_type.is_empty() && body.grant_type != "urn:ietf:params:oauth:grant-type:device_code" { let error_body = serde_json::json!({ "error": "unsupported_grant_type", "error_description": "grant_type must be urn:ietf:params:oauth:grant-type:device_code" }); return Err((StatusCode::BAD_REQUEST, Json(error_body)).into_response()); } match device_service.poll(&body.device_code).await { Ok(tokens) => Ok((StatusCode::OK, Json(tokens)).into_response()), Err(poll_err) => { let status = StatusCode::from_u16(poll_err.http_status()).unwrap_or(StatusCode::BAD_REQUEST); let error_body = serde_json::json!({ "error": poll_err.error_code(), "error_description": poll_err.description() }); Err((status, Json(error_body)).into_response()) } } } // ============================================================================ // GET /api/auth/device/verify?code=ABCD-1234 — Check if user_code is valid // ============================================================================ #[derive(serde::Deserialize)] pub struct VerifyQuery { #[serde(default)] pub code: String, } async fn device_verify_info( State(state): State>, _auth_user: AuthUser, Query(query): Query, ) -> Result { let device_service = get_device_service(&state)?; let info = device_service .verify_user_code(&query.code) .await .map_err(|e| { tracing::warn!("Device verify lookup failed: {}", e); AppError::from(e) })?; Ok((StatusCode::OK, Json(info))) } // ============================================================================ // POST /api/auth/device/verify — User approves or denies // ============================================================================ async fn device_verify_action( State(state): State>, auth_user: AuthUser, Json(body): Json, ) -> Result { let device_service = get_device_service(&state)?; match body.action.to_lowercase().as_str() { "approve" | "allow" | "accept" => { device_service .approve(&body.user_code, auth_user.id) .await .map_err(|e| { tracing::error!("Device approve failed: {}", e); AppError::from(e) })?; Ok(( StatusCode::OK, Json(serde_json::json!({ "status": "approved" })), )) } "deny" | "reject" | "cancel" => { device_service.deny(&body.user_code).await.map_err(|e| { tracing::error!("Device deny failed: {}", e); AppError::from(e) })?; Ok(( StatusCode::OK, Json(serde_json::json!({ "status": "denied" })), )) } _ => Err(AppError::bad_request("action must be 'approve' or 'deny'")), } } // ============================================================================ // GET /api/auth/device/devices — List user's authorized devices // ============================================================================ async fn list_devices( State(state): State>, auth_user: AuthUser, ) -> Result { let device_service = get_device_service(&state)?; let devices = device_service .list_user_devices(auth_user.id) .await .map_err(|e| { tracing::error!("List devices failed: {}", e); AppError::from(e) })?; Ok((StatusCode::OK, Json(devices))) } // ============================================================================ // DELETE /api/auth/device/devices/{id} — Revoke a device authorization // ============================================================================ async fn revoke_device( State(state): State>, auth_user: AuthUser, Path(device_id): Path, ) -> Result { let device_service = get_device_service(&state)?; let device_id = Uuid::parse_str(&device_id).map_err(|_| AppError::bad_request("Invalid device ID"))?; device_service .revoke_device(device_id, auth_user.id) .await .map_err(|e| { tracing::error!("Revoke device failed: {}", e); AppError::from(e) })?; Ok(StatusCode::NO_CONTENT) } // ============================================================================ // Helper // ============================================================================ fn get_device_service(state: &AppState) -> Result<&Arc, AppError> { state .device_auth_service .as_ref() .ok_or_else(|| AppError::internal_error("Device authorization service not configured")) }