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