use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AccountStatus {
Pending,
Active,
Locked,
Suspended,
Deleted,
}
impl AccountStatus {
pub fn can_authenticate(self) -> bool {
matches!(self, AccountStatus::Active)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MfaState {
pub totp_enrolled: bool,
pub recovery_codes_remaining: u32,
pub passkeys: u32,
}
impl MfaState {
pub fn is_enrolled(&self) -> bool {
self.totp_enrolled || self.passkeys > 0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(default)]
pub email_verified: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(default)]
pub phone_verified: bool,
pub status: AccountStatus,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub perms: Vec<String>,
#[serde(default)]
pub mfa: MfaState,
#[serde(default)]
pub attributes: serde_json::Map<String, serde_json::Value>,
}
impl Identity {
pub fn primary_role(&self) -> &str {
self.roles.first().map(|s| s.as_str()).unwrap_or("user")
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RegisterRequest {
pub email: String,
#[serde(default)]
pub password: Option<String>,
#[serde(default)]
pub tenant: Option<String>,
#[serde(default)]
pub attributes: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TokenResponse {
pub access_token: String,
pub refresh_token: String,
pub token_type: &'static str,
pub expires_in: u64,
}
impl TokenResponse {
pub fn bearer(access_token: String, refresh_token: String, expires_in: u64) -> Self {
Self {
access_token,
refresh_token,
token_type: "Bearer",
expires_in,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum LoginOutcome {
Authenticated(TokenResponse),
MfaChallenge {
mfa_required: bool,
challenge_id: String,
methods: Vec<String>,
},
}