Skip to main content

allsource_core/infrastructure/web/
tenant_api.rs

1use crate::{
2    application::services::tenant_service::{Tenant, TenantQuotas},
3    infrastructure::security::middleware::{Admin, Authenticated},
4};
5use axum::{Json, extract::State, http::StatusCode};
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8
9// AppState is defined in api_v1.rs
10use crate::infrastructure::web::api_v1::AppState;
11
12// ============================================================================
13// Request/Response Types
14// ============================================================================
15
16#[derive(Debug, Deserialize)]
17pub struct CreateTenantRequest {
18    pub id: String,
19    pub name: String,
20    pub description: Option<String>,
21    pub quota_preset: Option<String>, // "free", "professional", "unlimited"
22    pub quotas: Option<TenantQuotas>,
23    #[serde(default)]
24    pub is_demo: bool,
25}
26
27#[derive(Debug, Serialize)]
28pub struct TenantResponse {
29    pub id: String,
30    pub name: String,
31    pub description: Option<String>,
32    pub quotas: TenantQuotas,
33    pub created_at: chrono::DateTime<chrono::Utc>,
34    pub updated_at: chrono::DateTime<chrono::Utc>,
35    pub active: bool,
36    pub is_demo: bool,
37}
38
39impl From<Tenant> for TenantResponse {
40    fn from(tenant: Tenant) -> Self {
41        Self {
42            id: tenant.id,
43            name: tenant.name,
44            description: tenant.description,
45            quotas: tenant.quotas,
46            created_at: tenant.created_at,
47            updated_at: tenant.updated_at,
48            active: tenant.active,
49            is_demo: tenant.is_demo,
50        }
51    }
52}
53
54#[derive(Debug, Deserialize)]
55pub struct UpdateTenantRequest {
56    pub name: Option<String>,
57    pub description: Option<String>,
58    pub is_demo: Option<bool>,
59    pub quotas: Option<TenantQuotas>,
60}
61
62#[derive(Debug, Deserialize)]
63pub struct UpdateQuotasRequest {
64    pub quotas: TenantQuotas,
65}
66
67// ============================================================================
68// Handlers
69// ============================================================================
70
71/// Create tenant (admin only)
72/// POST /api/v1/tenants
73pub async fn create_tenant_handler(
74    State(state): State<AppState>,
75    Admin(_): Admin,
76    Json(req): Json<CreateTenantRequest>,
77) -> Result<(StatusCode, Json<TenantResponse>), (StatusCode, String)> {
78    // Determine quotas
79    let quotas = if let Some(quotas) = req.quotas {
80        quotas
81    } else if let Some(preset) = req.quota_preset {
82        match preset.as_str() {
83            "free" => TenantQuotas::free_tier(),
84            "professional" => TenantQuotas::professional(),
85            "unlimited" => TenantQuotas::unlimited(),
86            _ => TenantQuotas::default(),
87        }
88    } else {
89        TenantQuotas::default()
90    };
91
92    let mut tenant = state
93        .tenant_manager
94        .create_tenant(req.id, req.name, quotas)
95        .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
96
97    if let Some(desc) = req.description {
98        tenant.description = Some(desc);
99    }
100    tenant.is_demo = req.is_demo;
101
102    // Persist the updated fields back
103    state
104        .tenant_manager
105        .save_tenant(&tenant)
106        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
107
108    Ok((StatusCode::CREATED, Json(tenant.into())))
109}
110
111/// Get tenant
112/// GET /api/v1/tenants/:id
113pub async fn get_tenant_handler(
114    State(state): State<AppState>,
115    Authenticated(auth_ctx): Authenticated,
116    axum::extract::Path(tenant_id): axum::extract::Path<String>,
117) -> Result<Json<TenantResponse>, (StatusCode, String)> {
118    // Users can only view their own tenant, admins can view any
119    if tenant_id != auth_ctx.tenant_id() {
120        auth_ctx
121            .require_permission(crate::infrastructure::security::auth::Permission::Admin)
122            .map_err(|_| {
123                (
124                    StatusCode::FORBIDDEN,
125                    "Can only view own tenant".to_string(),
126                )
127            })?;
128    }
129
130    let tenant = state
131        .tenant_manager
132        .get_tenant(&tenant_id)
133        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
134
135    Ok(Json(tenant.into()))
136}
137
138/// List all tenants (admin only)
139/// GET /api/v1/tenants
140pub async fn list_tenants_handler(
141    State(state): State<AppState>,
142    Admin(_): Admin,
143) -> Json<Vec<TenantResponse>> {
144    let tenants = state.tenant_manager.list_tenants();
145    Json(tenants.into_iter().map(TenantResponse::from).collect())
146}
147
148/// Get tenant statistics
149/// GET /api/v1/tenants/:id/stats
150pub async fn get_tenant_stats_handler(
151    State(state): State<AppState>,
152    Authenticated(auth_ctx): Authenticated,
153    axum::extract::Path(tenant_id): axum::extract::Path<String>,
154) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
155    // Users can only view their own tenant stats
156    if tenant_id != auth_ctx.tenant_id() {
157        auth_ctx
158            .require_permission(crate::infrastructure::security::auth::Permission::Admin)
159            .map_err(|_| {
160                (
161                    StatusCode::FORBIDDEN,
162                    "Can only view own tenant stats".to_string(),
163                )
164            })?;
165    }
166
167    let stats = state
168        .tenant_manager
169        .get_stats(&tenant_id)
170        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
171
172    Ok(Json(stats))
173}
174
175/// Update tenant quotas (admin only)
176/// PUT /api/v1/tenants/:id/quotas
177pub async fn update_quotas_handler(
178    State(state): State<AppState>,
179    Admin(_): Admin,
180    axum::extract::Path(tenant_id): axum::extract::Path<String>,
181    Json(req): Json<UpdateQuotasRequest>,
182) -> Result<StatusCode, (StatusCode, String)> {
183    state
184        .tenant_manager
185        .update_quotas(&tenant_id, req.quotas)
186        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
187
188    Ok(StatusCode::NO_CONTENT)
189}
190
191/// Deactivate tenant (admin only)
192/// POST /api/v1/tenants/:id/deactivate
193pub async fn deactivate_tenant_handler(
194    State(state): State<AppState>,
195    Admin(_): Admin,
196    axum::extract::Path(tenant_id): axum::extract::Path<String>,
197) -> Result<StatusCode, (StatusCode, String)> {
198    state
199        .tenant_manager
200        .deactivate_tenant(&tenant_id)
201        .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
202
203    Ok(StatusCode::NO_CONTENT)
204}
205
206/// Activate tenant (admin only)
207/// POST /api/v1/tenants/:id/activate
208pub async fn activate_tenant_handler(
209    State(state): State<AppState>,
210    Admin(_): Admin,
211    axum::extract::Path(tenant_id): axum::extract::Path<String>,
212) -> Result<StatusCode, (StatusCode, String)> {
213    state
214        .tenant_manager
215        .activate_tenant(&tenant_id)
216        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
217
218    Ok(StatusCode::NO_CONTENT)
219}
220
221/// Update tenant (admin only)
222/// PUT /api/v1/tenants/:id
223pub async fn update_tenant_handler(
224    State(state): State<AppState>,
225    Admin(_): Admin,
226    axum::extract::Path(tenant_id): axum::extract::Path<String>,
227    Json(req): Json<UpdateTenantRequest>,
228) -> Result<Json<TenantResponse>, (StatusCode, String)> {
229    let mut tenant = state
230        .tenant_manager
231        .get_tenant(&tenant_id)
232        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
233
234    if let Some(name) = req.name {
235        tenant.name = name;
236    }
237    if let Some(desc) = req.description {
238        tenant.description = Some(desc);
239    }
240    if let Some(is_demo) = req.is_demo {
241        tenant.is_demo = is_demo;
242    }
243    if let Some(quotas) = req.quotas {
244        tenant.quotas = quotas;
245    }
246    tenant.updated_at = Utc::now();
247
248    state
249        .tenant_manager
250        .save_tenant(&tenant)
251        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
252
253    Ok(Json(tenant.into()))
254}
255
256/// Delete tenant (admin only)
257/// DELETE /api/v1/tenants/:id
258pub async fn delete_tenant_handler(
259    State(state): State<AppState>,
260    Admin(_): Admin,
261    axum::extract::Path(tenant_id): axum::extract::Path<String>,
262) -> Result<StatusCode, (StatusCode, String)> {
263    state
264        .tenant_manager
265        .delete_tenant(&tenant_id)
266        .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
267
268    Ok(StatusCode::NO_CONTENT)
269}