1use axum::{
7 Json,
8 extract::{Path, Query, State},
9 http::StatusCode,
10};
11use serde::Deserialize;
12
13use crate::{
14 extract::{Auth, OptionalRequestId},
15 prelude::*,
16 settings::types::{SettingScope, SettingValue},
17};
18use cloudillo_types::types::ApiResponse;
19
20#[derive(serde::Serialize)]
22pub struct SettingResponse {
23 pub key: String,
24 pub value: SettingValue,
25 pub scope: String,
26 pub permission: String,
27 pub description: String,
28}
29
30#[derive(Deserialize, Default)]
32pub struct ListSettingsQuery {
33 pub prefix: Option<String>,
36 pub level: Option<String>,
41 pub tenant: Option<String>,
43}
44
45pub async fn list_settings(
49 State(app): State<App>,
50 Auth(auth): Auth,
51 Query(query): Query<ListSettingsQuery>,
52 OptionalRequestId(req_id): OptionalRequestId,
53) -> ClResult<(StatusCode, Json<ApiResponse<Vec<SettingResponse>>>)> {
54 let mut settings_response = Vec::new();
55
56 if query.level.is_some() && query.prefix.is_none() {
60 return Err(Error::ValidationError("level requires prefix".into()));
61 }
62
63 if let Some(ref prefix) = query.prefix {
64 let prefixes: Vec<String> = prefix
66 .split(',')
67 .map(|s| s.trim().to_string())
68 .filter(|s| !s.is_empty())
69 .collect();
70
71 if matches!(query.level.as_deref(), Some("global")) && query.tenant.is_some() {
75 return Err(Error::ValidationError("tenant= is not allowed with level=global".into()));
76 }
77
78 let target_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
80
81 let rows = match query.level.as_deref() {
82 Some("global") => {
83 if !crate::abac::is_admin(&auth) {
89 return Err(Error::PermissionDenied);
90 }
91 app.settings.list_by_prefix_at(TnId(0), &prefixes).await?
92 }
93 Some("tenant") => app.settings.list_by_prefix_at(target_tn_id, &prefixes).await?,
94 Some(other) => {
95 return Err(Error::ValidationError(format!("unknown level: {}", other)));
96 }
97 None => app.settings.list_by_prefix(target_tn_id, &prefixes).await?,
98 };
99
100 for (key, value, definition) in rows {
101 settings_response.push(SettingResponse {
102 key,
103 value,
104 scope: format!("{:?}", definition.scope),
105 permission: format!("{:?}", definition.permission),
106 description: definition.description.clone(),
107 });
108 }
109 } else {
110 for definition in app.settings_registry.list() {
116 match app.settings.get(auth.tn_id, &definition.key).await {
117 Ok(Some(value)) => settings_response.push(SettingResponse {
118 key: definition.key.clone(),
119 value,
120 scope: format!("{:?}", definition.scope),
121 permission: format!("{:?}", definition.permission),
122 description: definition.description.clone(),
123 }),
124 Ok(None) | Err(Error::SettingNotFound(_)) => {}
130 Err(e) => return Err(e),
131 }
132 }
133 }
134
135 let total = settings_response.len();
136 let response = ApiResponse::with_pagination(settings_response, 0, 100, total)
137 .with_req_id(req_id.unwrap_or_default());
138
139 Ok((StatusCode::OK, Json(response)))
140}
141
142#[derive(Deserialize, Default)]
155pub struct SettingScopeQuery {
156 pub level: Option<String>,
157 pub tenant: Option<String>,
158}
159
160async fn resolve_target_tn_id(
167 app: &App,
168 auth: &cloudillo_types::auth_adapter::AuthCtx,
169 target: Option<&str>,
170) -> ClResult<TnId> {
171 match target {
172 None => Ok(auth.tn_id),
173 Some(id_tag) => {
174 if !crate::abac::is_admin(auth) {
179 return Err(Error::PermissionDenied);
180 }
181 app.auth_adapter.read_tn_id(id_tag).await.map_err(|_| Error::NotFound)
182 }
183 }
184}
185
186pub async fn get_setting(
188 State(app): State<App>,
189 Auth(auth): Auth,
190 Path(name): Path<String>,
191 Query(query): Query<SettingScopeQuery>,
192 OptionalRequestId(req_id): OptionalRequestId,
193) -> ClResult<(StatusCode, Json<ApiResponse<SettingResponse>>)> {
194 let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
196
197 let target_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
208
209 let value = match query.level.as_deref() {
210 Some("global") => {
211 if definition.scope == SettingScope::Global
217 && !auth.roles.iter().any(|r| r.as_ref() == "SADM")
218 {
219 return Err(Error::PermissionDenied);
220 }
221 app.settings.get_raw(TnId(0), &name).await?.ok_or(Error::NotFound)?
222 }
223 Some("tenant") => {
224 app.settings.get_raw(target_tn_id, &name).await?.ok_or(Error::NotFound)?
225 }
226 Some(other) => {
227 return Err(Error::ValidationError(format!("unknown level: {}", other)));
228 }
229 None => app.settings.get(target_tn_id, &name).await?.ok_or(Error::NotFound)?,
230 };
231
232 let response_data = SettingResponse {
233 key: definition.key.clone(),
234 value,
235 scope: format!("{:?}", definition.scope),
236 permission: format!("{:?}", definition.permission),
237 description: definition.description.clone(),
238 };
239
240 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
241
242 Ok((StatusCode::OK, Json(response)))
243}
244
245#[derive(Deserialize)]
248pub struct UpdateSettingRequest {
249 pub value: SettingValue,
250}
251
252pub async fn update_setting(
253 State(app): State<App>,
254 Auth(auth): Auth,
255 Path(name): Path<String>,
256 Query(query): Query<SettingScopeQuery>,
257 OptionalRequestId(req_id): OptionalRequestId,
258 Json(req): Json<UpdateSettingRequest>,
259) -> ClResult<(StatusCode, Json<ApiResponse<SettingResponse>>)> {
260 let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
262
263 if !definition.permission.check(&auth.roles) {
265 warn!("User {} attempted to update setting {} without permission", auth.id_tag, name);
266 return Err(Error::PermissionDenied);
267 }
268
269 if let Some(ref validator) = definition.validator {
271 validator(&req.value)?;
272 }
273
274 let target_tn_id = match query.level.as_deref() {
280 Some("global") => {
281 if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
285 return Err(Error::PermissionDenied);
286 }
287 TnId(0)
288 }
289 Some("tenant") | None => {
290 let acting_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
291 if query.level.as_deref() == Some("tenant") && definition.scope == SettingScope::Global
294 {
295 return Err(Error::ValidationError(
296 "level=tenant is not valid for Global-scoped setting; use level=global".into(),
297 ));
298 }
299 acting_tn_id
300 }
301 Some(other) => {
302 return Err(Error::ValidationError(format!("unknown level: {}", other)));
303 }
304 };
305
306 app.settings.set(target_tn_id, &name, req.value.clone(), &auth.roles).await?;
308
309 info!(
310 "User {} updated setting {} for tn_id={} (level={})",
311 auth.id_tag,
312 name,
313 target_tn_id.0,
314 query.level.as_deref().unwrap_or("(default)")
315 );
316
317 let value = app.settings.get(target_tn_id, &name).await?.ok_or(Error::NotFound)?;
319
320 let response_data = SettingResponse {
321 key: definition.key.clone(),
322 value,
323 scope: format!("{:?}", definition.scope),
324 permission: format!("{:?}", definition.permission),
325 description: definition.description.clone(),
326 };
327
328 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
329
330 Ok((StatusCode::OK, Json(response)))
331}
332
333pub async fn delete_setting(
340 State(app): State<App>,
341 Auth(auth): Auth,
342 Path(name): Path<String>,
343 Query(query): Query<SettingScopeQuery>,
344) -> ClResult<StatusCode> {
345 let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
346
347 let target_tn_id = match query.level.as_deref() {
348 Some("tenant") => {
349 if definition.scope == SettingScope::Global {
354 return Err(Error::ValidationError(
355 "level=tenant is not valid for Global-scoped setting; use level=global".into(),
356 ));
357 }
358 resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?
359 }
360 Some("global") => {
361 if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
370 return Err(Error::PermissionDenied);
371 }
372 TnId(0)
373 }
374 Some(other) => {
375 return Err(Error::ValidationError(format!("unknown level: {}", other)));
376 }
377 None => {
378 return Err(Error::ValidationError("level query parameter is required".into()));
379 }
380 };
381
382 app.settings.clear(target_tn_id, &name, &auth.roles).await?;
383
384 info!("User {} cleared setting {} at tn_id={}", auth.id_tag, name, target_tn_id.0);
385
386 Ok(StatusCode::NO_CONTENT)
387}
388
389