use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
pub use crate::address::AddressType;
use crate::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IdentityStatus {
Pending,
Active,
Suspended,
}
impl std::fmt::Display for IdentityStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IdentityStatus::Pending => write!(f, "pending"),
IdentityStatus::Active => write!(f, "active"),
IdentityStatus::Suspended => write!(f, "suspended"),
}
}
}
impl std::str::FromStr for IdentityStatus {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"pending" => Ok(IdentityStatus::Pending),
"active" => Ok(IdentityStatus::Active),
"suspended" => Ok(IdentityStatus::Suspended),
_ => Err(Error::ValidationError(format!("invalid identity status: {}", s))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrarQuota {
pub registrar_id_tag: Box<str>,
pub max_identities: i32,
pub max_storage_bytes: i64,
pub current_identities: i32,
pub current_storage_bytes: i64,
pub updated_at: Timestamp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identity {
pub id_tag_prefix: Box<str>,
pub id_tag_domain: Box<str>,
pub email: Option<Box<str>>,
pub registrar_id_tag: Box<str>,
pub owner_id_tag: Option<Box<str>>,
pub address: Option<Box<str>>,
pub address_type: Option<AddressType>,
pub address_updated_at: Option<Timestamp>,
pub dyndns: bool,
pub lang: Option<Box<str>>,
pub status: IdentityStatus,
pub created_at: Timestamp,
pub updated_at: Timestamp,
pub expires_at: Timestamp,
}
#[derive(Debug, Clone)]
pub struct CreateIdentityOptions<'a> {
pub id_tag_prefix: &'a str,
pub id_tag_domain: &'a str,
pub email: Option<&'a str>,
pub registrar_id_tag: &'a str,
pub owner_id_tag: Option<&'a str>,
pub status: IdentityStatus,
pub address: Option<&'a str>,
pub address_type: Option<AddressType>,
pub dyndns: bool,
pub lang: Option<&'a str>,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug, Clone, Default)]
pub struct UpdateIdentityOptions {
pub email: Option<Box<str>>,
pub owner_id_tag: Option<Box<str>>,
pub address: Option<Box<str>>,
pub address_type: Option<AddressType>,
pub dyndns: Option<bool>,
pub lang: Option<Option<Box<str>>>,
pub status: Option<IdentityStatus>,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug, Clone)]
pub struct ListIdentityOptions {
pub id_tag_domain: String,
pub email: Option<String>,
pub registrar_id_tag: Option<String>,
pub owner_id_tag: Option<String>,
pub status: Option<IdentityStatus>,
pub expires_after: Option<Timestamp>,
pub expired_only: bool,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct ApiKey {
pub id: i32,
pub id_tag_prefix: String,
pub id_tag_domain: String,
pub key_prefix: String,
pub name: Option<String>,
pub created_at: Timestamp,
pub last_used_at: Option<Timestamp>,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug)]
pub struct CreateApiKeyOptions<'a> {
pub id_tag_prefix: &'a str,
pub id_tag_domain: &'a str,
pub name: Option<&'a str>,
pub expires_at: Option<Timestamp>,
}
#[derive(Debug)]
pub struct CreatedApiKey {
pub api_key: ApiKey,
pub plaintext_key: String,
}
#[derive(Debug, Default)]
pub struct ListApiKeyOptions {
pub id_tag_prefix: Option<String>,
pub id_tag_domain: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
#[async_trait]
pub trait IdentityProviderAdapter: Debug + Send + Sync {
async fn create_identity(&self, opts: CreateIdentityOptions<'_>) -> ClResult<Identity>;
async fn read_identity(
&self,
id_tag_prefix: &str,
id_tag_domain: &str,
) -> ClResult<Option<Identity>>;
async fn read_identity_by_email(&self, email: &str) -> ClResult<Option<Identity>>;
async fn update_identity(
&self,
id_tag_prefix: &str,
id_tag_domain: &str,
opts: UpdateIdentityOptions,
) -> ClResult<Identity>;
async fn update_identity_address(
&self,
id_tag_prefix: &str,
id_tag_domain: &str,
address: &str,
address_type: AddressType,
) -> ClResult<Identity>;
async fn delete_identity(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<()>;
async fn list_identities(&self, opts: ListIdentityOptions) -> ClResult<Vec<Identity>>;
async fn identity_exists(&self, id_tag_prefix: &str, id_tag_domain: &str) -> ClResult<bool> {
Ok(self.read_identity(id_tag_prefix, id_tag_domain).await?.is_some())
}
async fn cleanup_expired_identities(&self) -> ClResult<u32>;
async fn renew_identity(
&self,
id_tag_prefix: &str,
id_tag_domain: &str,
new_expires_at: Timestamp,
) -> ClResult<Identity>;
async fn create_api_key(&self, opts: CreateApiKeyOptions<'_>) -> ClResult<CreatedApiKey>;
async fn verify_api_key(&self, key: &str) -> ClResult<Option<String>>;
async fn list_api_keys(&self, opts: ListApiKeyOptions) -> ClResult<Vec<ApiKey>>;
async fn delete_api_key(&self, id: i32) -> ClResult<()>;
async fn delete_api_key_for_identity(
&self,
id: i32,
id_tag_prefix: &str,
id_tag_domain: &str,
) -> ClResult<bool>;
async fn cleanup_expired_api_keys(&self) -> ClResult<u32>;
async fn list_identities_by_registrar(
&self,
registrar_id_tag: &str,
limit: Option<u32>,
offset: Option<u32>,
) -> ClResult<Vec<Identity>>;
async fn get_quota(&self, registrar_id_tag: &str) -> ClResult<RegistrarQuota>;
async fn set_quota_limits(
&self,
registrar_id_tag: &str,
max_identities: i32,
max_storage_bytes: i64,
) -> ClResult<RegistrarQuota>;
async fn check_quota(&self, registrar_id_tag: &str, storage_bytes: i64) -> ClResult<bool>;
async fn increment_quota(
&self,
registrar_id_tag: &str,
storage_bytes: i64,
) -> ClResult<RegistrarQuota>;
async fn decrement_quota(
&self,
registrar_id_tag: &str,
storage_bytes: i64,
) -> ClResult<RegistrarQuota>;
async fn update_quota_on_status_change(
&self,
registrar_id_tag: &str,
old_status: IdentityStatus,
new_status: IdentityStatus,
) -> ClResult<RegistrarQuota>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_structure() {
let now = Timestamp::now();
let identity = Identity {
id_tag_prefix: "test_user".into(),
id_tag_domain: "cloudillo.net".into(),
email: Some("test@example.com".into()),
registrar_id_tag: "registrar".into(),
owner_id_tag: None,
address: Some("192.168.1.1".into()),
address_type: Some(AddressType::Ipv4),
address_updated_at: Some(now),
dyndns: false,
lang: Some("hu".into()),
status: IdentityStatus::Active,
created_at: now,
updated_at: now,
expires_at: now.add_seconds(86400), };
assert_eq!(identity.id_tag_prefix.as_ref(), "test_user");
assert_eq!(identity.id_tag_domain.as_ref(), "cloudillo.net");
assert_eq!(identity.email.as_deref(), Some("test@example.com"));
assert_eq!(identity.registrar_id_tag.as_ref(), "registrar");
assert_eq!(identity.lang.as_deref(), Some("hu"));
assert_eq!(identity.status, IdentityStatus::Active);
assert!(!identity.dyndns);
assert!(identity.expires_at > identity.created_at);
}
#[test]
fn test_identity_with_owner() {
let now = Timestamp::now();
let identity = Identity {
id_tag_prefix: "community_member".into(),
id_tag_domain: "cloudillo.net".into(),
email: None, registrar_id_tag: "registrar".into(),
owner_id_tag: Some("community.cloudillo.net".into()),
address: None,
address_type: None,
address_updated_at: None,
dyndns: false,
lang: None,
status: IdentityStatus::Pending,
created_at: now,
updated_at: now,
expires_at: now.add_seconds(86400),
};
assert_eq!(identity.id_tag_prefix.as_ref(), "community_member");
assert!(identity.email.is_none());
assert_eq!(identity.owner_id_tag.as_deref(), Some("community.cloudillo.net"));
assert_eq!(identity.status, IdentityStatus::Pending);
}
#[test]
fn test_identity_status_display() {
assert_eq!(IdentityStatus::Pending.to_string(), "pending");
assert_eq!(IdentityStatus::Active.to_string(), "active");
assert_eq!(IdentityStatus::Suspended.to_string(), "suspended");
}
#[test]
fn test_identity_status_from_str() {
use std::str::FromStr;
assert_eq!(
IdentityStatus::from_str("pending").expect("should parse"),
IdentityStatus::Pending
);
assert_eq!(
IdentityStatus::from_str("active").expect("should parse"),
IdentityStatus::Active
);
assert_eq!(
IdentityStatus::from_str("suspended").expect("should parse"),
IdentityStatus::Suspended
);
assert!(IdentityStatus::from_str("invalid").is_err());
}
#[test]
fn test_create_identity_options() {
let opts = CreateIdentityOptions {
id_tag_prefix: "test_user",
id_tag_domain: "cloudillo.net",
email: Some("test@example.com"),
registrar_id_tag: "registrar",
owner_id_tag: None,
status: IdentityStatus::Pending,
address: Some("192.168.1.1"),
address_type: Some(AddressType::Ipv4),
dyndns: false,
lang: Some("de"),
expires_at: Some(Timestamp::now().add_seconds(86400)),
};
assert_eq!(opts.id_tag_prefix, "test_user");
assert_eq!(opts.id_tag_domain, "cloudillo.net");
assert_eq!(opts.email, Some("test@example.com"));
assert_eq!(opts.registrar_id_tag, "registrar");
assert_eq!(opts.lang, Some("de"));
assert_eq!(opts.status, IdentityStatus::Pending);
assert!(!opts.dyndns);
assert!(opts.expires_at.is_some());
}
#[test]
fn test_create_identity_options_with_owner() {
let opts = CreateIdentityOptions {
id_tag_prefix: "member",
id_tag_domain: "cloudillo.net",
email: None, registrar_id_tag: "registrar",
owner_id_tag: Some("owner.cloudillo.net"),
status: IdentityStatus::Pending,
address: None,
address_type: None,
dyndns: false,
lang: None,
expires_at: None,
};
assert_eq!(opts.id_tag_prefix, "member");
assert!(opts.email.is_none());
assert_eq!(opts.owner_id_tag, Some("owner.cloudillo.net"));
}
#[test]
fn test_registrar_quota() {
let now = Timestamp::now();
let quota = RegistrarQuota {
registrar_id_tag: "registrar".into(),
max_identities: 1000,
max_storage_bytes: 1_000_000_000,
current_identities: 50,
current_storage_bytes: 50_000_000,
updated_at: now,
};
assert_eq!(quota.registrar_id_tag.as_ref(), "registrar");
assert_eq!(quota.max_identities, 1000);
assert!(quota.current_identities < quota.max_identities);
}
}