revolt_database/util/
idempotency.rs1use std::num::NonZeroUsize;
2
3use revolt_result::{create_error, Result};
4
5#[cfg(feature = "rocket-impl")]
6use revolt_result::Error;
7
8use async_std::sync::Mutex;
9use once_cell::sync::Lazy;
10use serde::{Deserialize, Serialize};
11
12#[derive(Serialize, Deserialize)]
13pub struct IdempotencyKey {
14 key: String,
15}
16
17static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
18 Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
19
20impl IdempotencyKey {
21 pub fn unchecked_from_string(key: String) -> Self {
22 Self { key }
23 }
24
25 pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
28 if let Some(v) = v {
29 let mut cache = TOKEN_CACHE.lock().await;
30 if cache.get(&v).is_some() {
31 return Err(create_error!(DuplicateNonce));
32 }
33
34 cache.put(v.clone(), ());
35 self.key = v;
36 }
37
38 Ok(())
39 }
40
41 pub fn into_key(self) -> String {
42 self.key
43 }
44}
45
46#[cfg(feature = "rocket-impl")]
47use revolt_rocket_okapi::{
48 gen::OpenApiGenerator,
49 request::{OpenApiFromRequest, RequestHeaderInput},
50 revolt_okapi::openapi3::{Parameter, ParameterValue},
51};
52
53#[cfg(feature = "rocket-impl")]
54use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
55
56#[cfg(feature = "rocket-impl")]
57impl OpenApiFromRequest<'_> for IdempotencyKey {
58 fn from_request_input(
59 _gen: &mut OpenApiGenerator,
60 _name: String,
61 _required: bool,
62 ) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
63 Ok(RequestHeaderInput::Parameter(Parameter {
64 name: "Idempotency-Key".to_string(),
65 description: Some("Unique key to prevent duplicate requests".to_string()),
66 allow_empty_value: false,
67 required: false,
68 deprecated: false,
69 extensions: schemars::Map::new(),
70 location: "header".to_string(),
71 value: ParameterValue::Schema {
72 allow_reserved: false,
73 example: None,
74 examples: None,
75 explode: None,
76 style: None,
77 schema: SchemaObject {
78 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
79 ..Default::default()
80 },
81 },
82 }))
83 }
84}
85
86#[cfg(feature = "rocket-impl")]
87use rocket::{
88 http::Status,
89 request::{FromRequest, Outcome},
90};
91
92#[cfg(feature = "rocket-impl")]
93#[async_trait]
94impl<'r> FromRequest<'r> for IdempotencyKey {
95 type Error = Error;
96
97 async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
98 if let Some(key) = request
99 .headers()
100 .get("Idempotency-Key")
101 .next()
102 .map(|k| k.to_string())
103 {
104 if key.len() > 64 {
105 return Outcome::Error((
106 Status::BadRequest,
107 create_error!(FailedValidation {
108 error: "idempotency key too long".to_string(),
109 }),
110 ));
111 }
112
113 let idempotency = IdempotencyKey { key };
114 let mut cache = TOKEN_CACHE.lock().await;
115 if cache.get(&idempotency.key).is_some() {
116 return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce)));
117 }
118
119 cache.put(idempotency.key.clone(), ());
120 return Outcome::Success(idempotency);
121 }
122
123 Outcome::Success(IdempotencyKey {
124 key: ulid::Ulid::new().to_string(),
125 })
126 }
127}