1use crate::error::ProxyResult;
9use crate::saas::types::*;
10use chrono::{DateTime, Utc};
11use sqlx::{PgPool, Row};
12use uuid::Uuid;
13
14fn verification_status_str(status: &VerificationStatus) -> &'static str {
22 match status {
23 VerificationStatus::Pending => "pending",
24 VerificationStatus::Verified => "verified",
25 VerificationStatus::Failed => "failed",
26 VerificationStatus::Expired => "expired",
27 }
28}
29
30fn verification_method_str(method: &VerificationMethod) -> &'static str {
31 match method {
32 VerificationMethod::Dns => "dns",
33 VerificationMethod::Http => "http",
34 }
35}
36
37fn ssl_status_str(status: &SslStatus) -> &'static str {
38 match status {
39 SslStatus::Pending => "pending",
40 SslStatus::Provisioning => "provisioning",
41 SslStatus::Active => "active",
42 SslStatus::Failed => "failed",
43 SslStatus::Expired => "expired",
44 }
45}
46
47fn ssl_provider_str(provider: &SslProvider) -> &'static str {
48 match provider {
49 SslProvider::Acme => "acme",
50 SslProvider::Manual => "manual",
51 SslProvider::None => "none",
52 }
53}
54
55fn path_type_str(path_type: &DomainPathMatchType) -> &'static str {
56 match path_type {
57 DomainPathMatchType::Prefix => "prefix",
58 DomainPathMatchType::Exact => "exact",
59 DomainPathMatchType::Regex => "regex",
60 }
61}
62
63fn lb_strategy_str(strategy: &DomainLoadBalanceStrategy) -> &'static str {
64 match strategy {
65 DomainLoadBalanceStrategy::RoundRobin => "round_robin",
66 DomainLoadBalanceStrategy::LeastConnections => "least_connections",
67 DomainLoadBalanceStrategy::Weighted => "weighted",
68 DomainLoadBalanceStrategy::Random => "random",
69 DomainLoadBalanceStrategy::Sticky => "sticky",
70 }
71}
72
73pub async fn check_domain_quota(pool: &PgPool, tenant_id: Uuid) -> ProxyResult<(i64, i32)> {
79 let row = sqlx::query(
80 "SELECT \
81 (SELECT COUNT(*) FROM proxy_domains WHERE tenant_id = $1) AS current, \
82 COALESCE((SELECT max_domains FROM proxy_tenants WHERE id = $1), 0) AS max",
83 )
84 .bind(tenant_id)
85 .fetch_one(pool)
86 .await?;
87
88 Ok((row.get::<i64, _>("current"), row.get::<i32, _>("max")))
89}
90
91pub async fn domain_exists(pool: &PgPool, domain: &str) -> ProxyResult<bool> {
93 let row =
94 sqlx::query("SELECT EXISTS(SELECT 1 FROM proxy_domains WHERE domain = $1) AS present")
95 .bind(domain)
96 .fetch_one(pool)
97 .await?;
98 Ok(row.get::<bool, _>("present"))
99}
100
101pub async fn create_domain(
103 pool: &PgPool,
104 tenant_id: Uuid,
105 req: CreateDomainRequest,
106 verification_token: &str,
107) -> ProxyResult<Domain> {
108 let row = sqlx::query_as::<_, DomainRow>(
109 "INSERT INTO proxy_domains \
110 (tenant_id, domain, verification_method, verification_token, ssl_provider) \
111 VALUES ($1, $2, $3, $4, $5) \
112 RETURNING *",
113 )
114 .bind(tenant_id)
115 .bind(&req.domain)
116 .bind(verification_method_str(&req.verification_method))
117 .bind(verification_token)
118 .bind(ssl_provider_str(&req.ssl_provider))
119 .fetch_one(pool)
120 .await?;
121
122 Ok(Domain::from(row))
123}
124
125pub async fn create_verification_challenge(
127 pool: &PgPool,
128 domain_id: Uuid,
129 challenge_type: &str,
130 token: &str,
131 expected_value: &str,
132) -> ProxyResult<()> {
133 sqlx::query(
134 "INSERT INTO proxy_verification_challenges \
135 (domain_id, challenge_type, token, expected_value) \
136 VALUES ($1, $2, $3, $4)",
137 )
138 .bind(domain_id)
139 .bind(challenge_type)
140 .bind(token)
141 .bind(expected_value)
142 .execute(pool)
143 .await?;
144 Ok(())
145}
146
147pub async fn get_domain_for_tenant(
149 pool: &PgPool,
150 id: Uuid,
151 tenant_id: Uuid,
152) -> ProxyResult<Option<Domain>> {
153 let row = sqlx::query_as::<_, DomainRow>(
154 "SELECT * FROM proxy_domains WHERE id = $1 AND tenant_id = $2",
155 )
156 .bind(id)
157 .bind(tenant_id)
158 .fetch_optional(pool)
159 .await?;
160 Ok(row.map(Domain::from))
161}
162
163pub async fn list_domains(pool: &PgPool, tenant_id: Uuid) -> ProxyResult<Vec<Domain>> {
165 let rows = sqlx::query_as::<_, DomainRow>(
166 "SELECT * FROM proxy_domains WHERE tenant_id = $1 ORDER BY created_at DESC",
167 )
168 .bind(tenant_id)
169 .fetch_all(pool)
170 .await?;
171 Ok(rows.into_iter().map(Domain::from).collect())
172}
173
174pub async fn delete_domain(pool: &PgPool, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
176 let result = sqlx::query("DELETE FROM proxy_domains WHERE id = $1 AND tenant_id = $2")
177 .bind(id)
178 .bind(tenant_id)
179 .execute(pool)
180 .await?;
181 Ok(result.rows_affected() > 0)
182}
183
184pub async fn record_verification_attempt(pool: &PgPool, id: Uuid) -> ProxyResult<()> {
186 sqlx::query(
187 "UPDATE proxy_domains \
188 SET verification_attempts = verification_attempts + 1, \
189 last_verification_attempt = NOW(), \
190 updated_at = NOW() \
191 WHERE id = $1",
192 )
193 .bind(id)
194 .execute(pool)
195 .await?;
196 Ok(())
197}
198
199pub async fn update_verification_status(
201 pool: &PgPool,
202 id: Uuid,
203 status: VerificationStatus,
204) -> ProxyResult<()> {
205 let status_str = verification_status_str(&status);
206 sqlx::query(
207 "UPDATE proxy_domains \
208 SET verification_status = $2, \
209 verified_at = CASE WHEN $2 = 'verified' THEN NOW() ELSE verified_at END, \
210 updated_at = NOW() \
211 WHERE id = $1",
212 )
213 .bind(id)
214 .bind(status_str)
215 .execute(pool)
216 .await?;
217 Ok(())
218}
219
220pub async fn update_ssl_status(
222 pool: &PgPool,
223 id: Uuid,
224 status: SslStatus,
225 expires_at: Option<DateTime<Utc>>,
226) -> ProxyResult<()> {
227 sqlx::query(
228 "UPDATE proxy_domains \
229 SET ssl_status = $2, ssl_expires_at = $3, updated_at = NOW() \
230 WHERE id = $1",
231 )
232 .bind(id)
233 .bind(ssl_status_str(&status))
234 .bind(expires_at)
235 .execute(pool)
236 .await?;
237 Ok(())
238}
239
240pub async fn enable_domain(pool: &PgPool, id: Uuid) -> ProxyResult<bool> {
242 let result =
243 sqlx::query("UPDATE proxy_domains SET enabled = true, updated_at = NOW() WHERE id = $1")
244 .bind(id)
245 .execute(pool)
246 .await?;
247 Ok(result.rows_affected() > 0)
248}
249
250pub async fn disable_domain(pool: &PgPool, id: Uuid) -> ProxyResult<bool> {
252 let result =
253 sqlx::query("UPDATE proxy_domains SET enabled = false, updated_at = NOW() WHERE id = $1")
254 .bind(id)
255 .execute(pool)
256 .await?;
257 Ok(result.rows_affected() > 0)
258}
259
260pub async fn create_route(
266 pool: &PgPool,
267 domain_id: Uuid,
268 tenant_id: Uuid,
269 req: CreateDomainRouteRequest,
270) -> ProxyResult<DomainRoute> {
271 let add_headers = serde_json::to_value(&req.add_headers).unwrap_or_default();
272
273 let row = sqlx::query_as::<_, DomainRouteRow>(
274 "INSERT INTO proxy_domain_routes \
275 (domain_id, tenant_id, name, path_pattern, path_type, methods, priority, \
276 upstream_id, strip_path, add_headers, remove_headers, rate_limit_requests, \
277 rate_limit_window_secs, timeout_secs) \
278 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) \
279 RETURNING *",
280 )
281 .bind(domain_id)
282 .bind(tenant_id)
283 .bind(&req.name)
284 .bind(&req.path_pattern)
285 .bind(path_type_str(&req.path_type))
286 .bind(&req.methods)
287 .bind(req.priority)
288 .bind(req.upstream_id)
289 .bind(req.strip_path)
290 .bind(add_headers)
291 .bind(&req.remove_headers)
292 .bind(req.rate_limit_requests)
293 .bind(req.rate_limit_window_secs)
294 .bind(req.timeout_secs)
295 .fetch_one(pool)
296 .await?;
297
298 Ok(DomainRoute::from(row))
299}
300
301pub async fn get_route_for_tenant(
303 pool: &PgPool,
304 id: Uuid,
305 tenant_id: Uuid,
306) -> ProxyResult<Option<DomainRoute>> {
307 let row = sqlx::query_as::<_, DomainRouteRow>(
308 "SELECT * FROM proxy_domain_routes WHERE id = $1 AND tenant_id = $2",
309 )
310 .bind(id)
311 .bind(tenant_id)
312 .fetch_optional(pool)
313 .await?;
314 Ok(row.map(DomainRoute::from))
315}
316
317pub async fn list_routes_for_domain(
319 pool: &PgPool,
320 domain_id: Uuid,
321 tenant_id: Uuid,
322) -> ProxyResult<Vec<DomainRoute>> {
323 let rows = sqlx::query_as::<_, DomainRouteRow>(
324 "SELECT * FROM proxy_domain_routes \
325 WHERE domain_id = $1 AND tenant_id = $2 \
326 ORDER BY priority DESC, created_at",
327 )
328 .bind(domain_id)
329 .bind(tenant_id)
330 .fetch_all(pool)
331 .await?;
332 Ok(rows.into_iter().map(DomainRoute::from).collect())
333}
334
335pub async fn update_route(
337 pool: &PgPool,
338 id: Uuid,
339 tenant_id: Uuid,
340 req: UpdateDomainRouteRequest,
341) -> ProxyResult<Option<DomainRoute>> {
342 let path_type = req.path_type.as_ref().map(path_type_str);
343 let add_headers = req
344 .add_headers
345 .as_ref()
346 .map(|h| serde_json::to_value(h).unwrap_or_default());
347
348 let row = sqlx::query_as::<_, DomainRouteRow>(
349 "UPDATE proxy_domain_routes SET \
350 name = COALESCE($3, name), \
351 path_pattern = COALESCE($4, path_pattern), \
352 path_type = COALESCE($5, path_type), \
353 methods = COALESCE($6, methods), \
354 upstream_id = COALESCE($7, upstream_id), \
355 strip_path = COALESCE($8, strip_path), \
356 priority = COALESCE($9, priority), \
357 add_headers = COALESCE($10, add_headers), \
358 remove_headers = COALESCE($11, remove_headers), \
359 rate_limit_requests = COALESCE($12, rate_limit_requests), \
360 rate_limit_window_secs = COALESCE($13, rate_limit_window_secs), \
361 timeout_secs = COALESCE($14, timeout_secs), \
362 enabled = COALESCE($15, enabled), \
363 updated_at = NOW() \
364 WHERE id = $1 AND tenant_id = $2 \
365 RETURNING *",
366 )
367 .bind(id)
368 .bind(tenant_id)
369 .bind(req.name)
370 .bind(req.path_pattern)
371 .bind(path_type)
372 .bind(req.methods)
373 .bind(req.upstream_id)
374 .bind(req.strip_path)
375 .bind(req.priority)
376 .bind(add_headers)
377 .bind(req.remove_headers)
378 .bind(req.rate_limit_requests)
379 .bind(req.rate_limit_window_secs)
380 .bind(req.timeout_secs)
381 .bind(req.enabled)
382 .fetch_optional(pool)
383 .await?;
384
385 Ok(row.map(DomainRoute::from))
386}
387
388pub async fn delete_route(pool: &PgPool, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
390 let result = sqlx::query("DELETE FROM proxy_domain_routes WHERE id = $1 AND tenant_id = $2")
391 .bind(id)
392 .bind(tenant_id)
393 .execute(pool)
394 .await?;
395 Ok(result.rows_affected() > 0)
396}
397
398async fn load_backends(pool: &PgPool, upstream_id: Uuid) -> ProxyResult<Vec<DomainBackend>> {
404 let backends = sqlx::query_as::<_, DomainBackend>(
405 "SELECT * FROM proxy_domain_backends WHERE upstream_id = $1 ORDER BY created_at",
406 )
407 .bind(upstream_id)
408 .fetch_all(pool)
409 .await?;
410 Ok(backends)
411}
412
413pub async fn create_upstream(
415 pool: &PgPool,
416 tenant_id: Uuid,
417 req: CreateUpstreamRequest,
418) -> ProxyResult<DomainUpstream> {
419 let row = sqlx::query_as::<_, DomainUpstreamRow>(
420 "INSERT INTO proxy_domain_upstreams \
421 (tenant_id, name, lb_strategy, health_check_enabled, health_check_path, \
422 health_check_interval_secs, health_check_timeout_secs, healthy_threshold, \
423 unhealthy_threshold) \
424 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) \
425 RETURNING *",
426 )
427 .bind(tenant_id)
428 .bind(&req.name)
429 .bind(lb_strategy_str(&req.lb_strategy))
430 .bind(req.health_check_enabled)
431 .bind(&req.health_check_path)
432 .bind(req.health_check_interval_secs)
433 .bind(req.health_check_timeout_secs)
434 .bind(req.healthy_threshold)
435 .bind(req.unhealthy_threshold)
436 .fetch_one(pool)
437 .await?;
438
439 let mut upstream = DomainUpstream::from(row);
440
441 for backend in req.backends {
442 create_backend(pool, upstream.id, backend).await?;
443 }
444 upstream.backends = load_backends(pool, upstream.id).await?;
445
446 Ok(upstream)
447}
448
449pub async fn get_upstream_for_tenant(
451 pool: &PgPool,
452 id: Uuid,
453 tenant_id: Uuid,
454) -> ProxyResult<Option<DomainUpstream>> {
455 let row = sqlx::query_as::<_, DomainUpstreamRow>(
456 "SELECT * FROM proxy_domain_upstreams WHERE id = $1 AND tenant_id = $2",
457 )
458 .bind(id)
459 .bind(tenant_id)
460 .fetch_optional(pool)
461 .await?;
462
463 match row {
464 Some(row) => {
465 let mut upstream = DomainUpstream::from(row);
466 upstream.backends = load_backends(pool, upstream.id).await?;
467 Ok(Some(upstream))
468 }
469 None => Ok(None),
470 }
471}
472
473pub async fn list_upstreams(pool: &PgPool, tenant_id: Uuid) -> ProxyResult<Vec<DomainUpstream>> {
475 let rows = sqlx::query_as::<_, DomainUpstreamRow>(
476 "SELECT * FROM proxy_domain_upstreams WHERE tenant_id = $1 ORDER BY created_at",
477 )
478 .bind(tenant_id)
479 .fetch_all(pool)
480 .await?;
481
482 let mut upstreams = Vec::with_capacity(rows.len());
483 for row in rows {
484 let mut upstream = DomainUpstream::from(row);
485 upstream.backends = load_backends(pool, upstream.id).await?;
486 upstreams.push(upstream);
487 }
488 Ok(upstreams)
489}
490
491pub async fn update_upstream(
493 pool: &PgPool,
494 id: Uuid,
495 tenant_id: Uuid,
496 req: UpdateUpstreamRequest,
497) -> ProxyResult<Option<DomainUpstream>> {
498 let lb_strategy = req.lb_strategy.as_ref().map(lb_strategy_str);
499
500 let row = sqlx::query_as::<_, DomainUpstreamRow>(
501 "UPDATE proxy_domain_upstreams SET \
502 name = COALESCE($3, name), \
503 lb_strategy = COALESCE($4, lb_strategy), \
504 health_check_enabled = COALESCE($5, health_check_enabled), \
505 health_check_path = COALESCE($6, health_check_path), \
506 health_check_interval_secs = COALESCE($7, health_check_interval_secs), \
507 health_check_timeout_secs = COALESCE($8, health_check_timeout_secs), \
508 healthy_threshold = COALESCE($9, healthy_threshold), \
509 unhealthy_threshold = COALESCE($10, unhealthy_threshold), \
510 enabled = COALESCE($11, enabled), \
511 updated_at = NOW() \
512 WHERE id = $1 AND tenant_id = $2 \
513 RETURNING *",
514 )
515 .bind(id)
516 .bind(tenant_id)
517 .bind(req.name)
518 .bind(lb_strategy)
519 .bind(req.health_check_enabled)
520 .bind(req.health_check_path)
521 .bind(req.health_check_interval_secs)
522 .bind(req.health_check_timeout_secs)
523 .bind(req.healthy_threshold)
524 .bind(req.unhealthy_threshold)
525 .bind(req.enabled)
526 .fetch_optional(pool)
527 .await?;
528
529 match row {
530 Some(row) => {
531 let mut upstream = DomainUpstream::from(row);
532 upstream.backends = load_backends(pool, upstream.id).await?;
533 Ok(Some(upstream))
534 }
535 None => Ok(None),
536 }
537}
538
539pub async fn delete_upstream(pool: &PgPool, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
541 let result = sqlx::query("DELETE FROM proxy_domain_upstreams WHERE id = $1 AND tenant_id = $2")
542 .bind(id)
543 .bind(tenant_id)
544 .execute(pool)
545 .await?;
546 Ok(result.rows_affected() > 0)
547}
548
549pub async fn create_backend(
551 pool: &PgPool,
552 upstream_id: Uuid,
553 req: CreateBackendRequest,
554) -> ProxyResult<DomainBackend> {
555 let backend = sqlx::query_as::<_, DomainBackend>(
556 "INSERT INTO proxy_domain_backends (upstream_id, address, scheme, weight) \
557 VALUES ($1, $2, $3, $4) \
558 RETURNING *",
559 )
560 .bind(upstream_id)
561 .bind(&req.address)
562 .bind(&req.scheme)
563 .bind(req.weight)
564 .fetch_one(pool)
565 .await?;
566 Ok(backend)
567}
568
569pub async fn delete_backend(
571 pool: &PgPool,
572 backend_id: Uuid,
573 upstream_id: Uuid,
574 tenant_id: Uuid,
575) -> ProxyResult<bool> {
576 let result = sqlx::query(
577 "DELETE FROM proxy_domain_backends \
578 WHERE id = $1 AND upstream_id = $2 \
579 AND upstream_id IN (SELECT id FROM proxy_domain_upstreams WHERE id = $2 AND tenant_id = $3)",
580 )
581 .bind(backend_id)
582 .bind(upstream_id)
583 .bind(tenant_id)
584 .execute(pool)
585 .await?;
586 Ok(result.rows_affected() > 0)
587}
588
589pub async fn create_api_key(
595 pool: &PgPool,
596 tenant_id: Uuid,
597 req: CreateApiKeyRequest,
598 key_hash: &str,
599 key_prefix: &str,
600) -> ProxyResult<ApiKeyRow> {
601 let row = sqlx::query_as::<_, ApiKeyRow>(
602 "INSERT INTO proxy_api_keys (tenant_id, name, key_hash, key_prefix, scopes, expires_at) \
603 VALUES ($1, $2, $3, $4, $5, $6) \
604 RETURNING *",
605 )
606 .bind(tenant_id)
607 .bind(&req.name)
608 .bind(key_hash)
609 .bind(key_prefix)
610 .bind(&req.scopes)
611 .bind(req.expires_at)
612 .fetch_one(pool)
613 .await?;
614 Ok(row)
615}
616
617pub async fn validate_api_key_by_hash(
619 pool: &PgPool,
620 key_hash: &str,
621) -> ProxyResult<Option<ApiKeyRow>> {
622 let row = sqlx::query_as::<_, ApiKeyRow>(
623 "SELECT * FROM proxy_api_keys \
624 WHERE key_hash = $1 AND (expires_at IS NULL OR expires_at > NOW())",
625 )
626 .bind(key_hash)
627 .fetch_optional(pool)
628 .await?;
629 Ok(row)
630}
631
632pub async fn update_last_used(pool: &PgPool, key_id: Uuid) -> ProxyResult<()> {
634 sqlx::query("UPDATE proxy_api_keys SET last_used_at = NOW() WHERE id = $1")
635 .bind(key_id)
636 .execute(pool)
637 .await?;
638 Ok(())
639}
640
641pub async fn list_api_keys(pool: &PgPool, tenant_id: Uuid) -> ProxyResult<Vec<ApiKey>> {
643 let rows = sqlx::query_as::<_, ApiKeyRow>(
644 "SELECT * FROM proxy_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC",
645 )
646 .bind(tenant_id)
647 .fetch_all(pool)
648 .await?;
649 Ok(rows.into_iter().map(ApiKey::from).collect())
650}
651
652pub async fn get_api_key_for_tenant(
654 pool: &PgPool,
655 id: Uuid,
656 tenant_id: Uuid,
657) -> ProxyResult<Option<ApiKey>> {
658 let row = sqlx::query_as::<_, ApiKeyRow>(
659 "SELECT * FROM proxy_api_keys WHERE id = $1 AND tenant_id = $2",
660 )
661 .bind(id)
662 .bind(tenant_id)
663 .fetch_optional(pool)
664 .await?;
665 Ok(row.map(ApiKey::from))
666}
667
668pub async fn delete_api_key(pool: &PgPool, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
670 let result = sqlx::query("DELETE FROM proxy_api_keys WHERE id = $1 AND tenant_id = $2")
671 .bind(id)
672 .bind(tenant_id)
673 .execute(pool)
674 .await?;
675 Ok(result.rows_affected() > 0)
676}
677
678pub async fn disable_api_key(pool: &PgPool, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
680 let result =
681 sqlx::query("UPDATE proxy_api_keys SET enabled = false WHERE id = $1 AND tenant_id = $2")
682 .bind(id)
683 .bind(tenant_id)
684 .execute(pool)
685 .await?;
686 Ok(result.rows_affected() > 0)
687}
688
689pub async fn enable_api_key(pool: &PgPool, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
691 let result =
692 sqlx::query("UPDATE proxy_api_keys SET enabled = true WHERE id = $1 AND tenant_id = $2")
693 .bind(id)
694 .bind(tenant_id)
695 .execute(pool)
696 .await?;
697 Ok(result.rows_affected() > 0)
698}
699
700pub async fn is_tenant_active(pool: &PgPool, tenant_id: Uuid) -> ProxyResult<bool> {
706 let row = sqlx::query(
707 "SELECT EXISTS(SELECT 1 FROM proxy_tenants WHERE id = $1 AND status = 'active') AS active",
708 )
709 .bind(tenant_id)
710 .fetch_one(pool)
711 .await?;
712 Ok(row.get::<bool, _>("active"))
713}
714
715pub async fn get_tenant_usage(pool: &PgPool, tenant_id: Uuid) -> ProxyResult<TenantUsage> {
717 let row = sqlx::query(
718 "SELECT \
719 (SELECT COUNT(*) FROM proxy_domains WHERE tenant_id = $1) AS domains_count, \
720 COALESCE((SELECT max_domains FROM proxy_tenants WHERE id = $1), 0) AS domains_limit, \
721 (SELECT COUNT(*) FROM proxy_domains WHERE tenant_id = $1 AND verification_status = 'verified') AS verified_domains, \
722 (SELECT COUNT(*) FROM proxy_domain_routes WHERE tenant_id = $1) AS routes_count, \
723 (SELECT COUNT(*) FROM proxy_domain_upstreams WHERE tenant_id = $1) AS upstreams_count, \
724 (SELECT COUNT(*) FROM proxy_api_keys WHERE tenant_id = $1) AS api_keys_count",
725 )
726 .bind(tenant_id)
727 .fetch_one(pool)
728 .await?;
729
730 Ok(TenantUsage {
731 domains_count: row.get::<i64, _>("domains_count"),
732 domains_limit: row.get::<i32, _>("domains_limit"),
733 verified_domains: row.get::<i64, _>("verified_domains"),
734 routes_count: row.get::<i64, _>("routes_count"),
735 upstreams_count: row.get::<i64, _>("upstreams_count"),
736 api_keys_count: row.get::<i64, _>("api_keys_count"),
737 })
738}