1use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use std::fmt::Debug;
10
11pub use crate::address::AddressType;
12use crate::prelude::*;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum IdentityStatus {
17 Pending,
19 Active,
21 Suspended,
23}
24
25impl std::fmt::Display for IdentityStatus {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 IdentityStatus::Pending => write!(f, "pending"),
29 IdentityStatus::Active => write!(f, "active"),
30 IdentityStatus::Suspended => write!(f, "suspended"),
31 }
32 }
33}
34
35impl std::str::FromStr for IdentityStatus {
36 type Err = Error;
37 fn from_str(s: &str) -> Result<Self, Self::Err> {
38 match s {
39 "pending" => Ok(IdentityStatus::Pending),
40 "active" => Ok(IdentityStatus::Active),
41 "suspended" => Ok(IdentityStatus::Suspended),
42 _ => Err(Error::ValidationError(format!("invalid identity status: {}", s))),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct RegistrarQuota {
50 pub registrar_id_tag: Box<str>,
52 pub max_identities: i32,
54 pub max_storage_bytes: i64,
56 pub current_identities: i32,
58 pub current_storage_bytes: i64,
60 pub updated_at: Timestamp,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Identity {
67 pub id_tag_prefix: Box<str>,
69 pub id_tag_domain: Box<str>,
71 pub email: Option<Box<str>>,
73 pub registrar_id_tag: Box<str>,
75 pub owner_id_tag: Option<Box<str>>,
78 pub address: Option<Box<str>>,
80 pub address_type: Option<AddressType>,
82 pub address_updated_at: Option<Timestamp>,
84 pub dyndns: bool,
86 pub lang: Option<Box<str>>,
88 pub status: IdentityStatus,
90 pub created_at: Timestamp,
92 pub updated_at: Timestamp,
94 pub expires_at: Timestamp,
96}
97
98#[derive(Debug, Clone)]
100pub struct CreateIdentityOptions<'a> {
101 pub id_tag_prefix: &'a str,
103 pub id_tag_domain: &'a str,
105 pub email: Option<&'a str>,
107 pub registrar_id_tag: &'a str,
109 pub owner_id_tag: Option<&'a str>,
112 pub status: IdentityStatus,
114 pub address: Option<&'a str>,
116 pub address_type: Option<AddressType>,
118 pub dyndns: bool,
120 pub lang: Option<&'a str>,
122 pub expires_at: Option<Timestamp>,
124}
125
126#[derive(Debug, Clone, Default)]
128pub struct UpdateIdentityOptions {
129 pub email: Option<Box<str>>,
131 pub owner_id_tag: Option<Box<str>>,
133 pub address: Option<Box<str>>,
135 pub address_type: Option<AddressType>,
137 pub dyndns: Option<bool>,
139 pub lang: Option<Option<Box<str>>>,
141 pub status: Option<IdentityStatus>,
143 pub expires_at: Option<Timestamp>,
145}
146
147#[derive(Debug, Clone)]
149pub struct ListIdentityOptions {
150 pub id_tag_domain: String,
153 pub email: Option<String>,
155 pub registrar_id_tag: Option<String>,
157 pub owner_id_tag: Option<String>,
159 pub status: Option<IdentityStatus>,
161 pub expires_after: Option<Timestamp>,
163 pub expired_only: bool,
165 pub limit: Option<u32>,
167 pub offset: Option<u32>,
169}
170
171#[derive(Debug, Clone)]
173pub struct ApiKey {
174 pub id: i32,
175 pub id_tag_prefix: String,
176 pub id_tag_domain: String,
177 pub key_prefix: String,
178 pub name: Option<String>,
179 pub created_at: Timestamp,
180 pub last_used_at: Option<Timestamp>,
181 pub expires_at: Option<Timestamp>,
182}
183
184#[derive(Debug)]
186pub struct CreateApiKeyOptions<'a> {
187 pub id_tag_prefix: &'a str,
188 pub id_tag_domain: &'a str,
189 pub name: Option<&'a str>,
190 pub expires_at: Option<Timestamp>,
191}
192
193#[derive(Debug)]
195pub struct CreatedApiKey {
196 pub api_key: ApiKey,
197 pub plaintext_key: String,
198}
199
200#[derive(Debug, Default)]
202pub struct ListApiKeyOptions {
203 pub id_tag_prefix: Option<String>,
204 pub id_tag_domain: Option<String>,
205 pub limit: Option<u32>,
206 pub offset: Option<u32>,
207}
208
209#[async_trait]
215pub trait IdentityProviderAdapter: Debug + Send + Sync {
216 async fn create_identity(&self, opts: CreateIdentityOptions<'_>) -> ClResult<Identity>;
233
234 async fn read_identity(
242 &self,
243 id_tag_prefix: &str,
244 id_tag_domain: &str,
245 ) -> ClResult<Option<Identity>>;
246
247 async fn read_identity_by_email(&self, email: &str) -> ClResult<Option<Identity>>;
255
256 async fn update_identity(
265 &self,
266 id_tag_prefix: &str,
267 id_tag_domain: &str,
268 opts: UpdateIdentityOptions,
269 ) -> ClResult<Identity>;
270
271 async fn update_identity_address(
287 &self,
288 id_tag_prefix: &str,
289 id_tag_domain: &str,
290 address: &str,
291 address_type: AddressType,
292 ) -> ClResult<Identity>;
293
294 async fn delete_identity(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<()>;
302
303 async fn list_identities(&self, opts: ListIdentityOptions) -> ClResult<Vec<Identity>>;
311
312 async fn identity_exists(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<bool> {
320 Ok(self.read_identity(id_tag_prefix, id_tag_domain).await?.is_some())
321 }
322
323 async fn cleanup_expired_identities(&self) -> ClResult<u32>;
331
332 async fn renew_identity(
341 &self,
342 id_tag_prefix: &str,
343 id_tag_domain: &str,
344 new_expires_at: Timestamp,
345 ) -> ClResult<Identity>;
346
347 async fn create_api_key(&self, opts: CreateApiKeyOptions<'_>) -> ClResult<CreatedApiKey>;
351
352 async fn verify_api_key(&self, key: &str) -> ClResult<Option<String>>;
361
362 async fn list_api_keys(&self, opts: ListApiKeyOptions) -> ClResult<Vec<ApiKey>>;
366
367 async fn delete_api_key(&self, id: i32) -> ClResult<()>;
369
370 async fn delete_api_key_for_identity(
374 &self,
375 id: i32,
376 id_tag_prefix: &str,
377 id_tag_domain: &str,
378 ) -> ClResult<bool>;
379
380 async fn cleanup_expired_api_keys(&self) -> ClResult<u32>;
384
385 async fn list_identities_by_registrar(
395 &self,
396 registrar_id_tag: &str,
397 limit: Option<u32>,
398 offset: Option<u32>,
399 ) -> ClResult<Vec<Identity>>;
400
401 async fn get_quota(&self, registrar_id_tag: &str) -> ClResult<RegistrarQuota>;
409
410 async fn set_quota_limits(
420 &self,
421 registrar_id_tag: &str,
422 max_identities: i32,
423 max_storage_bytes: i64,
424 ) -> ClResult<RegistrarQuota>;
425
426 async fn check_quota(&self, registrar_id_tag: &str, storage_bytes: i64) -> ClResult<bool>;
435
436 async fn increment_quota(
445 &self,
446 registrar_id_tag: &str,
447 storage_bytes: i64,
448 ) -> ClResult<RegistrarQuota>;
449
450 async fn decrement_quota(
459 &self,
460 registrar_id_tag: &str,
461 storage_bytes: i64,
462 ) -> ClResult<RegistrarQuota>;
463
464 async fn update_quota_on_status_change(
476 &self,
477 registrar_id_tag: &str,
478 old_status: IdentityStatus,
479 new_status: IdentityStatus,
480 ) -> ClResult<RegistrarQuota>;
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 #[test]
488 fn test_identity_structure() {
489 let now = Timestamp::now();
490 let identity = Identity {
491 id_tag_prefix: "test_user".into(),
492 id_tag_domain: "cloudillo.net".into(),
493 email: Some("test@example.com".into()),
494 registrar_id_tag: "registrar".into(),
495 owner_id_tag: None,
496 address: Some("192.168.1.1".into()),
497 address_type: Some(AddressType::Ipv4),
498 address_updated_at: Some(now),
499 dyndns: false,
500 lang: Some("hu".into()),
501 status: IdentityStatus::Active,
502 created_at: now,
503 updated_at: now,
504 expires_at: now.add_seconds(86400), };
506
507 assert_eq!(identity.id_tag_prefix.as_ref(), "test_user");
508 assert_eq!(identity.id_tag_domain.as_ref(), "cloudillo.net");
509 assert_eq!(identity.email.as_deref(), Some("test@example.com"));
510 assert_eq!(identity.registrar_id_tag.as_ref(), "registrar");
511 assert_eq!(identity.lang.as_deref(), Some("hu"));
512 assert_eq!(identity.status, IdentityStatus::Active);
513 assert!(!identity.dyndns);
514 assert!(identity.expires_at > identity.created_at);
515 }
516
517 #[test]
518 fn test_identity_with_owner() {
519 let now = Timestamp::now();
520 let identity = Identity {
521 id_tag_prefix: "community_member".into(),
522 id_tag_domain: "cloudillo.net".into(),
523 email: None, registrar_id_tag: "registrar".into(),
525 owner_id_tag: Some("community.cloudillo.net".into()),
526 address: None,
527 address_type: None,
528 address_updated_at: None,
529 dyndns: false,
530 lang: None,
531 status: IdentityStatus::Pending,
532 created_at: now,
533 updated_at: now,
534 expires_at: now.add_seconds(86400),
535 };
536
537 assert_eq!(identity.id_tag_prefix.as_ref(), "community_member");
538 assert!(identity.email.is_none());
539 assert_eq!(identity.owner_id_tag.as_deref(), Some("community.cloudillo.net"));
540 assert_eq!(identity.status, IdentityStatus::Pending);
541 }
542
543 #[test]
544 fn test_identity_status_display() {
545 assert_eq!(IdentityStatus::Pending.to_string(), "pending");
546 assert_eq!(IdentityStatus::Active.to_string(), "active");
547 assert_eq!(IdentityStatus::Suspended.to_string(), "suspended");
548 }
549
550 #[test]
551 fn test_identity_status_from_str() {
552 use std::str::FromStr;
553 assert_eq!(
554 IdentityStatus::from_str("pending").expect("should parse"),
555 IdentityStatus::Pending
556 );
557 assert_eq!(
558 IdentityStatus::from_str("active").expect("should parse"),
559 IdentityStatus::Active
560 );
561 assert_eq!(
562 IdentityStatus::from_str("suspended").expect("should parse"),
563 IdentityStatus::Suspended
564 );
565 assert!(IdentityStatus::from_str("invalid").is_err());
566 }
567
568 #[test]
569 fn test_create_identity_options() {
570 let opts = CreateIdentityOptions {
571 id_tag_prefix: "test_user",
572 id_tag_domain: "cloudillo.net",
573 email: Some("test@example.com"),
574 registrar_id_tag: "registrar",
575 owner_id_tag: None,
576 status: IdentityStatus::Pending,
577 address: Some("192.168.1.1"),
578 address_type: Some(AddressType::Ipv4),
579 dyndns: false,
580 lang: Some("de"),
581 expires_at: Some(Timestamp::now().add_seconds(86400)),
582 };
583
584 assert_eq!(opts.id_tag_prefix, "test_user");
585 assert_eq!(opts.id_tag_domain, "cloudillo.net");
586 assert_eq!(opts.email, Some("test@example.com"));
587 assert_eq!(opts.registrar_id_tag, "registrar");
588 assert_eq!(opts.lang, Some("de"));
589 assert_eq!(opts.status, IdentityStatus::Pending);
590 assert!(!opts.dyndns);
591 assert!(opts.expires_at.is_some());
592 }
593
594 #[test]
595 fn test_create_identity_options_with_owner() {
596 let opts = CreateIdentityOptions {
597 id_tag_prefix: "member",
598 id_tag_domain: "cloudillo.net",
599 email: None, registrar_id_tag: "registrar",
601 owner_id_tag: Some("owner.cloudillo.net"),
602 status: IdentityStatus::Pending,
603 address: None,
604 address_type: None,
605 dyndns: false,
606 lang: None,
607 expires_at: None,
608 };
609
610 assert_eq!(opts.id_tag_prefix, "member");
611 assert!(opts.email.is_none());
612 assert_eq!(opts.owner_id_tag, Some("owner.cloudillo.net"));
613 }
614
615 #[test]
616 fn test_registrar_quota() {
617 let now = Timestamp::now();
618 let quota = RegistrarQuota {
619 registrar_id_tag: "registrar".into(),
620 max_identities: 1000,
621 max_storage_bytes: 1_000_000_000,
622 current_identities: 50,
623 current_storage_bytes: 50_000_000,
624 updated_at: now,
625 };
626
627 assert_eq!(quota.registrar_id_tag.as_ref(), "registrar");
628 assert_eq!(quota.max_identities, 1000);
629 assert!(quota.current_identities < quota.max_identities);
630 }
631}
632
633