//! App Password entity. //! //! Represents an application-specific password that clients (like DAVx⁵, Thunderbird) //! can use with HTTP Basic Auth to access WebDAV/CalDAV/CardDAV endpoints without //! requiring interactive OAuth flows. use chrono::{DateTime, Utc}; use uuid::Uuid; /// An application password created by a user for a specific client. #[derive(Debug, Clone)] pub struct AppPassword { /// Unique identifier. pub id: Uuid, /// Owner user ID. pub user_id: Uuid, /// Human-readable label chosen by the user (e.g. "DAVx5 on Pixel 8"). pub label: String, /// Argon2 hash of the generated password token. /// /// The plain text token is only returned once at creation time. pub password_hash: String, /// First 8 characters of the plain text token, stored for display purposes /// so the user can identify which token is which. pub prefix: String, /// Comma-separated scopes (e.g. "webdav,caldav,carddav"). pub scopes: String, /// When this app password was created. pub created_at: DateTime, /// When this app password was last used for authentication. pub last_used_at: Option>, /// Optional expiry — `None` means never expires. pub expires_at: Option>, /// Whether this app password is active. pub active: bool, } impl AppPassword { /// Create a new app password entity. /// /// The caller is responsible for hashing the raw token and passing /// the hash and prefix. pub fn new( user_id: Uuid, label: String, password_hash: String, prefix: String, scopes: String, expires_at: Option>, ) -> Self { Self { id: Uuid::new_v4(), user_id, label, password_hash, prefix, scopes, created_at: Utc::now(), last_used_at: None, expires_at, active: true, } } /// Check whether this app password has expired. pub fn is_expired(&self) -> bool { if let Some(exp) = self.expires_at { Utc::now() >= exp } else { false } } /// Check whether this app password is usable (active and not expired). pub fn is_usable(&self) -> bool { self.active && !self.is_expired() } /// Check whether the given scope is granted by this app password. pub fn has_scope(&self, scope: &str) -> bool { self.scopes.split(',').any(|s| s.trim() == scope) } }