1use axum::{
10 Json,
11 extract::{Path, Query, State},
12};
13use fraiseql_core::{db::traits::DatabaseAdapter, security::ActorType};
14use serde::{Deserialize, Serialize};
15use tracing::info;
16
17use crate::{
18 extractors::OptionalSecurityContext,
19 routes::{
20 api::types::ApiError,
21 graphql::{AppState, tenant_registry::TenantQuota},
22 },
23 tenancy::{
24 audit::{AuditActor, TenantEventKind},
25 pool_factory::TenantPoolConfig,
26 },
27};
28
29fn audit_actor(ctx: &OptionalSecurityContext) -> AuditActor {
37 ctx.0.as_ref().map_or_else(
38 || AuditActor {
39 id: Some("admin_token".to_string()),
40 actor_type: Some(ActorType::ServiceAccount.as_str().to_string()),
41 acting_for: None,
42 },
43 |c| AuditActor {
44 id: Some(c.user_id.as_str().to_string()),
45 actor_type: Some(c.actor_type().as_str().to_string()),
46 acting_for: c.acting_for(),
47 },
48 )
49}
50
51#[derive(Debug, Deserialize)]
55pub struct TenantRegistrationRequest {
56 pub schema: serde_json::Value,
58 pub connection: TenantPoolConfig,
60 #[serde(default)]
62 pub max_requests_per_sec: Option<u32>,
63 #[serde(default)]
65 pub max_concurrent: Option<u32>,
66 #[serde(default)]
72 pub max_storage_bytes: Option<u64>,
73 #[serde(default)]
76 pub cost_budget: Option<usize>,
77}
78
79#[derive(Debug, Serialize)]
81pub struct TenantResponse {
82 pub key: String,
84 pub status: &'static str,
86}
87
88#[derive(Debug, Serialize)]
90pub struct TenantMetadata {
91 pub key: String,
93 pub status: &'static str,
95 pub query_count: usize,
97 pub mutation_count: usize,
99}
100
101#[derive(Debug, Serialize)]
103pub struct TenantListResponse {
104 pub tenants: Vec<String>,
106 pub count: usize,
108}
109
110#[derive(Debug, Serialize)]
112pub struct TenantHealthResponse {
113 pub key: String,
115 pub status: &'static str,
117}
118
119#[derive(Debug, Deserialize)]
121pub struct EventsQuery {
122 #[serde(default = "default_events_limit")]
124 pub limit: usize,
125 #[serde(default)]
127 pub offset: usize,
128}
129
130const fn default_events_limit() -> usize {
131 50
132}
133
134#[derive(Debug, Serialize)]
136pub struct TenantEventsResponse {
137 pub key: String,
139 pub events: Vec<crate::tenancy::audit::TenantEvent>,
141 pub count: usize,
143}
144
145#[derive(Debug, Deserialize)]
147pub struct DomainRegistrationRequest {
148 pub tenant_key: String,
150}
151
152#[derive(Debug, Serialize)]
154pub struct DomainResponse {
155 pub domain: String,
157 pub status: &'static str,
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub tenant_key: Option<String>,
162}
163
164#[derive(Debug, Serialize)]
166pub struct DomainListResponse {
167 pub domains: Vec<DomainMapping>,
169 pub count: usize,
171}
172
173#[derive(Debug, Serialize)]
175pub struct DomainMapping {
176 pub domain: String,
178 pub tenant_key: String,
180}
181
182pub async fn upsert_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
197 State(state): State<AppState<A>>,
198 Path(key): Path<String>,
199 ctx: OptionalSecurityContext,
200 Json(body): Json<TenantRegistrationRequest>,
201) -> Result<Json<TenantResponse>, ApiError> {
202 crate::routes::graphql::tenant_key::validate_tenant_key(&key)
206 .map_err(|e| ApiError::validation_error(e.to_string()))?;
207
208 let registry = state
209 .tenant_registry()
210 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
211
212 let factory = state
213 .tenant_executor_factory()
214 .ok_or_else(|| ApiError::internal_error("tenant executor factory not configured"))?;
215
216 let schema_json = serde_json::to_string(&body.schema)
217 .map_err(|e| ApiError::validation_error(format!("invalid schema JSON: {e}")))?;
218
219 let executor =
220 factory(key.clone(), schema_json, body.connection).await.map_err(|e| match &e {
221 fraiseql_error::FraiseQLError::Parse { .. }
222 | fraiseql_error::FraiseQLError::Validation { .. } => ApiError::validation_error(e),
223 fraiseql_error::FraiseQLError::ConnectionPool { .. }
224 | fraiseql_error::FraiseQLError::Database { .. } => {
225 ApiError::new(format!("Connection failed: {e}"), "SERVICE_UNAVAILABLE")
226 },
227 _ => ApiError::internal_error(e),
228 })?;
229
230 let quota = TenantQuota {
231 max_requests_per_sec: body.max_requests_per_sec,
232 max_concurrent: body.max_concurrent,
233 max_storage_bytes: body.max_storage_bytes,
234 cost_budget: body.cost_budget,
235 };
236
237 let was_insert = registry.upsert_with_quota(&key, executor, quota);
238 let status = if was_insert { "created" } else { "updated" };
239
240 info!(tenant_key = %key, status, "tenant executor registered");
241
242 if let Some(audit_log) = state.tenant_audit_log() {
244 let event = if was_insert {
245 TenantEventKind::Created
246 } else {
247 TenantEventKind::ConfigChanged
248 };
249 if let Err(e) = audit_log.record(&key, event, Some(&audit_actor(&ctx)), None).await {
250 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
251 }
252 }
253
254 Ok(Json(TenantResponse { key, status }))
255}
256
257pub async fn delete_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
266 State(state): State<AppState<A>>,
267 Path(key): Path<String>,
268 ctx: OptionalSecurityContext,
269) -> Result<Json<TenantResponse>, ApiError> {
270 let registry = state
271 .tenant_registry()
272 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
273
274 registry
275 .remove(&key)
276 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
277
278 info!(tenant_key = %key, "tenant executor removed");
279
280 if let Some(audit_log) = state.tenant_audit_log() {
281 if let Err(e) = audit_log
282 .record(&key, TenantEventKind::Deleted, Some(&audit_actor(&ctx)), None)
283 .await
284 {
285 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
286 }
287 }
288
289 Ok(Json(TenantResponse {
290 key,
291 status: "removed",
292 }))
293}
294
295pub async fn suspend_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
305 State(state): State<AppState<A>>,
306 Path(key): Path<String>,
307 ctx: OptionalSecurityContext,
308) -> Result<Json<TenantResponse>, ApiError> {
309 let registry = state
310 .tenant_registry()
311 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
312
313 registry
314 .suspend(&key)
315 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
316
317 info!(tenant_key = %key, "tenant suspended");
318
319 if let Some(audit_log) = state.tenant_audit_log() {
320 if let Err(e) = audit_log
321 .record(&key, TenantEventKind::Suspended, Some(&audit_actor(&ctx)), None)
322 .await
323 {
324 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
325 }
326 }
327
328 Ok(Json(TenantResponse {
329 key,
330 status: "suspended",
331 }))
332}
333
334pub async fn resume_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
343 State(state): State<AppState<A>>,
344 Path(key): Path<String>,
345 ctx: OptionalSecurityContext,
346) -> Result<Json<TenantResponse>, ApiError> {
347 let registry = state
348 .tenant_registry()
349 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
350
351 registry
352 .resume(&key)
353 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
354
355 info!(tenant_key = %key, "tenant resumed");
356
357 if let Some(audit_log) = state.tenant_audit_log() {
358 if let Err(e) = audit_log
359 .record(&key, TenantEventKind::Resumed, Some(&audit_actor(&ctx)), None)
360 .await
361 {
362 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
363 }
364 }
365
366 Ok(Json(TenantResponse {
367 key,
368 status: "resumed",
369 }))
370}
371
372pub async fn get_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
381 State(state): State<AppState<A>>,
382 Path(key): Path<String>,
383) -> Result<Json<TenantMetadata>, ApiError> {
384 let registry = state
385 .tenant_registry()
386 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
387
388 let status = registry
389 .tenant_status(&key)
390 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
391
392 let executor = registry
393 .executor_for_admin(&key)
394 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
395
396 Ok(Json(TenantMetadata {
397 key,
398 status: status.as_str(),
399 query_count: executor.schema().queries.len(),
400 mutation_count: executor.schema().mutations.len(),
401 }))
402}
403
404pub async fn list_tenants_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
412 State(state): State<AppState<A>>,
413) -> Result<Json<TenantListResponse>, ApiError> {
414 let registry = state
415 .tenant_registry()
416 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
417
418 let tenants = registry.tenant_keys();
419 let count = tenants.len();
420
421 Ok(Json(TenantListResponse { tenants, count }))
422}
423
424pub async fn tenant_health_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
431 State(state): State<AppState<A>>,
432 Path(key): Path<String>,
433) -> Result<Json<TenantHealthResponse>, ApiError> {
434 let registry = state
435 .tenant_registry()
436 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
437
438 registry.health_check(&key).await.map_err(|e| match &e {
439 fraiseql_error::FraiseQLError::NotFound { .. } => {
440 ApiError::not_found(format!("tenant '{key}'"))
441 },
442 _ => ApiError::new(format!("Health check failed: {e}"), "SERVICE_UNAVAILABLE"),
443 })?;
444
445 Ok(Json(TenantHealthResponse {
446 key,
447 status: "healthy",
448 }))
449}
450
451const MAX_EVENTS_LIMIT: usize = 200;
453
454pub async fn tenant_events_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
464 State(state): State<AppState<A>>,
465 Path(key): Path<String>,
466 Query(params): Query<EventsQuery>,
467) -> Result<Json<TenantEventsResponse>, ApiError> {
468 let registry = state
470 .tenant_registry()
471 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
472
473 registry
474 .executor_for_admin(&key)
475 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
476
477 let audit_log = state
478 .tenant_audit_log()
479 .ok_or_else(|| ApiError::not_found("audit log not configured"))?;
480
481 let limit = params.limit.min(MAX_EVENTS_LIMIT);
482 let events = audit_log
483 .events_for(&key, limit, params.offset)
484 .await
485 .map_err(|e| ApiError::internal_error(format!("failed to query audit events: {e}")))?;
486
487 let count = events.len();
488
489 Ok(Json(TenantEventsResponse { key, events, count }))
490}
491
492pub async fn upsert_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
505 State(state): State<AppState<A>>,
506 Path(domain): Path<String>,
507 Json(body): Json<DomainRegistrationRequest>,
508) -> Result<Json<DomainResponse>, ApiError> {
509 let registry = state
511 .tenant_registry()
512 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
513
514 registry
516 .executor_for(Some(&body.tenant_key))
517 .map_err(|_| ApiError::not_found(format!("tenant '{}'", body.tenant_key)))?;
518
519 state.domain_registry().register(&domain, &body.tenant_key);
520
521 info!(domain = %domain, tenant_key = %body.tenant_key, "domain mapping registered");
522
523 Ok(Json(DomainResponse {
524 domain,
525 status: "registered",
526 tenant_key: Some(body.tenant_key),
527 }))
528}
529
530pub async fn delete_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
537 State(state): State<AppState<A>>,
538 Path(domain): Path<String>,
539) -> Result<Json<DomainResponse>, ApiError> {
540 state
541 .tenant_registry()
542 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
543
544 if !state.domain_registry().remove(&domain) {
545 return Err(ApiError::not_found(format!("domain '{domain}'")));
546 }
547
548 info!(domain = %domain, "domain mapping removed");
549
550 Ok(Json(DomainResponse {
551 domain,
552 status: "removed",
553 tenant_key: None,
554 }))
555}
556
557pub async fn list_domains_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
563 State(state): State<AppState<A>>,
564) -> Result<Json<DomainListResponse>, ApiError> {
565 state
566 .tenant_registry()
567 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
568
569 let mappings = state.domain_registry().domains();
570 let count = mappings.len();
571
572 Ok(Json(DomainListResponse {
573 domains: mappings
574 .into_iter()
575 .map(|(domain, tenant_key)| DomainMapping { domain, tenant_key })
576 .collect(),
577 count,
578 }))
579}