fraiseql_server/routes/api/
tenant_admin.rs1use axum::{
10 Json,
11 extract::{Path, Query, State},
12};
13use fraiseql_core::db::traits::DatabaseAdapter;
14use serde::{Deserialize, Serialize};
15use tracing::info;
16
17use crate::{
18 routes::{
19 api::types::ApiError,
20 graphql::{AppState, tenant_registry::TenantQuota},
21 },
22 tenancy::{audit::TenantEventKind, pool_factory::TenantPoolConfig},
23};
24
25#[derive(Debug, Deserialize)]
29pub struct TenantRegistrationRequest {
30 pub schema: serde_json::Value,
32 pub connection: TenantPoolConfig,
34 #[serde(default)]
36 pub max_requests_per_sec: Option<u32>,
37 #[serde(default)]
39 pub max_concurrent: Option<u32>,
40 #[serde(default)]
42 pub max_storage_bytes: Option<u64>,
43}
44
45#[derive(Debug, Serialize)]
47pub struct TenantResponse {
48 pub key: String,
50 pub status: &'static str,
52}
53
54#[derive(Debug, Serialize)]
56pub struct TenantMetadata {
57 pub key: String,
59 pub status: &'static str,
61 pub query_count: usize,
63 pub mutation_count: usize,
65}
66
67#[derive(Debug, Serialize)]
69pub struct TenantListResponse {
70 pub tenants: Vec<String>,
72 pub count: usize,
74}
75
76#[derive(Debug, Serialize)]
78pub struct TenantHealthResponse {
79 pub key: String,
81 pub status: &'static str,
83}
84
85#[derive(Debug, Deserialize)]
87pub struct EventsQuery {
88 #[serde(default = "default_events_limit")]
90 pub limit: usize,
91 #[serde(default)]
93 pub offset: usize,
94}
95
96const fn default_events_limit() -> usize {
97 50
98}
99
100#[derive(Debug, Serialize)]
102pub struct TenantEventsResponse {
103 pub key: String,
105 pub events: Vec<crate::tenancy::audit::TenantEvent>,
107 pub count: usize,
109}
110
111#[derive(Debug, Deserialize)]
113pub struct DomainRegistrationRequest {
114 pub tenant_key: String,
116}
117
118#[derive(Debug, Serialize)]
120pub struct DomainResponse {
121 pub domain: String,
123 pub status: &'static str,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub tenant_key: Option<String>,
128}
129
130#[derive(Debug, Serialize)]
132pub struct DomainListResponse {
133 pub domains: Vec<DomainMapping>,
135 pub count: usize,
137}
138
139#[derive(Debug, Serialize)]
141pub struct DomainMapping {
142 pub domain: String,
144 pub tenant_key: String,
146}
147
148pub async fn upsert_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
163 State(state): State<AppState<A>>,
164 Path(key): Path<String>,
165 Json(body): Json<TenantRegistrationRequest>,
166) -> Result<Json<TenantResponse>, ApiError> {
167 crate::routes::graphql::tenant_key::validate_tenant_key(&key)
171 .map_err(|e| ApiError::validation_error(e.to_string()))?;
172
173 let registry = state
174 .tenant_registry()
175 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
176
177 let factory = state
178 .tenant_executor_factory()
179 .ok_or_else(|| ApiError::internal_error("tenant executor factory not configured"))?;
180
181 let schema_json = serde_json::to_string(&body.schema)
182 .map_err(|e| ApiError::validation_error(format!("invalid schema JSON: {e}")))?;
183
184 let executor =
185 factory(key.clone(), schema_json, body.connection).await.map_err(|e| match &e {
186 fraiseql_error::FraiseQLError::Parse { .. }
187 | fraiseql_error::FraiseQLError::Validation { .. } => ApiError::validation_error(e),
188 fraiseql_error::FraiseQLError::ConnectionPool { .. }
189 | fraiseql_error::FraiseQLError::Database { .. } => {
190 ApiError::new(format!("Connection failed: {e}"), "SERVICE_UNAVAILABLE")
191 },
192 _ => ApiError::internal_error(e),
193 })?;
194
195 let quota = TenantQuota {
196 max_requests_per_sec: body.max_requests_per_sec,
197 max_concurrent: body.max_concurrent,
198 max_storage_bytes: body.max_storage_bytes,
199 };
200
201 let was_insert = registry.upsert_with_quota(&key, executor, quota);
202 let status = if was_insert { "created" } else { "updated" };
203
204 info!(tenant_key = %key, status, "tenant executor registered");
205
206 if let Some(audit_log) = state.tenant_audit_log() {
208 let event = if was_insert {
209 TenantEventKind::Created
210 } else {
211 TenantEventKind::ConfigChanged
212 };
213 if let Err(e) = audit_log.record(&key, event, None, None).await {
214 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
215 }
216 }
217
218 Ok(Json(TenantResponse { key, status }))
219}
220
221pub async fn delete_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
230 State(state): State<AppState<A>>,
231 Path(key): Path<String>,
232) -> Result<Json<TenantResponse>, ApiError> {
233 let registry = state
234 .tenant_registry()
235 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
236
237 registry
238 .remove(&key)
239 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
240
241 info!(tenant_key = %key, "tenant executor removed");
242
243 if let Some(audit_log) = state.tenant_audit_log() {
244 if let Err(e) = audit_log.record(&key, TenantEventKind::Deleted, None, None).await {
245 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
246 }
247 }
248
249 Ok(Json(TenantResponse {
250 key,
251 status: "removed",
252 }))
253}
254
255pub async fn suspend_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
265 State(state): State<AppState<A>>,
266 Path(key): Path<String>,
267) -> Result<Json<TenantResponse>, ApiError> {
268 let registry = state
269 .tenant_registry()
270 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
271
272 registry
273 .suspend(&key)
274 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
275
276 info!(tenant_key = %key, "tenant suspended");
277
278 if let Some(audit_log) = state.tenant_audit_log() {
279 if let Err(e) = audit_log.record(&key, TenantEventKind::Suspended, None, None).await {
280 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
281 }
282 }
283
284 Ok(Json(TenantResponse {
285 key,
286 status: "suspended",
287 }))
288}
289
290pub async fn resume_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
299 State(state): State<AppState<A>>,
300 Path(key): Path<String>,
301) -> Result<Json<TenantResponse>, ApiError> {
302 let registry = state
303 .tenant_registry()
304 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
305
306 registry
307 .resume(&key)
308 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
309
310 info!(tenant_key = %key, "tenant resumed");
311
312 if let Some(audit_log) = state.tenant_audit_log() {
313 if let Err(e) = audit_log.record(&key, TenantEventKind::Resumed, None, None).await {
314 tracing::warn!(tenant_key = %key, error = %e, "failed to record audit event");
315 }
316 }
317
318 Ok(Json(TenantResponse {
319 key,
320 status: "resumed",
321 }))
322}
323
324pub async fn get_tenant_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
333 State(state): State<AppState<A>>,
334 Path(key): Path<String>,
335) -> Result<Json<TenantMetadata>, ApiError> {
336 let registry = state
337 .tenant_registry()
338 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
339
340 let status = registry
341 .tenant_status(&key)
342 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
343
344 let executor = registry
345 .executor_for_admin(&key)
346 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
347
348 Ok(Json(TenantMetadata {
349 key,
350 status: status.as_str(),
351 query_count: executor.schema().queries.len(),
352 mutation_count: executor.schema().mutations.len(),
353 }))
354}
355
356pub async fn list_tenants_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
364 State(state): State<AppState<A>>,
365) -> Result<Json<TenantListResponse>, ApiError> {
366 let registry = state
367 .tenant_registry()
368 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
369
370 let tenants = registry.tenant_keys();
371 let count = tenants.len();
372
373 Ok(Json(TenantListResponse { tenants, count }))
374}
375
376pub async fn tenant_health_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
383 State(state): State<AppState<A>>,
384 Path(key): Path<String>,
385) -> Result<Json<TenantHealthResponse>, ApiError> {
386 let registry = state
387 .tenant_registry()
388 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
389
390 registry.health_check(&key).await.map_err(|e| match &e {
391 fraiseql_error::FraiseQLError::NotFound { .. } => {
392 ApiError::not_found(format!("tenant '{key}'"))
393 },
394 _ => ApiError::new(format!("Health check failed: {e}"), "SERVICE_UNAVAILABLE"),
395 })?;
396
397 Ok(Json(TenantHealthResponse {
398 key,
399 status: "healthy",
400 }))
401}
402
403const MAX_EVENTS_LIMIT: usize = 200;
405
406pub async fn tenant_events_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
416 State(state): State<AppState<A>>,
417 Path(key): Path<String>,
418 Query(params): Query<EventsQuery>,
419) -> Result<Json<TenantEventsResponse>, ApiError> {
420 let registry = state
422 .tenant_registry()
423 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
424
425 registry
426 .executor_for_admin(&key)
427 .map_err(|_| ApiError::not_found(format!("tenant '{key}'")))?;
428
429 let audit_log = state
430 .tenant_audit_log()
431 .ok_or_else(|| ApiError::not_found("audit log not configured"))?;
432
433 let limit = params.limit.min(MAX_EVENTS_LIMIT);
434 let events = audit_log
435 .events_for(&key, limit, params.offset)
436 .await
437 .map_err(|e| ApiError::internal_error(format!("failed to query audit events: {e}")))?;
438
439 let count = events.len();
440
441 Ok(Json(TenantEventsResponse { key, events, count }))
442}
443
444pub async fn upsert_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
457 State(state): State<AppState<A>>,
458 Path(domain): Path<String>,
459 Json(body): Json<DomainRegistrationRequest>,
460) -> Result<Json<DomainResponse>, ApiError> {
461 let registry = state
463 .tenant_registry()
464 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
465
466 registry
468 .executor_for(Some(&body.tenant_key))
469 .map_err(|_| ApiError::not_found(format!("tenant '{}'", body.tenant_key)))?;
470
471 state.domain_registry().register(&domain, &body.tenant_key);
472
473 info!(domain = %domain, tenant_key = %body.tenant_key, "domain mapping registered");
474
475 Ok(Json(DomainResponse {
476 domain,
477 status: "registered",
478 tenant_key: Some(body.tenant_key),
479 }))
480}
481
482pub async fn delete_domain_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
489 State(state): State<AppState<A>>,
490 Path(domain): Path<String>,
491) -> Result<Json<DomainResponse>, ApiError> {
492 state
493 .tenant_registry()
494 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
495
496 if !state.domain_registry().remove(&domain) {
497 return Err(ApiError::not_found(format!("domain '{domain}'")));
498 }
499
500 info!(domain = %domain, "domain mapping removed");
501
502 Ok(Json(DomainResponse {
503 domain,
504 status: "removed",
505 tenant_key: None,
506 }))
507}
508
509pub async fn list_domains_handler<A: DatabaseAdapter + Clone + Send + Sync + 'static>(
515 State(state): State<AppState<A>>,
516) -> Result<Json<DomainListResponse>, ApiError> {
517 state
518 .tenant_registry()
519 .ok_or_else(|| ApiError::not_found("multi-tenant mode not enabled"))?;
520
521 let mappings = state.domain_registry().domains();
522 let count = mappings.len();
523
524 Ok(Json(DomainListResponse {
525 domains: mappings
526 .into_iter()
527 .map(|(domain, tenant_key)| DomainMapping { domain, tenant_key })
528 .collect(),
529 count,
530 }))
531}