Skip to main content

postrust_proxy/saas/
types.rs

1//! Type definitions for the SaaS domain management module.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::net::IpAddr;
7use uuid::Uuid;
8
9/// Tenant status.
10#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
11#[serde(rename_all = "snake_case")]
12#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
13pub enum TenantStatus {
14    /// Active tenant
15    #[default]
16    Active,
17    /// Suspended tenant
18    Suspended,
19    /// Pending activation
20    Pending,
21}
22
23/// A SaaS tenant (customer).
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct Tenant {
26    pub id: Uuid,
27    pub name: String,
28    pub slug: String,
29    pub email: String,
30    pub status: TenantStatus,
31    pub plan: String,
32    pub max_domains: i32,
33    pub max_routes_per_domain: i32,
34    pub settings: serde_json::Value,
35    pub created_at: DateTime<Utc>,
36    pub updated_at: DateTime<Utc>,
37}
38
39/// Database row for tenant.
40#[derive(Clone, Debug, sqlx::FromRow)]
41pub struct TenantRow {
42    pub id: Uuid,
43    pub name: String,
44    pub slug: String,
45    pub email: String,
46    pub status: String,
47    pub plan: String,
48    pub max_domains: i32,
49    pub max_routes_per_domain: i32,
50    pub settings: serde_json::Value,
51    pub created_at: DateTime<Utc>,
52    pub updated_at: DateTime<Utc>,
53}
54
55impl From<TenantRow> for Tenant {
56    fn from(row: TenantRow) -> Self {
57        Self {
58            id: row.id,
59            name: row.name,
60            slug: row.slug,
61            email: row.email,
62            status: match row.status.as_str() {
63                "suspended" => TenantStatus::Suspended,
64                "pending" => TenantStatus::Pending,
65                _ => TenantStatus::Active,
66            },
67            plan: row.plan,
68            max_domains: row.max_domains,
69            max_routes_per_domain: row.max_routes_per_domain,
70            settings: row.settings,
71            created_at: row.created_at,
72            updated_at: row.updated_at,
73        }
74    }
75}
76
77/// Domain verification status.
78#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
79#[serde(rename_all = "snake_case")]
80#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
81pub enum VerificationStatus {
82    /// Pending verification
83    #[default]
84    Pending,
85    /// Successfully verified
86    Verified,
87    /// Verification failed
88    Failed,
89    /// Verification expired
90    Expired,
91}
92
93/// Domain verification method.
94#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
95#[serde(rename_all = "snake_case")]
96#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
97pub enum VerificationMethod {
98    /// DNS TXT record verification
99    #[default]
100    Dns,
101    /// HTTP file verification
102    Http,
103}
104
105/// SSL certificate status.
106#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
107#[serde(rename_all = "snake_case")]
108#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
109pub enum SslStatus {
110    /// Pending provisioning
111    #[default]
112    Pending,
113    /// Currently provisioning
114    Provisioning,
115    /// Certificate active
116    Active,
117    /// Provisioning failed
118    Failed,
119    /// Certificate expired
120    Expired,
121}
122
123/// SSL provider type.
124#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
125#[serde(rename_all = "snake_case")]
126#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
127pub enum SslProvider {
128    /// Automatic via ACME/Let's Encrypt
129    #[default]
130    Acme,
131    /// Manual certificate upload
132    Manual,
133    /// No SSL
134    None,
135}
136
137/// A custom domain.
138#[derive(Clone, Debug, Serialize, Deserialize)]
139pub struct Domain {
140    pub id: Uuid,
141    pub tenant_id: Uuid,
142    pub domain: String,
143    pub verification_status: VerificationStatus,
144    pub verification_method: VerificationMethod,
145    pub verification_token: String,
146    pub verification_attempts: i32,
147    pub verified_at: Option<DateTime<Utc>>,
148    pub last_verification_attempt: Option<DateTime<Utc>>,
149    pub ssl_status: SslStatus,
150    pub ssl_provider: SslProvider,
151    pub ssl_expires_at: Option<DateTime<Utc>>,
152    pub enabled: bool,
153    pub created_at: DateTime<Utc>,
154    pub updated_at: DateTime<Utc>,
155}
156
157/// Database row for domain with string types.
158#[derive(Clone, Debug, sqlx::FromRow)]
159pub struct DomainRow {
160    pub id: Uuid,
161    pub tenant_id: Uuid,
162    pub domain: String,
163    pub verification_status: String,
164    pub verification_method: String,
165    pub verification_token: String,
166    pub verification_attempts: i32,
167    pub verified_at: Option<DateTime<Utc>>,
168    pub last_verification_attempt: Option<DateTime<Utc>>,
169    pub ssl_status: String,
170    pub ssl_provider: String,
171    pub ssl_expires_at: Option<DateTime<Utc>>,
172    pub enabled: bool,
173    pub created_at: DateTime<Utc>,
174    pub updated_at: DateTime<Utc>,
175}
176
177impl From<DomainRow> for Domain {
178    fn from(row: DomainRow) -> Self {
179        Self {
180            id: row.id,
181            tenant_id: row.tenant_id,
182            domain: row.domain,
183            verification_status: match row.verification_status.as_str() {
184                "verified" => VerificationStatus::Verified,
185                "failed" => VerificationStatus::Failed,
186                "expired" => VerificationStatus::Expired,
187                _ => VerificationStatus::Pending,
188            },
189            verification_method: match row.verification_method.as_str() {
190                "http" => VerificationMethod::Http,
191                _ => VerificationMethod::Dns,
192            },
193            verification_token: row.verification_token,
194            verification_attempts: row.verification_attempts,
195            verified_at: row.verified_at,
196            last_verification_attempt: row.last_verification_attempt,
197            ssl_status: match row.ssl_status.as_str() {
198                "provisioning" => SslStatus::Provisioning,
199                "active" => SslStatus::Active,
200                "failed" => SslStatus::Failed,
201                "expired" => SslStatus::Expired,
202                _ => SslStatus::Pending,
203            },
204            ssl_provider: match row.ssl_provider.as_str() {
205                "manual" => SslProvider::Manual,
206                "none" => SslProvider::None,
207                _ => SslProvider::Acme,
208            },
209            ssl_expires_at: row.ssl_expires_at,
210            enabled: row.enabled,
211            created_at: row.created_at,
212            updated_at: row.updated_at,
213        }
214    }
215}
216
217/// Path matching type.
218#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
219#[serde(rename_all = "snake_case")]
220#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
221pub enum DomainPathMatchType {
222    /// Prefix matching (default)
223    #[default]
224    Prefix,
225    /// Exact matching
226    Exact,
227    /// Regex matching
228    Regex,
229}
230
231/// A domain route.
232#[derive(Clone, Debug, Serialize, Deserialize)]
233pub struct DomainRoute {
234    pub id: Uuid,
235    pub domain_id: Uuid,
236    pub tenant_id: Uuid,
237    pub name: String,
238    pub path_pattern: String,
239    pub path_type: DomainPathMatchType,
240    pub methods: Option<Vec<String>>,
241    pub priority: i32,
242    pub upstream_id: Option<Uuid>,
243    pub strip_path: bool,
244    pub add_headers: HashMap<String, String>,
245    pub remove_headers: Vec<String>,
246    pub rate_limit_requests: Option<i32>,
247    pub rate_limit_window_secs: Option<i32>,
248    pub timeout_secs: i32,
249    pub enabled: bool,
250    pub created_at: DateTime<Utc>,
251    pub updated_at: DateTime<Utc>,
252}
253
254/// Database row for domain route.
255#[derive(Clone, Debug, sqlx::FromRow)]
256pub struct DomainRouteRow {
257    pub id: Uuid,
258    pub domain_id: Uuid,
259    pub tenant_id: Uuid,
260    pub name: String,
261    pub path_pattern: String,
262    pub path_type: String,
263    pub methods: Option<Vec<String>>,
264    pub priority: i32,
265    pub upstream_id: Option<Uuid>,
266    pub strip_path: bool,
267    pub add_headers: serde_json::Value,
268    pub remove_headers: Vec<String>,
269    pub rate_limit_requests: Option<i32>,
270    pub rate_limit_window_secs: Option<i32>,
271    pub timeout_secs: i32,
272    pub enabled: bool,
273    pub created_at: DateTime<Utc>,
274    pub updated_at: DateTime<Utc>,
275}
276
277impl From<DomainRouteRow> for DomainRoute {
278    fn from(row: DomainRouteRow) -> Self {
279        let add_headers: HashMap<String, String> =
280            serde_json::from_value(row.add_headers).unwrap_or_default();
281
282        Self {
283            id: row.id,
284            domain_id: row.domain_id,
285            tenant_id: row.tenant_id,
286            name: row.name,
287            path_pattern: row.path_pattern,
288            path_type: match row.path_type.as_str() {
289                "exact" => DomainPathMatchType::Exact,
290                "regex" => DomainPathMatchType::Regex,
291                _ => DomainPathMatchType::Prefix,
292            },
293            methods: row.methods,
294            priority: row.priority,
295            upstream_id: row.upstream_id,
296            strip_path: row.strip_path,
297            add_headers,
298            remove_headers: row.remove_headers,
299            rate_limit_requests: row.rate_limit_requests,
300            rate_limit_window_secs: row.rate_limit_window_secs,
301            timeout_secs: row.timeout_secs,
302            enabled: row.enabled,
303            created_at: row.created_at,
304            updated_at: row.updated_at,
305        }
306    }
307}
308
309/// Load balancing strategy.
310#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
311#[serde(rename_all = "snake_case")]
312#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
313pub enum DomainLoadBalanceStrategy {
314    /// Round-robin (default)
315    #[default]
316    RoundRobin,
317    /// Least connections
318    LeastConnections,
319    /// Weighted
320    Weighted,
321    /// Random
322    Random,
323    /// Sticky (cookie-based)
324    Sticky,
325}
326
327/// A domain upstream (backend server group).
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub struct DomainUpstream {
330    pub id: Uuid,
331    pub tenant_id: Uuid,
332    pub name: String,
333    pub lb_strategy: DomainLoadBalanceStrategy,
334    pub health_check_enabled: bool,
335    pub health_check_path: String,
336    pub health_check_interval_secs: i32,
337    pub health_check_timeout_secs: i32,
338    pub healthy_threshold: i32,
339    pub unhealthy_threshold: i32,
340    pub enabled: bool,
341    pub created_at: DateTime<Utc>,
342    pub updated_at: DateTime<Utc>,
343    /// Backends (loaded separately)
344    #[serde(default)]
345    pub backends: Vec<DomainBackend>,
346}
347
348/// Database row for domain upstream.
349#[derive(Clone, Debug, sqlx::FromRow)]
350pub struct DomainUpstreamRow {
351    pub id: Uuid,
352    pub tenant_id: Uuid,
353    pub name: String,
354    pub lb_strategy: String,
355    pub health_check_enabled: bool,
356    pub health_check_path: String,
357    pub health_check_interval_secs: i32,
358    pub health_check_timeout_secs: i32,
359    pub healthy_threshold: i32,
360    pub unhealthy_threshold: i32,
361    pub enabled: bool,
362    pub created_at: DateTime<Utc>,
363    pub updated_at: DateTime<Utc>,
364}
365
366impl From<DomainUpstreamRow> for DomainUpstream {
367    fn from(row: DomainUpstreamRow) -> Self {
368        Self {
369            id: row.id,
370            tenant_id: row.tenant_id,
371            name: row.name,
372            lb_strategy: match row.lb_strategy.as_str() {
373                "least_connections" => DomainLoadBalanceStrategy::LeastConnections,
374                "weighted" => DomainLoadBalanceStrategy::Weighted,
375                "random" => DomainLoadBalanceStrategy::Random,
376                "sticky" => DomainLoadBalanceStrategy::Sticky,
377                _ => DomainLoadBalanceStrategy::RoundRobin,
378            },
379            health_check_enabled: row.health_check_enabled,
380            health_check_path: row.health_check_path,
381            health_check_interval_secs: row.health_check_interval_secs,
382            health_check_timeout_secs: row.health_check_timeout_secs,
383            healthy_threshold: row.healthy_threshold,
384            unhealthy_threshold: row.unhealthy_threshold,
385            enabled: row.enabled,
386            created_at: row.created_at,
387            updated_at: row.updated_at,
388            backends: Vec::new(),
389        }
390    }
391}
392
393/// A backend server.
394#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
395pub struct DomainBackend {
396    pub id: Uuid,
397    pub upstream_id: Uuid,
398    pub address: String,
399    pub scheme: String,
400    pub weight: i32,
401    pub enabled: bool,
402    pub created_at: DateTime<Utc>,
403}
404
405/// An API key for tenant authentication.
406#[derive(Clone, Debug, Serialize, Deserialize)]
407pub struct ApiKey {
408    pub id: Uuid,
409    pub tenant_id: Uuid,
410    pub name: String,
411    /// Only available on creation
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub key: Option<String>,
414    pub key_prefix: String,
415    pub scopes: Vec<String>,
416    pub last_used_at: Option<DateTime<Utc>>,
417    pub expires_at: Option<DateTime<Utc>>,
418    pub enabled: bool,
419    pub created_at: DateTime<Utc>,
420}
421
422/// Database row for API key.
423#[derive(Clone, Debug, sqlx::FromRow)]
424pub struct ApiKeyRow {
425    pub id: Uuid,
426    pub tenant_id: Uuid,
427    pub name: String,
428    pub key_hash: String,
429    pub key_prefix: String,
430    pub scopes: Vec<String>,
431    pub last_used_at: Option<DateTime<Utc>>,
432    pub expires_at: Option<DateTime<Utc>>,
433    pub enabled: bool,
434    pub created_at: DateTime<Utc>,
435}
436
437impl From<ApiKeyRow> for ApiKey {
438    fn from(row: ApiKeyRow) -> Self {
439        Self {
440            id: row.id,
441            tenant_id: row.tenant_id,
442            name: row.name,
443            key: None, // Never expose the key after creation
444            key_prefix: row.key_prefix,
445            scopes: row.scopes,
446            last_used_at: row.last_used_at,
447            expires_at: row.expires_at,
448            enabled: row.enabled,
449            created_at: row.created_at,
450        }
451    }
452}
453
454/// Verification challenge status.
455#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
456#[serde(rename_all = "snake_case")]
457pub enum ChallengeStatus {
458    /// Pending verification
459    #[default]
460    Pending,
461    /// Currently checking
462    Checking,
463    /// Successfully verified
464    Verified,
465    /// Verification failed
466    Failed,
467}
468
469/// A verification challenge.
470#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
471pub struct VerificationChallenge {
472    pub id: Uuid,
473    pub domain_id: Uuid,
474    pub challenge_type: String,
475    pub token: String,
476    pub expected_value: String,
477    pub status: String,
478    pub error_message: Option<String>,
479    pub created_at: DateTime<Utc>,
480    pub expires_at: DateTime<Utc>,
481    pub verified_at: Option<DateTime<Utc>>,
482}
483
484/// Audit log entry.
485#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
486pub struct AuditLogEntry {
487    pub id: Uuid,
488    pub tenant_id: Option<Uuid>,
489    pub domain_id: Option<Uuid>,
490    pub action: String,
491    pub actor_type: String,
492    pub actor_id: Option<String>,
493    pub details: serde_json::Value,
494    pub ip_address: Option<IpAddr>,
495    pub created_at: DateTime<Utc>,
496}
497
498// ============================================================================
499// API Request/Response Types
500// ============================================================================
501
502/// Create tenant request.
503#[derive(Clone, Debug, Deserialize)]
504pub struct CreateTenantRequest {
505    pub name: String,
506    pub slug: String,
507    pub email: String,
508    pub plan: Option<String>,
509}
510
511/// Update tenant request.
512#[derive(Clone, Debug, Deserialize)]
513pub struct UpdateTenantRequest {
514    pub name: Option<String>,
515    pub email: Option<String>,
516    pub plan: Option<String>,
517    pub settings: Option<serde_json::Value>,
518}
519
520/// Create domain request.
521#[derive(Clone, Debug, Deserialize)]
522pub struct CreateDomainRequest {
523    pub domain: String,
524    #[serde(default)]
525    pub verification_method: VerificationMethod,
526    #[serde(default)]
527    pub ssl_provider: SslProvider,
528}
529
530/// Domain response with verification instructions.
531#[derive(Clone, Debug, Serialize)]
532pub struct DomainResponse {
533    #[serde(flatten)]
534    pub domain: Domain,
535    pub verification_instructions: VerificationInstructions,
536}
537
538/// Verification instructions.
539#[derive(Clone, Debug, Serialize)]
540pub struct VerificationInstructions {
541    pub method: VerificationMethod,
542    /// For DNS verification
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub dns_record_type: Option<String>,
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub dns_record_name: Option<String>,
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub dns_record_value: Option<String>,
549    /// For HTTP verification
550    #[serde(skip_serializing_if = "Option::is_none")]
551    pub http_url: Option<String>,
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub http_expected_content: Option<String>,
554}
555
556impl VerificationInstructions {
557    /// Create DNS verification instructions.
558    pub fn dns(domain: &str, token: &str) -> Self {
559        Self {
560            method: VerificationMethod::Dns,
561            dns_record_type: Some("TXT".to_string()),
562            dns_record_name: Some(format!("_postrust-verification.{}", domain)),
563            dns_record_value: Some(format!("postrust-verify={}", token)),
564            http_url: None,
565            http_expected_content: None,
566        }
567    }
568
569    /// Create HTTP verification instructions.
570    pub fn http(domain: &str, token: &str) -> Self {
571        Self {
572            method: VerificationMethod::Http,
573            dns_record_type: None,
574            dns_record_name: None,
575            dns_record_value: None,
576            http_url: Some(format!(
577                "https://{}/.well-known/postrust-verification/{}",
578                domain, token
579            )),
580            http_expected_content: Some(format!("postrust-verify={}", token)),
581        }
582    }
583}
584
585/// Verification result.
586#[derive(Clone, Debug, Serialize)]
587#[serde(rename_all = "snake_case")]
588pub enum VerificationResult {
589    /// Domain verified successfully
590    Verified,
591    /// Verification pending
592    Pending,
593    /// Verification failed
594    Failed { reason: String },
595}
596
597/// Create domain route request.
598#[derive(Clone, Debug, Deserialize)]
599pub struct CreateDomainRouteRequest {
600    pub name: String,
601    #[serde(default = "default_path_pattern")]
602    pub path_pattern: String,
603    #[serde(default)]
604    pub path_type: DomainPathMatchType,
605    pub methods: Option<Vec<String>>,
606    pub upstream_id: Uuid,
607    #[serde(default)]
608    pub strip_path: bool,
609    #[serde(default = "default_priority")]
610    pub priority: i32,
611    #[serde(default)]
612    pub add_headers: HashMap<String, String>,
613    #[serde(default)]
614    pub remove_headers: Vec<String>,
615    pub rate_limit_requests: Option<i32>,
616    pub rate_limit_window_secs: Option<i32>,
617    #[serde(default = "default_timeout")]
618    pub timeout_secs: i32,
619}
620
621fn default_path_pattern() -> String {
622    "/".to_string()
623}
624
625fn default_priority() -> i32 {
626    100
627}
628
629fn default_timeout() -> i32 {
630    30
631}
632
633/// Update domain route request.
634#[derive(Clone, Debug, Deserialize)]
635pub struct UpdateDomainRouteRequest {
636    pub name: Option<String>,
637    pub path_pattern: Option<String>,
638    pub path_type: Option<DomainPathMatchType>,
639    pub methods: Option<Vec<String>>,
640    pub upstream_id: Option<Uuid>,
641    pub strip_path: Option<bool>,
642    pub priority: Option<i32>,
643    pub add_headers: Option<HashMap<String, String>>,
644    pub remove_headers: Option<Vec<String>>,
645    pub rate_limit_requests: Option<i32>,
646    pub rate_limit_window_secs: Option<i32>,
647    pub timeout_secs: Option<i32>,
648    pub enabled: Option<bool>,
649}
650
651/// Create upstream request.
652#[derive(Clone, Debug, Deserialize)]
653pub struct CreateUpstreamRequest {
654    pub name: String,
655    #[serde(default)]
656    pub lb_strategy: DomainLoadBalanceStrategy,
657    #[serde(default = "default_true")]
658    pub health_check_enabled: bool,
659    #[serde(default = "default_health_path")]
660    pub health_check_path: String,
661    #[serde(default = "default_health_interval")]
662    pub health_check_interval_secs: i32,
663    #[serde(default = "default_health_timeout")]
664    pub health_check_timeout_secs: i32,
665    #[serde(default = "default_healthy_threshold")]
666    pub healthy_threshold: i32,
667    #[serde(default = "default_unhealthy_threshold")]
668    pub unhealthy_threshold: i32,
669    #[serde(default)]
670    pub backends: Vec<CreateBackendRequest>,
671}
672
673fn default_true() -> bool {
674    true
675}
676
677fn default_health_path() -> String {
678    "/health".to_string()
679}
680
681fn default_health_interval() -> i32 {
682    30
683}
684
685fn default_health_timeout() -> i32 {
686    5
687}
688
689fn default_healthy_threshold() -> i32 {
690    2
691}
692
693fn default_unhealthy_threshold() -> i32 {
694    3
695}
696
697/// Update upstream request.
698#[derive(Clone, Debug, Deserialize)]
699pub struct UpdateUpstreamRequest {
700    pub name: Option<String>,
701    pub lb_strategy: Option<DomainLoadBalanceStrategy>,
702    pub health_check_enabled: Option<bool>,
703    pub health_check_path: Option<String>,
704    pub health_check_interval_secs: Option<i32>,
705    pub health_check_timeout_secs: Option<i32>,
706    pub healthy_threshold: Option<i32>,
707    pub unhealthy_threshold: Option<i32>,
708    pub enabled: Option<bool>,
709}
710
711/// Create backend request.
712#[derive(Clone, Debug, Deserialize)]
713pub struct CreateBackendRequest {
714    pub address: String,
715    #[serde(default = "default_scheme")]
716    pub scheme: String,
717    #[serde(default = "default_weight")]
718    pub weight: i32,
719}
720
721fn default_scheme() -> String {
722    "http".to_string()
723}
724
725fn default_weight() -> i32 {
726    100
727}
728
729/// Create API key request.
730#[derive(Clone, Debug, Deserialize)]
731pub struct CreateApiKeyRequest {
732    pub name: String,
733    #[serde(default = "default_scopes")]
734    pub scopes: Vec<String>,
735    pub expires_at: Option<DateTime<Utc>>,
736}
737
738fn default_scopes() -> Vec<String> {
739    vec!["domains:read".to_string(), "domains:write".to_string()]
740}
741
742/// Certificate upload request.
743#[derive(Clone, Debug, Deserialize)]
744pub struct UploadCertificateRequest {
745    pub cert_pem: String,
746    pub key_pem: String,
747}
748
749/// Tenant usage stats.
750#[derive(Clone, Debug, Serialize)]
751pub struct TenantUsage {
752    pub domains_count: i64,
753    pub domains_limit: i32,
754    pub verified_domains: i64,
755    pub routes_count: i64,
756    pub upstreams_count: i64,
757    pub api_keys_count: i64,
758}