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#[serde_with::skip_serializing_none]
146#[derive(Debug, Deserialize, Serialize, Validate)]
147#[serde(rename_all = "camelCase")]
148pub struct DeleteKeyRequest {
149 #[serde(default, deserialize_with = "present")]
150 pub config_id: Option<String>,
151 pub key_id: String,
152}
153
154#[serde_with::skip_serializing_none]
156#[derive(Debug, Default, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct ListKeysQuery {
159 pub config_id: Option<String>,
160 pub organization_id: Option<String>,
161 pub limit: Option<usize>,
162 pub offset: Option<usize>,
163 pub sort_by: Option<String>,
164 pub sort_direction: Option<String>,
165}
166
167impl ListKeysQuery {
168 #[expect(
169 clippy::as_conversions,
170 clippy::cast_possible_truncation,
171 clippy::cast_sign_loss,
172 reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
173 )]
174 pub(crate) fn from_request(req: &AuthRequest) -> Result<Self, AuthResponse> {
175 let number = |key: &str| -> Result<Option<usize>, AuthResponse> {
176 let Some(value) = req.query.get(key) else {
177 return Ok(None);
178 };
179 let parsed = if value.trim().is_empty() {
180 0.0
181 } else {
182 value.trim().parse::<f64>().map_err(|_error| {
183 query_error(key, "Invalid input: expected number, received NaN")
184 })?
185 };
186 if !parsed.is_finite() {
187 return Err(query_error(
188 key,
189 if parsed.is_nan() {
190 "Invalid input: expected number, received NaN"
191 } else {
192 "Invalid input: expected number, received number"
193 },
194 ));
195 }
196 if parsed.fract() != 0.0 {
197 return Err(query_error(
198 key,
199 "Invalid input: expected int, received number",
200 ));
201 }
202 if parsed < 0.0 {
203 return Err(query_error(key, "Too small: expected number to be >=0"));
204 }
205 Ok(Some(parsed as usize))
206 };
207 if let Some(direction) = req.query.get("sortDirection")
208 && !matches!(direction.as_str(), "asc" | "desc")
209 {
210 return Err(query_error(
211 "sortDirection",
212 "Invalid option: expected one of \"asc\"|\"desc\"",
213 ));
214 }
215 Ok(Self {
216 config_id: req.query.get("configId").cloned(),
217 organization_id: req.query.get("organizationId").cloned(),
218 limit: number("limit")?,
219 offset: number("offset")?,
220 sort_by: req.query.get("sortBy").cloned(),
221 sort_direction: req.query.get("sortDirection").cloned(),
222 })
223 }
224}
225
226#[derive(Debug, Serialize, Deserialize)]
228pub struct ListKeysResponse {
229 #[serde(rename = "apiKeys")]
230 pub api_keys: Vec<ApiKeyView>,
231 pub total: usize,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub limit: Option<usize>,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub offset: Option<usize>,
236}
237
238#[derive(Debug, Serialize, Deserialize)]
240pub struct CreateKeyResponse {
241 pub key: String,
243 #[serde(flatten)]
245 pub api_key: ApiKeyView,
246}
247
248#[derive(Debug, Serialize, Deserialize)]
250pub struct DeleteExpiredApiKeysResponse {
251 pub success: bool,
252 pub error: Option<String>,
253}
254
255pub(crate) fn parse_api_key_body<T>(request: &AuthRequest) -> Result<T, AuthResponse>
257where
258 T: serde::de::DeserializeOwned + Validate,
259{
260 let input: JsValue = request
261 .body_as_json()
262 .map_err(|_error| validation_response("body", "Invalid JSON"))?;
263 parse_api_key_value(&input)
264}
265
266pub(super) fn parse_api_key_value<T>(input: &JsValue) -> Result<T, AuthResponse>
267where
268 T: serde::de::DeserializeOwned + Validate,
269{
270 let body: T = serde_path_to_error::deserialize(input.clone()).map_err(|error| {
271 let mut path = error.path().to_string().replace('[', ".").replace(']', "");
272 if path == "." {
273 path.clear();
274 }
275 let detail = error.inner().to_string();
276 let input_number = {
277 let mut value = Some(input);
278 for segment in error.path() {
279 value = value.and_then(|current| match segment {
280 serde_path_to_error::Segment::Seq { index } => current.as_array()?.get(*index),
281 serde_path_to_error::Segment::Map { key } => current.get(key),
282 serde_path_to_error::Segment::Enum { variant } => current.get(variant),
283 serde_path_to_error::Segment::Unknown => None,
284 });
285 }
286 value.and_then(JsValue::as_f64)
287 };
288 let nonfinite_input = input_number.filter(|number| !number.is_finite());
289 let message = detail
290 .strip_prefix("missing field `")
291 .and_then(|rest| rest.split('`').next())
292 .map_or_else(
293 || {
294 if let Some((received, expected)) = detail
295 .strip_prefix("invalid type: ")
296 .and_then(|detail| detail.split_once(", expected "))
297 {
298 let expected = expected.split(" at line ").next().unwrap_or(expected);
299 let expected = match expected {
300 "a string" => "string",
301 "a boolean" => "boolean",
302 "f64" => "number",
303 "a sequence" => "array",
304 "a map" => "record",
305 _ => "object",
306 };
307 let received = nonfinite_input.map_or_else(
308 || {
309 if received.starts_with("string") {
310 "string"
311 } else if received.starts_with("integer")
312 || received.starts_with("floating point")
313 {
314 "number"
315 } else if received.starts_with("boolean") {
316 "boolean"
317 } else {
318 match received {
319 "sequence" => "array",
320 "map" => "object",
321 value => value,
322 }
323 }
324 },
325 |number| {
326 if number.is_sign_negative() {
327 "-Infinity"
328 } else {
329 "Infinity"
330 }
331 },
332 );
333 format!("Invalid input: expected {expected}, received {received}")
334 } else if detail.starts_with("number out of range") && nonfinite_input.is_some()
335 {
336 format!(
337 "Invalid input: expected number, received {}",
338 if nonfinite_input.is_some_and(f64::is_sign_negative) {
339 "-Infinity"
340 } else {
341 "Infinity"
342 }
343 )
344 } else {
345 detail.clone()
346 }
347 },
348 |field| {
349 field.clone_into(&mut path);
350 "Invalid input: expected string, received undefined".to_owned()
351 },
352 );
353 let location = if path.is_empty() {
354 "body".to_owned()
355 } else {
356 format!("body.{path}")
357 };
358 validation_response(&location, &message)
359 })?;
360 body.validate()
361 .map_err(|error| alibi_core::validation_error_response(&error))?;
362 Ok(body)
363}
364
365fn numeric_errors(
366 fields: &[(&'static str, Option<f64>, Option<f64>)],
367) -> validator::ValidationErrors {
368 let mut errors = validator::ValidationErrors::new();
369 for &(field, value, minimum) in fields {
370 let Some(value) = value else {
371 continue;
372 };
373 let message = if value.is_finite() {
374 minimum
375 .filter(|minimum| value < *minimum)
376 .map(|minimum| format!("Too small: expected number to be >={minimum}"))
377 } else {
378 Some(format!(
379 "Invalid input: expected number, received {}",
380 if value.is_nan() {
381 "NaN"
382 } else if value.is_sign_negative() {
383 "-Infinity"
384 } else {
385 "Infinity"
386 }
387 ))
388 };
389 if let Some(message) = message {
390 let mut error = validator::ValidationError::new("range");
391 error.message = Some(message.into());
392 errors.add(field, error);
393 }
394 }
395 errors
396}
397
398fn present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
400where
401 D: Deserializer<'de>,
402 T: Deserialize<'de>,
403{
404 T::deserialize(deserializer).map(Some)
405}
406
407fn coerced_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
408where
409 D: Deserializer<'de>,
410{
411 fn js_string(value: &JsValue) -> String {
412 match value {
413 JsValue::String(value) => value.clone(),
414 JsValue::Array(values) => values
415 .iter()
416 .map(|value_2| {
417 if value_2.is_null() {
418 String::new()
419 } else {
420 js_string(value_2)
421 }
422 })
423 .collect::<Vec<_>>()
424 .join(","),
425 JsValue::Object(_) => "[object Object]".into(),
426 JsValue::Number(number) => ryu_js::Buffer::new().format(*number).to_owned(),
427 JsValue::Null => "null".into(),
428 JsValue::Bool(value) => value.to_string(),
429 }
430 }
431 JsValue::deserialize(deserializer).map(|value| Some(js_string(&value)))
432}
433
434fn validate_prefix(prefix: &str) -> Result<(), validator::ValidationError> {
435 if !prefix.is_empty()
436 && prefix
437 .bytes()
438 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
439 {
440 Ok(())
441 } else {
442 let mut error = validator::ValidationError::new("regex");
443 error.message = Some(
444 "Invalid prefix format, must be alphanumeric and contain only underscores and hyphens."
445 .into(),
446 );
447 Err(error)
448 }
449}
450
451fn query_error(field: &str, message: &str) -> AuthResponse {
452 validation_response(&format!("query.{field}"), message)
453}
454
455fn validation_response(location: &str, message: &str) -> AuthResponse {
456 let body = serde_json::json!({
457 "code": "VALIDATION_ERROR", "message": format!("[{location}] {message}")
458 });
459 AuthResponse::text(400, body.to_string()).with_header("content-type", "application/json")
460}