1use std::sync::{Arc, LazyLock};
4
5use time::OffsetDateTime;
6
7use crate::context::AuthContext;
8use crate::crypto::random::generate_random_string;
9use crate::db::{
10 auth_schema, AuthSchemaOptions, Create, DbAdapter, DbRecord, DbSchema, DbValue, Delete,
11 DeleteMany, FindMany, FindOne, SchemaTable, Session, Update, UpdateMany, Where, WhereOperator,
12};
13use crate::error::RustAuthError;
14use crate::options::SecondaryStorage;
15
16const SESSION_MODEL: &str = "session";
17const SESSION_FIELDS: [&str; 8] = [
18 "id",
19 "user_id",
20 "expires_at",
21 "token",
22 "ip_address",
23 "user_agent",
24 "created_at",
25 "updated_at",
26];
27const DEFAULT_SESSION_ID_LENGTH: usize = 32;
28const DEFAULT_SESSION_TOKEN_LENGTH: usize = 32;
29
30fn default_auth_schema() -> &'static DbSchema {
31 static SCHEMA: LazyLock<DbSchema> = LazyLock::new(|| auth_schema(AuthSchemaOptions::default()));
32 &SCHEMA
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct CreateSessionInput {
38 pub id: Option<String>,
39 pub user_id: String,
40 pub expires_at: OffsetDateTime,
41 pub token: Option<String>,
42 pub ip_address: Option<String>,
43 pub user_agent: Option<String>,
44 pub additional_fields: DbRecord,
45}
46
47impl CreateSessionInput {
48 pub fn new(user_id: impl Into<String>, expires_at: OffsetDateTime) -> Self {
49 Self {
50 id: None,
51 user_id: user_id.into(),
52 expires_at,
53 token: None,
54 ip_address: None,
55 user_agent: None,
56 additional_fields: DbRecord::new(),
57 }
58 }
59
60 #[must_use]
61 pub fn id(mut self, id: impl Into<String>) -> Self {
62 self.id = Some(id.into());
63 self
64 }
65
66 #[must_use]
67 pub fn token(mut self, token: impl Into<String>) -> Self {
68 self.token = Some(token.into());
69 self
70 }
71
72 #[must_use]
73 pub fn ip_address(mut self, ip_address: impl Into<String>) -> Self {
74 self.ip_address = Some(ip_address.into());
75 self
76 }
77
78 #[must_use]
79 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
80 self.user_agent = Some(user_agent.into());
81 self
82 }
83
84 #[must_use]
85 pub fn additional_fields(mut self, additional_fields: DbRecord) -> Self {
86 self.additional_fields = additional_fields;
87 self
88 }
89}
90
91#[derive(Clone)]
93pub struct DbSessionStore<'a> {
94 adapter: &'a dyn DbAdapter,
95 schema: DbSchema,
96}
97
98impl<'a> DbSessionStore<'a> {
99 pub fn new(adapter: &'a dyn DbAdapter) -> Self {
100 Self::with_schema(adapter, default_auth_schema().clone())
101 }
102
103 pub fn with_schema(adapter: &'a dyn DbAdapter, schema: DbSchema) -> Self {
104 Self { adapter, schema }
105 }
106
107 pub fn from_context(context: &'a AuthContext) -> Result<Self, RustAuthError> {
108 Ok(Self::with_schema(
109 context.adapter_ref()?,
110 context.db_schema.clone(),
111 ))
112 }
113
114 fn sessions(&self) -> Result<SchemaTable<'_>, RustAuthError> {
115 SchemaTable::new(&self.schema, SESSION_MODEL)
116 }
117
118 fn parse_session(&self, record: DbRecord) -> Result<Session, RustAuthError> {
119 session_from_record(self.sessions()?.map_record(record)?)
120 }
121
122 pub async fn create_session(
123 &self,
124 input: CreateSessionInput,
125 ) -> Result<Session, RustAuthError> {
126 let now = OffsetDateTime::now_utc();
127 let id = input
128 .id
129 .unwrap_or_else(|| generate_random_string(DEFAULT_SESSION_ID_LENGTH));
130 let token = input
131 .token
132 .unwrap_or_else(|| generate_random_string(DEFAULT_SESSION_TOKEN_LENGTH));
133
134 let mut query = Create::new(SESSION_MODEL)
135 .data("id", DbValue::String(id))
136 .data("user_id", DbValue::String(input.user_id))
137 .data("expires_at", DbValue::Timestamp(input.expires_at))
138 .data("token", DbValue::String(token))
139 .data("ip_address", optional_string(input.ip_address))
140 .data("user_agent", optional_string(input.user_agent))
141 .data("created_at", DbValue::Timestamp(now))
142 .data("updated_at", DbValue::Timestamp(now))
143 .select(SESSION_FIELDS)
144 .force_allow_id();
145 for (field, value) in input.additional_fields {
146 query = query.data(field, value);
147 }
148
149 let record = self.adapter.create(query).await?;
150
151 self.parse_session(record)
152 }
153
154 pub async fn find_session(&self, token: &str) -> Result<Option<Session>, RustAuthError> {
155 let Some(session) = self.find_session_including_expired(token).await? else {
156 return Ok(None);
157 };
158
159 if session.expires_at <= OffsetDateTime::now_utc() {
160 return Ok(None);
161 }
162
163 Ok(Some(session))
164 }
165
166 pub async fn find_session_including_expired(
167 &self,
168 token: &str,
169 ) -> Result<Option<Session>, RustAuthError> {
170 self.adapter
171 .find_one(
172 FindOne::new(SESSION_MODEL)
173 .where_clause(token_where(token))
174 .select(SESSION_FIELDS),
175 )
176 .await?
177 .map(|record| self.parse_session(record))
178 .transpose()
179 }
180
181 pub async fn find_sessions<I, S>(&self, tokens: I) -> Result<Vec<Session>, RustAuthError>
182 where
183 I: IntoIterator<Item = S>,
184 S: AsRef<str>,
185 {
186 let tokens = tokens
187 .into_iter()
188 .map(|token| token.as_ref().to_owned())
189 .collect::<Vec<_>>();
190 if tokens.is_empty() {
191 return Ok(Vec::new());
192 }
193 let now = OffsetDateTime::now_utc();
194 self.adapter
195 .find_many(
196 FindMany::new(SESSION_MODEL)
197 .where_clause(
198 Where::new("token", DbValue::StringArray(tokens))
199 .operator(WhereOperator::In),
200 )
201 .select(SESSION_FIELDS),
202 )
203 .await?
204 .into_iter()
205 .map(|record| self.parse_session(record))
206 .filter_map(|result| match result {
207 Ok(session) if session.expires_at > now => Some(Ok(session)),
208 Ok(_) => None,
209 Err(error) => Some(Err(error)),
210 })
211 .collect()
212 }
213
214 pub async fn update_session_expiry(
215 &self,
216 token: &str,
217 expires_at: OffsetDateTime,
218 ) -> Result<Option<Session>, RustAuthError> {
219 let record = self
220 .adapter
221 .update(
222 Update::new(SESSION_MODEL)
223 .where_clause(token_where(token))
224 .data("expires_at", DbValue::Timestamp(expires_at))
225 .data("updated_at", DbValue::Timestamp(OffsetDateTime::now_utc())),
226 )
227 .await?;
228
229 record.map(|record| self.parse_session(record)).transpose()
230 }
231
232 pub async fn delete_session(&self, token: &str) -> Result<(), RustAuthError> {
233 self.adapter
234 .delete(Delete::new(SESSION_MODEL).where_clause(token_where(token)))
235 .await
236 }
237
238 pub async fn delete_user_sessions(&self, user_id: &str) -> Result<u64, RustAuthError> {
239 self.adapter
240 .delete_many(
241 DeleteMany::new(SESSION_MODEL)
242 .where_clause(Where::new("user_id", DbValue::String(user_id.to_owned()))),
243 )
244 .await
245 }
246
247 pub async fn refresh_user_sessions(&self, user_id: &str) -> Result<u64, RustAuthError> {
248 self.adapter
249 .update_many(
250 UpdateMany::new(SESSION_MODEL)
251 .where_clause(Where::new("user_id", DbValue::String(user_id.to_owned())))
252 .data("updated_at", DbValue::Timestamp(OffsetDateTime::now_utc())),
253 )
254 .await
255 }
256
257 pub async fn list_user_sessions(&self, user_id: &str) -> Result<Vec<Session>, RustAuthError> {
258 let now = OffsetDateTime::now_utc();
259 self.adapter
260 .find_many(
261 FindMany::new(SESSION_MODEL)
262 .where_clause(Where::new("user_id", DbValue::String(user_id.to_owned())))
263 .select(SESSION_FIELDS),
264 )
265 .await?
266 .into_iter()
267 .map(|record| self.parse_session(record))
268 .filter_map(|result| match result {
269 Ok(session) if session.expires_at > now => Some(Ok(session)),
270 Ok(_) => None,
271 Err(error) => Some(Err(error)),
272 })
273 .collect()
274 }
275}
276
277#[derive(Clone)]
279pub struct SessionStore<'a> {
280 database: DbSessionStore<'a>,
281 secondary_storage: Option<Arc<dyn SecondaryStorage>>,
282 store_session_in_database: bool,
283 preserve_session_in_database: bool,
284}
285
286impl<'a> SessionStore<'a> {
287 pub fn new(context: &'a AuthContext) -> Result<Self, RustAuthError> {
288 Ok(Self {
289 database: DbSessionStore::from_context(context)?,
290 secondary_storage: context.secondary_storage(),
291 store_session_in_database: context.options.session.store_session_in_database,
292 preserve_session_in_database: context.options.session.preserve_session_in_database,
293 })
294 }
295
296 pub fn with_storage(
297 adapter: &'a dyn DbAdapter,
298 secondary_storage: Option<Arc<dyn SecondaryStorage>>,
299 store_session_in_database: bool,
300 preserve_session_in_database: bool,
301 ) -> Self {
302 Self::with_storage_and_schema(
303 adapter,
304 default_auth_schema().clone(),
305 secondary_storage,
306 store_session_in_database,
307 preserve_session_in_database,
308 )
309 }
310
311 pub fn with_storage_and_schema(
312 adapter: &'a dyn DbAdapter,
313 schema: DbSchema,
314 secondary_storage: Option<Arc<dyn SecondaryStorage>>,
315 store_session_in_database: bool,
316 preserve_session_in_database: bool,
317 ) -> Self {
318 Self {
319 database: DbSessionStore::with_schema(adapter, schema),
320 secondary_storage,
321 store_session_in_database,
322 preserve_session_in_database,
323 }
324 }
325
326 pub async fn create_session(
327 &self,
328 input: CreateSessionInput,
329 ) -> Result<Session, RustAuthError> {
330 let Some(storage) = &self.secondary_storage else {
331 return self.database.create_session(input).await;
332 };
333 let session = if self.store_session_in_database {
334 self.database.create_session(input).await?
335 } else {
336 session_from_input(input)
337 };
338 self.set_secondary_session(storage.as_ref(), &session)
339 .await?;
340 self.add_user_session_token(storage.as_ref(), &session)
341 .await?;
342 Ok(session)
343 }
344
345 pub async fn find_session(&self, token: &str) -> Result<Option<Session>, RustAuthError> {
346 let Some(session) = self.find_session_including_expired(token).await? else {
347 return Ok(None);
348 };
349 if session.expires_at <= OffsetDateTime::now_utc() {
350 self.delete_session(token).await?;
351 return Ok(None);
352 }
353 Ok(Some(session))
354 }
355
356 pub async fn find_session_including_expired(
357 &self,
358 token: &str,
359 ) -> Result<Option<Session>, RustAuthError> {
360 let Some(storage) = &self.secondary_storage else {
361 return self.database.find_session_including_expired(token).await;
362 };
363 match self.find_secondary_session(storage.as_ref(), token).await? {
364 Some(session) => Ok(Some(session)),
365 None if self.store_session_in_database => {
366 self.database.find_session_including_expired(token).await
367 }
368 None => Ok(None),
369 }
370 }
371
372 pub async fn find_sessions<I, S>(&self, tokens: I) -> Result<Vec<Session>, RustAuthError>
373 where
374 I: IntoIterator<Item = S>,
375 S: AsRef<str>,
376 {
377 let tokens = tokens
378 .into_iter()
379 .map(|token| token.as_ref().to_owned())
380 .collect::<Vec<_>>();
381 if tokens.is_empty() {
382 return Ok(Vec::new());
383 }
384 let Some(storage) = &self.secondary_storage else {
385 return self.database.find_sessions(tokens).await;
386 };
387 let now = OffsetDateTime::now_utc();
388 let mut sessions = Vec::new();
389 let mut missing_tokens = Vec::new();
390 for token in tokens {
391 let Some(session) = self
392 .find_secondary_session(storage.as_ref(), &token)
393 .await?
394 else {
395 missing_tokens.push(token);
396 continue;
397 };
398 if session.expires_at > now {
399 sessions.push(session);
400 } else {
401 storage.delete(&session_key(&token)).await?;
402 }
403 }
404 if self.store_session_in_database && !missing_tokens.is_empty() {
405 sessions.extend(self.database.find_sessions(missing_tokens).await?);
406 }
407 Ok(sessions)
408 }
409
410 pub async fn update_session_expiry(
411 &self,
412 token: &str,
413 expires_at: OffsetDateTime,
414 ) -> Result<Option<Session>, RustAuthError> {
415 let Some(storage) = &self.secondary_storage else {
416 return self.database.update_session_expiry(token, expires_at).await;
417 };
418 let Some(mut session) = self.find_session_including_expired(token).await? else {
419 return Ok(None);
420 };
421 session.expires_at = expires_at;
422 session.updated_at = OffsetDateTime::now_utc();
423 self.set_secondary_session(storage.as_ref(), &session)
424 .await?;
425 let tokens = self
426 .user_session_tokens(storage.as_ref(), &session.user_id)
427 .await?;
428 self.set_user_session_tokens(storage.as_ref(), &session.user_id, &tokens)
429 .await?;
430 if self.store_session_in_database {
431 let _updated = self
432 .database
433 .update_session_expiry(token, expires_at)
434 .await?;
435 }
436 Ok(Some(session))
437 }
438
439 pub async fn delete_session(&self, token: &str) -> Result<(), RustAuthError> {
440 let Some(storage) = &self.secondary_storage else {
441 return self.database.delete_session(token).await;
442 };
443 if let Some(session) = self.find_secondary_session(storage.as_ref(), token).await? {
444 self.remove_user_session_token(storage.as_ref(), &session.user_id, token)
445 .await?;
446 }
447 storage.delete(&session_key(token)).await?;
448 if self.store_session_in_database && !self.preserve_session_in_database {
449 self.database.delete_session(token).await?;
450 }
451 Ok(())
452 }
453
454 pub async fn delete_user_sessions(&self, user_id: &str) -> Result<u64, RustAuthError> {
455 let Some(storage) = &self.secondary_storage else {
456 return self.database.delete_user_sessions(user_id).await;
457 };
458 let tokens = self.user_session_tokens(storage.as_ref(), user_id).await?;
459 let mut deleted = 0;
460 for token in &tokens {
461 if self
462 .find_secondary_session(storage.as_ref(), token)
463 .await?
464 .is_some()
465 {
466 deleted += 1;
467 }
468 storage.delete(&session_key(token)).await?;
469 }
470 storage.delete(&user_sessions_key(user_id)).await?;
471 if self.store_session_in_database && !self.preserve_session_in_database {
472 self.database.delete_user_sessions(user_id).await?;
473 }
474 Ok(deleted)
475 }
476
477 pub async fn refresh_user_sessions(&self, user_id: &str) -> Result<u64, RustAuthError> {
478 let Some(storage) = &self.secondary_storage else {
479 return self.database.refresh_user_sessions(user_id).await;
480 };
481 let tokens = self.user_session_tokens(storage.as_ref(), user_id).await?;
482 let now = OffsetDateTime::now_utc();
483 let mut refreshed = 0;
484 for token in &tokens {
485 let Some(mut session) = self.find_secondary_session(storage.as_ref(), token).await?
486 else {
487 continue;
488 };
489 if session.expires_at <= now {
490 storage.delete(&session_key(&session.token)).await?;
491 continue;
492 }
493 session.updated_at = now;
494 self.set_secondary_session(storage.as_ref(), &session)
495 .await?;
496 refreshed += 1;
497 }
498 self.set_user_session_tokens(storage.as_ref(), user_id, &tokens)
499 .await?;
500 if self.store_session_in_database {
501 self.database.refresh_user_sessions(user_id).await?;
502 }
503 Ok(refreshed)
504 }
505
506 pub async fn list_user_sessions(&self, user_id: &str) -> Result<Vec<Session>, RustAuthError> {
507 let Some(storage) = &self.secondary_storage else {
508 return self.database.list_user_sessions(user_id).await;
509 };
510 let tokens = self.user_session_tokens(storage.as_ref(), user_id).await?;
511 let now = OffsetDateTime::now_utc();
512 let mut sessions = Vec::new();
513 let mut active_tokens = Vec::new();
514 for token in tokens {
515 let Some(session) = self
516 .find_secondary_session(storage.as_ref(), &token)
517 .await?
518 else {
519 continue;
520 };
521 if session.expires_at > now {
522 active_tokens.push(token);
523 sessions.push(session);
524 } else {
525 storage.delete(&session_key(&token)).await?;
526 }
527 }
528 self.set_user_session_tokens(storage.as_ref(), user_id, &active_tokens)
529 .await?;
530 if sessions.is_empty() && self.store_session_in_database {
531 return self.database.list_user_sessions(user_id).await;
532 }
533 Ok(sessions)
534 }
535
536 async fn set_secondary_session(
537 &self,
538 storage: &dyn SecondaryStorage,
539 session: &Session,
540 ) -> Result<(), RustAuthError> {
541 storage
542 .set(
543 &session_key(&session.token),
544 serialize_session(session)?,
545 ttl_seconds(session.expires_at),
546 )
547 .await
548 }
549
550 async fn find_secondary_session(
551 &self,
552 storage: &dyn SecondaryStorage,
553 token: &str,
554 ) -> Result<Option<Session>, RustAuthError> {
555 storage
556 .get(&session_key(token))
557 .await?
558 .map(|value| deserialize_session(&value))
559 .transpose()
560 }
561
562 async fn add_user_session_token(
563 &self,
564 storage: &dyn SecondaryStorage,
565 session: &Session,
566 ) -> Result<(), RustAuthError> {
567 let mut tokens = self
568 .user_session_tokens(storage, &session.user_id)
569 .await?
570 .into_iter()
571 .filter(|token| token != &session.token)
572 .collect::<Vec<_>>();
573 tokens.push(session.token.clone());
574 self.set_user_session_tokens(storage, &session.user_id, &tokens)
575 .await
576 }
577
578 async fn remove_user_session_token(
579 &self,
580 storage: &dyn SecondaryStorage,
581 user_id: &str,
582 token: &str,
583 ) -> Result<(), RustAuthError> {
584 let tokens = self
585 .user_session_tokens(storage, user_id)
586 .await?
587 .into_iter()
588 .filter(|stored| stored != token)
589 .collect::<Vec<_>>();
590 self.set_user_session_tokens(storage, user_id, &tokens)
591 .await
592 }
593
594 async fn user_session_tokens(
595 &self,
596 storage: &dyn SecondaryStorage,
597 user_id: &str,
598 ) -> Result<Vec<String>, RustAuthError> {
599 storage
600 .get(&user_sessions_key(user_id))
601 .await?
602 .map(|value| deserialize_user_session_tokens(&value))
603 .transpose()
604 .map(|tokens| tokens.unwrap_or_default())
605 }
606
607 async fn set_user_session_tokens(
608 &self,
609 storage: &dyn SecondaryStorage,
610 user_id: &str,
611 tokens: &[String],
612 ) -> Result<(), RustAuthError> {
613 let now = OffsetDateTime::now_utc();
614 let mut active_tokens = Vec::with_capacity(tokens.len());
615 let mut furthest_expiry = None;
616
617 for token in tokens {
618 let Some(session) = self.find_secondary_session(storage, token).await? else {
619 continue;
620 };
621 if session.expires_at <= now {
622 storage.delete(&session_key(token)).await?;
623 continue;
624 }
625 active_tokens.push(token.clone());
626 furthest_expiry = Some(match furthest_expiry {
627 Some(current) if current >= session.expires_at => current,
628 _ => session.expires_at,
629 });
630 }
631
632 if active_tokens.is_empty() {
633 return storage.delete(&user_sessions_key(user_id)).await;
634 }
635
636 let ttl = furthest_expiry.and_then(index_ttl_seconds);
637 storage
638 .set(
639 &user_sessions_key(user_id),
640 serialize_user_session_tokens(&active_tokens)?,
641 ttl,
642 )
643 .await
644 }
645}
646
647fn optional_string(value: Option<String>) -> DbValue {
648 value.map(DbValue::String).unwrap_or(DbValue::Null)
649}
650
651fn session_from_input(input: CreateSessionInput) -> Session {
652 let now = OffsetDateTime::now_utc();
653 Session {
654 id: input
655 .id
656 .unwrap_or_else(|| generate_random_string(DEFAULT_SESSION_ID_LENGTH)),
657 user_id: input.user_id,
658 expires_at: input.expires_at,
659 token: input
660 .token
661 .unwrap_or_else(|| generate_random_string(DEFAULT_SESSION_TOKEN_LENGTH)),
662 ip_address: input.ip_address,
663 user_agent: input.user_agent,
664 created_at: now,
665 updated_at: now,
666 }
667}
668
669fn session_key(token: &str) -> String {
670 format!("session:{token}")
671}
672
673fn user_sessions_key(user_id: &str) -> String {
674 format!("session:user:{user_id}")
675}
676
677fn serialize_session(session: &Session) -> Result<String, RustAuthError> {
678 serde_json::to_string(session).map_err(|error| RustAuthError::Serialization {
679 context: "serializing session",
680 message: error.to_string(),
681 })
682}
683
684fn deserialize_session(value: &str) -> Result<Session, RustAuthError> {
685 serde_json::from_str(value).map_err(|error| RustAuthError::Serialization {
686 context: "deserializing session",
687 message: error.to_string(),
688 })
689}
690
691fn serialize_user_session_tokens(tokens: &[String]) -> Result<String, RustAuthError> {
692 serde_json::to_string(tokens).map_err(|error| RustAuthError::Serialization {
693 context: "serializing user session index",
694 message: error.to_string(),
695 })
696}
697
698fn deserialize_user_session_tokens(value: &str) -> Result<Vec<String>, RustAuthError> {
699 serde_json::from_str(value).map_err(|error| RustAuthError::Serialization {
700 context: "deserializing user session index",
701 message: error.to_string(),
702 })
703}
704
705fn ttl_seconds(expires_at: OffsetDateTime) -> Option<u64> {
706 let seconds = (expires_at - OffsetDateTime::now_utc()).whole_seconds();
707 Some(u64::try_from(seconds.max(0)).unwrap_or(0))
708}
709
710fn index_ttl_seconds(expires_at: OffsetDateTime) -> Option<u64> {
711 let seconds = (expires_at - OffsetDateTime::now_utc()).whole_seconds();
712 let ttl = u64::try_from(seconds.max(0)).unwrap_or(0);
713 if ttl == 0 {
714 None
715 } else {
716 Some(ttl)
717 }
718}
719
720fn token_where(token: &str) -> Where {
721 Where::new("token", DbValue::String(token.to_owned()))
722}
723
724fn session_from_record(record: DbRecord) -> Result<Session, RustAuthError> {
725 Ok(Session {
726 id: required_string(&record, "id")?.to_owned(),
727 user_id: required_string(&record, "user_id")?.to_owned(),
728 expires_at: required_timestamp(&record, "expires_at")?,
729 token: required_string(&record, "token")?.to_owned(),
730 ip_address: optional_string_field(&record, "ip_address")?,
731 user_agent: optional_string_field(&record, "user_agent")?,
732 created_at: required_timestamp(&record, "created_at")?,
733 updated_at: required_timestamp(&record, "updated_at")?,
734 })
735}
736
737fn required_string<'a>(record: &'a DbRecord, field: &str) -> Result<&'a str, RustAuthError> {
738 match record.get(field) {
739 Some(DbValue::String(value)) => Ok(value),
740 Some(_) => Err(invalid_field(field, "string")),
741 None => Err(missing_field(field)),
742 }
743}
744
745fn optional_string_field(record: &DbRecord, field: &str) -> Result<Option<String>, RustAuthError> {
746 match record.get(field) {
747 Some(DbValue::String(value)) => Ok(Some(value.to_owned())),
748 Some(DbValue::Null) | None => Ok(None),
749 Some(_) => Err(invalid_field(field, "string or null")),
750 }
751}
752
753fn required_timestamp(record: &DbRecord, field: &str) -> Result<OffsetDateTime, RustAuthError> {
754 match record.get(field) {
755 Some(DbValue::Timestamp(value)) => Ok(*value),
756 Some(_) => Err(invalid_field(field, "timestamp")),
757 None => Err(missing_field(field)),
758 }
759}
760
761fn missing_field(field: &str) -> RustAuthError {
762 RustAuthError::MissingRecordField {
763 record: "session",
764 field: field.to_owned(),
765 }
766}
767
768fn invalid_field(field: &str, expected: &'static str) -> RustAuthError {
769 RustAuthError::InvalidRecordField {
770 record: "session",
771 field: field.to_owned(),
772 expected,
773 }
774}