Skip to main content

cloudillo_admin/
tenant.rs

1//! Admin tenant management handlers
2
3use 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/// Combined tenant view response (auth + meta data)
21#[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/// Query parameters for listing tenants
38#[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/// Response for password reset
47#[derive(Debug, Serialize)]
48pub struct PasswordResetResponse {
49	pub message: String,
50}
51
52/// GET /api/admin/tenants - List all tenants (combines auth + meta data)
53#[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	// Get auth data (email, roles, status)
67	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	// Get meta data (name, profile_pic, type)
76	let meta_opts = ListTenantsMetaOptions { limit: query.limit, offset: query.offset };
77	let meta_tenants = app.meta_adapter.list_tenants(&meta_opts).await?;
78
79	// Create a map from tn_id to meta data for quick lookup
80	let meta_map: HashMap<u32, _> = meta_tenants.into_iter().map(|t| (t.tn_id.0, t)).collect();
81
82	// Combine auth and meta data
83	let tenants: Vec<TenantView> = auth_tenants
84		.into_iter()
85		.map(|auth| {
86			let meta = meta_map.get(&auth.tn_id.0);
87			// Prefer meta's created_at as it's more reliably populated
88			// Fall back to auth's created_at if meta is unavailable
89			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/// POST /api/admin/tenants/{id_tag}/password-reset - Send password reset email
113#[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	// Get the tenant's tn_id
124	let tn_id = app.auth_adapter.read_tn_id(&id_tag).await?;
125
126	// Get tenant auth data to get email
127	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	// Get tenant meta data for the name
141	let tenant = app.meta_adapter.read_tenant(tn_id).await?;
142	let user_name = tenant.name.to_string();
143
144	// Create password reset ref with type "password" (compatible with /auth/set-password)
145	let expires_at = Some(Timestamp(Timestamp::now().0 + 86400)); // 24 hours
146	let (_ref_id, reset_url) = create_ref_internal(
147		&app,
148		tn_id,
149		CreateRefInternalParams {
150			id_tag: &id_tag,
151			typ: "password", // CRITICAL: must be "password" for /auth/set-password to accept it
152			description: Some("Admin-initiated password reset"),
153			expires_at,
154			path_prefix: "/reset-password", // Frontend route (must match AuthRoutes in shell)
155			resource_id: None,
156			count: None,
157		},
158	)
159	.await?;
160
161	// Get tenant's preferred language
162	let lang = get_tenant_lang(&app.settings, tn_id).await;
163
164	// Get base_id_tag for sender name
165	let base_id_tag = app.opts.base_id_tag.as_ref().map(|s| s.as_ref()).unwrap_or("cloudillo");
166
167	// Schedule email with password_reset template
168	// Subject is defined in the template frontmatter for multi-language support
169	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// vim: ts=4