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>,
35}
36
37pub async fn list_settings(
41 State(app): State<App>,
42 Auth(auth): Auth,
43 Query(query): Query<ListSettingsQuery>,
44 OptionalRequestId(req_id): OptionalRequestId,
45) -> ClResult<(StatusCode, Json<ApiResponse<Vec<SettingResponse>>>)> {
46 let mut settings_response = Vec::new();
47
48 if let Some(ref prefix) = query.prefix {
49 for (key, value, definition) in app.settings.list_by_prefix(auth.tn_id, prefix).await? {
52 settings_response.push(SettingResponse {
53 key,
54 value,
55 scope: format!("{:?}", definition.scope),
56 permission: format!("{:?}", definition.permission),
57 description: definition.description.clone(),
58 });
59 }
60 } else {
61 for definition in app.settings_registry.list() {
67 match app.settings.get(auth.tn_id, &definition.key).await {
68 Ok(Some(value)) => settings_response.push(SettingResponse {
69 key: definition.key.clone(),
70 value,
71 scope: format!("{:?}", definition.scope),
72 permission: format!("{:?}", definition.permission),
73 description: definition.description.clone(),
74 }),
75 Ok(None) | Err(Error::SettingNotFound(_)) => {}
81 Err(e) => return Err(e),
82 }
83 }
84 }
85
86 let total = settings_response.len();
87 let response = ApiResponse::with_pagination(settings_response, 0, 100, total)
88 .with_req_id(req_id.unwrap_or_default());
89
90 Ok((StatusCode::OK, Json(response)))
91}
92
93#[derive(Deserialize, Default)]
106pub struct SettingScopeQuery {
107 pub level: Option<String>,
108 pub tenant: Option<String>,
109}
110
111async fn resolve_target_tn_id(
118 app: &App,
119 auth: &cloudillo_types::auth_adapter::AuthCtx,
120 target: Option<&str>,
121) -> ClResult<TnId> {
122 match target {
123 None => Ok(auth.tn_id),
124 Some(id_tag) => {
125 if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
130 return Err(Error::PermissionDenied);
131 }
132 app.auth_adapter.read_tn_id(id_tag).await.map_err(|_| Error::NotFound)
133 }
134 }
135}
136
137pub async fn get_setting(
139 State(app): State<App>,
140 Auth(auth): Auth,
141 Path(name): Path<String>,
142 Query(query): Query<SettingScopeQuery>,
143 OptionalRequestId(req_id): OptionalRequestId,
144) -> ClResult<(StatusCode, Json<ApiResponse<SettingResponse>>)> {
145 let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
147
148 let target_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
159
160 let value = match query.level.as_deref() {
161 Some("global") => {
162 if definition.scope == SettingScope::Global
168 && !auth.roles.iter().any(|r| r.as_ref() == "SADM")
169 {
170 return Err(Error::PermissionDenied);
171 }
172 app.settings.get_raw(TnId(0), &name).await?.ok_or(Error::NotFound)?
173 }
174 Some("tenant") => {
175 app.settings.get_raw(target_tn_id, &name).await?.ok_or(Error::NotFound)?
176 }
177 Some(other) => {
178 return Err(Error::ValidationError(format!("unknown level: {}", other)));
179 }
180 None => app.settings.get(target_tn_id, &name).await?.ok_or(Error::NotFound)?,
181 };
182
183 let response_data = SettingResponse {
184 key: definition.key.clone(),
185 value,
186 scope: format!("{:?}", definition.scope),
187 permission: format!("{:?}", definition.permission),
188 description: definition.description.clone(),
189 };
190
191 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
192
193 Ok((StatusCode::OK, Json(response)))
194}
195
196#[derive(Deserialize)]
199pub struct UpdateSettingRequest {
200 pub value: SettingValue,
201}
202
203pub async fn update_setting(
204 State(app): State<App>,
205 Auth(auth): Auth,
206 Path(name): Path<String>,
207 Query(query): Query<SettingScopeQuery>,
208 OptionalRequestId(req_id): OptionalRequestId,
209 Json(req): Json<UpdateSettingRequest>,
210) -> ClResult<(StatusCode, Json<ApiResponse<SettingResponse>>)> {
211 let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
213
214 if !definition.permission.check(&auth.roles) {
216 warn!("User {} attempted to update setting {} without permission", auth.id_tag, name);
217 return Err(Error::PermissionDenied);
218 }
219
220 if let Some(ref validator) = definition.validator {
222 validator(&req.value)?;
223 }
224
225 let target_tn_id = match query.level.as_deref() {
231 Some("global") => {
232 if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
236 return Err(Error::PermissionDenied);
237 }
238 TnId(0)
239 }
240 Some("tenant") | None => {
241 let acting_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
242 if query.level.as_deref() == Some("tenant") && definition.scope == SettingScope::Global
245 {
246 return Err(Error::ValidationError(
247 "level=tenant is not valid for Global-scoped setting; use level=global".into(),
248 ));
249 }
250 acting_tn_id
251 }
252 Some(other) => {
253 return Err(Error::ValidationError(format!("unknown level: {}", other)));
254 }
255 };
256
257 app.settings.set(target_tn_id, &name, req.value.clone(), &auth.roles).await?;
259
260 info!(
261 "User {} updated setting {} for tn_id={} (level={})",
262 auth.id_tag,
263 name,
264 target_tn_id.0,
265 query.level.as_deref().unwrap_or("(default)")
266 );
267
268 let value = app.settings.get(target_tn_id, &name).await?.ok_or(Error::NotFound)?;
270
271 let response_data = SettingResponse {
272 key: definition.key.clone(),
273 value,
274 scope: format!("{:?}", definition.scope),
275 permission: format!("{:?}", definition.permission),
276 description: definition.description.clone(),
277 };
278
279 let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
280
281 Ok((StatusCode::OK, Json(response)))
282}
283
284pub async fn delete_setting(
291 State(app): State<App>,
292 Auth(auth): Auth,
293 Path(name): Path<String>,
294 Query(query): Query<SettingScopeQuery>,
295) -> ClResult<StatusCode> {
296 let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
297
298 let target_tn_id = match query.level.as_deref() {
299 Some("tenant") => {
300 if definition.scope == SettingScope::Global {
305 return Err(Error::ValidationError(
306 "level=tenant is not valid for Global-scoped setting; use level=global".into(),
307 ));
308 }
309 resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?
310 }
311 Some("global") => {
312 if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
321 return Err(Error::PermissionDenied);
322 }
323 TnId(0)
324 }
325 Some(other) => {
326 return Err(Error::ValidationError(format!("unknown level: {}", other)));
327 }
328 None => {
329 return Err(Error::ValidationError("level query parameter is required".into()));
330 }
331 };
332
333 app.settings.clear(target_tn_id, &name, &auth.roles).await?;
334
335 info!("User {} cleared setting {} at tn_id={}", auth.id_tag, name, target_tn_id.0);
336
337 Ok(StatusCode::NO_CONTENT)
338}
339
340