cat_gateway/service/api/config/
mod.rs1use poem::web::RealIp;
4use poem_openapi::{param::Query, payload::Json, ApiResponse, OpenApi};
5use serde_json::{Map, Value};
6use tracing::error;
7
8use crate::{
9 db::event::{
10 config::{key::ConfigKey, Config},
11 error::NotFoundError,
12 },
13 service::common::{
14 auth::{api_key::InternalApiKeyAuthorization, none_or_rbac::NoneOrRBAC},
15 responses::WithErrorResponses,
16 tags::ApiTags,
17 types::generic::{ip_addr::IpAddr, json_object::JsonObject},
18 },
19};
20
21pub(crate) struct ConfigApi;
23
24#[derive(ApiResponse)]
26enum GetConfigResponses {
27 #[oai(status = 200)]
29 Ok(Json<JsonObject>),
30
31 #[oai(status = 404)]
33 NotFound,
34}
35type GetConfigAllResponses = WithErrorResponses<GetConfigResponses>;
37
38#[derive(ApiResponse)]
40enum SetConfigResponse {
41 #[oai(status = 204)]
43 Ok,
44}
45type SetConfigAllResponses = WithErrorResponses<SetConfigResponse>;
47
48#[OpenApi(tag = "ApiTags::Config")]
49impl ConfigApi {
50 #[oai(
58 path = "/v1/config/frontend",
59 method = "get",
60 operation_id = "get_config_frontend"
61 )]
62 async fn get_frontend(
63 &self,
64 ip_address: RealIp,
65 _auth: NoneOrRBAC,
66 ) -> GetConfigAllResponses {
67 let general_config: JsonObject = match Config::get(ConfigKey::Frontend)
68 .await
69 .and_then(TryInto::try_into)
70 {
71 Ok(value) => value,
72 Err(err) if err.is::<NotFoundError>() => return GetConfigResponses::NotFound.into(),
73 Err(err) => {
74 error!(id="get_frontend_config_general", error=?err, "Failed to get general frontend configuration");
75 return GetConfigAllResponses::handle_error(&err);
76 },
77 };
78
79 let ip_config: Option<JsonObject> = if let Some(ip) = ip_address.0 {
81 match Config::get(ConfigKey::FrontendForIp(ip))
82 .await
83 .and_then(TryInto::try_into)
84 {
85 Ok(value) => Some(value),
86 Err(err) if err.is::<NotFoundError>() => None,
87 Err(err) => {
88 error!(id = "get_frontend_config_ip", error = ?err, "Failed to get frontend configuration for IP");
89 return GetConfigAllResponses::handle_error(&err);
90 },
91 }
92 } else {
93 None
94 };
95 if let Some(ip_config) = ip_config {
96 let config = merge_configs(general_config, ip_config);
97 GetConfigResponses::Ok(Json(config)).into()
98 } else {
99 GetConfigResponses::Ok(Json(general_config)).into()
100 }
101 }
102
103 #[oai(
112 path = "/v1/config/frontend",
113 method = "put",
114 operation_id = "put_config_frontend",
115 hidden = true
116 )]
117 async fn put_frontend(
118 &self,
119 #[oai(name = "IP")]
121 Query(ip_address): Query<Option<IpAddr>>,
122 Json(json_config): Json<JsonObject>,
123 _auth: InternalApiKeyAuthorization,
124 ) -> SetConfigAllResponses {
125 if let Some(ip) = ip_address {
126 set(ConfigKey::FrontendForIp(ip.into()), json_config.into()).await
127 } else {
128 set(ConfigKey::Frontend, json_config.into()).await
129 }
130 }
131}
132
133fn merge_configs(
135 general: JsonObject,
136 ip_specific: JsonObject,
137) -> JsonObject {
138 let mut merged = general;
139
140 for (key, value) in Map::<String, Value>::from(ip_specific) {
141 if let Some(existing_value) = merged.get_mut(&key) {
142 *existing_value = value;
143 } else {
144 merged.insert(key, value);
145 }
146 }
147
148 merged
149}
150
151async fn set(
153 key: ConfigKey,
154 value: Value,
155) -> SetConfigAllResponses {
156 match Config::set(key, value).await {
157 Ok(()) => SetConfigResponse::Ok.into(),
158 Err(err) => {
159 error!(id="set_config_frontend", error=?err, "Failed to set frontend configuration");
160 SetConfigAllResponses::handle_error(&err)
161 },
162 }
163}