//! Device Authorization Code entity (RFC 8628). //! //! Represents a pending or completed OAuth 2.0 Device Authorization Grant flow. use chrono::{DateTime, Duration, Utc}; use uuid::Uuid; /// Status of a device authorization flow. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeviceCodeStatus { /// Waiting for the user to authorize on the verification page. Pending, /// User approved — tokens are ready for the polling client. Authorized, /// User explicitly denied the request. Denied, /// The code expired before the user acted. Expired, } impl DeviceCodeStatus { pub fn as_str(&self) -> &'static str { match self { Self::Pending => "pending", Self::Authorized => "authorized", Self::Denied => "denied", Self::Expired => "expired", } } pub fn parse(s: &str) -> Option { match s { "pending" => Some(Self::Pending), "authorized" => Some(Self::Authorized), "denied" => Some(Self::Denied), "expired" => Some(Self::Expired), _ => None, } } } impl std::fmt::Display for DeviceCodeStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } /// Domain entity for a Device Authorization flow. #[derive(Debug, Clone)] pub struct DeviceCode { id: String, device_code: String, user_code: String, client_name: String, scopes: String, status: DeviceCodeStatus, user_id: Option, access_token: Option, refresh_token: Option, verification_uri: String, verification_uri_complete: Option, expires_at: DateTime, poll_interval_secs: i32, last_poll_at: Option>, created_at: DateTime, authorized_at: Option>, } impl DeviceCode { /// Create a new pending device code flow. /// /// * `device_code` — opaque token for client polling (64 hex chars) /// * `user_code` — short human-readable code (e.g. "ABCD-1234") /// * `client_name` — display name of the requesting client /// * `scopes` — requested scopes (e.g. "webdav,caldav,carddav") /// * `verification_uri` — URL the user must visit /// * `expires_in_secs` — TTL for the device code /// * `poll_interval_secs` — minimum polling interval #[allow(clippy::too_many_arguments)] pub fn new( device_code: String, user_code: String, client_name: String, scopes: String, verification_uri: String, verification_uri_complete: Option, expires_in_secs: i64, poll_interval_secs: i32, ) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4().to_string(), device_code, user_code, client_name, scopes, status: DeviceCodeStatus::Pending, user_id: None, access_token: None, refresh_token: None, verification_uri, verification_uri_complete, expires_at: now + Duration::seconds(expires_in_secs), poll_interval_secs, last_poll_at: None, created_at: now, authorized_at: None, } } /// Reconstruct from database row. #[allow(clippy::too_many_arguments)] pub fn from_raw( id: String, device_code: String, user_code: String, client_name: String, scopes: String, status: DeviceCodeStatus, user_id: Option, access_token: Option, refresh_token: Option, verification_uri: String, verification_uri_complete: Option, expires_at: DateTime, poll_interval_secs: i32, last_poll_at: Option>, created_at: DateTime, authorized_at: Option>, ) -> Self { Self { id, device_code, user_code, client_name, scopes, status, user_id, access_token, refresh_token, verification_uri, verification_uri_complete, expires_at, poll_interval_secs, last_poll_at, created_at, authorized_at, } } // ── Getters ────────────────────────────────────────────────── pub fn id(&self) -> &str { &self.id } pub fn device_code(&self) -> &str { &self.device_code } pub fn user_code(&self) -> &str { &self.user_code } pub fn client_name(&self) -> &str { &self.client_name } pub fn scopes(&self) -> &str { &self.scopes } pub fn status(&self) -> DeviceCodeStatus { self.status } pub fn user_id(&self) -> Option<&str> { self.user_id.as_deref() } pub fn access_token(&self) -> Option<&str> { self.access_token.as_deref() } pub fn refresh_token(&self) -> Option<&str> { self.refresh_token.as_deref() } pub fn verification_uri(&self) -> &str { &self.verification_uri } pub fn verification_uri_complete(&self) -> Option<&str> { self.verification_uri_complete.as_deref() } pub fn expires_at(&self) -> DateTime { self.expires_at } pub fn poll_interval_secs(&self) -> i32 { self.poll_interval_secs } pub fn last_poll_at(&self) -> Option> { self.last_poll_at } pub fn created_at(&self) -> DateTime { self.created_at } pub fn authorized_at(&self) -> Option> { self.authorized_at } // ── Business logic ─────────────────────────────────────────── /// Whether the device code has expired. pub fn is_expired(&self) -> bool { Utc::now() > self.expires_at } /// Seconds remaining until expiry (clamped to 0). pub fn seconds_remaining(&self) -> i64 { let remaining = (self.expires_at - Utc::now()).num_seconds(); remaining.max(0) } /// Whether the client is polling too fast (within poll_interval_secs). pub fn is_polling_too_fast(&self) -> bool { if let Some(last) = self.last_poll_at { let elapsed = (Utc::now() - last).num_seconds(); elapsed < self.poll_interval_secs as i64 } else { false } } /// Record a poll attempt timestamp. pub fn record_poll(&mut self) { self.last_poll_at = Some(Utc::now()); } /// Authorize this device code for a specific user, storing the tokens. pub fn authorize(&mut self, user_id: String, access_token: String, refresh_token: String) { self.status = DeviceCodeStatus::Authorized; self.user_id = Some(user_id); self.access_token = Some(access_token); self.refresh_token = Some(refresh_token); self.authorized_at = Some(Utc::now()); } /// Deny this device code. pub fn deny(&mut self) { self.status = DeviceCodeStatus::Denied; } /// Mark as expired. pub fn mark_expired(&mut self) { self.status = DeviceCodeStatus::Expired; } }