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