48d853360e
Adds full Device Authorization Grant flow so DAV clients (rclone, etc.) can authenticate without browser-based OAuth redirects. New files: - Domain entity: DeviceCode with status lifecycle (pending/authorized/denied/expired) - Port: DeviceCodeStoragePort trait (7 async methods) - DTOs: request/response types for all device auth endpoints - Repository: DeviceCodePgRepository (PostgreSQL implementation) - Service: DeviceAuthService (initiate, verify, approve, deny, poll, cleanup) - Handler: 6 HTTP endpoints (2 public + 4 protected) - Static: device-verify.html verification page served at /device Flow: 1. Client POST /api/auth/device/authorize → device_code + user_code 2. User opens /device?code=XXXX in browser, approves 3. Client polls POST /api/auth/device/token → receives JWT tokens 4. Client uses Bearer token with existing WebDAV/CalDAV/CardDAV middleware Schema: auth.device_codes table + device_code_status enum added to schema.sql Closes #152
97 lines
3.2 KiB
Rust
97 lines
3.2 KiB
Rust
//! DTOs for OAuth 2.0 Device Authorization Grant (RFC 8628).
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
// ============================================================================
|
|
// Request DTOs
|
|
// ============================================================================
|
|
|
|
/// POST /api/auth/device/authorize — request body
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceAuthorizeRequestDto {
|
|
/// Human-readable name of the client (e.g. "rclone", "DAVx⁵")
|
|
#[serde(default = "default_client_name")]
|
|
pub client_name: String,
|
|
/// Comma-separated scopes (e.g. "webdav,caldav,carddav")
|
|
#[serde(default = "default_scopes")]
|
|
pub scope: String,
|
|
}
|
|
|
|
fn default_client_name() -> String {
|
|
"Unknown Client".to_string()
|
|
}
|
|
|
|
fn default_scopes() -> String {
|
|
"webdav,caldav,carddav".to_string()
|
|
}
|
|
|
|
/// POST /api/auth/device/verify — user submits the code from the browser
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceVerifyRequestDto {
|
|
/// The user_code displayed on the client device
|
|
pub user_code: String,
|
|
/// Whether the user approves ("approve") or denies ("deny")
|
|
pub action: String,
|
|
}
|
|
|
|
/// POST /api/auth/device/token — client polls for tokens
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceTokenRequestDto {
|
|
/// The device_code received from the initial authorize call
|
|
pub device_code: String,
|
|
/// Must be "urn:ietf:params:oauth:grant-type:device_code"
|
|
#[serde(default)]
|
|
pub grant_type: String,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Response DTOs
|
|
// ============================================================================
|
|
|
|
/// Response to POST /api/auth/device/authorize (RFC 8628 §3.2)
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceAuthorizeResponseDto {
|
|
/// The device verification code
|
|
pub device_code: String,
|
|
/// The end-user verification code (short, human-readable)
|
|
pub user_code: String,
|
|
/// The end-user verification URI
|
|
pub verification_uri: String,
|
|
/// Optional: verification URI with user_code pre-filled
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub verification_uri_complete: Option<String>,
|
|
/// Lifetime in seconds of the device_code and user_code
|
|
pub expires_in: i64,
|
|
/// Minimum polling interval in seconds
|
|
pub interval: i32,
|
|
}
|
|
|
|
/// Response to POST /api/auth/device/token when authorization is still pending
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceTokenPendingDto {
|
|
pub error: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error_description: Option<String>,
|
|
}
|
|
|
|
/// Response to POST /api/auth/device/token when authorization is complete
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceTokenSuccessDto {
|
|
pub access_token: String,
|
|
pub token_type: String,
|
|
pub refresh_token: String,
|
|
pub expires_in: i64,
|
|
pub scope: String,
|
|
}
|
|
|
|
/// GET /api/auth/device/verify — info about the pending device code
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceVerifyInfoDto {
|
|
/// The client name requesting access
|
|
pub client_name: String,
|
|
/// Scopes being requested
|
|
pub scopes: String,
|
|
/// Whether the user_code is valid and pending
|
|
pub valid: bool,
|
|
}
|