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 {
163 db::update_ssl_status(&self.pool, id, SslStatus::Provisioning, None).await?;
165 }
166
167 tracing::info!(domain = %domain.domain, "Domain verified successfully");
168 }
169 VerificationResult::Failed { reason } => {
170 tracing::warn!(domain = %domain.domain, reason = %reason, "Domain verification failed");
171 }
172 VerificationResult::Pending => {}
173 }
174
175 Ok(result)
176 }
177
178 pub async fn enable_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
180 let domain = db::get_domain_for_tenant(&self.pool, id, tenant_id)
182 .await?
183 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
184
185 if domain.verification_status != VerificationStatus::Verified {
186 return Err(ProxyError::Validation(
187 "Domain must be verified before enabling".into(),
188 ));
189 }
190
191 db::enable_domain(&self.pool, id).await
192 }
193
194 pub async fn disable_domain(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
196 db::get_domain_for_tenant(&self.pool, id, tenant_id)
198 .await?
199 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
200
201 db::disable_domain(&self.pool, id).await
202 }
203
204 pub async fn create_route(
210 &self,
211 domain_id: Uuid,
212 tenant_id: Uuid,
213 req: CreateDomainRouteRequest,
214 ) -> ProxyResult<DomainRoute> {
215 let _domain = db::get_domain_for_tenant(&self.pool, domain_id, tenant_id)
217 .await?
218 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
219
220 db::get_upstream_for_tenant(&self.pool, req.upstream_id, tenant_id)
222 .await?
223 .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
224
225 db::create_route(&self.pool, domain_id, tenant_id, req).await
226 }
227
228 pub async fn get_route(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Option<DomainRoute>> {
230 db::get_route_for_tenant(&self.pool, id, tenant_id).await
231 }
232
233 pub async fn list_routes_for_domain(
235 &self,
236 domain_id: Uuid,
237 tenant_id: Uuid,
238 ) -> ProxyResult<Vec<DomainRoute>> {
239 db::get_domain_for_tenant(&self.pool, domain_id, tenant_id)
241 .await?
242 .ok_or_else(|| ProxyError::NotFound("Domain not found".into()))?;
243
244 db::list_routes_for_domain(&self.pool, domain_id, tenant_id).await
245 }
246
247 pub async fn update_route(
249 &self,
250 id: Uuid,
251 tenant_id: Uuid,
252 req: UpdateDomainRouteRequest,
253 ) -> ProxyResult<Option<DomainRoute>> {
254 if let Some(upstream_id) = req.upstream_id {
256 db::get_upstream_for_tenant(&self.pool, upstream_id, tenant_id)
257 .await?
258 .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
259 }
260
261 db::update_route(&self.pool, id, tenant_id, req).await
262 }
263
264 pub async fn delete_route(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
266 db::delete_route(&self.pool, id, tenant_id).await
267 }
268
269 pub async fn create_upstream(
275 &self,
276 tenant_id: Uuid,
277 req: CreateUpstreamRequest,
278 ) -> ProxyResult<DomainUpstream> {
279 db::create_upstream(&self.pool, tenant_id, req).await
280 }
281
282 pub async fn get_upstream(
284 &self,
285 id: Uuid,
286 tenant_id: Uuid,
287 ) -> ProxyResult<Option<DomainUpstream>> {
288 db::get_upstream_for_tenant(&self.pool, id, tenant_id).await
289 }
290
291 pub async fn list_upstreams(&self, tenant_id: Uuid) -> ProxyResult<Vec<DomainUpstream>> {
293 db::list_upstreams(&self.pool, tenant_id).await
294 }
295
296 pub async fn update_upstream(
298 &self,
299 id: Uuid,
300 tenant_id: Uuid,
301 req: UpdateUpstreamRequest,
302 ) -> ProxyResult<Option<DomainUpstream>> {
303 db::update_upstream(&self.pool, id, tenant_id, req).await
304 }
305
306 pub async fn delete_upstream(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
308 db::delete_upstream(&self.pool, id, tenant_id).await
309 }
310
311 pub async fn add_backend(
313 &self,
314 upstream_id: Uuid,
315 tenant_id: Uuid,
316 req: CreateBackendRequest,
317 ) -> ProxyResult<DomainBackend> {
318 db::get_upstream_for_tenant(&self.pool, upstream_id, tenant_id)
320 .await?
321 .ok_or_else(|| ProxyError::NotFound("Upstream not found".into()))?;
322
323 db::create_backend(&self.pool, upstream_id, req).await
324 }
325
326 pub async fn remove_backend(
328 &self,
329 backend_id: Uuid,
330 upstream_id: Uuid,
331 tenant_id: Uuid,
332 ) -> ProxyResult<bool> {
333 db::delete_backend(&self.pool, backend_id, upstream_id, tenant_id).await
334 }
335
336 pub async fn get_tenant_usage(&self, tenant_id: Uuid) -> ProxyResult<TenantUsage> {
342 db::get_tenant_usage(&self.pool, tenant_id).await
343 }
344
345 fn validate_domain_format(&self, domain: &str) -> ProxyResult<()> {
351 if domain.is_empty() || domain.len() > 253 {
353 return Err(ProxyError::Validation("Invalid domain length".into()));
354 }
355
356 if !domain.contains('.') {
358 return Err(ProxyError::Validation(
359 "Domain must have at least one dot".into(),
360 ));
361 }
362
363 for label in domain.split('.') {
365 if label.is_empty() || label.len() > 63 {
366 return Err(ProxyError::Validation("Invalid domain label length".into()));
367 }
368
369 let chars: Vec<char> = label.chars().collect();
371 if chars.first().is_none_or(|c| !c.is_alphanumeric())
372 || chars.last().is_none_or(|c| !c.is_alphanumeric())
373 {
374 return Err(ProxyError::Validation(
375 "Domain labels must start and end with alphanumeric characters".into(),
376 ));
377 }
378
379 for c in label.chars() {
381 if !c.is_alphanumeric() && c != '-' {
382 return Err(ProxyError::Validation(
383 "Domain labels can only contain alphanumeric characters and hyphens".into(),
384 ));
385 }
386 }
387 }
388
389 Ok(())
390 }
391}
392
393fn generate_verification_token() -> String {
395 const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
396 let mut rng = rand::rng();
397
398 (0..32)
399 .map(|_| {
400 let idx = rng.random_range(0..CHARSET.len());
401 CHARSET[idx] as char
402 })
403 .collect()
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
416 fn test_generate_verification_token() {
417 let token = generate_verification_token();
418 assert_eq!(token.len(), 32);
419 assert!(token.chars().all(|c| c.is_alphanumeric()));
420 }
421}