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)]
68 pub max_storage_bytes: Option<u64>,
69 #[serde(default)]
72 pub cost_budget: Option<usize>,
73}
74
75#[derive(Debug, Serialize)]
77pub struct TenantResponse {
78 pub key: String,
80 pub status: &'static str,
82}
83
84#[derive(Debug, Serialize)]
86pub struct TenantMetadata {
87 pub key: String,
89 pub status: &'static str,
91 pub query_count: usize,
93 pub mutation_count: usize,
95}
96
97#[derive(Debug, Serialize)]
99pub struct TenantListResponse {
100 pub tenants: Vec<String>,
102 pub count: usize,
104}
105
106#[derive(Debug, Serialize)]
108pub struct TenantHealthResponse {
109 pub key: String,
111 pub status: &'static str,
113}
114
115#[derive(Debug, Deserialize)]
117pub struct EventsQuery {
118 #[serde(default = "default_events_limit")]
120 pub limit: usize,
121 #[serde(default)]
123 pub offset: usize,
124}
125
126const fn default_events_limit() -> usize {
127 50
128}
129
130#[derive(Debug, Serialize)]
132pub struct TenantEventsResponse {
133 pub key: String,
135 pub events: Vec<crate::tenancy::audit::TenantEvent>,
137 pub count: usize,
139}
140
141#[derive(Debug, Deserialize)]
143pub struct DomainRegistrationRequest {
144 pub tenant_key: String,
146}
147
148#[derive(Debug, Serialize)]
150pub struct DomainResponse {
151 pub domain: String,
153 pub status: &'static str,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub tenant_key: Option<String>,
158}
159
160#[derive(Debug, Serialize)]
162pub struct DomainListResponse {
163 pub domains: Vec<DomainMapping>,
165 pub count: usize,
167}
168
169#[derive(Debug, Serialize)]
171pub struct DomainMapping {
172 pub domain: String,
174 pub tenant_key: String,
176}
177
178pub async fn upsert_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
193 State(state): State<AppState<A>>,
194 Path(key): Path<String>,
195 ctx: OptionalSecurityContext,
196 Json(body): Json<TenantRegistrationRequest>,
197) -> Result<Json<TenantResponse>, ApiError> {
198 crate::routes::graphql::tenant_key::validate_tenant_key(&key)
202 .map_err(|e| ApiError::validation_error(e.to_string()))?;
203
204 let registry = state
205 .tenant_registry()
206 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
207
208 let factory = state
209 .tenant_executor_factory()
210 .ok_or_else(|| ApiError::internal_error("tenant executor factory not configured"))?;
211
212 let schema_json = serde_json::to_string(&body.schema)
213 .map_err(|e| ApiError::validation_error(format!("invalid schema JSON: {e}")))?;
214
215 let executor =
216 factory(key.clone(), schema_json, body.connection).await.map_err(|e| match &e {
217 fraiseql_error::FraiseQLError::Parse { .. }
218 | fraiseql_error::FraiseQLError::Validation { .. } => ApiError::validation_error(e),
219 fraiseql_error::FraiseQLError::ConnectionPool { .. }
220 | fraiseql_error::FraiseQLError::Database { .. } => {
221 ApiError::new(format!("Connection failed: {e}"), "SERVICE_UNAVAILABLE")
222 },
223 _ => ApiError::internal_error(e),
224 })?;
225
226 let quota = TenantQuota {
227 max_requests_per_sec: body.max_requests_per_sec,
228 max_concurrent: body.max_concurrent,
229 max_storage_bytes: body.max_storage_bytes,
230 cost_budget: body.cost_budget,
231 };
232
233 let was_insert = registry.upsert_with_quota(&key, executor, quota);
234 let status = if was_insert { "created" } else { "updated" };
235
236 info!(tenant_key = %key, status, "tenant executor registered");
237
238 if let Some(audit_log) = state.tenant_audit_log() {
240 let event = if was_insert {
241 TenantEventKind::Created
242 } else {
243 TenantEventKind::ConfigChanged
244 };
245 if let Err(e) = audit_log.record(&key, event, Some(&audit_actor(&ctx)), None).await {
246 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
247 }
248 }
249
250 Ok(Json(TenantResponse { key, status }))
251}
252
253pub async fn delete_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
262 State(state): State<AppState<A>>,
263 Path(key): Path<String>,
264 ctx: OptionalSecurityContext,
265) -> Result<Json<TenantResponse>, ApiError> {
266 let registry = state
267 .tenant_registry()
268 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
269
270 registry
271 .remove(&key)
272 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
273
274 info!(tenant_key = %key, "tenant executor removed");
275
276 if let Some(audit_log) = state.tenant_audit_log() {
277 if let Err(e) = audit_log
278 .record(&key, TenantEventKind::Deleted, Some(&audit_actor(&ctx)), None)
279 .await
280 {
281 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
282 }
283 }
284
285 Ok(Json(TenantResponse {
286 key,
287 status: "removed",
288 }))
289}
290
291pub async fn suspend_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
301 State(state): State<AppState<A>>,
302 Path(key): Path<String>,
303 ctx: OptionalSecurityContext,
304) -> Result<Json<TenantResponse>, ApiError> {
305 let registry = state
306 .tenant_registry()
307 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
308
309 registry
310 .suspend(&key)
311 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
312
313 info!(tenant_key = %key, "tenant suspended");
314
315 if let Some(audit_log) = state.tenant_audit_log() {
316 if let Err(e) = audit_log
317 .record(&key, TenantEventKind::Suspended, Some(&audit_actor(&ctx)), None)
318 .await
319 {
320 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
321 }
322 }
323
324 Ok(Json(TenantResponse {
325 key,
326 status: "suspended",
327 }))
328}
329
330pub async fn resume_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
339 State(state): State<AppState<A>>,
340 Path(key): Path<String>,
341 ctx: OptionalSecurityContext,
342) -> Result<Json<TenantResponse>, ApiError> {
343 let registry = state
344 .tenant_registry()
345 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
346
347 registry
348 .resume(&key)
349 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
350
351 info!(tenant_key = %key, "tenant resumed");
352
353 if let Some(audit_log) = state.tenant_audit_log() {
354 if let Err(e) = audit_log
355 .record(&key, TenantEventKind::Resumed, Some(&audit_actor(&ctx)), None)
356 .await
357 {
358 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
359 }
360 }
361
362 Ok(Json(TenantResponse {
363 key,
364 status: "resumed",
365 }))
366}
367
368pub async fn get_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
377 State(state): State<AppState<A>>,
378 Path(key): Path<String>,
379) -> Result<Json<TenantMetadata>, ApiError> {
380 let registry = state
381 .tenant_registry()
382 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
383
384 let status = registry
385 .tenant_status(&key)
386 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
387
388 let executor = registry
389 .executor_for_admin(&key)
390 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
391
392 Ok(Json(TenantMetadata {
393 key,
394 status: status.as_str(),
395 query_count: executor.schema().queries.len(),
396 mutation_count: executor.schema().mutations.len(),
397 }))
398}
399
400pub async fn list_tenants_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
408 State(state): State<AppState<A>>,
409) -> Result<Json<TenantListResponse>, ApiError> {
410 let registry = state
411 .tenant_registry()
412 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
413
414 let tenants = registry.tenant_keys();
415 let count = tenants.len();
416
417 Ok(Json(TenantListResponse { tenants, count }))
418}
419
420pub async fn tenant_health_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
427 State(state): State<AppState<A>>,
428 Path(key): Path<String>,
429) -> Result<Json<TenantHealthResponse>, ApiError> {
430 let registry = state
431 .tenant_registry()
432 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
433
434 registry.health_check(&key).await.map_err(|e| match &e {
435 fraiseql_error::FraiseQLError::NotFound { .. } => {
436 ApiError::not_found(format!("tenant '{key}'"))
437 },
438 _ => ApiError::new(format!("Health check failed: {e}"), "SERVICE_UNAVAILABLE"),
439 })?;
440
441 Ok(Json(TenantHealthResponse {
442 key,
443 status: "healthy",
444 }))
445}
446
447const MAX_EVENTS_LIMIT: usize = 200;
449
450pub async fn tenant_events_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
460 State(state): State<AppState<A>>,
461 Path(key): Path<String>,
462 Query(params): Query<EventsQuery>,
463) -> Result<Json<TenantEventsResponse>, ApiError> {
464 let registry = state
466 .tenant_registry()
467 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
468
469 registry
470 .executor_for_admin(&key)
471 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
472
473 let audit_log = state
474 .tenant_audit_log()
475 .ok_or_else(|| ApiError::not_found("audit log not configured"))?;
476
477 let limit = params.limit.min(MAX_EVENTS_LIMIT);
478 let events = audit_log
479 .events_for(&key, limit, params.offset)
480 .await
481 .map_err(|e| ApiError::internal_error(format!("failed to query audit events: {e}")))?;
482
483 let count = events.len();
484
485 Ok(Json(TenantEventsResponse { key, events, count }))
486}
487
488pub async fn upsert_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
501 State(state): State<AppState<A>>,
502 Path(domain): Path<String>,
503 Json(body): Json<DomainRegistrationRequest>,
504) -> Result<Json<DomainResponse>, ApiError> {
505 let registry = state
507 .tenant_registry()
508 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
509
510 registry
512 .executor_for(Some(&body.tenant_key))
513 .map_err(|_| ApiError::not_found(format!("tenant '{}'", body.tenant_key)))?;
514
515 state.domain_registry().register(&domain, &body.tenant_key);
516
517 info!(domain = %domain, tenant_key = %body.tenant_key, "domain mapping registered");
518
519 Ok(Json(DomainResponse {
520 domain,
521 status: "registered",
522 tenant_key: Some(body.tenant_key),
523 }))
524}
525
526pub async fn delete_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
533 State(state): State<AppState<A>>,
534 Path(domain): Path<String>,
535) -> Result<Json<DomainResponse>, ApiError> {
536 state
537 .tenant_registry()
538 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
539
540 if !state.domain_registry().remove(&domain) {
541 return Err(ApiError::not_found(format!("domain '{domain}'")));
542 }
543
544 info!(domain = %domain, "domain mapping removed");
545
546 Ok(Json(DomainResponse {
547 domain,
548 status: "removed",
549 tenant_key: None,
550 }))
551}
552
553pub async fn list_domains_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
559 State(state): State<AppState<A>>,
560) -> Result<Json<DomainListResponse>, ApiError> {
561 state
562 .tenant_registry()
563 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
564
565 let mappings = state.domain_registry().domains();
566 let count = mappings.len();
567
568 Ok(Json(DomainListResponse {
569 domains: mappings
570 .into_iter()
571 .map(|(domain, tenant_key)| DomainMapping { domain, tenant_key })
572 .collect(),
573 count,
574 }))
575}