use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
};
use serde::Deserialize;
use crate::{
extract::{Auth, OptionalRequestId},
prelude::*,
settings::types::{SettingScope, SettingValue},
};
use cloudillo_types::types::ApiResponse;
#[derive(serde::Serialize)]
pub struct SettingResponse {
pub key: String,
pub value: SettingValue,
pub scope: String,
pub permission: String,
pub description: String,
}
#[derive(Deserialize, Default)]
pub struct ListSettingsQuery {
pub prefix: Option<String>,
}
pub async fn list_settings(
State(app): State<App>,
Auth(auth): Auth,
Query(query): Query<ListSettingsQuery>,
OptionalRequestId(req_id): OptionalRequestId,
) -> ClResult<(StatusCode, Json<ApiResponse<Vec<SettingResponse>>>)> {
let mut settings_response = Vec::new();
if let Some(ref prefix) = query.prefix {
for (key, value, definition) in app.settings.list_by_prefix(auth.tn_id, prefix).await? {
settings_response.push(SettingResponse {
key,
value,
scope: format!("{:?}", definition.scope),
permission: format!("{:?}", definition.permission),
description: definition.description.clone(),
});
}
} else {
for definition in app.settings_registry.list() {
match app.settings.get(auth.tn_id, &definition.key).await {
Ok(Some(value)) => settings_response.push(SettingResponse {
key: definition.key.clone(),
value,
scope: format!("{:?}", definition.scope),
permission: format!("{:?}", definition.permission),
description: definition.description.clone(),
}),
Ok(None) | Err(Error::SettingNotFound(_)) => {}
Err(e) => return Err(e),
}
}
}
let total = settings_response.len();
let response = ApiResponse::with_pagination(settings_response, 0, 100, total)
.with_req_id(req_id.unwrap_or_default());
Ok((StatusCode::OK, Json(response)))
}
#[derive(Deserialize, Default)]
pub struct SettingScopeQuery {
pub level: Option<String>,
pub tenant: Option<String>,
}
async fn resolve_target_tn_id(
app: &App,
auth: &cloudillo_types::auth_adapter::AuthCtx,
target: Option<&str>,
) -> ClResult<TnId> {
match target {
None => Ok(auth.tn_id),
Some(id_tag) => {
if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
return Err(Error::PermissionDenied);
}
app.auth_adapter.read_tn_id(id_tag).await.map_err(|_| Error::NotFound)
}
}
}
pub async fn get_setting(
State(app): State<App>,
Auth(auth): Auth,
Path(name): Path<String>,
Query(query): Query<SettingScopeQuery>,
OptionalRequestId(req_id): OptionalRequestId,
) -> ClResult<(StatusCode, Json<ApiResponse<SettingResponse>>)> {
let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
let target_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
let value = match query.level.as_deref() {
Some("global") => {
if definition.scope == SettingScope::Global
&& !auth.roles.iter().any(|r| r.as_ref() == "SADM")
{
return Err(Error::PermissionDenied);
}
app.settings.get_raw(TnId(0), &name).await?.ok_or(Error::NotFound)?
}
Some("tenant") => {
app.settings.get_raw(target_tn_id, &name).await?.ok_or(Error::NotFound)?
}
Some(other) => {
return Err(Error::ValidationError(format!("unknown level: {}", other)));
}
None => app.settings.get(target_tn_id, &name).await?.ok_or(Error::NotFound)?,
};
let response_data = SettingResponse {
key: definition.key.clone(),
value,
scope: format!("{:?}", definition.scope),
permission: format!("{:?}", definition.permission),
description: definition.description.clone(),
};
let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
Ok((StatusCode::OK, Json(response)))
}
#[derive(Deserialize)]
pub struct UpdateSettingRequest {
pub value: SettingValue,
}
pub async fn update_setting(
State(app): State<App>,
Auth(auth): Auth,
Path(name): Path<String>,
Query(query): Query<SettingScopeQuery>,
OptionalRequestId(req_id): OptionalRequestId,
Json(req): Json<UpdateSettingRequest>,
) -> ClResult<(StatusCode, Json<ApiResponse<SettingResponse>>)> {
let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
if !definition.permission.check(&auth.roles) {
warn!("User {} attempted to update setting {} without permission", auth.id_tag, name);
return Err(Error::PermissionDenied);
}
if let Some(ref validator) = definition.validator {
validator(&req.value)?;
}
let target_tn_id = match query.level.as_deref() {
Some("global") => {
if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
return Err(Error::PermissionDenied);
}
TnId(0)
}
Some("tenant") | None => {
let acting_tn_id = resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?;
if query.level.as_deref() == Some("tenant") && definition.scope == SettingScope::Global
{
return Err(Error::ValidationError(
"level=tenant is not valid for Global-scoped setting; use level=global".into(),
));
}
acting_tn_id
}
Some(other) => {
return Err(Error::ValidationError(format!("unknown level: {}", other)));
}
};
app.settings.set(target_tn_id, &name, req.value.clone(), &auth.roles).await?;
info!(
"User {} updated setting {} for tn_id={} (level={})",
auth.id_tag,
name,
target_tn_id.0,
query.level.as_deref().unwrap_or("(default)")
);
let value = app.settings.get(target_tn_id, &name).await?.ok_or(Error::NotFound)?;
let response_data = SettingResponse {
key: definition.key.clone(),
value,
scope: format!("{:?}", definition.scope),
permission: format!("{:?}", definition.permission),
description: definition.description.clone(),
};
let response = ApiResponse::new(response_data).with_req_id(req_id.unwrap_or_default());
Ok((StatusCode::OK, Json(response)))
}
pub async fn delete_setting(
State(app): State<App>,
Auth(auth): Auth,
Path(name): Path<String>,
Query(query): Query<SettingScopeQuery>,
) -> ClResult<StatusCode> {
let definition = app.settings_registry.get(&name).ok_or(Error::NotFound)?;
let target_tn_id = match query.level.as_deref() {
Some("tenant") => {
if definition.scope == SettingScope::Global {
return Err(Error::ValidationError(
"level=tenant is not valid for Global-scoped setting; use level=global".into(),
));
}
resolve_target_tn_id(&app, &auth, query.tenant.as_deref()).await?
}
Some("global") => {
if !auth.roles.iter().any(|r| r.as_ref() == "SADM") {
return Err(Error::PermissionDenied);
}
TnId(0)
}
Some(other) => {
return Err(Error::ValidationError(format!("unknown level: {}", other)));
}
None => {
return Err(Error::ValidationError("level query parameter is required".into()));
}
};
app.settings.clear(target_tn_id, &name, &auth.roles).await?;
info!("User {} cleared setting {} at tn_id={}", auth.id_tag, name, target_tn_id.0);
Ok(StatusCode::NO_CONTENT)
}