use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize, Serializer};
use std::borrow::Cow;
use crate::entity::{
AuthAccount, AuthApiKey, AuthInvitation, AuthOrganization, AuthPasskey, AuthSession, AuthUser,
AuthVerification,
};
use crate::types::InvitationStatus;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserView {
pub id: String,
pub name: Option<String>,
pub email: Option<String>,
#[serde(rename = "emailVerified")]
pub email_verified: bool,
pub image: Option<String>,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
pub username: Option<String>,
#[serde(rename = "displayUsername")]
pub display_username: Option<String>,
#[serde(rename = "twoFactorEnabled", default)]
pub two_factor_enabled: bool,
pub role: Option<String>,
#[serde(default)]
pub banned: bool,
#[serde(rename = "banReason")]
pub ban_reason: Option<String>,
#[serde(rename = "banExpires")]
pub ban_expires: Option<DateTime<Utc>>,
#[serde(skip)]
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionView {
pub id: String,
#[serde(rename = "expiresAt")]
pub expires_at: DateTime<Utc>,
pub token: String,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
#[serde(rename = "ipAddress")]
pub ip_address: Option<String>,
#[serde(rename = "userAgent")]
pub user_agent: Option<String>,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "impersonatedBy")]
pub impersonated_by: Option<String>,
#[serde(rename = "activeOrganizationId")]
pub active_organization_id: Option<String>,
#[serde(skip)]
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AccountView {
pub id: String,
#[serde(rename = "accountId")]
pub account_id: String,
#[serde(rename = "providerId")]
pub provider_id: String,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "accessToken")]
pub access_token: Option<String>,
#[serde(rename = "refreshToken")]
pub refresh_token: Option<String>,
#[serde(rename = "idToken")]
pub id_token: Option<String>,
#[serde(rename = "accessTokenExpiresAt")]
pub access_token_expires_at: Option<DateTime<Utc>>,
#[serde(rename = "refreshTokenExpiresAt")]
pub refresh_token_expires_at: Option<DateTime<Utc>>,
pub scope: Option<String>,
#[serde(skip_serializing)]
pub password: Option<String>,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerificationView {
pub id: String,
pub identifier: String,
pub value: String,
#[serde(rename = "expiresAt")]
pub expires_at: DateTime<Utc>,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
}
impl<T: AuthUser> From<&T> for UserView {
fn from(user: &T) -> Self {
Self {
id: user.id().into_owned(),
name: user.name().map(str::to_owned),
email: user.email().map(str::to_owned),
email_verified: user.email_verified(),
image: user.image().map(str::to_owned),
created_at: user.created_at(),
updated_at: user.updated_at(),
username: user.username().map(str::to_owned),
display_username: user.display_username().map(str::to_owned),
two_factor_enabled: user.two_factor_enabled(),
role: user.role().map(str::to_owned),
banned: user.banned(),
ban_reason: user.ban_reason().map(str::to_owned),
ban_expires: user.ban_expires(),
metadata: user.metadata().clone(),
}
}
}
impl<T: AuthSession> From<&T> for SessionView {
fn from(session: &T) -> Self {
Self {
id: session.id().into_owned(),
expires_at: session.expires_at(),
token: session.token().to_owned(),
created_at: session.created_at(),
updated_at: session.updated_at(),
ip_address: session.ip_address().map(str::to_owned),
user_agent: session.user_agent().map(str::to_owned),
user_id: session.user_id().into_owned(),
impersonated_by: session.impersonated_by().map(str::to_owned),
active_organization_id: session.active_organization_id().map(str::to_owned),
active: session.active(),
}
}
}
impl<T: AuthAccount> From<&T> for AccountView {
fn from(account: &T) -> Self {
Self {
id: account.id().into_owned(),
account_id: account.account_id().to_owned(),
provider_id: account.provider_id().to_owned(),
user_id: account.user_id().into_owned(),
access_token: account.access_token().map(str::to_owned),
refresh_token: account.refresh_token().map(str::to_owned),
id_token: account.id_token().map(str::to_owned),
access_token_expires_at: account.access_token_expires_at(),
refresh_token_expires_at: account.refresh_token_expires_at(),
scope: account.scope().map(str::to_owned),
password: account.password().map(str::to_owned),
created_at: account.created_at(),
updated_at: account.updated_at(),
}
}
}
impl<T: AuthVerification> From<&T> for VerificationView {
fn from(verification: &T) -> Self {
Self {
id: verification.id().into_owned(),
identifier: verification.identifier().to_owned(),
value: verification.value().to_owned(),
expires_at: verification.expires_at(),
created_at: verification.created_at(),
updated_at: verification.updated_at(),
}
}
}
impl AuthUser for UserView {
fn id(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.id)
}
fn email(&self) -> Option<&str> {
self.email.as_deref()
}
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn email_verified(&self) -> bool {
self.email_verified
}
fn image(&self) -> Option<&str> {
self.image.as_deref()
}
fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
fn username(&self) -> Option<&str> {
self.username.as_deref()
}
fn display_username(&self) -> Option<&str> {
self.display_username.as_deref()
}
fn two_factor_enabled(&self) -> bool {
self.two_factor_enabled
}
fn role(&self) -> Option<&str> {
self.role.as_deref()
}
fn banned(&self) -> bool {
self.banned
}
fn ban_reason(&self) -> Option<&str> {
self.ban_reason.as_deref()
}
fn ban_expires(&self) -> Option<DateTime<Utc>> {
self.ban_expires
}
fn metadata(&self) -> &serde_json::Value {
&self.metadata
}
}
impl AuthSession for SessionView {
fn id(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.id)
}
fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
fn token(&self) -> &str {
&self.token
}
fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
fn ip_address(&self) -> Option<&str> {
self.ip_address.as_deref()
}
fn user_agent(&self) -> Option<&str> {
self.user_agent.as_deref()
}
fn user_id(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.user_id)
}
fn impersonated_by(&self) -> Option<&str> {
self.impersonated_by.as_deref()
}
fn active_organization_id(&self) -> Option<&str> {
self.active_organization_id.as_deref()
}
fn active(&self) -> bool {
self.active
}
}
impl AuthAccount for AccountView {
fn id(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.id)
}
fn account_id(&self) -> &str {
&self.account_id
}
fn provider_id(&self) -> &str {
&self.provider_id
}
fn user_id(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.user_id)
}
fn access_token(&self) -> Option<&str> {
self.access_token.as_deref()
}
fn refresh_token(&self) -> Option<&str> {
self.refresh_token.as_deref()
}
fn id_token(&self) -> Option<&str> {
self.id_token.as_deref()
}
fn access_token_expires_at(&self) -> Option<DateTime<Utc>> {
self.access_token_expires_at
}
fn refresh_token_expires_at(&self) -> Option<DateTime<Utc>> {
self.refresh_token_expires_at
}
fn scope(&self) -> Option<&str> {
self.scope.as_deref()
}
fn password(&self) -> Option<&str> {
self.password.as_deref()
}
fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}
impl AuthVerification for VerificationView {
fn id(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.id)
}
fn identifier(&self) -> &str {
&self.identifier
}
fn value(&self) -> &str {
&self.value
}
fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}
fn serialize_json_option_as_string<S>(
value: &Option<serde_json::Value>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(inner) => serializer
.serialize_some(&serde_json::to_string(inner).map_err(serde::ser::Error::custom)?),
None => serializer.serialize_none(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OrganizationView {
pub id: String,
pub name: String,
pub slug: String,
pub logo: Option<String>,
#[serde(
serialize_with = "serialize_json_option_as_string",
skip_serializing_if = "Option::is_none"
)]
pub metadata: Option<serde_json::Value>,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
#[serde(rename = "updatedAt")]
pub updated_at: DateTime<Utc>,
}
impl<T: AuthOrganization> From<&T> for OrganizationView {
fn from(org: &T) -> Self {
Self {
id: org.id().into_owned(),
name: org.name().to_owned(),
slug: org.slug().to_owned(),
logo: org.logo().map(str::to_owned),
metadata: org.metadata().cloned(),
created_at: org.created_at(),
updated_at: org.updated_at(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InvitationView {
pub id: String,
#[serde(rename = "organizationId")]
pub organization_id: String,
pub email: String,
pub role: String,
pub status: InvitationStatus,
#[serde(rename = "inviterId")]
pub inviter_id: String,
#[serde(rename = "expiresAt")]
pub expires_at: DateTime<Utc>,
#[serde(rename = "createdAt")]
pub created_at: DateTime<Utc>,
}
impl<T: AuthInvitation> From<&T> for InvitationView {
fn from(inv: &T) -> Self {
Self {
id: inv.id().into_owned(),
organization_id: inv.organization_id().into_owned(),
email: inv.email().to_owned(),
role: inv.role().to_owned(),
status: inv.status().clone(),
inviter_id: inv.inviter_id().into_owned(),
expires_at: inv.expires_at(),
created_at: inv.created_at(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PasskeyView {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(rename = "credentialID")]
pub credential_id: String,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "publicKey")]
pub public_key: String,
pub counter: u64,
#[serde(rename = "deviceType")]
pub device_type: String,
#[serde(rename = "backedUp")]
pub backed_up: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub transports: Option<String>,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "updatedAt")]
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub aaguid: Option<String>,
}
impl<T: AuthPasskey> From<&T> for PasskeyView {
fn from(pk: &T) -> Self {
Self {
id: pk.id().into_owned(),
name: pk.name().map(str::to_owned),
credential_id: pk.credential_id().to_owned(),
user_id: pk.user_id().into_owned(),
public_key: pk.public_key().to_owned(),
counter: pk.counter(),
device_type: pk.device_type().to_owned(),
backed_up: pk.backed_up(),
transports: pk.transports().map(str::to_owned),
created_at: pk.created_at().to_rfc3339(),
updated_at: pk.updated_at().to_rfc3339(),
aaguid: pk.aaguid().map(str::to_owned),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiKeyView {
pub id: String,
pub name: Option<String>,
pub start: Option<String>,
pub prefix: Option<String>,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "refillInterval")]
pub refill_interval: Option<i64>,
#[serde(rename = "refillAmount")]
pub refill_amount: Option<i64>,
#[serde(rename = "lastRefillAt")]
pub last_refill_at: Option<String>,
pub enabled: bool,
#[serde(rename = "rateLimitEnabled")]
pub rate_limit_enabled: bool,
#[serde(rename = "rateLimitTimeWindow")]
pub rate_limit_time_window: Option<i64>,
#[serde(rename = "rateLimitMax")]
pub rate_limit_max: Option<i64>,
#[serde(rename = "requestCount")]
pub request_count: Option<i64>,
pub remaining: Option<i64>,
#[serde(rename = "lastRequest")]
pub last_request: Option<String>,
#[serde(rename = "expiresAt")]
pub expires_at: Option<String>,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "updatedAt")]
pub updated_at: String,
pub permissions: Option<serde_json::Value>,
pub metadata: Option<serde_json::Value>,
}
impl<T: AuthApiKey> From<&T> for ApiKeyView {
fn from(ak: &T) -> Self {
Self {
id: ak.id().into_owned(),
name: ak.name().map(str::to_owned),
start: ak.start().map(str::to_owned),
prefix: ak.prefix().map(str::to_owned),
user_id: ak.user_id().into_owned(),
refill_interval: ak.refill_interval(),
refill_amount: ak.refill_amount(),
last_refill_at: ak.last_refill_at().map(str::to_owned),
enabled: ak.enabled(),
rate_limit_enabled: ak.rate_limit_enabled(),
rate_limit_time_window: ak.rate_limit_time_window(),
rate_limit_max: ak.rate_limit_max(),
request_count: ak.request_count(),
remaining: ak.remaining(),
last_request: ak.last_request().map(str::to_owned),
expires_at: ak.expires_at().map(str::to_owned),
created_at: ak.created_at().to_owned(),
updated_at: ak.updated_at().to_owned(),
permissions: ak.permissions().and_then(|s| serde_json::from_str(s).ok()),
metadata: ak.metadata().and_then(|s| serde_json::from_str(s).ok()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_view_serializes_camel_case() {
let user = UserView {
id: "user-1".to_string(),
name: Some("Ada".to_string()),
email: Some("ada@example.com".to_string()),
email_verified: true,
image: None,
created_at: Utc::now(),
updated_at: Utc::now(),
username: Some("ada".to_string()),
display_username: Some("Ada".to_string()),
two_factor_enabled: true,
role: Some("admin".to_string()),
banned: false,
ban_reason: None,
ban_expires: None,
metadata: serde_json::json!({}),
};
let json = serde_json::to_value(UserView::from(&user)).expect("serialize user view");
assert_eq!(json["emailVerified"], true);
assert_eq!(json["displayUsername"], "Ada");
assert_eq!(json["twoFactorEnabled"], true);
}
#[test]
fn session_view_serializes_camel_case() {
let session = SessionView {
id: "session-1".to_string(),
expires_at: Utc::now(),
token: "token".to_string(),
created_at: Utc::now(),
updated_at: Utc::now(),
ip_address: Some("127.0.0.1".to_string()),
user_agent: Some("agent".to_string()),
user_id: "user-1".to_string(),
impersonated_by: Some("admin-1".to_string()),
active_organization_id: Some("org-1".to_string()),
active: true,
};
let json =
serde_json::to_value(SessionView::from(&session)).expect("serialize session view");
assert_eq!(json["expiresAt"].is_string(), true);
assert_eq!(json["ipAddress"], "127.0.0.1");
assert_eq!(json["activeOrganizationId"], "org-1");
}
#[test]
fn account_view_omits_password_on_serialize() {
let account = AccountView {
id: "acc-1".to_string(),
account_id: "account-id".to_string(),
provider_id: "credential".to_string(),
user_id: "user-1".to_string(),
access_token: None,
refresh_token: None,
id_token: None,
access_token_expires_at: None,
refresh_token_expires_at: None,
scope: None,
password: Some("$2a$hash".to_string()),
created_at: Utc::now(),
updated_at: Utc::now(),
};
let json = serde_json::to_value(&account).expect("serialize account view");
assert!(
json.get("password").is_none(),
"password field must not appear in serialized output"
);
}
}