1use async_trait::async_trait;
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13
14pub use crate::address::AddressType;
15use crate::prelude::*;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum IdentityStatus {
20 Pending,
22 Active,
24 Suspended,
26}
27
28impl std::fmt::Display for IdentityStatus {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 IdentityStatus::Pending => write!(f, "pending"),
32 IdentityStatus::Active => write!(f, "active"),
33 IdentityStatus::Suspended => write!(f, "suspended"),
34 }
35 }
36}
37
38impl std::str::FromStr for IdentityStatus {
39 type Err = Error;
40 fn from_str(s: &str) -> Result<Self, Self::Err> {
41 match s {
42 "pending" => Ok(IdentityStatus::Pending),
43 "active" => Ok(IdentityStatus::Active),
44 "suspended" => Ok(IdentityStatus::Suspended),
45 _ => Err(Error::ValidationError(format!("invalid identity status: {}", s))),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RegistrarQuota {
53 pub registrar_id_tag: Box<str>,
55 pub max_identities: i32,
57 pub max_storage_bytes: i64,
59 pub current_identities: i32,
61 pub current_storage_bytes: i64,
63 pub updated_at: Timestamp,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Identity {
70 pub id_tag_prefix: Box<str>,
72 pub id_tag_domain: Box<str>,
74 pub email: Option<Box<str>>,
76 pub registrar_id_tag: Box<str>,
78 pub owner_id_tag: Option<Box<str>>,
81 pub address: Option<Box<str>>,
83 pub address_type: Option<AddressType>,
85 pub address_updated_at: Option<Timestamp>,
87 pub dyndns: bool,
89 pub lang: Option<Box<str>>,
91 pub status: IdentityStatus,
93 pub created_at: Timestamp,
95 pub updated_at: Timestamp,
97 pub expires_at: Timestamp,
99}
100
101#[derive(Debug, Clone)]
103pub struct CreateIdentityOptions<'a> {
104 pub id_tag_prefix: &'a str,
106 pub id_tag_domain: &'a str,
108 pub email: Option<&'a str>,
110 pub registrar_id_tag: &'a str,
112 pub owner_id_tag: Option<&'a str>,
115 pub status: IdentityStatus,
117 pub address: Option<&'a str>,
119 pub address_type: Option<AddressType>,
121 pub dyndns: bool,
123 pub lang: Option<&'a str>,
125 pub expires_at: Option<Timestamp>,
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct UpdateIdentityOptions {
132 pub email: Option<Box<str>>,
134 pub owner_id_tag: Option<Box<str>>,
136 pub address: Option<Box<str>>,
138 pub address_type: Option<AddressType>,
140 pub dyndns: Option<bool>,
142 pub lang: Option<Option<Box<str>>>,
144 pub status: Option<IdentityStatus>,
146 pub expires_at: Option<Timestamp>,
148}
149
150#[derive(Debug, Clone)]
152pub struct ListIdentityOptions {
153 pub id_tag_domain: String,
156 pub email: Option<String>,
158 pub registrar_id_tag: Option<String>,
160 pub owner_id_tag: Option<String>,
162 pub status: Option<IdentityStatus>,
164 pub expires_after: Option<Timestamp>,
166 pub expired_only: bool,
168 pub limit: Option<u32>,
170 pub offset: Option<u32>,
172}
173
174#[derive(Debug, Clone)]
176pub struct ApiKey {
177 pub id: i32,
178 pub id_tag_prefix: String,
179 pub id_tag_domain: String,
180 pub key_prefix: String,
181 pub name: Option<String>,
182 pub created_at: Timestamp,
183 pub last_used_at: Option<Timestamp>,
184 pub expires_at: Option<Timestamp>,
185}
186
187#[derive(Debug)]
189pub struct CreateApiKeyOptions<'a> {
190 pub id_tag_prefix: &'a str,
191 pub id_tag_domain: &'a str,
192 pub name: Option<&'a str>,
193 pub expires_at: Option<Timestamp>,
194}
195
196#[derive(Debug)]
198pub struct CreatedApiKey {
199 pub api_key: ApiKey,
200 pub plaintext_key: String,
201}
202
203#[derive(Debug, Default)]
205pub struct ListApiKeyOptions {
206 pub id_tag_prefix: Option<String>,
207 pub id_tag_domain: Option<String>,
208 pub limit: Option<u32>,
209 pub offset: Option<u32>,
210}
211
212#[async_trait]
218pub trait IdentityProviderAdapter: Debug + Send + Sync {
219 async fn create_identity(&self, opts: CreateIdentityOptions<'_>) -> ClResult<Identity>;
236
237 async fn read_identity(
245 &self,
246 id_tag_prefix: &str,
247 id_tag_domain: &str,
248 ) -> ClResult<Option<Identity>>;
249
250 async fn read_identity_by_email(&self, email: &str) -> ClResult<Option<Identity>>;
258
259 async fn update_identity(
268 &self,
269 id_tag_prefix: &str,
270 id_tag_domain: &str,
271 opts: UpdateIdentityOptions,
272 ) -> ClResult<Identity>;
273
274 async fn update_identity_address(
290 &self,
291 id_tag_prefix: &str,
292 id_tag_domain: &str,
293 address: &str,
294 address_type: AddressType,
295 ) -> ClResult<Identity>;
296
297 async fn delete_identity(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<()>;
305
306 async fn list_identities(&self, opts: ListIdentityOptions) -> ClResult<Vec<Identity>>;
314
315 async fn identity_exists(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<bool> {
323 Ok(self.read_identity(id_tag_prefix, id_tag_domain).await?.is_some())
324 }
325
326 async fn cleanup_expired_identities(&self) -> ClResult<u32>;
334
335 async fn renew_identity(
344 &self,
345 id_tag_prefix: &str,
346 id_tag_domain: &str,
347 new_expires_at: Timestamp,
348 ) -> ClResult<Identity>;
349
350 async fn create_api_key(&self, opts: CreateApiKeyOptions<'_>) -> ClResult<CreatedApiKey>;
354
355 async fn verify_api_key(&self, key: &str) -> ClResult<Option<String>>;
364
365 async fn list_api_keys(&self, opts: ListApiKeyOptions) -> ClResult<Vec<ApiKey>>;
369
370 async fn delete_api_key(&self, id: i32) -> ClResult<()>;
372
373 async fn delete_api_key_for_identity(
377 &self,
378 id: i32,
379 id_tag_prefix: &str,
380 id_tag_domain: &str,
381 ) -> ClResult<bool>;
382
383 async fn cleanup_expired_api_keys(&self) -> ClResult<u32>;
387
388 async fn list_identities_by_registrar(
398 &self,
399 registrar_id_tag: &str,
400 limit: Option<u32>,
401 offset: Option<u32>,
402 ) -> ClResult<Vec<Identity>>;
403
404 async fn get_quota(&self, registrar_id_tag: &str) -> ClResult<RegistrarQuota>;
412
413 async fn set_quota_limits(
423 &self,
424 registrar_id_tag: &str,
425 max_identities: i32,
426 max_storage_bytes: i64,
427 ) -> ClResult<RegistrarQuota>;
428
429 async fn check_quota(&self, registrar_id_tag: &str, storage_bytes: i64) -> ClResult<bool>;
438
439 async fn increment_quota(
448 &self,
449 registrar_id_tag: &str,
450 storage_bytes: i64,
451 ) -> ClResult<RegistrarQuota>;
452
453 async fn decrement_quota(
462 &self,
463 registrar_id_tag: &str,
464 storage_bytes: i64,
465 ) -> ClResult<RegistrarQuota>;
466
467 async fn update_quota_on_status_change(
479 &self,
480 registrar_id_tag: &str,
481 old_status: IdentityStatus,
482 new_status: IdentityStatus,
483 ) -> ClResult<RegistrarQuota>;
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_identity_structure() {
492 let now = Timestamp::now();
493 let identity = Identity {
494 id_tag_prefix: "test_user".into(),
495 id_tag_domain: "cloudillo.net".into(),
496 email: Some("test@example.com".into()),
497 registrar_id_tag: "registrar".into(),
498 owner_id_tag: None,
499 address: Some("192.168.1.1".into()),
500 address_type: Some(AddressType::Ipv4),
501 address_updated_at: Some(now),
502 dyndns: false,
503 lang: Some("hu".into()),
504 status: IdentityStatus::Active,
505 created_at: now,
506 updated_at: now,
507 expires_at: now.add_seconds(86400), };
509
510 assert_eq!(identity.id_tag_prefix.as_ref(), "test_user");
511 assert_eq!(identity.id_tag_domain.as_ref(), "cloudillo.net");
512 assert_eq!(identity.email.as_deref(), Some("test@example.com"));
513 assert_eq!(identity.registrar_id_tag.as_ref(), "registrar");
514 assert_eq!(identity.lang.as_deref(), Some("hu"));
515 assert_eq!(identity.status, IdentityStatus::Active);
516 assert!(!identity.dyndns);
517 assert!(identity.expires_at > identity.created_at);
518 }
519
520 #[test]
521 fn test_identity_with_owner() {
522 let now = Timestamp::now();
523 let identity = Identity {
524 id_tag_prefix: "community_member".into(),
525 id_tag_domain: "cloudillo.net".into(),
526 email: None, registrar_id_tag: "registrar".into(),
528 owner_id_tag: Some("community.cloudillo.net".into()),
529 address: None,
530 address_type: None,
531 address_updated_at: None,
532 dyndns: false,
533 lang: None,
534 status: IdentityStatus::Pending,
535 created_at: now,
536 updated_at: now,
537 expires_at: now.add_seconds(86400),
538 };
539
540 assert_eq!(identity.id_tag_prefix.as_ref(), "community_member");
541 assert!(identity.email.is_none());
542 assert_eq!(identity.owner_id_tag.as_deref(), Some("community.cloudillo.net"));
543 assert_eq!(identity.status, IdentityStatus::Pending);
544 }
545
546 #[test]
547 fn test_identity_status_display() {
548 assert_eq!(IdentityStatus::Pending.to_string(), "pending");
549 assert_eq!(IdentityStatus::Active.to_string(), "active");
550 assert_eq!(IdentityStatus::Suspended.to_string(), "suspended");
551 }
552
553 #[test]
554 fn test_identity_status_from_str() {
555 use std::str::FromStr;
556 assert_eq!(
557 IdentityStatus::from_str("pending").expect("should parse"),
558 IdentityStatus::Pending
559 );
560 assert_eq!(
561 IdentityStatus::from_str("active").expect("should parse"),
562 IdentityStatus::Active
563 );
564 assert_eq!(
565 IdentityStatus::from_str("suspended").expect("should parse"),
566 IdentityStatus::Suspended
567 );
568 assert!(IdentityStatus::from_str("invalid").is_err());
569 }
570
571 #[test]
572 fn test_create_identity_options() {
573 let opts = CreateIdentityOptions {
574 id_tag_prefix: "test_user",
575 id_tag_domain: "cloudillo.net",
576 email: Some("test@example.com"),
577 registrar_id_tag: "registrar",
578 owner_id_tag: None,
579 status: IdentityStatus::Pending,
580 address: Some("192.168.1.1"),
581 address_type: Some(AddressType::Ipv4),
582 dyndns: false,
583 lang: Some("de"),
584 expires_at: Some(Timestamp::now().add_seconds(86400)),
585 };
586
587 assert_eq!(opts.id_tag_prefix, "test_user");
588 assert_eq!(opts.id_tag_domain, "cloudillo.net");
589 assert_eq!(opts.email, Some("test@example.com"));
590 assert_eq!(opts.registrar_id_tag, "registrar");
591 assert_eq!(opts.lang, Some("de"));
592 assert_eq!(opts.status, IdentityStatus::Pending);
593 assert!(!opts.dyndns);
594 assert!(opts.expires_at.is_some());
595 }
596
597 #[test]
598 fn test_create_identity_options_with_owner() {
599 let opts = CreateIdentityOptions {
600 id_tag_prefix: "member",
601 id_tag_domain: "cloudillo.net",
602 email: None, registrar_id_tag: "registrar",
604 owner_id_tag: Some("owner.cloudillo.net"),
605 status: IdentityStatus::Pending,
606 address: None,
607 address_type: None,
608 dyndns: false,
609 lang: None,
610 expires_at: None,
611 };
612
613 assert_eq!(opts.id_tag_prefix, "member");
614 assert!(opts.email.is_none());
615 assert_eq!(opts.owner_id_tag, Some("owner.cloudillo.net"));
616 }
617
618 #[test]
619 fn test_registrar_quota() {
620 let now = Timestamp::now();
621 let quota = RegistrarQuota {
622 registrar_id_tag: "registrar".into(),
623 max_identities: 1000,
624 max_storage_bytes: 1_000_000_000,
625 current_identities: 50,
626 current_storage_bytes: 50_000_000,
627 updated_at: now,
628 };
629
630 assert_eq!(quota.registrar_id_tag.as_ref(), "registrar");
631 assert_eq!(quota.max_identities, 1000);
632 assert!(quota.current_identities < quota.max_identities);
633 }
634}
635
636