Skip to main content

allsource_core/infrastructure/web/
config_api.rs

1use crate::{
2    error::{AllSourceError, Result},
3    infrastructure::{security::middleware::Admin, web::api_v1::AppState},
4};
5use axum::{Json, extract::State, http::StatusCode};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9// ============================================================================
10// Request/Response Types
11// ============================================================================
12
13#[derive(Debug, Deserialize)]
14pub struct SetConfigRequest {
15    pub key: String,
16    pub value: serde_json::Value,
17    pub changed_by: Option<String>,
18}
19
20#[derive(Debug, Deserialize)]
21pub struct UpdateConfigRequest {
22    pub value: serde_json::Value,
23    pub changed_by: Option<String>,
24}
25
26#[derive(Debug, Serialize)]
27pub struct ConfigEntryResponse {
28    pub key: String,
29    pub value: serde_json::Value,
30    pub updated_at: DateTime<Utc>,
31    pub updated_by: Option<String>,
32}
33
34#[derive(Debug, Serialize)]
35pub struct ListConfigsResponse {
36    pub configs: Vec<ConfigEntryResponse>,
37    pub total: usize,
38}
39
40// ============================================================================
41// Handlers
42// ============================================================================
43
44/// List all config entries
45/// GET /api/v1/config
46pub async fn list_configs(
47    State(state): State<AppState>,
48    Admin(_): Admin,
49) -> Result<Json<ListConfigsResponse>> {
50    let config_repo = state
51        .service_container
52        .config_repository()
53        .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
54
55    let entries = config_repo.list();
56    let total = entries.len();
57
58    let configs: Vec<ConfigEntryResponse> = entries
59        .into_iter()
60        .map(|e| ConfigEntryResponse {
61            key: e.key,
62            value: e.value,
63            updated_at: e.updated_at,
64            updated_by: e.updated_by,
65        })
66        .collect();
67
68    Ok(Json(ListConfigsResponse { configs, total }))
69}
70
71/// Get a config entry by key
72/// GET /api/v1/config/:key
73pub async fn get_config(
74    State(state): State<AppState>,
75    Admin(_): Admin,
76    axum::extract::Path(key): axum::extract::Path<String>,
77) -> Result<Json<ConfigEntryResponse>> {
78    let config_repo = state
79        .service_container
80        .config_repository()
81        .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
82
83    let entry = config_repo
84        .get(&key)
85        .ok_or_else(|| AllSourceError::EntityNotFound(format!("Config key not found: {key}")))?;
86
87    Ok(Json(ConfigEntryResponse {
88        key: entry.key,
89        value: entry.value,
90        updated_at: entry.updated_at,
91        updated_by: entry.updated_by,
92    }))
93}
94
95/// Set a config entry (upsert)
96/// POST /api/v1/config
97pub async fn set_config(
98    State(state): State<AppState>,
99    Admin(_): Admin,
100    Json(req): Json<SetConfigRequest>,
101) -> Result<(StatusCode, Json<serde_json::Value>)> {
102    let config_repo = state
103        .service_container
104        .config_repository()
105        .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
106
107    if req.key.is_empty() {
108        return Err(AllSourceError::InvalidInput(
109            "Config key cannot be empty".into(),
110        ));
111    }
112
113    config_repo.set(&req.key, req.value, req.changed_by.as_deref())?;
114
115    tracing::debug!("Config set: {}", req.key);
116
117    Ok((
118        StatusCode::OK,
119        Json(serde_json::json!({
120            "key": req.key,
121            "saved": true,
122        })),
123    ))
124}
125
126/// Update a config entry
127/// PUT /api/v1/config/:key
128pub async fn update_config(
129    State(state): State<AppState>,
130    Admin(_): Admin,
131    axum::extract::Path(key): axum::extract::Path<String>,
132    Json(req): Json<UpdateConfigRequest>,
133) -> Result<Json<ConfigEntryResponse>> {
134    let config_repo = state
135        .service_container
136        .config_repository()
137        .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
138
139    // Verify key exists
140    if config_repo.get(&key).is_none() {
141        return Err(AllSourceError::EntityNotFound(format!(
142            "Config key not found: {key}"
143        )));
144    }
145
146    config_repo.set(&key, req.value.clone(), req.changed_by.as_deref())?;
147
148    let entry = config_repo.get(&key).ok_or_else(|| {
149        AllSourceError::InternalError("Config entry disappeared after update".into())
150    })?;
151
152    tracing::debug!("Config updated: {}", key);
153
154    Ok(Json(ConfigEntryResponse {
155        key: entry.key,
156        value: entry.value,
157        updated_at: entry.updated_at,
158        updated_by: entry.updated_by,
159    }))
160}
161
162/// Delete a config entry
163/// DELETE /api/v1/config/:key
164pub async fn delete_config(
165    State(state): State<AppState>,
166    Admin(_): Admin,
167    axum::extract::Path(key): axum::extract::Path<String>,
168) -> Result<StatusCode> {
169    let config_repo = state
170        .service_container
171        .config_repository()
172        .ok_or_else(|| AllSourceError::InternalError("Config repository not configured".into()))?;
173
174    let deleted = config_repo.delete(&key, None)?;
175
176    if !deleted {
177        return Err(AllSourceError::EntityNotFound(format!(
178            "Config key not found: {key}"
179        )));
180    }
181
182    tracing::debug!("Config deleted: {}", key);
183
184    Ok(StatusCode::NO_CONTENT)
185}