1use axum::{
4 extract::{Path, Query, State},
5 http::StatusCode,
6 Json,
7};
8use serde::{Deserialize, Serialize};
9use serde_with::skip_serializing_none;
10use std::collections::HashMap;
11
12use cloudillo_email::{get_tenant_lang, EmailModule, EmailTaskParams};
13use cloudillo_ref::service::{create_ref_internal, CreateRefInternalParams};
14use cloudillo_types::auth_adapter::ListTenantsOptions;
15use cloudillo_types::meta_adapter::{ListTenantsMetaOptions, ProfileType};
16use cloudillo_types::types::{ApiResponse, Timestamp};
17
18use crate::prelude::*;
19
20#[skip_serializing_none]
22#[derive(Debug, Clone, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct TenantView {
25 pub tn_id: u32,
26 pub id_tag: String,
27 pub name: String,
28 #[serde(rename = "type")]
29 pub typ: ProfileType,
30 pub email: Option<String>,
31 pub status: Option<String>,
32 pub roles: Option<Vec<String>>,
33 pub profile_pic: Option<String>,
34 pub created_at: i64,
35}
36
37#[derive(Debug, Default, Deserialize)]
39pub struct ListTenantsQuery {
40 pub status: Option<String>,
41 pub q: Option<String>,
42 pub limit: Option<u32>,
43 pub offset: Option<u32>,
44}
45
46#[derive(Debug, Serialize)]
48pub struct PasswordResetResponse {
49 pub message: String,
50}
51
52#[axum::debug_handler]
54pub async fn list_tenants(
55 State(app): State<App>,
56 Query(query): Query<ListTenantsQuery>,
57) -> ClResult<(StatusCode, Json<ApiResponse<Vec<TenantView>>>)> {
58 info!(
59 status = ?query.status,
60 q = ?query.q,
61 limit = ?query.limit,
62 offset = ?query.offset,
63 "GET /api/admin/tenants - Listing tenants"
64 );
65
66 let auth_opts = ListTenantsOptions {
68 status: query.status.as_deref(),
69 q: query.q.as_deref(),
70 limit: query.limit,
71 offset: query.offset,
72 };
73 let auth_tenants = app.auth_adapter.list_tenants(&auth_opts).await?;
74
75 let meta_opts = ListTenantsMetaOptions { limit: query.limit, offset: query.offset };
77 let meta_tenants = app.meta_adapter.list_tenants(&meta_opts).await?;
78
79 let meta_map: HashMap<u32, _> = meta_tenants.into_iter().map(|t| (t.tn_id.0, t)).collect();
81
82 let tenants: Vec<TenantView> = auth_tenants
84 .into_iter()
85 .map(|auth| {
86 let meta = meta_map.get(&auth.tn_id.0);
87 let created_at =
90 meta.map(|m| m.created_at.0).filter(|&ts| ts > 0).unwrap_or(auth.created_at.0);
91 TenantView {
92 tn_id: auth.tn_id.0,
93 id_tag: auth.id_tag.to_string(),
94 name: meta.map(|m| m.name.to_string()).unwrap_or_else(|| auth.id_tag.to_string()),
95 typ: meta.map(|m| m.typ).unwrap_or(ProfileType::Person),
96 email: auth.email.map(|e| e.to_string()),
97 status: auth.status.map(|s| s.to_string()),
98 roles: auth.roles.map(|r| r.iter().map(|s| s.to_string()).collect()),
99 profile_pic: meta.and_then(|m| m.profile_pic.as_ref().map(|p| p.to_string())),
100 created_at,
101 }
102 })
103 .collect();
104
105 let total = tenants.len();
106 let offset = query.offset.unwrap_or(0) as usize;
107 let response = ApiResponse::with_pagination(tenants, offset, total, total);
108
109 Ok((StatusCode::OK, Json(response)))
110}
111
112#[axum::debug_handler]
114pub async fn send_password_reset(
115 State(app): State<App>,
116 Path(id_tag): Path<String>,
117) -> ClResult<(StatusCode, Json<ApiResponse<PasswordResetResponse>>)> {
118 info!(
119 id_tag = %id_tag,
120 "POST /api/admin/tenants/:id_tag/password-reset - Sending password reset email"
121 );
122
123 let tn_id = app.auth_adapter.read_tn_id(&id_tag).await?;
125
126 let auth_opts =
128 ListTenantsOptions { status: None, q: Some(&id_tag), limit: Some(1), offset: None };
129 let auth_tenants = app.auth_adapter.list_tenants(&auth_opts).await?;
130
131 let auth_tenant = auth_tenants
132 .into_iter()
133 .find(|t| t.id_tag.as_ref() == id_tag)
134 .ok_or(Error::NotFound)?;
135
136 let email = auth_tenant.email.ok_or_else(|| {
137 Error::ValidationError("Tenant does not have an email address".to_string())
138 })?;
139
140 let tenant = app.meta_adapter.read_tenant(tn_id).await?;
142 let user_name = tenant.name.to_string();
143
144 let expires_at = Some(Timestamp(Timestamp::now().0 + 86400)); let (_ref_id, reset_url) = create_ref_internal(
147 &app,
148 tn_id,
149 CreateRefInternalParams {
150 id_tag: &id_tag,
151 typ: "password", description: Some("Admin-initiated password reset"),
153 expires_at,
154 path_prefix: "/reset-password", resource_id: None,
156 count: None,
157 },
158 )
159 .await?;
160
161 let lang = get_tenant_lang(&app.settings, tn_id).await;
163
164 let base_id_tag = app.opts.base_id_tag.as_ref().map(|s| s.as_ref()).unwrap_or("cloudillo");
166
167 let email_params = EmailTaskParams {
170 to: email.to_string(),
171 subject: None,
172 template_name: "password_reset".to_string(),
173 template_vars: serde_json::json!({
174 "identity_tag": user_name,
175 "base_id_tag": base_id_tag,
176 "instance_name": "Cloudillo",
177 "reset_link": reset_url,
178 "expire_hours": 24,
179 }),
180 lang,
181 custom_key: Some(format!("pw-reset:{}:{}", tn_id.0, Timestamp::now().0)),
182 from_name_override: Some(format!("Cloudillo | {}", base_id_tag.to_uppercase())),
183 };
184
185 EmailModule::schedule_email_task(&app.scheduler, &app.settings, tn_id, email_params).await?;
186
187 info!(
188 tn_id = ?tn_id,
189 id_tag = %id_tag,
190 email = %email,
191 "Password reset email scheduled"
192 );
193
194 let response = ApiResponse::new(PasswordResetResponse {
195 message: format!("Password reset email sent to {}", email),
196 });
197
198 Ok((StatusCode::OK, Json(response)))
199}
200
201