allsource_core/infrastructure/web/
tenant_api.rs1use crate::{
2 domain::{
3 entities::TenantQuotas,
4 value_objects::TenantId,
5 },
6 infrastructure::security::middleware::{Admin, Authenticated},
7};
8use axum::{Json, extract::State, http::StatusCode};
9use serde::{Deserialize, Serialize};
10
11use crate::infrastructure::web::api_v1::AppState;
13
14#[derive(Debug, Deserialize)]
19pub struct CreateTenantRequest {
20 pub id: String,
21 pub name: String,
22 pub description: Option<String>,
23 pub quota_preset: Option<String>, pub quotas: Option<TenantQuotas>,
25 #[serde(default)]
26 pub is_demo: bool,
27}
28
29#[derive(Debug, Serialize)]
30pub struct TenantResponse {
31 pub id: String,
32 pub name: String,
33 pub description: Option<String>,
34 pub quotas: TenantQuotas,
35 pub created_at: chrono::DateTime<chrono::Utc>,
36 pub updated_at: chrono::DateTime<chrono::Utc>,
37 pub active: bool,
38 pub is_demo: bool,
39}
40
41impl TenantResponse {
42 fn from_domain(tenant: &crate::domain::entities::Tenant) -> Self {
43 Self {
44 id: tenant.id().as_str().to_string(),
45 name: tenant.name().to_string(),
46 description: tenant.description().map(|s| s.to_string()),
47 quotas: tenant.quotas().clone(),
48 created_at: tenant.created_at(),
49 updated_at: tenant.updated_at(),
50 active: tenant.is_active(),
51 is_demo: tenant.is_demo(),
52 }
53 }
54}
55
56#[derive(Debug, Deserialize)]
57pub struct UpdateTenantRequest {
58 pub name: Option<String>,
59 pub description: Option<String>,
60 pub is_demo: Option<bool>,
61 pub quotas: Option<TenantQuotas>,
62}
63
64#[derive(Debug, Deserialize)]
65pub struct UpdateQuotasRequest {
66 pub quotas: TenantQuotas,
67}
68
69pub async fn create_tenant_handler(
76 State(state): State<AppState>,
77 Admin(_): Admin,
78 Json(req): Json<CreateTenantRequest>,
79) -> Result<(StatusCode, Json<TenantResponse>), (StatusCode, String)> {
80 let quotas = if let Some(quotas) = req.quotas {
82 quotas
83 } else if let Some(preset) = req.quota_preset {
84 match preset.as_str() {
85 "free" => TenantQuotas::free_tier(),
86 "professional" => TenantQuotas::professional(),
87 "unlimited" => TenantQuotas::unlimited(),
88 _ => TenantQuotas::default(),
89 }
90 } else {
91 TenantQuotas::default()
92 };
93
94 let tenant_id = TenantId::new(req.id)
95 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
96
97 let mut tenant = state
98 .tenant_repo
99 .create(tenant_id, req.name, quotas)
100 .await
101 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
102
103 if let Some(desc) = req.description {
105 tenant.update_description(Some(desc));
106 }
107 if req.is_demo {
108 tenant.set_is_demo(true);
109 }
110
111 state
113 .tenant_repo
114 .save(&tenant)
115 .await
116 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
117
118 Ok((StatusCode::CREATED, Json(TenantResponse::from_domain(&tenant))))
119}
120
121pub async fn get_tenant_handler(
124 State(state): State<AppState>,
125 Authenticated(auth_ctx): Authenticated,
126 axum::extract::Path(tenant_id): axum::extract::Path<String>,
127) -> Result<Json<TenantResponse>, (StatusCode, String)> {
128 if tenant_id != auth_ctx.tenant_id() {
130 auth_ctx
131 .require_permission(crate::infrastructure::security::auth::Permission::Admin)
132 .map_err(|_| {
133 (
134 StatusCode::FORBIDDEN,
135 "Can only view own tenant".to_string(),
136 )
137 })?;
138 }
139
140 let tid = TenantId::new(tenant_id.clone())
141 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
142
143 let tenant = state
144 .tenant_repo
145 .find_by_id(&tid)
146 .await
147 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
148 .ok_or_else(|| (StatusCode::NOT_FOUND, format!("Tenant not found: {}", tenant_id)))?;
149
150 Ok(Json(TenantResponse::from_domain(&tenant)))
151}
152
153pub async fn list_tenants_handler(
156 State(state): State<AppState>,
157 Admin(_): Admin,
158) -> Result<Json<Vec<TenantResponse>>, (StatusCode, String)> {
159 let tenants = state
160 .tenant_repo
161 .find_all(10_000, 0)
162 .await
163 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
164 Ok(Json(tenants.iter().map(TenantResponse::from_domain).collect()))
165}
166
167pub async fn get_tenant_stats_handler(
170 State(state): State<AppState>,
171 Authenticated(auth_ctx): Authenticated,
172 axum::extract::Path(tenant_id): axum::extract::Path<String>,
173) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
174 if tenant_id != auth_ctx.tenant_id() {
176 auth_ctx
177 .require_permission(crate::infrastructure::security::auth::Permission::Admin)
178 .map_err(|_| {
179 (
180 StatusCode::FORBIDDEN,
181 "Can only view own tenant stats".to_string(),
182 )
183 })?;
184 }
185
186 let tid = TenantId::new(tenant_id.clone())
187 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
188
189 let tenant = state
190 .tenant_repo
191 .find_by_id(&tid)
192 .await
193 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
194 .ok_or_else(|| (StatusCode::NOT_FOUND, format!("Tenant not found: {}", tenant_id)))?;
195
196 let stats = build_tenant_stats(&tenant);
197 Ok(Json(stats))
198}
199
200pub async fn update_quotas_handler(
203 State(state): State<AppState>,
204 Admin(_): Admin,
205 axum::extract::Path(tenant_id): axum::extract::Path<String>,
206 Json(req): Json<UpdateQuotasRequest>,
207) -> Result<StatusCode, (StatusCode, String)> {
208 let tid = TenantId::new(tenant_id.clone())
209 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
210
211 let updated = state
212 .tenant_repo
213 .update_quotas(&tid, req.quotas)
214 .await
215 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
216
217 if !updated {
218 return Err((StatusCode::NOT_FOUND, format!("Tenant not found: {}", tenant_id)));
219 }
220
221 Ok(StatusCode::NO_CONTENT)
222}
223
224pub async fn deactivate_tenant_handler(
227 State(state): State<AppState>,
228 Admin(_): Admin,
229 axum::extract::Path(tenant_id): axum::extract::Path<String>,
230) -> Result<StatusCode, (StatusCode, String)> {
231 let tid = TenantId::new(tenant_id.clone())
232 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
233
234 let deactivated = state
235 .tenant_repo
236 .deactivate(&tid)
237 .await
238 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
239
240 if !deactivated {
241 return Err((StatusCode::BAD_REQUEST, format!("Tenant not found: {}", tenant_id)));
242 }
243
244 Ok(StatusCode::NO_CONTENT)
245}
246
247pub async fn activate_tenant_handler(
250 State(state): State<AppState>,
251 Admin(_): Admin,
252 axum::extract::Path(tenant_id): axum::extract::Path<String>,
253) -> Result<StatusCode, (StatusCode, String)> {
254 let tid = TenantId::new(tenant_id.clone())
255 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
256
257 let activated = state
258 .tenant_repo
259 .activate(&tid)
260 .await
261 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
262
263 if !activated {
264 return Err((StatusCode::NOT_FOUND, format!("Tenant not found: {}", tenant_id)));
265 }
266
267 Ok(StatusCode::NO_CONTENT)
268}
269
270pub async fn update_tenant_handler(
273 State(state): State<AppState>,
274 Admin(_): Admin,
275 axum::extract::Path(tenant_id): axum::extract::Path<String>,
276 Json(req): Json<UpdateTenantRequest>,
277) -> Result<Json<TenantResponse>, (StatusCode, String)> {
278 let tid = TenantId::new(tenant_id.clone())
279 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
280
281 let mut tenant = state
282 .tenant_repo
283 .find_by_id(&tid)
284 .await
285 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
286 .ok_or_else(|| (StatusCode::NOT_FOUND, format!("Tenant not found: {}", tenant_id)))?;
287
288 if let Some(name) = req.name {
289 tenant.update_name(name)
290 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
291 }
292 if let Some(desc) = req.description {
293 tenant.update_description(Some(desc));
294 }
295 if let Some(is_demo) = req.is_demo {
296 tenant.set_is_demo(is_demo);
297 }
298 if let Some(quotas) = req.quotas {
299 tenant.update_quotas(quotas);
300 }
301
302 state
303 .tenant_repo
304 .save(&tenant)
305 .await
306 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
307
308 Ok(Json(TenantResponse::from_domain(&tenant)))
309}
310
311pub async fn delete_tenant_handler(
314 State(state): State<AppState>,
315 Admin(_): Admin,
316 axum::extract::Path(tenant_id): axum::extract::Path<String>,
317) -> Result<StatusCode, (StatusCode, String)> {
318 let tid = TenantId::new(tenant_id.clone())
319 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
320
321 let deleted = state
322 .tenant_repo
323 .delete(&tid)
324 .await
325 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
326
327 if !deleted {
328 return Err((StatusCode::BAD_REQUEST, format!("Tenant not found: {}", tenant_id)));
329 }
330
331 Ok(StatusCode::NO_CONTENT)
332}
333
334fn build_tenant_stats(tenant: &crate::domain::entities::Tenant) -> serde_json::Value {
340 let quotas = tenant.quotas();
341 let usage = tenant.usage();
342
343 let events_pct = if quotas.max_events_per_day() > 0 {
344 (usage.events_today() as f64 / quotas.max_events_per_day() as f64) * 100.0
345 } else {
346 0.0
347 };
348
349 let storage_pct = if quotas.max_storage_bytes() > 0 {
350 (usage.storage_bytes() as f64 / quotas.max_storage_bytes() as f64) * 100.0
351 } else {
352 0.0
353 };
354
355 let queries_pct = if quotas.max_queries_per_hour() > 0 {
356 (usage.queries_this_hour() as f64 / quotas.max_queries_per_hour() as f64) * 100.0
357 } else {
358 0.0
359 };
360
361 serde_json::json!({
362 "tenant_id": tenant.id().as_str(),
363 "name": tenant.name(),
364 "active": tenant.is_active(),
365 "is_demo": tenant.is_demo(),
366 "usage": tenant.usage(),
367 "quotas": tenant.quotas(),
368 "utilization": {
369 "events_today": {
370 "used": usage.events_today(),
371 "limit": quotas.max_events_per_day(),
372 "percentage": events_pct.min(100.0)
373 },
374 "storage": {
375 "used_bytes": usage.storage_bytes(),
376 "limit_bytes": quotas.max_storage_bytes(),
377 "percentage": storage_pct.min(100.0)
378 },
379 "queries_this_hour": {
380 "used": usage.queries_this_hour(),
381 "limit": quotas.max_queries_per_hour(),
382 "percentage": queries_pct.min(100.0)
383 }
384 },
385 "created_at": tenant.created_at(),
386 "updated_at": tenant.updated_at()
387 })
388}