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    /// Primary key.
27    pub id: Uuid,
28    /// Display name, for humans.
29    pub name: String,
30    /// Short URL-safe identifier, unique across tenants.
31    pub slug: String,
32    /// Contact address for this tenant.
33    pub email: String,
34    /// Whether the tenant may act. A suspended tenant keeps its data.
35    pub status: TenantStatus,
36    /// Plan name. Free-form: quotas are the two fields below, not this string.
37    pub plan: String,
38    /// How many domains this tenant may register.
39    pub max_domains: i32,
40    /// How many routes each of its domains may have.
41    pub max_routes_per_domain: i32,
42    /// Arbitrary per-tenant settings, untouched by the proxy.
43    pub settings: serde_json::Value,
44    /// When the tenant was created.
45    pub created_at: DateTime<Utc>,
46    /// When it last changed.
47    pub updated_at: DateTime<Utc>,
48}
49
50/// Database row for tenant.
51///
52/// The `*Row` types in this module are `pub(crate)`: they mirror the schema
53/// column for column and exist only so `sqlx::query_as` has something to decode
54/// into. Making them public would freeze the schema under semver, which is a
55/// promise about the database rather than about the API.
56#[derive(Clone, Debug, sqlx::FromRow)]
57pub(crate) struct TenantRow {
58    pub(crate) id: Uuid,
59    pub(crate) name: String,
60    pub(crate) slug: String,
61    pub(crate) email: String,
62    pub(crate) status: String,
63    pub(crate) plan: String,
64    pub(crate) max_domains: i32,
65    pub(crate) max_routes_per_domain: i32,
66    pub(crate) settings: serde_json::Value,
67    pub(crate) created_at: DateTime<Utc>,
68    pub(crate) updated_at: DateTime<Utc>,
69}
70
71impl From<TenantRow> for Tenant {
72    fn from(row: TenantRow) -> Self {
73        Self {
74            id: row.id,
75            name: row.name,
76            slug: row.slug,
77            email: row.email,
78            status: match row.status.as_str() {
79                "suspended" => TenantStatus::Suspended,
80                "pending" => TenantStatus::Pending,
81                _ => TenantStatus::Active,
82            },
83            plan: row.plan,
84            max_domains: row.max_domains,
85            max_routes_per_domain: row.max_routes_per_domain,
86            settings: row.settings,
87            created_at: row.created_at,
88            updated_at: row.updated_at,
89        }
90    }
91}
92
93/// Domain verification status.
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 VerificationStatus {
98    /// Pending verification
99    #[default]
100    Pending,
101    /// Successfully verified
102    Verified,
103    /// Verification failed
104    Failed,
105    /// Verification expired
106    Expired,
107}
108
109/// Domain verification method.
110#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
111#[serde(rename_all = "snake_case")]
112#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
113pub enum VerificationMethod {
114    /// DNS TXT record verification
115    #[default]
116    Dns,
117    /// HTTP file verification
118    Http,
119}
120
121/// SSL certificate status.
122#[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 SslStatus {
126    /// Pending provisioning
127    #[default]
128    Pending,
129    /// Currently provisioning
130    Provisioning,
131    /// Certificate active
132    Active,
133    /// Provisioning failed
134    Failed,
135    /// Certificate expired
136    Expired,
137}
138
139/// SSL provider type.
140#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
141#[serde(rename_all = "snake_case")]
142#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
143pub enum SslProvider {
144    /// Automatic via ACME/Let's Encrypt
145    #[default]
146    Acme,
147    /// Manual certificate upload
148    Manual,
149    /// No SSL
150    None,
151}
152
153/// A custom domain.
154#[derive(Clone, Debug, Serialize, Deserialize)]
155pub struct Domain {
156    /// Primary key.
157    pub id: Uuid,
158    /// The tenant that owns this domain.
159    pub tenant_id: Uuid,
160    /// The name itself, globally unique across all tenants. Not updatable: it
161    /// is what [`Self::verification_token`] proves control of.
162    pub domain: String,
163    /// Whether ownership has been proved.
164    pub verification_status: VerificationStatus,
165    /// How ownership is to be proved. DNS is the default, and the sound one.
166    pub verification_method: VerificationMethod,
167    /// The secret the challenge carries. Bearer-equivalent for this domain.
168    pub verification_token: String,
169    /// How many verification attempts have been made.
170    pub verification_attempts: i32,
171    /// When verification succeeded, if it has.
172    pub verified_at: Option<DateTime<Utc>>,
173    /// When verification was last attempted.
174    pub last_verification_attempt: Option<DateTime<Utc>>,
175    /// Where the certificate has got to. `pending` is the issuance worker's
176    /// inbox.
177    pub ssl_status: SslStatus,
178    /// Where the certificate comes from.
179    pub ssl_provider: SslProvider,
180    /// When the current certificate expires, if there is one.
181    pub ssl_expires_at: Option<DateTime<Utc>>,
182    /// Whether the proxy should serve this domain. Set independently of
183    /// verification, so a verified domain can be taken out of service.
184    pub enabled: bool,
185    /// When the domain was registered.
186    pub created_at: DateTime<Utc>,
187    /// When it last changed.
188    pub updated_at: DateTime<Utc>,
189}
190
191/// Database row for domain with string types.
192#[derive(Clone, Debug, sqlx::FromRow)]
193pub(crate) struct DomainRow {
194    pub(crate) id: Uuid,
195    pub(crate) tenant_id: Uuid,
196    pub(crate) domain: String,
197    pub(crate) verification_status: String,
198    pub(crate) verification_method: String,
199    pub(crate) verification_token: String,
200    pub(crate) verification_attempts: i32,
201    pub(crate) verified_at: Option<DateTime<Utc>>,
202    pub(crate) last_verification_attempt: Option<DateTime<Utc>>,
203    pub(crate) ssl_status: String,
204    pub(crate) ssl_provider: String,
205    pub(crate) ssl_expires_at: Option<DateTime<Utc>>,
206    pub(crate) enabled: bool,
207    pub(crate) created_at: DateTime<Utc>,
208    pub(crate) updated_at: DateTime<Utc>,
209}
210
211impl From<DomainRow> for Domain {
212    fn from(row: DomainRow) -> Self {
213        Self {
214            id: row.id,
215            tenant_id: row.tenant_id,
216            domain: row.domain,
217            verification_status: match row.verification_status.as_str() {
218                "verified" => VerificationStatus::Verified,
219                "failed" => VerificationStatus::Failed,
220                "expired" => VerificationStatus::Expired,
221                _ => VerificationStatus::Pending,
222            },
223            verification_method: match row.verification_method.as_str() {
224                "http" => VerificationMethod::Http,
225                _ => VerificationMethod::Dns,
226            },
227            verification_token: row.verification_token,
228            verification_attempts: row.verification_attempts,
229            verified_at: row.verified_at,
230            last_verification_attempt: row.last_verification_attempt,
231            ssl_status: match row.ssl_status.as_str() {
232                "provisioning" => SslStatus::Provisioning,
233                "active" => SslStatus::Active,
234                "failed" => SslStatus::Failed,
235                "expired" => SslStatus::Expired,
236                _ => SslStatus::Pending,
237            },
238            ssl_provider: match row.ssl_provider.as_str() {
239                "manual" => SslProvider::Manual,
240                "none" => SslProvider::None,
241                _ => SslProvider::Acme,
242            },
243            ssl_expires_at: row.ssl_expires_at,
244            enabled: row.enabled,
245            created_at: row.created_at,
246            updated_at: row.updated_at,
247        }
248    }
249}
250
251/// Path matching type.
252#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
253#[serde(rename_all = "snake_case")]
254#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
255pub enum DomainPathMatchType {
256    /// Prefix matching (default)
257    #[default]
258    Prefix,
259    /// Exact matching
260    Exact,
261    /// Regex matching
262    Regex,
263}
264
265/// A domain route.
266#[derive(Clone, Debug, Serialize, Deserialize)]
267pub struct DomainRoute {
268    /// Primary key.
269    pub id: Uuid,
270    /// The domain this route belongs to.
271    pub domain_id: Uuid,
272    /// The owning tenant, denormalised so a route can be authorised without
273    /// joining to its domain.
274    pub tenant_id: Uuid,
275    /// Name, unique within the domain.
276    pub name: String,
277    /// The path to match, read according to [`Self::path_type`].
278    pub path_pattern: String,
279    /// How to read [`Self::path_pattern`].
280    pub path_type: DomainPathMatchType,
281    /// Methods to match. `None` or empty means any.
282    pub methods: Option<Vec<String>>,
283    /// Higher is matched first.
284    pub priority: i32,
285    /// The upstream to forward to.
286    pub upstream_id: Option<Uuid>,
287    /// Whether to remove the matched prefix before forwarding.
288    pub strip_path: bool,
289    /// Headers to add to the forwarded request.
290    pub add_headers: HashMap<String, String>,
291    /// Headers to remove from the forwarded request.
292    pub remove_headers: Vec<String>,
293    /// Requests allowed per window. Set with
294    /// [`Self::rate_limit_window_secs`] or not at all.
295    pub rate_limit_requests: Option<i32>,
296    /// The window those requests are counted over, in seconds.
297    pub rate_limit_window_secs: Option<i32>,
298    /// Request timeout in seconds.
299    pub timeout_secs: i32,
300    /// Whether this route participates in matching.
301    pub enabled: bool,
302    /// When the route was created.
303    pub created_at: DateTime<Utc>,
304    /// When it last changed.
305    pub updated_at: DateTime<Utc>,
306}
307
308/// Database row for domain route.
309#[derive(Clone, Debug, sqlx::FromRow)]
310pub(crate) struct DomainRouteRow {
311    pub(crate) id: Uuid,
312    pub(crate) domain_id: Uuid,
313    pub(crate) tenant_id: Uuid,
314    pub(crate) name: String,
315    pub(crate) path_pattern: String,
316    pub(crate) path_type: String,
317    pub(crate) methods: Option<Vec<String>>,
318    pub(crate) priority: i32,
319    pub(crate) upstream_id: Option<Uuid>,
320    pub(crate) strip_path: bool,
321    pub(crate) add_headers: serde_json::Value,
322    pub(crate) remove_headers: Vec<String>,
323    pub(crate) rate_limit_requests: Option<i32>,
324    pub(crate) rate_limit_window_secs: Option<i32>,
325    pub(crate) timeout_secs: i32,
326    pub(crate) enabled: bool,
327    pub(crate) created_at: DateTime<Utc>,
328    pub(crate) updated_at: DateTime<Utc>,
329}
330
331impl From<DomainRouteRow> for DomainRoute {
332    fn from(row: DomainRouteRow) -> Self {
333        let add_headers: HashMap<String, String> =
334            serde_json::from_value(row.add_headers).unwrap_or_default();
335
336        Self {
337            id: row.id,
338            domain_id: row.domain_id,
339            tenant_id: row.tenant_id,
340            name: row.name,
341            path_pattern: row.path_pattern,
342            path_type: match row.path_type.as_str() {
343                "exact" => DomainPathMatchType::Exact,
344                "regex" => DomainPathMatchType::Regex,
345                _ => DomainPathMatchType::Prefix,
346            },
347            methods: row.methods,
348            priority: row.priority,
349            upstream_id: row.upstream_id,
350            strip_path: row.strip_path,
351            add_headers,
352            remove_headers: row.remove_headers,
353            rate_limit_requests: row.rate_limit_requests,
354            rate_limit_window_secs: row.rate_limit_window_secs,
355            timeout_secs: row.timeout_secs,
356            enabled: row.enabled,
357            created_at: row.created_at,
358            updated_at: row.updated_at,
359        }
360    }
361}
362
363/// Load balancing strategy.
364#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
365#[serde(rename_all = "snake_case")]
366#[sqlx(type_name = "VARCHAR", rename_all = "snake_case")]
367pub enum DomainLoadBalanceStrategy {
368    /// Round-robin (default)
369    #[default]
370    RoundRobin,
371    /// Least connections
372    LeastConnections,
373    /// Weighted
374    Weighted,
375    /// Random
376    Random,
377    /// Sticky (cookie-based)
378    Sticky,
379}
380
381/// A domain upstream (backend server group).
382#[derive(Clone, Debug, Serialize, Deserialize)]
383pub struct DomainUpstream {
384    /// Primary key.
385    pub id: Uuid,
386    /// The owning tenant.
387    pub tenant_id: Uuid,
388    /// Name, unique within the tenant. Routes reference it.
389    pub name: String,
390    /// How requests are spread across the backends.
391    pub lb_strategy: DomainLoadBalanceStrategy,
392    /// Whether backends are health-checked.
393    pub health_check_enabled: bool,
394    /// The path health checks request.
395    pub health_check_path: String,
396    /// Seconds between checks.
397    pub health_check_interval_secs: i32,
398    /// Seconds a single check may take.
399    pub health_check_timeout_secs: i32,
400    /// Consecutive successes before a backend is used again.
401    pub healthy_threshold: i32,
402    /// Consecutive failures before a backend is taken out.
403    pub unhealthy_threshold: i32,
404    /// Whether this upstream may receive traffic.
405    pub enabled: bool,
406    /// When the upstream was created.
407    pub created_at: DateTime<Utc>,
408    /// When it last changed.
409    pub updated_at: DateTime<Utc>,
410    /// Backends (loaded separately)
411    #[serde(default)]
412    pub backends: Vec<DomainBackend>,
413}
414
415/// Database row for domain upstream.
416#[derive(Clone, Debug, sqlx::FromRow)]
417pub(crate) struct DomainUpstreamRow {
418    pub(crate) id: Uuid,
419    pub(crate) tenant_id: Uuid,
420    pub(crate) name: String,
421    pub(crate) lb_strategy: String,
422    pub(crate) health_check_enabled: bool,
423    pub(crate) health_check_path: String,
424    pub(crate) health_check_interval_secs: i32,
425    pub(crate) health_check_timeout_secs: i32,
426    pub(crate) healthy_threshold: i32,
427    pub(crate) unhealthy_threshold: i32,
428    pub(crate) enabled: bool,
429    pub(crate) created_at: DateTime<Utc>,
430    pub(crate) updated_at: DateTime<Utc>,
431}
432
433impl From<DomainUpstreamRow> for DomainUpstream {
434    fn from(row: DomainUpstreamRow) -> Self {
435        Self {
436            id: row.id,
437            tenant_id: row.tenant_id,
438            name: row.name,
439            lb_strategy: match row.lb_strategy.as_str() {
440                "least_connections" => DomainLoadBalanceStrategy::LeastConnections,
441                "weighted" => DomainLoadBalanceStrategy::Weighted,
442                "random" => DomainLoadBalanceStrategy::Random,
443                "sticky" => DomainLoadBalanceStrategy::Sticky,
444                _ => DomainLoadBalanceStrategy::RoundRobin,
445            },
446            health_check_enabled: row.health_check_enabled,
447            health_check_path: row.health_check_path,
448            health_check_interval_secs: row.health_check_interval_secs,
449            health_check_timeout_secs: row.health_check_timeout_secs,
450            healthy_threshold: row.healthy_threshold,
451            unhealthy_threshold: row.unhealthy_threshold,
452            enabled: row.enabled,
453            created_at: row.created_at,
454            updated_at: row.updated_at,
455            backends: Vec::new(),
456        }
457    }
458}
459
460/// A backend server.
461#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
462pub struct DomainBackend {
463    /// Primary key.
464    pub id: Uuid,
465    /// The upstream this backend belongs to.
466    pub upstream_id: Uuid,
467    /// `host:port`.
468    pub address: String,
469    /// `http` or `https`.
470    pub scheme: String,
471    /// Relative share of traffic under a weighted strategy.
472    pub weight: i32,
473    /// Whether this backend may receive traffic.
474    pub enabled: bool,
475    /// When the backend was added.
476    pub created_at: DateTime<Utc>,
477}
478
479/// An API key for tenant authentication.
480#[derive(Clone, Debug, Serialize, Deserialize)]
481pub struct ApiKey {
482    /// Primary key.
483    pub id: Uuid,
484    /// The tenant this key authenticates.
485    pub tenant_id: Uuid,
486    /// Label, for telling keys apart.
487    pub name: String,
488    /// The key itself, **present only in the response that created it**.
489    ///
490    /// Only a hash is stored, so this cannot be shown again. A caller that does
491    /// not keep it has to create another key.
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub key: Option<String>,
494    /// The first few characters, enough to identify a key in a log without
495    /// being enough to use it.
496    pub key_prefix: String,
497    /// What this key may do, as `resource:action` strings.
498    pub scopes: Vec<String>,
499    /// When the key was last used to authenticate.
500    pub last_used_at: Option<DateTime<Utc>>,
501    /// When the key stops working, if it ever does.
502    pub expires_at: Option<DateTime<Utc>>,
503    /// Whether the key is accepted. Revoking sets this rather than deleting,
504    /// so `last_used_at` survives for an audit.
505    pub enabled: bool,
506    /// When the key was created.
507    pub created_at: DateTime<Utc>,
508}
509
510/// Database row for API key.
511#[derive(Clone, Debug, sqlx::FromRow)]
512pub(crate) struct ApiKeyRow {
513    pub(crate) id: Uuid,
514    pub(crate) tenant_id: Uuid,
515    pub(crate) name: String,
516    pub(crate) key_prefix: String,
517    pub(crate) scopes: Vec<String>,
518    pub(crate) last_used_at: Option<DateTime<Utc>>,
519    pub(crate) expires_at: Option<DateTime<Utc>>,
520    pub(crate) enabled: bool,
521    pub(crate) created_at: DateTime<Utc>,
522}
523
524impl From<ApiKeyRow> for ApiKey {
525    fn from(row: ApiKeyRow) -> Self {
526        Self {
527            id: row.id,
528            tenant_id: row.tenant_id,
529            name: row.name,
530            key: None, // Never expose the key after creation
531            key_prefix: row.key_prefix,
532            scopes: row.scopes,
533            last_used_at: row.last_used_at,
534            expires_at: row.expires_at,
535            enabled: row.enabled,
536            created_at: row.created_at,
537        }
538    }
539}
540
541/// Verification challenge status.
542#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
543#[serde(rename_all = "snake_case")]
544pub enum ChallengeStatus {
545    /// Pending verification
546    #[default]
547    Pending,
548    /// Currently checking
549    Checking,
550    /// Successfully verified
551    Verified,
552    /// Verification failed
553    Failed,
554}
555
556/// A verification challenge.
557#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
558pub struct VerificationChallenge {
559    /// Primary key.
560    pub id: Uuid,
561    /// The domain whose ownership this challenge proves.
562    pub domain_id: Uuid,
563    /// `dns` or `http`.
564    pub challenge_type: String,
565    /// The token the challenge is keyed by. A bearer secret for one domain.
566    pub token: String,
567    /// The exact content the verifier expects. Served verbatim rather than
568    /// recomputed, so the check cannot be satisfied by anyone who can guess the
569    /// formula.
570    pub expected_value: String,
571    /// `pending`, `checking`, `verified` or `failed`.
572    pub status: String,
573    /// Why the last attempt failed, if it did.
574    pub error_message: Option<String>,
575    /// When the challenge was issued.
576    pub created_at: DateTime<Utc>,
577    /// When it stops being answerable.
578    pub expires_at: DateTime<Utc>,
579    /// When it was satisfied, if it was.
580    pub verified_at: Option<DateTime<Utc>>,
581}
582
583/// Audit log entry.
584#[derive(Clone, Debug, Serialize, Deserialize, sqlx::FromRow)]
585pub struct AuditLogEntry {
586    /// Primary key.
587    pub id: Uuid,
588    /// The tenant acted on, if the action had one.
589    pub tenant_id: Option<Uuid>,
590    /// The domain acted on, if the action had one.
591    pub domain_id: Option<Uuid>,
592    /// What happened.
593    pub action: String,
594    /// What kind of actor did it -- an API key, a JWT, the system.
595    pub actor_type: String,
596    /// Which actor, as far as it is known.
597    pub actor_id: Option<String>,
598    /// Anything else worth keeping about the action.
599    pub details: serde_json::Value,
600    /// Where the request came from.
601    pub ip_address: Option<IpAddr>,
602    /// When it happened.
603    pub created_at: DateTime<Utc>,
604}
605
606// ============================================================================
607// API Request/Response Types
608// ============================================================================
609
610/// Create tenant request.
611#[derive(Clone, Debug, Deserialize)]
612pub struct CreateTenantRequest {
613    /// Display name.
614    pub name: String,
615    /// Short URL-safe identifier. Must not already be taken.
616    pub slug: String,
617    /// Contact address.
618    pub email: String,
619    /// Plan name. Defaults to the free plan.
620    pub plan: Option<String>,
621}
622
623/// Update tenant request.
624#[derive(Clone, Debug, Deserialize)]
625pub struct UpdateTenantRequest {
626    /// New display name, if it is changing.
627    pub name: Option<String>,
628    /// New contact address, if it is changing.
629    pub email: Option<String>,
630    /// New plan, if it is changing.
631    pub plan: Option<String>,
632    /// Replacement settings blob. Replaces rather than merges.
633    pub settings: Option<serde_json::Value>,
634}
635
636/// Create domain request.
637#[derive(Clone, Debug, Deserialize)]
638pub struct CreateDomainRequest {
639    /// The name to register. Globally unique across tenants.
640    pub domain: String,
641    /// How ownership will be proved. Defaults to DNS, which is the sound one:
642    /// HTTP verification is answered by this proxy once the domain points at
643    /// it, so it shows the domain resolves here rather than that the caller
644    /// controls it.
645    #[serde(default)]
646    pub verification_method: VerificationMethod,
647    /// Where the certificate will come from. Defaults to ACME.
648    #[serde(default)]
649    pub ssl_provider: SslProvider,
650}
651
652/// Fields of a domain that can be changed after it is created.
653///
654/// The domain name itself is deliberately absent. It is the identity of the
655/// record and what the verification token proves control of, so renaming would
656/// silently carry a proof of ownership over to a name nobody has proved
657/// anything about. Delete and re-add instead.
658#[derive(Clone, Debug, Deserialize, Default)]
659pub struct UpdateDomainRequest {
660    /// Switch between DNS and HTTP verification.
661    ///
662    /// Takes effect on the next verification attempt. Changing it does not
663    /// un-verify a domain that is already verified.
664    #[serde(default)]
665    pub verification_method: Option<VerificationMethod>,
666
667    /// Switch how the certificate is obtained.
668    ///
669    /// Moving a verified domain to `acme` queues it for issuance.
670    #[serde(default)]
671    pub ssl_provider: Option<SslProvider>,
672}
673
674/// Domain response with verification instructions.
675#[derive(Clone, Debug, Serialize)]
676pub struct DomainResponse {
677    /// The domain as stored. Flattened, so the response is the domain's own
678    /// fields plus the instructions.
679    #[serde(flatten)]
680    pub domain: Domain,
681    /// What the caller has to do to prove ownership.
682    pub verification_instructions: VerificationInstructions,
683}
684
685/// Verification instructions.
686#[derive(Clone, Debug, Serialize)]
687pub struct VerificationInstructions {
688    /// Which method these instructions are for. Only that method's fields are
689    /// populated.
690    pub method: VerificationMethod,
691    /// DNS: the record type to create, always `TXT`.
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub dns_record_type: Option<String>,
694    /// DNS: the name to create it at.
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub dns_record_name: Option<String>,
697    /// DNS: the value it must hold.
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub dns_record_value: Option<String>,
700    /// HTTP: the URL the verifier will fetch.
701    #[serde(skip_serializing_if = "Option::is_none")]
702    pub http_url: Option<String>,
703    /// HTTP: the content it must return.
704    #[serde(skip_serializing_if = "Option::is_none")]
705    pub http_expected_content: Option<String>,
706}
707
708impl VerificationInstructions {
709    /// Create DNS verification instructions.
710    pub fn dns(domain: &str, token: &str) -> Self {
711        Self {
712            method: VerificationMethod::Dns,
713            dns_record_type: Some("TXT".to_string()),
714            dns_record_name: Some(format!("_postrust-verification.{}", domain)),
715            dns_record_value: Some(format!("postrust-verify={}", token)),
716            http_url: None,
717            http_expected_content: None,
718        }
719    }
720
721    /// Create HTTP verification instructions.
722    pub fn http(domain: &str, token: &str) -> Self {
723        Self {
724            method: VerificationMethod::Http,
725            dns_record_type: None,
726            dns_record_name: None,
727            dns_record_value: None,
728            http_url: Some(format!(
729                "https://{}/.well-known/postrust-verification/{}",
730                domain, token
731            )),
732            http_expected_content: Some(format!("postrust-verify={}", token)),
733        }
734    }
735}
736
737/// Verification result.
738#[derive(Clone, Debug, Serialize)]
739#[serde(rename_all = "snake_case")]
740pub enum VerificationResult {
741    /// Domain verified successfully
742    Verified,
743    /// Verification pending
744    Pending,
745    /// Verification failed
746    Failed {
747        /// Why, in terms the caller can act on.
748        reason: String,
749    },
750}
751
752/// Create domain route request.
753#[derive(Clone, Debug, Deserialize)]
754pub struct CreateDomainRouteRequest {
755    /// Name, unique within the domain.
756    pub name: String,
757    /// The path to match. Defaults to `/`, which catches everything.
758    #[serde(default = "default_path_pattern")]
759    pub path_pattern: String,
760    /// How to read the pattern. Defaults to prefix.
761    #[serde(default)]
762    pub path_type: DomainPathMatchType,
763    /// Methods to match. Omit for any.
764    pub methods: Option<Vec<String>>,
765    /// The upstream to forward to. Must belong to the same tenant.
766    pub upstream_id: Uuid,
767    /// Whether to remove the matched prefix before forwarding.
768    #[serde(default)]
769    pub strip_path: bool,
770    /// Higher is matched first.
771    #[serde(default = "default_priority")]
772    pub priority: i32,
773    /// Headers to add to the forwarded request.
774    #[serde(default)]
775    pub add_headers: HashMap<String, String>,
776    /// Headers to remove from the forwarded request.
777    #[serde(default)]
778    pub remove_headers: Vec<String>,
779    /// Requests allowed per window. Set with the window or not at all.
780    pub rate_limit_requests: Option<i32>,
781    /// The window those requests are counted over, in seconds.
782    pub rate_limit_window_secs: Option<i32>,
783    /// Request timeout in seconds.
784    #[serde(default = "default_timeout")]
785    pub timeout_secs: i32,
786}
787
788fn default_path_pattern() -> String {
789    "/".to_string()
790}
791
792fn default_priority() -> i32 {
793    100
794}
795
796fn default_timeout() -> i32 {
797    30
798}
799
800/// Update domain route request.
801#[derive(Clone, Debug, Deserialize)]
802pub struct UpdateDomainRouteRequest {
803    /// New name, if it is changing.
804    pub name: Option<String>,
805    /// New path pattern, if it is changing.
806    pub path_pattern: Option<String>,
807    /// New path-match type, if it is changing.
808    pub path_type: Option<DomainPathMatchType>,
809    /// Replacement method list. Replaces rather than adds.
810    pub methods: Option<Vec<String>>,
811    /// A different upstream to forward to.
812    pub upstream_id: Option<Uuid>,
813    /// Whether to strip the matched prefix.
814    pub strip_path: Option<bool>,
815    /// New priority.
816    pub priority: Option<i32>,
817    /// Replacement set of headers to add. Replaces rather than merges.
818    pub add_headers: Option<HashMap<String, String>>,
819    /// Replacement list of headers to remove.
820    pub remove_headers: Option<Vec<String>>,
821    /// New request allowance per window.
822    pub rate_limit_requests: Option<i32>,
823    /// New window, in seconds.
824    pub rate_limit_window_secs: Option<i32>,
825    /// New request timeout, in seconds.
826    pub timeout_secs: Option<i32>,
827    /// Whether the route participates in matching.
828    pub enabled: Option<bool>,
829}
830
831/// Create upstream request.
832#[derive(Clone, Debug, Deserialize)]
833pub struct CreateUpstreamRequest {
834    /// Name, unique within the tenant. Routes reference it.
835    pub name: String,
836    /// How requests are spread across the backends.
837    #[serde(default)]
838    pub lb_strategy: DomainLoadBalanceStrategy,
839    /// Whether backends are health-checked.
840    #[serde(default = "default_true")]
841    pub health_check_enabled: bool,
842    /// The path health checks request.
843    #[serde(default = "default_health_path")]
844    pub health_check_path: String,
845    /// Seconds between checks.
846    #[serde(default = "default_health_interval")]
847    pub health_check_interval_secs: i32,
848    /// Seconds a single check may take.
849    #[serde(default = "default_health_timeout")]
850    pub health_check_timeout_secs: i32,
851    /// Consecutive successes before a backend is used again.
852    #[serde(default = "default_healthy_threshold")]
853    pub healthy_threshold: i32,
854    /// Consecutive failures before a backend is taken out.
855    #[serde(default = "default_unhealthy_threshold")]
856    pub unhealthy_threshold: i32,
857    /// Backends to create alongside the upstream.
858    #[serde(default)]
859    pub backends: Vec<CreateBackendRequest>,
860}
861
862fn default_true() -> bool {
863    true
864}
865
866fn default_health_path() -> String {
867    "/health".to_string()
868}
869
870fn default_health_interval() -> i32 {
871    30
872}
873
874fn default_health_timeout() -> i32 {
875    5
876}
877
878fn default_healthy_threshold() -> i32 {
879    2
880}
881
882fn default_unhealthy_threshold() -> i32 {
883    3
884}
885
886/// Update upstream request.
887#[derive(Clone, Debug, Deserialize)]
888pub struct UpdateUpstreamRequest {
889    /// New name, if it is changing.
890    pub name: Option<String>,
891    /// New load-balancing strategy.
892    pub lb_strategy: Option<DomainLoadBalanceStrategy>,
893    /// Whether to health-check the backends.
894    pub health_check_enabled: Option<bool>,
895    /// New health-check path.
896    pub health_check_path: Option<String>,
897    /// New interval between checks, in seconds.
898    pub health_check_interval_secs: Option<i32>,
899    /// New per-check timeout, in seconds.
900    pub health_check_timeout_secs: Option<i32>,
901    /// New healthy threshold.
902    pub healthy_threshold: Option<i32>,
903    /// New unhealthy threshold.
904    pub unhealthy_threshold: Option<i32>,
905    /// Whether the upstream may receive traffic.
906    ///
907    /// Backends are deliberately absent: this type cannot express a backend's
908    /// id, so treating a list here as the new set would re-create every backend
909    /// under a fresh id. Use the backend endpoints.
910    pub enabled: Option<bool>,
911}
912
913/// Create backend request.
914#[derive(Clone, Debug, Deserialize)]
915pub struct CreateBackendRequest {
916    /// `host:port`.
917    pub address: String,
918    /// `http` or `https`. Defaults to `http`.
919    #[serde(default = "default_scheme")]
920    pub scheme: String,
921    /// Relative share of traffic under a weighted strategy.
922    #[serde(default = "default_weight")]
923    pub weight: i32,
924}
925
926fn default_scheme() -> String {
927    "http".to_string()
928}
929
930fn default_weight() -> i32 {
931    100
932}
933
934/// Create API key request.
935#[derive(Clone, Debug, Deserialize)]
936pub struct CreateApiKeyRequest {
937    /// Label, for telling keys apart later.
938    pub name: String,
939    /// What the key may do, as `resource:action` strings. Defaults to reading
940    /// and writing domains.
941    #[serde(default = "default_scopes")]
942    pub scopes: Vec<String>,
943    /// When the key should stop working. `None` means never.
944    pub expires_at: Option<DateTime<Utc>>,
945}
946
947fn default_scopes() -> Vec<String> {
948    vec!["domains:read".to_string(), "domains:write".to_string()]
949}
950
951/// Certificate upload request.
952#[derive(Clone, Debug, Deserialize)]
953pub struct UploadCertificateRequest {
954    /// The certificate chain, PEM-encoded, leaf first.
955    pub cert_pem: String,
956    /// The private key, PEM-encoded. Must match the leaf: the upload is
957    /// refused otherwise, because a mismatch produces a listener that fails
958    /// every handshake instead.
959    pub key_pem: String,
960}
961
962/// Tenant usage stats.
963#[derive(Clone, Debug, Serialize)]
964pub struct TenantUsage {
965    /// Domains registered.
966    pub domains_count: i64,
967    /// How many the tenant's plan allows.
968    pub domains_limit: i32,
969    /// How many of those domains are verified.
970    pub verified_domains: i64,
971    /// Routes across all of them.
972    pub routes_count: i64,
973    /// Upstreams defined.
974    pub upstreams_count: i64,
975    /// API keys, including revoked ones.
976    pub api_keys_count: i64,
977}