1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::net::IpAddr;
7use uuid::Uuid;
8
9#[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 #[default]
16 Active,
17 Suspended,
19 Pending,
21}
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct Tenant {
26 pub id: Uuid,
28 pub name: String,
30 pub slug: String,
32 pub email: String,
34 pub status: TenantStatus,
36 pub plan: String,
38 pub max_domains: i32,
40 pub max_routes_per_domain: i32,
42 pub max_upstreams: i32,
44 pub max_backends_per_upstream: i32,
46 pub max_api_keys: i32,
48 pub settings: serde_json::Value,
50 pub created_at: DateTime<Utc>,
52 pub updated_at: DateTime<Utc>,
54}
55
56#[derive(Clone, Debug, sqlx::FromRow)]
63pub(crate) struct TenantRow {
64 pub(crate) id: Uuid,
65 pub(crate) name: String,
66 pub(crate) slug: String,
67 pub(crate) email: String,
68 pub(crate) status: String,
69 pub(crate) plan: String,
70 pub(crate) max_domains: i32,
71 pub(crate) max_routes_per_domain: i32,
72 pub(crate) max_upstreams: i32,
73 pub(crate) max_backends_per_upstream: i32,
74 pub(crate) max_api_keys: i32,
75 pub(crate) settings: serde_json::Value,
76 pub(crate) created_at: DateTime<Utc>,
77 pub(crate) updated_at: DateTime<Utc>,
78}
79
80impl From<TenantRow> for Tenant {
81 fn from(row: TenantRow) -> Self {
82 Self {
83 id: row.id,
84 name: row.name,
85 slug: row.slug,
86 email: row.email,
87 status: match row.status.as_str() {
88 "suspended" => TenantStatus::Suspended,
89 "pending" => TenantStatus::Pending,
90 _ => TenantStatus::Active,
91 },
92 plan: row.plan,
93 max_domains: row.max_domains,
94 max_routes_per_domain: row.max_routes_per_domain,
95 max_upstreams: row.max_upstreams,
96 max_backends_per_upstream: row.max_backends_per_upstream,
97 max_api_keys: row.max_api_keys,
98 settings: row.settings,
99 created_at: row.created_at,
100 updated_at: row.updated_at,
101 }
102 }
103}
104
105#[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 VerificationStatus {
110 #[default]
112 Pending,
113 Verified,
115 Failed,
117 Expired,
119}
120
121#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
123#[serde(rename_all = "snake_case")]
124#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
125pub enum VerificationMethod {
126 #[default]
128 Dns,
129 Http,
131}
132
133#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
135#[serde(rename_all = "snake_case")]
136#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
137pub enum SslStatus {
138 #[default]
140 Pending,
141 Provisioning,
143 Active,
145 Failed,
147 Expired,
149}
150
151#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
153#[serde(rename_all = "snake_case")]
154#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
155pub enum SslProvider {
156 #[default]
158 Acme,
159 Manual,
161 None,
163}
164
165#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct Domain {
168 pub id: Uuid,
170 pub tenant_id: Uuid,
172 pub domain: String,
175 pub verification_status: VerificationStatus,
177 pub verification_method: VerificationMethod,
179 pub verification_token: String,
181 pub verification_attempts: i32,
183 pub verified_at: Option<DateTime<Utc>>,
185 pub last_verification_attempt: Option<DateTime<Utc>>,
187 pub ssl_status: SslStatus,
190 pub ssl_provider: SslProvider,
192 pub ssl_expires_at: Option<DateTime<Utc>>,
194 pub enabled: bool,
197 pub created_at: DateTime<Utc>,
199 pub updated_at: DateTime<Utc>,
201}
202
203#[derive(Clone, Debug, sqlx::FromRow)]
205pub(crate) struct DomainRow {
206 pub(crate) id: Uuid,
207 pub(crate) tenant_id: Uuid,
208 pub(crate) domain: String,
209 pub(crate) verification_status: String,
210 pub(crate) verification_method: String,
211 pub(crate) verification_token: String,
212 pub(crate) verification_attempts: i32,
213 pub(crate) verified_at: Option<DateTime<Utc>>,
214 pub(crate) last_verification_attempt: Option<DateTime<Utc>>,
215 pub(crate) ssl_status: String,
216 pub(crate) ssl_provider: String,
217 pub(crate) ssl_expires_at: Option<DateTime<Utc>>,
218 pub(crate) enabled: bool,
219 pub(crate) created_at: DateTime<Utc>,
220 pub(crate) updated_at: DateTime<Utc>,
221}
222
223impl From<DomainRow> for Domain {
224 fn from(row: DomainRow) -> Self {
225 Self {
226 id: row.id,
227 tenant_id: row.tenant_id,
228 domain: row.domain,
229 verification_status: match row.verification_status.as_str() {
230 "verified" => VerificationStatus::Verified,
231 "failed" => VerificationStatus::Failed,
232 "expired" => VerificationStatus::Expired,
233 _ => VerificationStatus::Pending,
234 },
235 verification_method: match row.verification_method.as_str() {
236 "http" => VerificationMethod::Http,
237 _ => VerificationMethod::Dns,
238 },
239 verification_token: row.verification_token,
240 verification_attempts: row.verification_attempts,
241 verified_at: row.verified_at,
242 last_verification_attempt: row.last_verification_attempt,
243 ssl_status: match row.ssl_status.as_str() {
244 "provisioning" => SslStatus::Provisioning,
245 "active" => SslStatus::Active,
246 "failed" => SslStatus::Failed,
247 "expired" => SslStatus::Expired,
248 _ => SslStatus::Pending,
249 },
250 ssl_provider: match row.ssl_provider.as_str() {
251 "manual" => SslProvider::Manual,
252 "none" => SslProvider::None,
253 _ => SslProvider::Acme,
254 },
255 ssl_expires_at: row.ssl_expires_at,
256 enabled: row.enabled,
257 created_at: row.created_at,
258 updated_at: row.updated_at,
259 }
260 }
261}
262
263#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
265#[serde(rename_all = "snake_case")]
266#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
267pub enum DomainPathMatchType {
268 #[default]
270 Prefix,
271 Exact,
273 Regex,
275}
276
277#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct DomainRoute {
280 pub id: Uuid,
282 pub domain_id: Uuid,
284 pub tenant_id: Uuid,
287 pub name: String,
289 pub path_pattern: String,
291 pub path_type: DomainPathMatchType,
293 pub methods: Option<Vec<String>>,
295 pub priority: i32,
297 pub upstream_id: Option<Uuid>,
299 pub strip_path: bool,
301 pub add_headers: HashMap<String, String>,
303 pub remove_headers: Vec<String>,
305 pub rate_limit_requests: Option<i32>,
308 pub rate_limit_window_secs: Option<i32>,
310 pub timeout_secs: i32,
312 pub enabled: bool,
314 pub created_at: DateTime<Utc>,
316 pub updated_at: DateTime<Utc>,
318}
319
320#[derive(Clone, Debug, sqlx::FromRow)]
322pub(crate) struct DomainRouteRow {
323 pub(crate) id: Uuid,
324 pub(crate) domain_id: Uuid,
325 pub(crate) tenant_id: Uuid,
326 pub(crate) name: String,
327 pub(crate) path_pattern: String,
328 pub(crate) path_type: String,
329 pub(crate) methods: Option<Vec<String>>,
330 pub(crate) priority: i32,
331 pub(crate) upstream_id: Option<Uuid>,
332 pub(crate) strip_path: bool,
333 pub(crate) add_headers: serde_json::Value,
334 pub(crate) remove_headers: Vec<String>,
335 pub(crate) rate_limit_requests: Option<i32>,
336 pub(crate) rate_limit_window_secs: Option<i32>,
337 pub(crate) timeout_secs: i32,
338 pub(crate) enabled: bool,
339 pub(crate) created_at: DateTime<Utc>,
340 pub(crate) updated_at: DateTime<Utc>,
341}
342
343impl From<DomainRouteRow> for DomainRoute {
344 fn from(row: DomainRouteRow) -> Self {
345 let add_headers: HashMap<String, String> =
346 serde_json::from_value(row.add_headers).unwrap_or_default();
347
348 Self {
349 id: row.id,
350 domain_id: row.domain_id,
351 tenant_id: row.tenant_id,
352 name: row.name,
353 path_pattern: row.path_pattern,
354 path_type: match row.path_type.as_str() {
355 "exact" => DomainPathMatchType::Exact,
356 "regex" => DomainPathMatchType::Regex,
357 _ => DomainPathMatchType::Prefix,
358 },
359 methods: row.methods,
360 priority: row.priority,
361 upstream_id: row.upstream_id,
362 strip_path: row.strip_path,
363 add_headers,
364 remove_headers: row.remove_headers,
365 rate_limit_requests: row.rate_limit_requests,
366 rate_limit_window_secs: row.rate_limit_window_secs,
367 timeout_secs: row.timeout_secs,
368 enabled: row.enabled,
369 created_at: row.created_at,
370 updated_at: row.updated_at,
371 }
372 }
373}
374
375#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
377#[serde(rename_all = "snake_case")]
378#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
379pub enum DomainLoadBalanceStrategy {
380 #[default]
382 RoundRobin,
383 LeastConnections,
385 Weighted,
387 Random,
389 Sticky,
391}
392
393#[derive(Clone, Debug, Serialize, Deserialize)]
395pub struct DomainUpstream {
396 pub id: Uuid,
398 pub tenant_id: Uuid,
400 pub name: String,
402 pub lb_strategy: DomainLoadBalanceStrategy,
404 pub health_check_enabled: bool,
406 pub health_check_path: String,
408 pub health_check_interval_secs: i32,
410 pub health_check_timeout_secs: i32,
412 pub healthy_threshold: i32,
414 pub unhealthy_threshold: i32,
416 pub enabled: bool,
418 pub created_at: DateTime<Utc>,
420 pub updated_at: DateTime<Utc>,
422 #[serde(default)]
424 pub backends: Vec<DomainBackend>,
425}
426
427#[derive(Clone, Debug, sqlx::FromRow)]
429pub(crate) struct DomainUpstreamRow {
430 pub(crate) id: Uuid,
431 pub(crate) tenant_id: Uuid,
432 pub(crate) name: String,
433 pub(crate) lb_strategy: String,
434 pub(crate) health_check_enabled: bool,
435 pub(crate) health_check_path: String,
436 pub(crate) health_check_interval_secs: i32,
437 pub(crate) health_check_timeout_secs: i32,
438 pub(crate) healthy_threshold: i32,
439 pub(crate) unhealthy_threshold: i32,
440 pub(crate) enabled: bool,
441 pub(crate) created_at: DateTime<Utc>,
442 pub(crate) updated_at: DateTime<Utc>,
443}
444
445impl From<DomainUpstreamRow> for DomainUpstream {
446 fn from(row: DomainUpstreamRow) -> Self {
447 Self {
448 id: row.id,
449 tenant_id: row.tenant_id,
450 name: row.name,
451 lb_strategy: match row.lb_strategy.as_str() {
452 "least_connections" => DomainLoadBalanceStrategy::LeastConnections,
453 "weighted" => DomainLoadBalanceStrategy::Weighted,
454 "random" => DomainLoadBalanceStrategy::Random,
455 "sticky" => DomainLoadBalanceStrategy::Sticky,
456 _ => DomainLoadBalanceStrategy::RoundRobin,
457 },
458 health_check_enabled: row.health_check_enabled,
459 health_check_path: row.health_check_path,
460 health_check_interval_secs: row.health_check_interval_secs,
461 health_check_timeout_secs: row.health_check_timeout_secs,
462 healthy_threshold: row.healthy_threshold,
463 unhealthy_threshold: row.unhealthy_threshold,
464 enabled: row.enabled,
465 created_at: row.created_at,
466 updated_at: row.updated_at,
467 backends: Vec::new(),
468 }
469 }
470}
471
472#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
474pub struct DomainBackend {
475 pub id: Uuid,
477 pub upstream_id: Uuid,
479 pub address: String,
481 pub scheme: String,
483 pub weight: i32,
485 pub enabled: bool,
487 pub created_at: DateTime<Utc>,
489}
490
491#[derive(Clone, Debug, Serialize, Deserialize)]
493pub struct ApiKey {
494 pub id: Uuid,
496 pub tenant_id: Uuid,
498 pub name: String,
500 #[serde(skip_serializing_if = "Option::is_none")]
505 pub key: Option<String>,
506 pub key_prefix: String,
509 pub scopes: Vec<String>,
511 pub last_used_at: Option<DateTime<Utc>>,
513 pub expires_at: Option<DateTime<Utc>>,
515 pub enabled: bool,
518 pub created_at: DateTime<Utc>,
520}
521
522#[derive(Clone, Debug, sqlx::FromRow)]
524pub(crate) struct ApiKeyRow {
525 pub(crate) id: Uuid,
526 pub(crate) tenant_id: Uuid,
527 pub(crate) name: String,
528 pub(crate) key_prefix: String,
529 pub(crate) scopes: Vec<String>,
530 pub(crate) last_used_at: Option<DateTime<Utc>>,
531 pub(crate) expires_at: Option<DateTime<Utc>>,
532 pub(crate) enabled: bool,
533 pub(crate) created_at: DateTime<Utc>,
534}
535
536impl From<ApiKeyRow> for ApiKey {
537 fn from(row: ApiKeyRow) -> Self {
538 Self {
539 id: row.id,
540 tenant_id: row.tenant_id,
541 name: row.name,
542 key: None, key_prefix: row.key_prefix,
544 scopes: row.scopes,
545 last_used_at: row.last_used_at,
546 expires_at: row.expires_at,
547 enabled: row.enabled,
548 created_at: row.created_at,
549 }
550 }
551}
552
553#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
555#[serde(rename_all = "snake_case")]
556pub enum ChallengeStatus {
557 #[default]
559 Pending,
560 Checking,
562 Verified,
564 Failed,
566}
567
568#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
570pub struct VerificationChallenge {
571 pub id: Uuid,
573 pub domain_id: Uuid,
575 pub challenge_type: String,
577 pub token: String,
579 pub expected_value: String,
583 pub status: String,
585 pub error_message: Option<String>,
587 pub created_at: DateTime<Utc>,
589 pub expires_at: DateTime<Utc>,
591 pub verified_at: Option<DateTime<Utc>>,
593}
594
595#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
597pub struct AuditLogEntry {
598 pub id: Uuid,
600 pub tenant_id: Option<Uuid>,
602 pub domain_id: Option<Uuid>,
604 pub action: String,
606 pub actor_type: String,
608 pub actor_id: Option<String>,
610 pub details: serde_json::Value,
612 pub ip_address: Option<IpAddr>,
614 pub created_at: DateTime<Utc>,
616}
617
618#[derive(Clone, Debug, Deserialize)]
624pub struct CreateTenantRequest {
625 pub name: String,
627 pub slug: String,
629 pub email: String,
631 pub plan: Option<String>,
633}
634
635#[derive(Clone, Debug, Deserialize)]
637pub struct UpdateTenantRequest {
638 pub name: Option<String>,
640 pub email: Option<String>,
642 pub plan: Option<String>,
644 pub settings: Option<serde_json::Value>,
646}
647
648#[derive(Clone, Debug, Deserialize)]
650pub struct CreateDomainRequest {
651 pub domain: String,
656 #[serde(default)]
661 pub verification_method: VerificationMethod,
662 #[serde(default)]
664 pub ssl_provider: SslProvider,
665}
666
667#[derive(Clone, Debug, Deserialize, Default)]
674pub struct UpdateDomainRequest {
675 #[serde(default)]
680 pub verification_method: Option<VerificationMethod>,
681
682 #[serde(default)]
686 pub ssl_provider: Option<SslProvider>,
687}
688
689#[derive(Clone, Debug, Serialize)]
691pub struct DomainResponse {
692 #[serde(flatten)]
695 pub domain: Domain,
696 pub verification_instructions: VerificationInstructions,
698}
699
700#[derive(Clone, Debug, Serialize)]
702pub struct VerificationInstructions {
703 pub method: VerificationMethod,
706 #[serde(skip_serializing_if = "Option::is_none")]
708 pub dns_record_type: Option<String>,
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub dns_record_name: Option<String>,
712 #[serde(skip_serializing_if = "Option::is_none")]
714 pub dns_record_value: Option<String>,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub http_url: Option<String>,
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub http_expected_content: Option<String>,
721}
722
723impl VerificationInstructions {
724 pub fn dns(domain: &str, token: &str) -> Self {
726 Self {
727 method: VerificationMethod::Dns,
728 dns_record_type: Some("TXT".to_string()),
729 dns_record_name: Some(format!("_postrust-verification.{}", domain)),
730 dns_record_value: Some(format!("postrust-verify={}", token)),
731 http_url: None,
732 http_expected_content: None,
733 }
734 }
735
736 pub fn http(domain: &str, token: &str) -> Self {
738 Self {
739 method: VerificationMethod::Http,
740 dns_record_type: None,
741 dns_record_name: None,
742 dns_record_value: None,
743 http_url: Some(format!(
744 "https://{}/.well-known/postrust-verification/{}",
745 domain, token
746 )),
747 http_expected_content: Some(format!("postrust-verify={}", token)),
748 }
749 }
750}
751
752#[derive(Clone, Debug, Serialize)]
754#[serde(rename_all = "snake_case")]
755pub enum VerificationResult {
756 Verified,
758 Pending,
760 Failed {
762 reason: String,
764 },
765}
766
767#[derive(Clone, Debug, Deserialize)]
769pub struct CreateDomainRouteRequest {
770 pub name: String,
772 #[serde(default = "default_path_pattern")]
774 pub path_pattern: String,
775 #[serde(default)]
777 pub path_type: DomainPathMatchType,
778 pub methods: Option<Vec<String>>,
780 pub upstream_id: Uuid,
782 #[serde(default)]
784 pub strip_path: bool,
785 #[serde(default = "default_priority")]
787 pub priority: i32,
788 #[serde(default)]
790 pub add_headers: HashMap<String, String>,
791 #[serde(default)]
793 pub remove_headers: Vec<String>,
794 pub rate_limit_requests: Option<i32>,
796 pub rate_limit_window_secs: Option<i32>,
798 #[serde(default = "default_timeout")]
800 pub timeout_secs: i32,
801}
802
803fn default_path_pattern() -> String {
804 "/".to_string()
805}
806
807fn default_priority() -> i32 {
808 100
809}
810
811fn default_timeout() -> i32 {
812 30
813}
814
815#[derive(Clone, Debug, Deserialize)]
817pub struct UpdateDomainRouteRequest {
818 pub name: Option<String>,
820 pub path_pattern: Option<String>,
822 pub path_type: Option<DomainPathMatchType>,
824 pub methods: Option<Vec<String>>,
826 pub upstream_id: Option<Uuid>,
828 pub strip_path: Option<bool>,
830 pub priority: Option<i32>,
832 pub add_headers: Option<HashMap<String, String>>,
834 pub remove_headers: Option<Vec<String>>,
836 pub rate_limit_requests: Option<i32>,
838 pub rate_limit_window_secs: Option<i32>,
840 pub timeout_secs: Option<i32>,
842 pub enabled: Option<bool>,
844}
845
846#[derive(Clone, Debug, Deserialize)]
848pub struct CreateUpstreamRequest {
849 pub name: String,
851 #[serde(default)]
853 pub lb_strategy: DomainLoadBalanceStrategy,
854 #[serde(default = "default_true")]
856 pub health_check_enabled: bool,
857 #[serde(default = "default_health_path")]
859 pub health_check_path: String,
860 #[serde(default = "default_health_interval")]
862 pub health_check_interval_secs: i32,
863 #[serde(default = "default_health_timeout")]
865 pub health_check_timeout_secs: i32,
866 #[serde(default = "default_healthy_threshold")]
868 pub healthy_threshold: i32,
869 #[serde(default = "default_unhealthy_threshold")]
871 pub unhealthy_threshold: i32,
872 #[serde(default)]
874 pub backends: Vec<CreateBackendRequest>,
875}
876
877fn default_true() -> bool {
878 true
879}
880
881fn default_health_path() -> String {
882 "/health".to_string()
883}
884
885fn default_health_interval() -> i32 {
886 30
887}
888
889fn default_health_timeout() -> i32 {
890 5
891}
892
893fn default_healthy_threshold() -> i32 {
894 2
895}
896
897fn default_unhealthy_threshold() -> i32 {
898 3
899}
900
901#[derive(Clone, Debug, Deserialize)]
903pub struct UpdateUpstreamRequest {
904 pub name: Option<String>,
906 pub lb_strategy: Option<DomainLoadBalanceStrategy>,
908 pub health_check_enabled: Option<bool>,
910 pub health_check_path: Option<String>,
912 pub health_check_interval_secs: Option<i32>,
914 pub health_check_timeout_secs: Option<i32>,
916 pub healthy_threshold: Option<i32>,
918 pub unhealthy_threshold: Option<i32>,
920 pub enabled: Option<bool>,
926}
927
928#[derive(Clone, Debug, Deserialize)]
930pub struct CreateBackendRequest {
931 pub address: String,
933 #[serde(default = "default_scheme")]
935 pub scheme: String,
936 #[serde(default = "default_weight")]
938 pub weight: i32,
939}
940
941fn default_scheme() -> String {
942 "http".to_string()
943}
944
945fn default_weight() -> i32 {
946 100
947}
948
949#[derive(Clone, Debug, Deserialize)]
951pub struct CreateApiKeyRequest {
952 pub name: String,
954 #[serde(default = "default_scopes")]
959 pub scopes: Vec<String>,
960 pub expires_at: Option<DateTime<Utc>>,
962}
963
964fn default_scopes() -> Vec<String> {
965 vec!["domains:read".to_string(), "domains:write".to_string()]
966}
967
968#[derive(Clone, Debug, Deserialize)]
970pub struct UploadCertificateRequest {
971 pub cert_pem: String,
973 pub key_pem: String,
977}
978
979#[derive(Clone, Debug, Serialize)]
981pub struct TenantUsage {
982 pub domains_count: i64,
984 pub domains_limit: i32,
986 pub verified_domains: i64,
988 pub routes_count: i64,
990 pub routes_per_domain_limit: i32,
992 pub upstreams_count: i64,
994 pub upstreams_limit: i32,
996 pub backends_per_upstream_limit: i32,
998 pub api_keys_count: i64,
1000 pub api_keys_limit: i32,
1002}