1use crate::error::{ProxyError, ProxyResult};
6use crate::saas::db;
7use crate::saas::types::*;
8use crate::saas::verification::DomainVerificationService;
9use rand::Rng;
10use sqlx::PgPool;
11use std::sync::Arc;
12use uuid::Uuid;
13
14pub struct DomainManager {
16 pool: PgPool,
17 verification_service: Arc<DomainVerificationService>,
18}
19
20impl DomainManager {
21 pub fn new(pool: PgPool, verification_service: Arc<DomainVerificationService>) -> Self {
23 Self {
24 pool,
25 verification_service,
26 }
27 }
28
29 pub async fn create_domain(
35 &self,
36 tenant_id: Uuid,
37 req: CreateDomainRequest,
38 ) -> ProxyResult<DomainResponse> {
39 self.validate_domain_format(&req.domain)?;
41
42 let (current, max) = db::check_domain_quota(&self.pool, tenant_id).await?;
44 if current >= max as i64 {
45 return Err(ProxyError::QuotaExceeded(format!(
46 "Domain limit reached ({}/{})",
47 current, max
48 )));
49 }
50
51 if db::domain_exists(&self.pool, &req.domain).await? {
53 return Err(ProxyError::Conflict("Domain already registered".into()));
54 }
55
56 let verification_token = generate_verification_token();
58
59 let domain =
61 db::create_domain(&self.pool, tenant_id, req.clone(), &verification_token).await?;
62
63 let expected_value = format!("postrust-verify={}", verification_token);
65 let challenge_type = match req.verification_method {
66 VerificationMethod::Dns => "dns",
67 VerificationMethod::Http => "http",
68 };
69 db::create_verification_challenge(
70 &self.pool,
71 domain.id,
72 challenge_type,
73 &verification_token,
74 &expected_value,
75 )
76 .await?;
77
78 let instructions = match domain.verification_method {
80 VerificationMethod::Dns => {
81 VerificationInstructions::dns(&domain.domain, &verification_token)
82 }
83 VerificationMethod::Http => {
84 VerificationInstructions::http(&domain.domain, &verification_token)
85 }
86 };
87
88 Ok(DomainResponse {
89 domain,
90 verification_instructions: instructions,
91 })
92 }
93
94 pub async fn get_domain(
96 &self,
97 id: Uuid,
98 tenant_id: Uuid,
99 ) -> ProxyResult<Option<DomainResponse>> {
100 let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id).await?;
101
102 Ok(domain.map(|d| {
103 let instructions = match d.verification_method {
104 VerificationMethod::Dns => {
105 VerificationInstructions::dns(&d.domain, &d.verification_token)
106 }
107 VerificationMethod::Http => {
108 VerificationInstructions::http(&d.domain, &d.verification_token)
109 }
110 };
111 DomainResponse {
112 domain: d,
113 verification_instructions: instructions,
114 }
115 }))
116 }
117
118 pub async fn list_domains(&self, tenant_id: Uuid) -> ProxyResult<Vec<Domain>> {
120 db::list_domains(&self.pool, tenant_id).await
121 }
122
123 pub async fn delete_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
125 db::delete_domain(&self.pool, id, tenant_id).await
126 }
127
128 pub async fn verify_domain(
130 &self,
131 id: Uuid,
132 tenant_id: Uuid,
133 ) -> ProxyResult<VerificationResult> {
134 let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id)
135 .await?
136 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
137
138 db::record_verification_attempt(&self.pool, id).await?;
140
141 let result = match domain.verification_method {
143 VerificationMethod::Dns => {
144 self.verification_service
145 .verify_dns(&domain.domain, &domain.verification_token)
146 .await
147 }
148 VerificationMethod::Http => {
149 self.verification_service
150 .verify_http(&domain.domain, &domain.verification_token)
151 .await
152 }
153 };
154
155 match &result {
156 VerificationResult::Verified => {
157 db::update_verification_status(&self.pool, id, VerificationStatus::Verified)
159 .await?;
160
161 if domain.ssl_provider == SslProvider::Acme {
171 tracing::info!(
172 domain = %domain.domain,
173 "queued for ACME certificate issuance"
174 );
175 db::update_ssl_status(&self.pool, id, SslStatus::Pending, None).await?;
176 }
177
178 tracing::info!(domain = %domain.domain, "Domain verified successfully");
179 }
180 VerificationResult::Failed { reason } => {
181 tracing::warn!(domain = %domain.domain, reason = %reason, "Domain verification failed");
182 }
183 VerificationResult::Pending => {}
184 }
185
186 Ok(result)
187 }
188
189 pub async fn find_live_http_challenge(
195 &self,
196 token: &str,
197 ) -> ProxyResult<Option<db::HttpChallenge>> {
198 db::find_live_http_challenge(&self.pool, token).await
199 }
200
201 pub async fn update_domain(
206 &self,
207 id: Uuid,
208 tenant_id: Uuid,
209 req: UpdateDomainRequest,
210 ) -> ProxyResult<Domain> {
211 db::update_domain(
212 &self.pool,
213 id,
214 tenant_id,
215 req.verification_method.as_ref(),
216 req.ssl_provider.as_ref(),
217 )
218 .await?
219 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))
220 }
221
222 pub async fn upload_certificate(
230 &self,
231 id: Uuid,
232 tenant_id: Uuid,
233 cert_store: &crate::tls::CertificateStore,
234 req: UploadCertificateRequest,
235 ) -> ProxyResult<Domain> {
236 let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id)
237 .await?
238 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
239
240 let facts = crate::tls::validate_for_domain(
241 &domain.domain,
242 req.cert_pem.as_bytes(),
243 req.key_pem.as_bytes(),
244 )?;
245
246 cert_store
247 .save(crate::tls::Certificate {
248 domain: domain.domain.clone(),
249 cert_pem: req.cert_pem.into_bytes(),
250 key_pem: req.key_pem.into_bytes(),
251 expires_at: Some(facts.expires_at),
252 })
253 .await?;
254
255 let updated = db::record_uploaded_certificate(&self.pool, id, tenant_id, facts.expires_at)
256 .await?
257 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
258
259 tracing::info!(
260 domain = %updated.domain,
261 expires_at = %facts.expires_at,
262 "stored an uploaded certificate"
263 );
264 Ok(updated)
265 }
266
267 pub async fn queue_ssl_issuance(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Domain> {
279 db::queue_for_issuance(&self.pool, id, tenant_id)
280 .await?
281 .ok_or_else(|| {
282 ProxyError::NotFound(
283 "No verified domain with ssl_provider = acme for this tenant".into(),
284 )
285 })
286 }
287
288 pub async fn enable_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
290 let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id)
292 .await?
293 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
294
295 if domain.verification_status != VerificationStatus::Verified {
296 return Err(ProxyError::Validation(
297 "Domain must be verified before enabling".into(),
298 ));
299 }
300
301 db::enable_domain(&self.pool, id).await
302 }
303
304 pub async fn disable_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
306 db::get_domain_for_tenant(&self.pool, id, tenant_id)
308 .await?
309 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
310
311 db::disable_domain(&self.pool, id).await
312 }
313
314 pub async fn create_route(
320 &self,
321 domain_id: Uuid,
322 tenant_id: Uuid,
323 req: CreateDomainRouteRequest,
324 ) -> ProxyResult<DomainRoute> {
325 let _domain = db::get_domain_for_tenant(&self.pool, domain_id, tenant_id)
327 .await?
328 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
329
330 db::get_upstream_for_tenant(&self.pool, req.upstream_id, tenant_id)
332 .await?
333 .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
334
335 db::create_route(&self.pool, domain_id, tenant_id, req).await
336 }
337
338 pub async fn get_route(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Option<DomainRoute>> {
340 db::get_route_for_tenant(&self.pool, id, tenant_id).await
341 }
342
343 pub async fn list_routes_for_domain(
345 &self,
346 domain_id: Uuid,
347 tenant_id: Uuid,
348 ) -> ProxyResult<Vec<DomainRoute>> {
349 db::get_domain_for_tenant(&self.pool, domain_id, tenant_id)
351 .await?
352 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
353
354 db::list_routes_for_domain(&self.pool, domain_id, tenant_id).await
355 }
356
357 pub async fn update_route(
359 &self,
360 id: Uuid,
361 tenant_id: Uuid,
362 req: UpdateDomainRouteRequest,
363 ) -> ProxyResult<Option<DomainRoute>> {
364 if let Some(upstream_id) = req.upstream_id {
366 db::get_upstream_for_tenant(&self.pool, upstream_id, tenant_id)
367 .await?
368 .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
369 }
370
371 db::update_route(&self.pool, id, tenant_id, req).await
372 }
373
374 pub async fn delete_route(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
376 db::delete_route(&self.pool, id, tenant_id).await
377 }
378
379 pub async fn create_upstream(
385 &self,
386 tenant_id: Uuid,
387 req: CreateUpstreamRequest,
388 ) -> ProxyResult<DomainUpstream> {
389 db::create_upstream(&self.pool, tenant_id, req).await
390 }
391
392 pub async fn get_upstream(
394 &self,
395 id: Uuid,
396 tenant_id: Uuid,
397 ) -> ProxyResult<Option<DomainUpstream>> {
398 db::get_upstream_for_tenant(&self.pool, id, tenant_id).await
399 }
400
401 pub async fn list_upstreams(&self, tenant_id: Uuid) -> ProxyResult<Vec<DomainUpstream>> {
403 db::list_upstreams(&self.pool, tenant_id).await
404 }
405
406 pub async fn update_upstream(
408 &self,
409 id: Uuid,
410 tenant_id: Uuid,
411 req: UpdateUpstreamRequest,
412 ) -> ProxyResult<Option<DomainUpstream>> {
413 db::update_upstream(&self.pool, id, tenant_id, req).await
414 }
415
416 pub async fn delete_upstream(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
418 db::delete_upstream(&self.pool, id, tenant_id).await
419 }
420
421 pub async fn add_backend(
423 &self,
424 upstream_id: Uuid,
425 tenant_id: Uuid,
426 req: CreateBackendRequest,
427 ) -> ProxyResult<DomainBackend> {
428 db::get_upstream_for_tenant(&self.pool, upstream_id, tenant_id)
430 .await?
431 .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
432
433 db::create_backend(&self.pool, upstream_id, req).await
434 }
435
436 pub async fn remove_backend(
438 &self,
439 backend_id: Uuid,
440 upstream_id: Uuid,
441 tenant_id: Uuid,
442 ) -> ProxyResult<bool> {
443 db::delete_backend(&self.pool, backend_id, upstream_id, tenant_id).await
444 }
445
446 pub async fn get_tenant_usage(&self, tenant_id: Uuid) -> ProxyResult<TenantUsage> {
452 db::get_tenant_usage(&self.pool, tenant_id).await
453 }
454
455 fn validate_domain_format(&self, domain: &str) -> ProxyResult<()> {
461 if domain.is_empty() || domain.len() > 253 {
463 return Err(ProxyError::Validation("Invalid domain length".into()));
464 }
465
466 if !domain.contains('.') {
468 return Err(ProxyError::Validation(
469 "Domain must have at least one dot".into(),
470 ));
471 }
472
473 for label in domain.split('.') {
475 if label.is_empty() || label.len() > 63 {
476 return Err(ProxyError::Validation("Invalid domain label length".into()));
477 }
478
479 let chars: Vec<char> = label.chars().collect();
481 if chars.first().is_none_or(|c| !c.is_alphanumeric())
482 || chars.last().is_none_or(|c| !c.is_alphanumeric())
483 {
484 return Err(ProxyError::Validation(
485 "Domain labels must start and end with alphanumeric characters".into(),
486 ));
487 }
488
489 for c in label.chars() {
491 if !c.is_alphanumeric() && c != '-' {
492 return Err(ProxyError::Validation(
493 "Domain labels can only contain alphanumeric characters and hyphens".into(),
494 ));
495 }
496 }
497 }
498
499 Ok(())
500 }
501}
502
503fn generate_verification_token() -> String {
505 const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
506 let mut rng = rand::rng();
507
508 (0..32)
509 .map(|_| {
510 let idx = rng.random_range(0..CHARSET.len());
511 CHARSET[idx] as char
512 })
513 .collect()
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 #[test]
526 fn test_generate_verification_token() {
527 let token = generate_verification_token();
528 assert_eq!(token.len(), 32);
529 assert!(token.chars().all(|c| c.is_alphanumeric()));
530 }
531}