1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize, Serializer};
9use std::borrow::Cow;
10
11use crate::entity::{
12 AuthAccount, AuthApiKey, AuthInvitation, AuthOrganization, AuthPasskey, AuthSession, AuthUser,
13 AuthVerification,
14};
15use crate::types::InvitationStatus;
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct UserView {
20 pub id: String,
21 pub name: Option<String>,
22 pub email: Option<String>,
23 #[serde(rename = "emailVerified")]
24 pub email_verified: bool,
25 pub image: Option<String>,
26 #[serde(rename = "createdAt")]
27 pub created_at: DateTime<Utc>,
28 #[serde(rename = "updatedAt")]
29 pub updated_at: DateTime<Utc>,
30 pub username: Option<String>,
31 #[serde(rename = "displayUsername")]
32 pub display_username: Option<String>,
33 #[serde(rename = "twoFactorEnabled", default)]
34 pub two_factor_enabled: bool,
35 pub role: Option<String>,
36 #[serde(default)]
37 pub banned: bool,
38 #[serde(rename = "banReason")]
39 pub ban_reason: Option<String>,
40 #[serde(rename = "banExpires")]
41 pub ban_expires: Option<DateTime<Utc>>,
42 #[serde(skip)]
43 pub metadata: serde_json::Value,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct SessionView {
49 pub id: String,
50 #[serde(rename = "expiresAt")]
51 pub expires_at: DateTime<Utc>,
52 pub token: String,
53 #[serde(rename = "createdAt")]
54 pub created_at: DateTime<Utc>,
55 #[serde(rename = "updatedAt")]
56 pub updated_at: DateTime<Utc>,
57 #[serde(rename = "ipAddress")]
58 pub ip_address: Option<String>,
59 #[serde(rename = "userAgent")]
60 pub user_agent: Option<String>,
61 #[serde(rename = "userId")]
62 pub user_id: String,
63 #[serde(rename = "impersonatedBy")]
64 pub impersonated_by: Option<String>,
65 #[serde(rename = "activeOrganizationId")]
66 pub active_organization_id: Option<String>,
67 #[serde(skip)]
68 pub active: bool,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct AccountView {
74 pub id: String,
75 #[serde(rename = "accountId")]
76 pub account_id: String,
77 #[serde(rename = "providerId")]
78 pub provider_id: String,
79 #[serde(rename = "userId")]
80 pub user_id: String,
81 #[serde(rename = "accessToken")]
82 pub access_token: Option<String>,
83 #[serde(rename = "refreshToken")]
84 pub refresh_token: Option<String>,
85 #[serde(rename = "idToken")]
86 pub id_token: Option<String>,
87 #[serde(rename = "accessTokenExpiresAt")]
88 pub access_token_expires_at: Option<DateTime<Utc>>,
89 #[serde(rename = "refreshTokenExpiresAt")]
90 pub refresh_token_expires_at: Option<DateTime<Utc>>,
91 pub scope: Option<String>,
92 #[serde(skip_serializing)]
93 pub password: Option<String>,
94 #[serde(rename = "createdAt")]
95 pub created_at: DateTime<Utc>,
96 #[serde(rename = "updatedAt")]
97 pub updated_at: DateTime<Utc>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102pub struct VerificationView {
103 pub id: String,
104 pub identifier: String,
105 pub value: String,
106 #[serde(rename = "expiresAt")]
107 pub expires_at: DateTime<Utc>,
108 #[serde(rename = "createdAt")]
109 pub created_at: DateTime<Utc>,
110 #[serde(rename = "updatedAt")]
111 pub updated_at: DateTime<Utc>,
112}
113
114impl<T: AuthUser> From<&T> for UserView {
115 fn from(user: &T) -> Self {
116 Self {
117 id: user.id().into_owned(),
118 name: user.name().map(str::to_owned),
119 email: user.email().map(str::to_owned),
120 email_verified: user.email_verified(),
121 image: user.image().map(str::to_owned),
122 created_at: user.created_at(),
123 updated_at: user.updated_at(),
124 username: user.username().map(str::to_owned),
125 display_username: user.display_username().map(str::to_owned),
126 two_factor_enabled: user.two_factor_enabled(),
127 role: user.role().map(str::to_owned),
128 banned: user.banned(),
129 ban_reason: user.ban_reason().map(str::to_owned),
130 ban_expires: user.ban_expires(),
131 metadata: user.metadata().clone(),
132 }
133 }
134}
135
136impl<T: AuthSession> From<&T> for SessionView {
137 fn from(session: &T) -> Self {
138 Self {
139 id: session.id().into_owned(),
140 expires_at: session.expires_at(),
141 token: session.token().to_owned(),
142 created_at: session.created_at(),
143 updated_at: session.updated_at(),
144 ip_address: session.ip_address().map(str::to_owned),
145 user_agent: session.user_agent().map(str::to_owned),
146 user_id: session.user_id().into_owned(),
147 impersonated_by: session.impersonated_by().map(str::to_owned),
148 active_organization_id: session.active_organization_id().map(str::to_owned),
149 active: session.active(),
150 }
151 }
152}
153
154impl<T: AuthAccount> From<&T> for AccountView {
155 fn from(account: &T) -> Self {
156 Self {
157 id: account.id().into_owned(),
158 account_id: account.account_id().to_owned(),
159 provider_id: account.provider_id().to_owned(),
160 user_id: account.user_id().into_owned(),
161 access_token: account.access_token().map(str::to_owned),
162 refresh_token: account.refresh_token().map(str::to_owned),
163 id_token: account.id_token().map(str::to_owned),
164 access_token_expires_at: account.access_token_expires_at(),
165 refresh_token_expires_at: account.refresh_token_expires_at(),
166 scope: account.scope().map(str::to_owned),
167 password: account.password().map(str::to_owned),
168 created_at: account.created_at(),
169 updated_at: account.updated_at(),
170 }
171 }
172}
173
174impl<T: AuthVerification> From<&T> for VerificationView {
175 fn from(verification: &T) -> Self {
176 Self {
177 id: verification.id().into_owned(),
178 identifier: verification.identifier().to_owned(),
179 value: verification.value().to_owned(),
180 expires_at: verification.expires_at(),
181 created_at: verification.created_at(),
182 updated_at: verification.updated_at(),
183 }
184 }
185}
186
187impl AuthUser for UserView {
188 fn id(&self) -> Cow<'_, str> {
189 Cow::Borrowed(&self.id)
190 }
191 fn email(&self) -> Option<&str> {
192 self.email.as_deref()
193 }
194 fn name(&self) -> Option<&str> {
195 self.name.as_deref()
196 }
197 fn email_verified(&self) -> bool {
198 self.email_verified
199 }
200 fn image(&self) -> Option<&str> {
201 self.image.as_deref()
202 }
203 fn created_at(&self) -> DateTime<Utc> {
204 self.created_at
205 }
206 fn updated_at(&self) -> DateTime<Utc> {
207 self.updated_at
208 }
209 fn username(&self) -> Option<&str> {
210 self.username.as_deref()
211 }
212 fn display_username(&self) -> Option<&str> {
213 self.display_username.as_deref()
214 }
215 fn two_factor_enabled(&self) -> bool {
216 self.two_factor_enabled
217 }
218 fn role(&self) -> Option<&str> {
219 self.role.as_deref()
220 }
221 fn banned(&self) -> bool {
222 self.banned
223 }
224 fn ban_reason(&self) -> Option<&str> {
225 self.ban_reason.as_deref()
226 }
227 fn ban_expires(&self) -> Option<DateTime<Utc>> {
228 self.ban_expires
229 }
230 fn metadata(&self) -> &serde_json::Value {
231 &self.metadata
232 }
233}
234
235impl AuthSession for SessionView {
236 fn id(&self) -> Cow<'_, str> {
237 Cow::Borrowed(&self.id)
238 }
239 fn expires_at(&self) -> DateTime<Utc> {
240 self.expires_at
241 }
242 fn token(&self) -> &str {
243 &self.token
244 }
245 fn created_at(&self) -> DateTime<Utc> {
246 self.created_at
247 }
248 fn updated_at(&self) -> DateTime<Utc> {
249 self.updated_at
250 }
251 fn ip_address(&self) -> Option<&str> {
252 self.ip_address.as_deref()
253 }
254 fn user_agent(&self) -> Option<&str> {
255 self.user_agent.as_deref()
256 }
257 fn user_id(&self) -> Cow<'_, str> {
258 Cow::Borrowed(&self.user_id)
259 }
260 fn impersonated_by(&self) -> Option<&str> {
261 self.impersonated_by.as_deref()
262 }
263 fn active_organization_id(&self) -> Option<&str> {
264 self.active_organization_id.as_deref()
265 }
266 fn active(&self) -> bool {
267 self.active
268 }
269}
270
271impl AuthAccount for AccountView {
272 fn id(&self) -> Cow<'_, str> {
273 Cow::Borrowed(&self.id)
274 }
275 fn account_id(&self) -> &str {
276 &self.account_id
277 }
278 fn provider_id(&self) -> &str {
279 &self.provider_id
280 }
281 fn user_id(&self) -> Cow<'_, str> {
282 Cow::Borrowed(&self.user_id)
283 }
284 fn access_token(&self) -> Option<&str> {
285 self.access_token.as_deref()
286 }
287 fn refresh_token(&self) -> Option<&str> {
288 self.refresh_token.as_deref()
289 }
290 fn id_token(&self) -> Option<&str> {
291 self.id_token.as_deref()
292 }
293 fn access_token_expires_at(&self) -> Option<DateTime<Utc>> {
294 self.access_token_expires_at
295 }
296 fn refresh_token_expires_at(&self) -> Option<DateTime<Utc>> {
297 self.refresh_token_expires_at
298 }
299 fn scope(&self) -> Option<&str> {
300 self.scope.as_deref()
301 }
302 fn password(&self) -> Option<&str> {
303 self.password.as_deref()
304 }
305 fn created_at(&self) -> DateTime<Utc> {
306 self.created_at
307 }
308 fn updated_at(&self) -> DateTime<Utc> {
309 self.updated_at
310 }
311}
312
313impl AuthVerification for VerificationView {
314 fn id(&self) -> Cow<'_, str> {
315 Cow::Borrowed(&self.id)
316 }
317 fn identifier(&self) -> &str {
318 &self.identifier
319 }
320 fn value(&self) -> &str {
321 &self.value
322 }
323 fn expires_at(&self) -> DateTime<Utc> {
324 self.expires_at
325 }
326 fn created_at(&self) -> DateTime<Utc> {
327 self.created_at
328 }
329 fn updated_at(&self) -> DateTime<Utc> {
330 self.updated_at
331 }
332}
333
334fn serialize_json_option_as_string<S>(
339 value: &Option<serde_json::Value>,
340 serializer: S,
341) -> Result<S::Ok, S::Error>
342where
343 S: Serializer,
344{
345 match value {
346 Some(inner) => serializer
347 .serialize_some(&serde_json::to_string(inner).map_err(serde::ser::Error::custom)?),
348 None => serializer.serialize_none(),
349 }
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
354pub struct OrganizationView {
355 pub id: String,
356 pub name: String,
357 pub slug: String,
358 pub logo: Option<String>,
359 #[serde(
360 serialize_with = "serialize_json_option_as_string",
361 skip_serializing_if = "Option::is_none"
362 )]
363 pub metadata: Option<serde_json::Value>,
364 #[serde(rename = "createdAt")]
365 pub created_at: DateTime<Utc>,
366 #[serde(rename = "updatedAt")]
367 pub updated_at: DateTime<Utc>,
368}
369
370impl<T: AuthOrganization> From<&T> for OrganizationView {
371 fn from(org: &T) -> Self {
372 Self {
373 id: org.id().into_owned(),
374 name: org.name().to_owned(),
375 slug: org.slug().to_owned(),
376 logo: org.logo().map(str::to_owned),
377 metadata: org.metadata().cloned(),
378 created_at: org.created_at(),
379 updated_at: org.updated_at(),
380 }
381 }
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
386pub struct InvitationView {
387 pub id: String,
388 #[serde(rename = "organizationId")]
389 pub organization_id: String,
390 pub email: String,
391 pub role: String,
392 pub status: InvitationStatus,
393 #[serde(rename = "inviterId")]
394 pub inviter_id: String,
395 #[serde(rename = "expiresAt")]
396 pub expires_at: DateTime<Utc>,
397 #[serde(rename = "createdAt")]
398 pub created_at: DateTime<Utc>,
399}
400
401impl<T: AuthInvitation> From<&T> for InvitationView {
402 fn from(inv: &T) -> Self {
403 Self {
404 id: inv.id().into_owned(),
405 organization_id: inv.organization_id().into_owned(),
406 email: inv.email().to_owned(),
407 role: inv.role().to_owned(),
408 status: inv.status().clone(),
409 inviter_id: inv.inviter_id().into_owned(),
410 expires_at: inv.expires_at(),
411 created_at: inv.created_at(),
412 }
413 }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
418pub struct PasskeyView {
419 pub id: String,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub name: Option<String>,
422 #[serde(rename = "credentialID")]
423 pub credential_id: String,
424 #[serde(rename = "userId")]
425 pub user_id: String,
426 #[serde(rename = "publicKey")]
427 pub public_key: String,
428 pub counter: u64,
429 #[serde(rename = "deviceType")]
430 pub device_type: String,
431 #[serde(rename = "backedUp")]
432 pub backed_up: bool,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub transports: Option<String>,
435 #[serde(rename = "createdAt")]
436 pub created_at: String,
437 #[serde(rename = "updatedAt")]
438 pub updated_at: String,
439 #[serde(skip_serializing_if = "Option::is_none")]
440 pub aaguid: Option<String>,
441}
442
443impl<T: AuthPasskey> From<&T> for PasskeyView {
444 fn from(pk: &T) -> Self {
445 Self {
446 id: pk.id().into_owned(),
447 name: pk.name().map(str::to_owned),
448 credential_id: pk.credential_id().to_owned(),
449 user_id: pk.user_id().into_owned(),
450 public_key: pk.public_key().to_owned(),
451 counter: pk.counter(),
452 device_type: pk.device_type().to_owned(),
453 backed_up: pk.backed_up(),
454 transports: pk.transports().map(str::to_owned),
455 created_at: pk.created_at().to_rfc3339(),
456 updated_at: pk.updated_at().to_rfc3339(),
457 aaguid: pk.aaguid().map(str::to_owned),
458 }
459 }
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
467pub struct ApiKeyView {
468 pub id: String,
469 pub name: Option<String>,
470 pub start: Option<String>,
471 pub prefix: Option<String>,
472 #[serde(rename = "userId")]
473 pub user_id: String,
474 #[serde(rename = "refillInterval")]
475 pub refill_interval: Option<i64>,
476 #[serde(rename = "refillAmount")]
477 pub refill_amount: Option<i64>,
478 #[serde(rename = "lastRefillAt")]
479 pub last_refill_at: Option<String>,
480 pub enabled: bool,
481 #[serde(rename = "rateLimitEnabled")]
482 pub rate_limit_enabled: bool,
483 #[serde(rename = "rateLimitTimeWindow")]
484 pub rate_limit_time_window: Option<i64>,
485 #[serde(rename = "rateLimitMax")]
486 pub rate_limit_max: Option<i64>,
487 #[serde(rename = "requestCount")]
488 pub request_count: Option<i64>,
489 pub remaining: Option<i64>,
490 #[serde(rename = "lastRequest")]
491 pub last_request: Option<String>,
492 #[serde(rename = "expiresAt")]
493 pub expires_at: Option<String>,
494 #[serde(rename = "createdAt")]
495 pub created_at: String,
496 #[serde(rename = "updatedAt")]
497 pub updated_at: String,
498 pub permissions: Option<serde_json::Value>,
499 pub metadata: Option<serde_json::Value>,
500}
501
502impl<T: AuthApiKey> From<&T> for ApiKeyView {
503 fn from(ak: &T) -> Self {
504 Self {
505 id: ak.id().into_owned(),
506 name: ak.name().map(str::to_owned),
507 start: ak.start().map(str::to_owned),
508 prefix: ak.prefix().map(str::to_owned),
509 user_id: ak.user_id().into_owned(),
510 refill_interval: ak.refill_interval(),
511 refill_amount: ak.refill_amount(),
512 last_refill_at: ak.last_refill_at().map(str::to_owned),
513 enabled: ak.enabled(),
514 rate_limit_enabled: ak.rate_limit_enabled(),
515 rate_limit_time_window: ak.rate_limit_time_window(),
516 rate_limit_max: ak.rate_limit_max(),
517 request_count: ak.request_count(),
518 remaining: ak.remaining(),
519 last_request: ak.last_request().map(str::to_owned),
520 expires_at: ak.expires_at().map(str::to_owned),
521 created_at: ak.created_at().to_owned(),
522 updated_at: ak.updated_at().to_owned(),
523 permissions: ak.permissions().and_then(|s| serde_json::from_str(s).ok()),
524 metadata: ak.metadata().and_then(|s| serde_json::from_str(s).ok()),
525 }
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn user_view_serializes_camel_case() {
535 let user = UserView {
536 id: "user-1".to_string(),
537 name: Some("Ada".to_string()),
538 email: Some("ada@example.com".to_string()),
539 email_verified: true,
540 image: None,
541 created_at: Utc::now(),
542 updated_at: Utc::now(),
543 username: Some("ada".to_string()),
544 display_username: Some("Ada".to_string()),
545 two_factor_enabled: true,
546 role: Some("admin".to_string()),
547 banned: false,
548 ban_reason: None,
549 ban_expires: None,
550 metadata: serde_json::json!({}),
551 };
552
553 let json = serde_json::to_value(UserView::from(&user)).expect("serialize user view");
554 assert_eq!(json["emailVerified"], true);
555 assert_eq!(json["displayUsername"], "Ada");
556 assert_eq!(json["twoFactorEnabled"], true);
557 }
558
559 #[test]
560 fn session_view_serializes_camel_case() {
561 let session = SessionView {
562 id: "session-1".to_string(),
563 expires_at: Utc::now(),
564 token: "token".to_string(),
565 created_at: Utc::now(),
566 updated_at: Utc::now(),
567 ip_address: Some("127.0.0.1".to_string()),
568 user_agent: Some("agent".to_string()),
569 user_id: "user-1".to_string(),
570 impersonated_by: Some("admin-1".to_string()),
571 active_organization_id: Some("org-1".to_string()),
572 active: true,
573 };
574
575 let json =
576 serde_json::to_value(SessionView::from(&session)).expect("serialize session view");
577 assert_eq!(json["expiresAt"].is_string(), true);
578 assert_eq!(json["ipAddress"], "127.0.0.1");
579 assert_eq!(json["activeOrganizationId"], "org-1");
580 }
581
582 #[test]
583 fn account_view_omits_password_on_serialize() {
584 let account = AccountView {
585 id: "acc-1".to_string(),
586 account_id: "account-id".to_string(),
587 provider_id: "credential".to_string(),
588 user_id: "user-1".to_string(),
589 access_token: None,
590 refresh_token: None,
591 id_token: None,
592 access_token_expires_at: None,
593 refresh_token_expires_at: None,
594 scope: None,
595 password: Some("$2a$hash".to_string()),
596 created_at: Utc::now(),
597 updated_at: Utc::now(),
598 };
599
600 let json = serde_json::to_value(&account).expect("serialize account view");
601 assert!(
602 json.get("password").is_none(),
603 "password field must not appear in serialized output"
604 );
605 }
606}