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,
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#[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#[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 #[default]
84 Pending,
85 Verified,
87 Failed,
89 Expired,
91}
92
93#[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 #[default]
100 Dns,
101 Http,
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 SslStatus {
110 #[default]
112 Pending,
113 Provisioning,
115 Active,
117 Failed,
119 Expired,
121}
122
123#[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 #[default]
130 Acme,
131 Manual,
133 None,
135}
136
137#[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#[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#[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 #[default]
224 Prefix,
225 Exact,
227 Regex,
229}
230
231#[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#[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#[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 #[default]
316 RoundRobin,
317 LeastConnections,
319 Weighted,
321 Random,
323 Sticky,
325}
326
327#[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 #[serde(default)]
345 pub backends: Vec<DomainBackend>,
346}
347
348#[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#[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#[derive(Clone, Debug, Serialize, Deserialize)]
407pub struct ApiKey {
408 pub id: Uuid,
409 pub tenant_id: Uuid,
410 pub name: String,
411 #[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#[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, 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#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
456#[serde(rename_all = "snake_case")]
457pub enum ChallengeStatus {
458 #[default]
460 Pending,
461 Checking,
463 Verified,
465 Failed,
467}
468
469#[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#[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#[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#[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#[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#[derive(Clone, Debug, Serialize)]
532pub struct DomainResponse {
533 #[serde(flatten)]
534 pub domain: Domain,
535 pub verification_instructions: VerificationInstructions,
536}
537
538#[derive(Clone, Debug, Serialize)]
540pub struct VerificationInstructions {
541 pub method: VerificationMethod,
542 #[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 #[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 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 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#[derive(Clone, Debug, Serialize)]
587#[serde(rename_all = "snake_case")]
588pub enum VerificationResult {
589 Verified,
591 Pending,
593 Failed { reason: String },
595}
596
597#[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#[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#[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#[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#[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#[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#[derive(Clone, Debug, Deserialize)]
744pub struct UploadCertificateRequest {
745 pub cert_pem: String,
746 pub key_pem: String,
747}
748
749#[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}