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}
70
71#[derive(Debug, Serialize)]
73pub struct TenantResponse {
74 pub key: String,
76 pub status: &'static str,
78}
79
80#[derive(Debug, Serialize)]
82pub struct TenantMetadata {
83 pub key: String,
85 pub status: &'static str,
87 pub query_count: usize,
89 pub mutation_count: usize,
91}
92
93#[derive(Debug, Serialize)]
95pub struct TenantListResponse {
96 pub tenants: Vec<String>,
98 pub count: usize,
100}
101
102#[derive(Debug, Serialize)]
104pub struct TenantHealthResponse {
105 pub key: String,
107 pub status: &'static str,
109}
110
111#[derive(Debug, Deserialize)]
113pub struct EventsQuery {
114 #[serde(default = "default_events_limit")]
116 pub limit: usize,
117 #[serde(default)]
119 pub offset: usize,
120}
121
122const fn default_events_limit() -> usize {
123 50
124}
125
126#[derive(Debug, Serialize)]
128pub struct TenantEventsResponse {
129 pub key: String,
131 pub events: Vec<crate::tenancy::audit::TenantEvent>,
133 pub count: usize,
135}
136
137#[derive(Debug, Deserialize)]
139pub struct DomainRegistrationRequest {
140 pub tenant_key: String,
142}
143
144#[derive(Debug, Serialize)]
146pub struct DomainResponse {
147 pub domain: String,
149 pub status: &'static str,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub tenant_key: Option<String>,
154}
155
156#[derive(Debug, Serialize)]
158pub struct DomainListResponse {
159 pub domains: Vec<DomainMapping>,
161 pub count: usize,
163}
164
165#[derive(Debug, Serialize)]
167pub struct DomainMapping {
168 pub domain: String,
170 pub tenant_key: String,
172}
173
174pub async fn upsert_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
189 State(state): State<AppState<A>>,
190 Path(key): Path<String>,
191 ctx: OptionalSecurityContext,
192 Json(body): Json<TenantRegistrationRequest>,
193) -> Result<Json<TenantResponse>, ApiError> {
194 crate::routes::graphql::tenant_key::validate_tenant_key(&key)
198 .map_err(|e| ApiError::validation_error(e.to_string()))?;
199
200 let registry = state
201 .tenant_registry()
202 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
203
204 let factory = state
205 .tenant_executor_factory()
206 .ok_or_else(|| ApiError::internal_error("tenant executor factory not configured"))?;
207
208 let schema_json = serde_json::to_string(&body.schema)
209 .map_err(|e| ApiError::validation_error(format!("invalid schema JSON: {e}")))?;
210
211 let executor =
212 factory(key.clone(), schema_json, body.connection).await.map_err(|e| match &e {
213 fraiseql_error::FraiseQLError::Parse { .. }
214 | fraiseql_error::FraiseQLError::Validation { .. } => ApiError::validation_error(e),
215 fraiseql_error::FraiseQLError::ConnectionPool { .. }
216 | fraiseql_error::FraiseQLError::Database { .. } => {
217 ApiError::new(format!("Connection failed: {e}"), "SERVICE_UNAVAILABLE")
218 },
219 _ => ApiError::internal_error(e),
220 })?;
221
222 let quota = TenantQuota {
223 max_requests_per_sec: body.max_requests_per_sec,
224 max_concurrent: body.max_concurrent,
225 max_storage_bytes: body.max_storage_bytes,
226 };
227
228 let was_insert = registry.upsert_with_quota(&key, executor, quota);
229 let status = if was_insert { "created" } else { "updated" };
230
231 info!(tenant_key = %key, status, "tenant executor registered");
232
233 if let Some(audit_log) = state.tenant_audit_log() {
235 let event = if was_insert {
236 TenantEventKind::Created
237 } else {
238 TenantEventKind::ConfigChanged
239 };
240 if let Err(e) = audit_log.record(&key, event, Some(&audit_actor(&ctx)), None).await {
241 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
242 }
243 }
244
245 Ok(Json(TenantResponse { key, status }))
246}
247
248pub async fn delete_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
257 State(state): State<AppState<A>>,
258 Path(key): Path<String>,
259 ctx: OptionalSecurityContext,
260) -> Result<Json<TenantResponse>, ApiError> {
261 let registry = state
262 .tenant_registry()
263 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
264
265 registry
266 .remove(&key)
267 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
268
269 info!(tenant_key = %key, "tenant executor removed");
270
271 if let Some(audit_log) = state.tenant_audit_log() {
272 if let Err(e) = audit_log
273 .record(&key, TenantEventKind::Deleted, Some(&audit_actor(&ctx)), None)
274 .await
275 {
276 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
277 }
278 }
279
280 Ok(Json(TenantResponse {
281 key,
282 status: "removed",
283 }))
284}
285
286pub async fn suspend_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
296 State(state): State<AppState<A>>,
297 Path(key): Path<String>,
298 ctx: OptionalSecurityContext,
299) -> Result<Json<TenantResponse>, ApiError> {
300 let registry = state
301 .tenant_registry()
302 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
303
304 registry
305 .suspend(&key)
306 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
307
308 info!(tenant_key = %key, "tenant suspended");
309
310 if let Some(audit_log) = state.tenant_audit_log() {
311 if let Err(e) = audit_log
312 .record(&key, TenantEventKind::Suspended, Some(&audit_actor(&ctx)), None)
313 .await
314 {
315 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
316 }
317 }
318
319 Ok(Json(TenantResponse {
320 key,
321 status: "suspended",
322 }))
323}
324
325pub async fn resume_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
334 State(state): State<AppState<A>>,
335 Path(key): Path<String>,
336 ctx: OptionalSecurityContext,
337) -> Result<Json<TenantResponse>, ApiError> {
338 let registry = state
339 .tenant_registry()
340 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
341
342 registry
343 .resume(&key)
344 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
345
346 info!(tenant_key = %key, "tenant resumed");
347
348 if let Some(audit_log) = state.tenant_audit_log() {
349 if let Err(e) = audit_log
350 .record(&key, TenantEventKind::Resumed, Some(&audit_actor(&ctx)), None)
351 .await
352 {
353 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
354 }
355 }
356
357 Ok(Json(TenantResponse {
358 key,
359 status: "resumed",
360 }))
361}
362
363pub async fn get_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
372 State(state): State<AppState<A>>,
373 Path(key): Path<String>,
374) -> Result<Json<TenantMetadata>, ApiError> {
375 let registry = state
376 .tenant_registry()
377 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
378
379 let status = registry
380 .tenant_status(&key)
381 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
382
383 let executor = registry
384 .executor_for_admin(&key)
385 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
386
387 Ok(Json(TenantMetadata {
388 key,
389 status: status.as_str(),
390 query_count: executor.schema().queries.len(),
391 mutation_count: executor.schema().mutations.len(),
392 }))
393}
394
395pub async fn list_tenants_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
403 State(state): State<AppState<A>>,
404) -> Result<Json<TenantListResponse>, ApiError> {
405 let registry = state
406 .tenant_registry()
407 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
408
409 let tenants = registry.tenant_keys();
410 let count = tenants.len();
411
412 Ok(Json(TenantListResponse { tenants, count }))
413}
414
415pub async fn tenant_health_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
422 State(state): State<AppState<A>>,
423 Path(key): Path<String>,
424) -> Result<Json<TenantHealthResponse>, ApiError> {
425 let registry = state
426 .tenant_registry()
427 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
428
429 registry.health_check(&key).await.map_err(|e| match &e {
430 fraiseql_error::FraiseQLError::NotFound { .. } => {
431 ApiError::not_found(format!("tenant '{key}'"))
432 },
433 _ => ApiError::new(format!("Health check failed: {e}"), "SERVICE_UNAVAILABLE"),
434 })?;
435
436 Ok(Json(TenantHealthResponse {
437 key,
438 status: "healthy",
439 }))
440}
441
442const MAX_EVENTS_LIMIT: usize = 200;
444
445pub async fn tenant_events_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
455 State(state): State<AppState<A>>,
456 Path(key): Path<String>,
457 Query(params): Query<EventsQuery>,
458) -> Result<Json<TenantEventsResponse>, ApiError> {
459 let registry = state
461 .tenant_registry()
462 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
463
464 registry
465 .executor_for_admin(&key)
466 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
467
468 let audit_log = state
469 .tenant_audit_log()
470 .ok_or_else(|| ApiError::not_found("audit log not configured"))?;
471
472 let limit = params.limit.min(MAX_EVENTS_LIMIT);
473 let events = audit_log
474 .events_for(&key, limit, params.offset)
475 .await
476 .map_err(|e| ApiError::internal_error(format!("failed to query audit events: {e}")))?;
477
478 let count = events.len();
479
480 Ok(Json(TenantEventsResponse { key, events, count }))
481}
482
483pub async fn upsert_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
496 State(state): State<AppState<A>>,
497 Path(domain): Path<String>,
498 Json(body): Json<DomainRegistrationRequest>,
499) -> Result<Json<DomainResponse>, ApiError> {
500 let registry = state
502 .tenant_registry()
503 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
504
505 registry
507 .executor_for(Some(&body.tenant_key))
508 .map_err(|_| ApiError::not_found(format!("tenant '{}'", body.tenant_key)))?;
509
510 state.domain_registry().register(&domain, &body.tenant_key);
511
512 info!(domain = %domain, tenant_key = %body.tenant_key, "domain mapping registered");
513
514 Ok(Json(DomainResponse {
515 domain,
516 status: "registered",
517 tenant_key: Some(body.tenant_key),
518 }))
519}
520
521pub async fn delete_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
528 State(state): State<AppState<A>>,
529 Path(domain): Path<String>,
530) -> Result<Json<DomainResponse>, ApiError> {
531 state
532 .tenant_registry()
533 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
534
535 if !state.domain_registry().remove(&domain) {
536 return Err(ApiError::not_found(format!("domain '{domain}'")));
537 }
538
539 info!(domain = %domain, "domain mapping removed");
540
541 Ok(Json(DomainResponse {
542 domain,
543 status: "removed",
544 tenant_key: None,
545 }))
546}
547
548pub async fn list_domains_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
554 State(state): State<AppState<A>>,
555) -> Result<Json<DomainListResponse>, ApiError> {
556 state
557 .tenant_registry()
558 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
559
560 let mappings = state.domain_registry().domains();
561 let count = mappings.len();
562
563 Ok(Json(DomainListResponse {
564 domains: mappings
565 .into_iter()
566 .map(|(domain, tenant_key)| DomainMapping { domain, tenant_key })
567 .collect(),
568 count,
569 }))
570}