1use super::ApiKeyPermissions;
2use alibi_core::utils::json::JsValue;
3pub(crate) use alibi_core::wire::ApiKeyView;
4use alibi_core::{AuthRequest, AuthResponse};
5use serde::{Deserialize, Deserializer, Serialize};
6use validator::Validate;
7
8#[serde_with::skip_serializing_none]
10#[derive(Debug, Default, Deserialize, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct CreateKeyRequest {
13 #[serde(default, deserialize_with = "present")]
15 pub config_id: Option<String>,
16 #[serde(default, deserialize_with = "coerced_string")]
18 pub user_id: Option<String>,
19 #[serde(default, deserialize_with = "coerced_string")]
21 pub organization_id: Option<String>,
22 #[serde(default, deserialize_with = "present")]
24 pub name: Option<String>,
25 #[serde(default, deserialize_with = "present")]
27 pub prefix: Option<String>,
28 pub expires_in: Option<f64>,
30 pub remaining: Option<f64>,
32 #[serde(default, deserialize_with = "present")]
34 pub rate_limit_enabled: Option<bool>,
35 #[serde(default, deserialize_with = "present")]
37 pub rate_limit_time_window: Option<f64>,
38 #[serde(default, deserialize_with = "present")]
40 pub rate_limit_max: Option<f64>,
41 #[serde(default, deserialize_with = "present")]
43 pub refill_interval: Option<f64>,
44 #[serde(default, deserialize_with = "present")]
46 pub refill_amount: Option<f64>,
47 #[serde(default, deserialize_with = "present")]
49 pub permissions: Option<ApiKeyPermissions>,
50 #[serde(default, deserialize_with = "present")]
52 pub metadata: Option<JsValue>,
53}
54
55#[serde_with::skip_serializing_none]
57#[derive(Debug, Default, Deserialize, Serialize)]
58#[serde(rename_all = "camelCase")]
59pub struct UpdateKeyRequest {
60 #[serde(default, deserialize_with = "present")]
62 pub config_id: Option<String>,
63 pub key_id: String,
65 #[serde(default, deserialize_with = "coerced_string")]
67 pub user_id: Option<String>,
68 #[serde(default, deserialize_with = "present")]
70 pub name: Option<String>,
71 #[serde(default, deserialize_with = "present")]
73 pub enabled: Option<bool>,
74 #[serde(default, deserialize_with = "present")]
76 pub remaining: Option<f64>,
77 #[serde(default, deserialize_with = "present")]
79 pub rate_limit_enabled: Option<bool>,
80 #[serde(default, deserialize_with = "present")]
82 pub rate_limit_time_window: Option<f64>,
83 #[serde(default, deserialize_with = "present")]
85 pub rate_limit_max: Option<f64>,
86 #[serde(default, deserialize_with = "present")]
88 pub refill_interval: Option<f64>,
89 #[serde(default, deserialize_with = "present")]
91 pub refill_amount: Option<f64>,
92 #[serde(default, with = "::serde_with::rust::double_option")]
94 pub permissions: Option<Option<ApiKeyPermissions>>,
95 #[serde(default, deserialize_with = "present")]
97 pub metadata: Option<JsValue>,
98 #[serde(default, with = "::serde_with::rust::double_option")]
100 pub expires_in: Option<Option<f64>>,
101}
102
103impl Validate for CreateKeyRequest {
104 fn validate(&self) -> Result<(), validator::ValidationErrors> {
105 let mut errors = numeric_errors(&[
106 ("expiresIn", self.expires_in, Some(1.0)),
107 ("remaining", self.remaining, Some(0.0)),
108 ("refillAmount", self.refill_amount, Some(1.0)),
109 ("refillInterval", self.refill_interval, None),
110 ("rateLimitTimeWindow", self.rate_limit_time_window, None),
111 ("rateLimitMax", self.rate_limit_max, None),
112 ]);
113 if let Some(prefix) = self.prefix.as_deref()
114 && let Err(error) = validate_prefix(prefix)
115 {
116 errors.add("prefix", error);
117 }
118 if errors.is_empty() {
119 Ok(())
120 } else {
121 Err(errors)
122 }
123 }
124}
125
126impl Validate for UpdateKeyRequest {
127 fn validate(&self) -> Result<(), validator::ValidationErrors> {
128 let errors = numeric_errors(&[
129 ("expiresIn", self.expires_in.flatten(), Some(1.0)),
130 ("remaining", self.remaining, Some(1.0)),
131 ("refillAmount", self.refill_amount, None),
132 ("refillInterval", self.refill_interval, None),
133 ("rateLimitTimeWindow", self.rate_limit_time_window, None),
134 ("rateLimitMax", self.rate_limit_max, None),
135 ]);
136 if errors.is_empty() {
137 Ok(())
138 } else {
139 Err(errors)
140 }
141 }
142}
143
144#[derive(Debug, Deserialize, Validate)]
145#[serde(rename_all = "camelCase")]
146pub(crate) struct DeleteKeyRequest {
147 #[serde(default, deserialize_with = "present")]
148 pub config_id: Option<String>,
149 pub key_id: String,
150}
151
152#[derive(Debug, Default)]
154pub(crate) struct ListKeysQuery {
155 pub config_id: Option<String>,
156 pub organization_id: Option<String>,
157 pub limit: Option<usize>,
158 pub offset: Option<usize>,
159 pub sort_by: Option<String>,
160 pub sort_direction: Option<String>,
161}
162
163impl ListKeysQuery {
164 #[expect(
165 clippy::as_conversions,
166 clippy::cast_possible_truncation,
167 clippy::cast_sign_loss,
168 reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
169 )]
170 pub(crate) fn from_request(req: &AuthRequest) -> Result<Self, AuthResponse> {
171 let number = |key: &str| -> Result<Option<usize>, AuthResponse> {
172 let Some(value) = req.query.get(key) else {
173 return Ok(None);
174 };
175 let parsed = if value.trim().is_empty() {
176 0.0
177 } else {
178 value.trim().parse::<f64>().map_err(|_error| {
179 query_error(key, "Invalid input: expected number, received NaN")
180 })?
181 };
182 if !parsed.is_finite() {
183 return Err(query_error(
184 key,
185 if parsed.is_nan() {
186 "Invalid input: expected number, received NaN"
187 } else {
188 "Invalid input: expected number, received number"
189 },
190 ));
191 }
192 if parsed.fract() != 0.0 {
193 return Err(query_error(
194 key,
195 "Invalid input: expected int, received number",
196 ));
197 }
198 if parsed < 0.0 {
199 return Err(query_error(key, "Too small: expected number to be >=0"));
200 }
201 Ok(Some(parsed as usize))
202 };
203 if let Some(direction) = req.query.get("sortDirection")
204 && !matches!(direction.as_str(), "asc" | "desc")
205 {
206 return Err(query_error(
207 "sortDirection",
208 "Invalid option: expected one of \"asc\"|\"desc\"",
209 ));
210 }
211 Ok(Self {
212 config_id: req.query.get("configId").cloned(),
213 organization_id: req.query.get("organizationId").cloned(),
214 limit: number("limit")?,
215 offset: number("offset")?,
216 sort_by: req.query.get("sortBy").cloned(),
217 sort_direction: req.query.get("sortDirection").cloned(),
218 })
219 }
220}
221
222#[derive(Debug, Serialize)]
224pub(crate) struct ListKeysResponse {
225 #[serde(rename = "apiKeys")]
226 pub api_keys: Vec<ApiKeyView>,
227 pub total: usize,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub limit: Option<usize>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub offset: Option<usize>,
232}
233
234#[derive(Debug, Serialize, Deserialize)]
236pub struct CreateKeyResponse {
237 pub key: String,
239 #[serde(flatten)]
241 pub api_key: ApiKeyView,
242}
243
244#[derive(Debug, Serialize, Deserialize)]
246pub struct DeleteExpiredApiKeysResponse {
247 pub success: bool,
248 pub error: Option<String>,
249}
250
251pub(crate) fn parse_api_key_body<T>(request: &AuthRequest) -> Result<T, AuthResponse>
253where
254 T: serde::de::DeserializeOwned + Validate,
255{
256 let input: JsValue = request
257 .body_as_json()
258 .map_err(|_error| validation_response("body", "Invalid JSON"))?;
259 parse_api_key_value(&input)
260}
261
262pub(super) fn parse_api_key_value<T>(input: &JsValue) -> Result<T, AuthResponse>
263where
264 T: serde::de::DeserializeOwned + Validate,
265{
266 let body: T = serde_path_to_error::deserialize(input.clone()).map_err(|error| {
267 let mut path = error.path().to_string().replace('[', ".").replace(']', "");
268 if path == "." {
269 path.clear();
270 }
271 let detail = error.inner().to_string();
272 let input_number = {
273 let mut value = Some(input);
274 for segment in error.path() {
275 value = value.and_then(|current| match segment {
276 serde_path_to_error::Segment::Seq { index } => current.as_array()?.get(*index),
277 serde_path_to_error::Segment::Map { key } => current.get(key),
278 serde_path_to_error::Segment::Enum { variant } => current.get(variant),
279 serde_path_to_error::Segment::Unknown => None,
280 });
281 }
282 value.and_then(JsValue::as_f64)
283 };
284 let nonfinite_input = input_number.filter(|number| !number.is_finite());
285 let message = detail
286 .strip_prefix("missing field `")
287 .and_then(|rest| rest.split('`').next())
288 .map_or_else(
289 || {
290 if let Some((received, expected)) = detail
291 .strip_prefix("invalid type: ")
292 .and_then(|detail| detail.split_once(", expected "))
293 {
294 let expected = expected.split(" at line ").next().unwrap_or(expected);
295 let expected = match expected {
296 "a string" => "string",
297 "a boolean" => "boolean",
298 "f64" => "number",
299 "a sequence" => "array",
300 "a map" => "record",
301 _ => "object",
302 };
303 let received = nonfinite_input.map_or_else(
304 || {
305 if received.starts_with("string") {
306 "string"
307 } else if received.starts_with("integer")
308 || received.starts_with("floating point")
309 {
310 "number"
311 } else if received.starts_with("boolean") {
312 "boolean"
313 } else {
314 match received {
315 "sequence" => "array",
316 "map" => "object",
317 value => value,
318 }
319 }
320 },
321 |number| {
322 if number.is_sign_negative() {
323 "-Infinity"
324 } else {
325 "Infinity"
326 }
327 },
328 );
329 format!("Invalid input: expected {expected}, received {received}")
330 } else if detail.starts_with("number out of range") && nonfinite_input.is_some()
331 {
332 format!(
333 "Invalid input: expected number, received {}",
334 if nonfinite_input.is_some_and(f64::is_sign_negative) {
335 "-Infinity"
336 } else {
337 "Infinity"
338 }
339 )
340 } else {
341 detail.clone()
342 }
343 },
344 |field| {
345 field.clone_into(&mut path);
346 "Invalid input: expected string, received undefined".to_owned()
347 },
348 );
349 let location = if path.is_empty() {
350 "body".to_owned()
351 } else {
352 format!("body.{path}")
353 };
354 validation_response(&location, &message)
355 })?;
356 body.validate()
357 .map_err(|error| alibi_core::validation_error_response(&error))?;
358 Ok(body)
359}
360
361fn numeric_errors(
362 fields: &[(&'static str, Option<f64>, Option<f64>)],
363) -> validator::ValidationErrors {
364 let mut errors = validator::ValidationErrors::new();
365 for &(field, value, minimum) in fields {
366 let Some(value) = value else {
367 continue;
368 };
369 let message = if value.is_finite() {
370 minimum
371 .filter(|minimum| value < *minimum)
372 .map(|minimum| format!("Too small: expected number to be >={minimum}"))
373 } else {
374 Some(format!(
375 "Invalid input: expected number, received {}",
376 if value.is_nan() {
377 "NaN"
378 } else if value.is_sign_negative() {
379 "-Infinity"
380 } else {
381 "Infinity"
382 }
383 ))
384 };
385 if let Some(message) = message {
386 let mut error = validator::ValidationError::new("range");
387 error.message = Some(message.into());
388 errors.add(field, error);
389 }
390 }
391 errors
392}
393
394fn present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
396where
397 D: Deserializer<'de>,
398 T: Deserialize<'de>,
399{
400 T::deserialize(deserializer).map(Some)
401}
402
403fn coerced_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
404where
405 D: Deserializer<'de>,
406{
407 fn js_string(value: &JsValue) -> String {
408 match value {
409 JsValue::String(value) => value.clone(),
410 JsValue::Array(values) => values
411 .iter()
412 .map(|value_2| {
413 if value_2.is_null() {
414 String::new()
415 } else {
416 js_string(value_2)
417 }
418 })
419 .collect::<Vec<_>>()
420 .join(","),
421 JsValue::Object(_) => "[object Object]".into(),
422 JsValue::Number(number) => ryu_js::Buffer::new().format(*number).to_owned(),
423 JsValue::Null => "null".into(),
424 JsValue::Bool(value) => value.to_string(),
425 }
426 }
427 JsValue::deserialize(deserializer).map(|value| Some(js_string(&value)))
428}
429
430fn validate_prefix(prefix: &str) -> Result<(), validator::ValidationError> {
431 if !prefix.is_empty()
432 && prefix
433 .bytes()
434 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
435 {
436 Ok(())
437 } else {
438 let mut error = validator::ValidationError::new("regex");
439 error.message = Some(
440 "Invalid prefix format, must be alphanumeric and contain only underscores and hyphens."
441 .into(),
442 );
443 Err(error)
444 }
445}
446
447fn query_error(field: &str, message: &str) -> AuthResponse {
448 validation_response(&format!("query.{field}"), message)
449}
450
451fn validation_response(location: &str, message: &str) -> AuthResponse {
452 let body = serde_json::json!({
453 "code": "VALIDATION_ERROR", "message": format!("[{location}] {message}")
454 });
455 AuthResponse::text(400, body.to_string()).with_header("content-type", "application/json")
456}