1use rustauth_core::db::{DbRecord, DbValue};
2use rustauth_core::error::RustAuthError;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use time::OffsetDateTime;
6
7use super::options::ApiKeyPermissions;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct ApiKeyRecord {
12 pub id: String,
13 pub config_id: String,
14 pub name: Option<String>,
15 pub start: Option<String>,
16 pub prefix: Option<String>,
17 pub key: String,
18 pub reference_id: String,
19 pub refill_interval: Option<i64>,
20 pub refill_amount: Option<i64>,
21 pub last_refill_at: Option<OffsetDateTime>,
22 pub enabled: bool,
23 pub rate_limit_enabled: bool,
24 pub rate_limit_time_window: Option<i64>,
25 pub rate_limit_max: Option<i64>,
26 pub request_count: i64,
27 pub remaining: Option<i64>,
28 pub last_request: Option<OffsetDateTime>,
29 pub expires_at: Option<OffsetDateTime>,
30 pub created_at: OffsetDateTime,
31 pub updated_at: OffsetDateTime,
32 pub metadata: Option<Value>,
33 pub permissions: Option<ApiKeyPermissions>,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ApiKeyPublicRecord {
39 pub id: String,
40 pub config_id: String,
41 pub name: Option<String>,
42 pub start: Option<String>,
43 pub prefix: Option<String>,
44 pub reference_id: String,
45 pub refill_interval: Option<i64>,
46 pub refill_amount: Option<i64>,
47 pub last_refill_at: Option<OffsetDateTime>,
48 pub enabled: bool,
49 pub rate_limit_enabled: bool,
50 pub rate_limit_time_window: Option<i64>,
51 pub rate_limit_max: Option<i64>,
52 pub request_count: i64,
53 pub remaining: Option<i64>,
54 pub last_request: Option<OffsetDateTime>,
55 pub expires_at: Option<OffsetDateTime>,
56 pub created_at: OffsetDateTime,
57 pub updated_at: OffsetDateTime,
58 pub metadata: Option<Value>,
59 pub permissions: Option<ApiKeyPermissions>,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct ApiKeyCreateRecord {
65 #[serde(flatten)]
66 pub record: ApiKeyPublicRecord,
67 pub key: String,
68}
69
70impl ApiKeyRecord {
71 pub fn public(&self) -> ApiKeyPublicRecord {
72 ApiKeyPublicRecord {
73 id: self.id.clone(),
74 config_id: self.config_id.clone(),
75 name: self.name.clone(),
76 start: self.start.clone(),
77 prefix: self.prefix.clone(),
78 reference_id: self.reference_id.clone(),
79 refill_interval: self.refill_interval,
80 refill_amount: self.refill_amount,
81 last_refill_at: self.last_refill_at,
82 enabled: self.enabled,
83 rate_limit_enabled: self.rate_limit_enabled,
84 rate_limit_time_window: self.rate_limit_time_window,
85 rate_limit_max: self.rate_limit_max,
86 request_count: self.request_count,
87 remaining: self.remaining,
88 last_request: self.last_request,
89 expires_at: self.expires_at,
90 created_at: self.created_at,
91 updated_at: self.updated_at,
92 metadata: normalize_metadata(self.metadata.clone()),
93 permissions: self.permissions.clone(),
94 }
95 }
96
97 pub(crate) fn normalized_metadata(&self) -> Option<Value> {
98 normalize_metadata(self.metadata.clone())
99 }
100
101 pub(crate) fn needs_metadata_migration(&self) -> bool {
102 matches!(self.metadata, Some(Value::String(_)))
103 }
104
105 pub fn to_record(&self) -> DbRecord {
106 DbRecord::from([
107 ("id".to_owned(), DbValue::String(self.id.clone())),
108 (
109 "config_id".to_owned(),
110 DbValue::String(self.config_id.clone()),
111 ),
112 ("name".to_owned(), optional_string(self.name.clone())),
113 ("start".to_owned(), optional_string(self.start.clone())),
114 ("prefix".to_owned(), optional_string(self.prefix.clone())),
115 ("key".to_owned(), DbValue::String(self.key.clone())),
116 (
117 "reference_id".to_owned(),
118 DbValue::String(self.reference_id.clone()),
119 ),
120 (
121 "refill_interval".to_owned(),
122 optional_number(self.refill_interval),
123 ),
124 (
125 "refill_amount".to_owned(),
126 optional_number(self.refill_amount),
127 ),
128 (
129 "last_refill_at".to_owned(),
130 optional_timestamp(self.last_refill_at),
131 ),
132 ("enabled".to_owned(), DbValue::Boolean(self.enabled)),
133 (
134 "rate_limit_enabled".to_owned(),
135 DbValue::Boolean(self.rate_limit_enabled),
136 ),
137 (
138 "rate_limit_time_window".to_owned(),
139 optional_number(self.rate_limit_time_window),
140 ),
141 (
142 "rate_limit_max".to_owned(),
143 optional_number(self.rate_limit_max),
144 ),
145 (
146 "request_count".to_owned(),
147 DbValue::Number(self.request_count),
148 ),
149 ("remaining".to_owned(), optional_number(self.remaining)),
150 (
151 "last_request".to_owned(),
152 optional_timestamp(self.last_request),
153 ),
154 ("expires_at".to_owned(), optional_timestamp(self.expires_at)),
155 ("created_at".to_owned(), DbValue::Timestamp(self.created_at)),
156 ("updated_at".to_owned(), DbValue::Timestamp(self.updated_at)),
157 (
158 "metadata".to_owned(),
159 self.metadata
160 .clone()
161 .map(DbValue::Json)
162 .unwrap_or(DbValue::Null),
163 ),
164 (
165 "permissions".to_owned(),
166 self.permissions
167 .as_ref()
168 .and_then(|permissions| serde_json::to_value(permissions).ok())
169 .map(DbValue::Json)
170 .unwrap_or(DbValue::Null),
171 ),
172 ])
173 }
174}
175
176pub(crate) const API_KEY_FIELDS: [&str; 22] = [
177 "id",
178 "config_id",
179 "name",
180 "start",
181 "prefix",
182 "key",
183 "reference_id",
184 "refill_interval",
185 "refill_amount",
186 "last_refill_at",
187 "enabled",
188 "rate_limit_enabled",
189 "rate_limit_time_window",
190 "rate_limit_max",
191 "request_count",
192 "remaining",
193 "last_request",
194 "expires_at",
195 "created_at",
196 "updated_at",
197 "metadata",
198 "permissions",
199];
200
201pub(crate) fn record_from_db(record: DbRecord) -> Result<ApiKeyRecord, RustAuthError> {
202 Ok(ApiKeyRecord {
203 id: required_string(&record, "id")?.to_owned(),
204 config_id: required_string(&record, "config_id")?.to_owned(),
205 name: optional_string_field(&record, "name")?,
206 start: optional_string_field(&record, "start")?,
207 prefix: optional_string_field(&record, "prefix")?,
208 key: required_string(&record, "key")?.to_owned(),
209 reference_id: required_string(&record, "reference_id")?.to_owned(),
210 refill_interval: optional_number_field(&record, "refill_interval")?,
211 refill_amount: optional_number_field(&record, "refill_amount")?,
212 last_refill_at: optional_timestamp_field(&record, "last_refill_at")?,
213 enabled: optional_bool_field(&record, "enabled")?.unwrap_or(true),
214 rate_limit_enabled: optional_bool_field(&record, "rate_limit_enabled")?.unwrap_or(true),
215 rate_limit_time_window: optional_number_field(&record, "rate_limit_time_window")?,
216 rate_limit_max: optional_number_field(&record, "rate_limit_max")?,
217 request_count: optional_number_field(&record, "request_count")?.unwrap_or(0),
218 remaining: optional_number_field(&record, "remaining")?,
219 last_request: optional_timestamp_field(&record, "last_request")?,
220 expires_at: optional_timestamp_field(&record, "expires_at")?,
221 created_at: required_timestamp(&record, "created_at")?,
222 updated_at: required_timestamp(&record, "updated_at")?,
223 metadata: optional_json_field(&record, "metadata")?,
224 permissions: optional_json_field(&record, "permissions")?
225 .map(serde_json::from_value)
226 .transpose()
227 .map_err(|error| RustAuthError::Adapter(error.to_string()))?,
228 })
229}
230
231fn normalize_metadata(metadata: Option<Value>) -> Option<Value> {
232 match metadata {
233 Some(Value::String(value)) => serde_json::from_str(&value).ok(),
234 other => other,
235 }
236}
237
238fn optional_string(value: Option<String>) -> DbValue {
239 value.map(DbValue::String).unwrap_or(DbValue::Null)
240}
241
242fn optional_number(value: Option<i64>) -> DbValue {
243 value.map(DbValue::Number).unwrap_or(DbValue::Null)
244}
245
246fn optional_timestamp(value: Option<OffsetDateTime>) -> DbValue {
247 value.map(DbValue::Timestamp).unwrap_or(DbValue::Null)
248}
249
250fn required_string<'a>(record: &'a DbRecord, field: &str) -> Result<&'a str, RustAuthError> {
251 match record.get(field) {
252 Some(DbValue::String(value)) => Ok(value),
253 Some(DbValue::Null) | None => Err(RustAuthError::Adapter(format!(
254 "api key field `{field}` is missing"
255 ))),
256 Some(_) => Err(RustAuthError::Adapter(format!(
257 "api key field `{field}` has invalid type"
258 ))),
259 }
260}
261
262fn required_timestamp(record: &DbRecord, field: &str) -> Result<OffsetDateTime, RustAuthError> {
263 match record.get(field) {
264 Some(DbValue::Timestamp(value)) => Ok(*value),
265 Some(DbValue::Null) | None => Err(RustAuthError::Adapter(format!(
266 "api key field `{field}` is missing"
267 ))),
268 Some(_) => Err(RustAuthError::Adapter(format!(
269 "api key field `{field}` has invalid type"
270 ))),
271 }
272}
273
274fn optional_string_field(record: &DbRecord, field: &str) -> Result<Option<String>, RustAuthError> {
275 match record.get(field) {
276 Some(DbValue::String(value)) => Ok(Some(value.clone())),
277 Some(DbValue::Null) | None => Ok(None),
278 Some(_) => Err(RustAuthError::Adapter(format!(
279 "api key field `{field}` has invalid type"
280 ))),
281 }
282}
283
284fn optional_number_field(record: &DbRecord, field: &str) -> Result<Option<i64>, RustAuthError> {
285 match record.get(field) {
286 Some(DbValue::Number(value)) => Ok(Some(*value)),
287 Some(DbValue::Null) | None => Ok(None),
288 Some(_) => Err(RustAuthError::Adapter(format!(
289 "api key field `{field}` has invalid type"
290 ))),
291 }
292}
293
294fn optional_bool_field(record: &DbRecord, field: &str) -> Result<Option<bool>, RustAuthError> {
295 match record.get(field) {
296 Some(DbValue::Boolean(value)) => Ok(Some(*value)),
297 Some(DbValue::Null) | None => Ok(None),
298 Some(_) => Err(RustAuthError::Adapter(format!(
299 "api key field `{field}` has invalid type"
300 ))),
301 }
302}
303
304fn optional_timestamp_field(
305 record: &DbRecord,
306 field: &str,
307) -> Result<Option<OffsetDateTime>, RustAuthError> {
308 match record.get(field) {
309 Some(DbValue::Timestamp(value)) => Ok(Some(*value)),
310 Some(DbValue::Null) | None => Ok(None),
311 Some(_) => Err(RustAuthError::Adapter(format!(
312 "api key field `{field}` has invalid type"
313 ))),
314 }
315}
316
317fn optional_json_field(record: &DbRecord, field: &str) -> Result<Option<Value>, RustAuthError> {
318 match record.get(field) {
319 Some(DbValue::Json(value)) => Ok(Some(value.clone())),
320 Some(DbValue::String(value)) => serde_json::from_str(value)
321 .map(Some)
322 .map_err(|error| RustAuthError::Adapter(error.to_string())),
323 Some(DbValue::Null) | None => Ok(None),
324 Some(_) => Err(RustAuthError::Adapter(format!(
325 "api key field `{field}` has invalid type"
326 ))),
327 }
328}