allsource_core/infrastructure/web/
config_api.rs1use crate::{
2 error::{AllSourceError, Result},
3 infrastructure::{
4 repositories::event_sourced_config_repository::ConfigCondition,
5 security::middleware::Admin, web::api_v1::AppState,
6 },
7};
8use axum::{Json, extract::State, http::StatusCode};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Deserialize)]
17pub struct SetConfigRequest {
18 pub key: String,
19 pub value: serde_json::Value,
20 pub changed_by: Option<String>,
21 pub condition: Option<ConfigCondition>,
22}
23
24#[derive(Debug, Deserialize)]
25pub struct UpdateConfigRequest {
26 pub value: serde_json::Value,
27 pub changed_by: Option<String>,
28}
29
30#[derive(Debug, Serialize)]
31pub struct ConfigEntryResponse {
32 pub key: String,
33 pub value: serde_json::Value,
34 pub updated_at: DateTime<Utc>,
35 pub updated_by: Option<String>,
36 pub revision: uuid::Uuid,
37}
38
39#[derive(Debug, Serialize)]
40pub struct ListConfigsResponse {
41 pub configs: Vec<ConfigEntryResponse>,
42 pub total: usize,
43}
44
45pub async fn list_configs(
52 State(state): State<AppState>,
53 Admin(_): Admin,
54) -> Result<Json<ListConfigsResponse>> {
55 let config_repo = state
56 .service_container
57 .config_repository()
58 .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
59
60 let entries = config_repo.list();
61 let total = entries.len();
62
63 let configs: Vec<ConfigEntryResponse> = entries
64 .into_iter()
65 .map(|e| ConfigEntryResponse {
66 key: e.key,
67 value: e.value,
68 updated_at: e.updated_at,
69 updated_by: e.updated_by,
70 revision: e.revision,
71 })
72 .collect();
73
74 Ok(Json(ListConfigsResponse { configs, total }))
75}
76
77pub async fn get_config(
80 State(state): State<AppState>,
81 Admin(_): Admin,
82 axum::extract::Path(key): axum::extract::Path<String>,
83) -> Result<Json<ConfigEntryResponse>> {
84 let config_repo = state
85 .service_container
86 .config_repository()
87 .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
88
89 let entry = config_repo
90 .get(&key)
91 .ok_or_else(|| AllSourceError::EntityNotFound(format!("Config key not found: {key}")))?;
92
93 Ok(Json(ConfigEntryResponse {
94 key: entry.key,
95 value: entry.value,
96 updated_at: entry.updated_at,
97 updated_by: entry.updated_by,
98 revision: entry.revision,
99 }))
100}
101
102pub async fn set_config(
105 State(state): State<AppState>,
106 Admin(_): Admin,
107 Json(req): Json<SetConfigRequest>,
108) -> Result<(StatusCode, Json<serde_json::Value>)> {
109 let config_repo = state
110 .service_container
111 .config_repository()
112 .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
113
114 if req.key.is_empty() {
115 return Err(AllSourceError::InvalidInput(
116 "Config key cannot be empty".into(),
117 ));
118 }
119
120 let entry = config_repo.set_conditionally(
121 &req.key,
122 req.value,
123 req.changed_by.as_deref(),
124 req.condition.as_ref(),
125 )?;
126
127 tracing::debug!("Config set: {}", req.key);
128
129 Ok((
130 StatusCode::OK,
131 Json(serde_json::json!({
132 "key": req.key,
133 "saved": true,
134 "revision": entry.revision,
135 })),
136 ))
137}
138
139pub async fn set_config_conditionally(
144 state: State<AppState>,
145 admin: Admin,
146 Json(req): Json<SetConfigRequest>,
147) -> Result<(StatusCode, Json<serde_json::Value>)> {
148 if req.condition.is_none() {
149 return Err(AllSourceError::InvalidInput(
150 "A configuration condition is required".into(),
151 ));
152 }
153 set_config(state, admin, Json(req)).await
154}
155
156pub async fn update_config(
159 State(state): State<AppState>,
160 Admin(_): Admin,
161 axum::extract::Path(key): axum::extract::Path<String>,
162 Json(req): Json<UpdateConfigRequest>,
163) -> Result<Json<ConfigEntryResponse>> {
164 let config_repo = state
165 .service_container
166 .config_repository()
167 .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
168
169 if config_repo.get(&key).is_none() {
171 return Err(AllSourceError::EntityNotFound(format!(
172 "Config key not found: {key}"
173 )));
174 }
175
176 config_repo.set(&key, req.value.clone(), req.changed_by.as_deref())?;
177
178 let entry = config_repo.get(&key).ok_or_else(|| {
179 AllSourceError::InternalError("Config entry disappeared after update".into())
180 })?;
181
182 tracing::debug!("Config updated: {}", key);
183
184 Ok(Json(ConfigEntryResponse {
185 key: entry.key,
186 value: entry.value,
187 updated_at: entry.updated_at,
188 updated_by: entry.updated_by,
189 revision: entry.revision,
190 }))
191}
192
193pub async fn delete_config(
196 State(state): State<AppState>,
197 Admin(_): Admin,
198 axum::extract::Path(key): axum::extract::Path<String>,
199) -> Result<StatusCode> {
200 let config_repo = state
201 .service_container
202 .config_repository()
203 .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
204
205 let deleted = config_repo.delete(&key, None)?;
206
207 if !deleted {
208 return Err(AllSourceError::EntityNotFound(format!(
209 "Config key not found: {key}"
210 )));
211 }
212
213 tracing::debug!("Config deleted: {}", key);
214
215 Ok(StatusCode::NO_CONTENT)
216}