Skip to main content

rustauth_core/
session.rs

1//! Database-backed session lifecycle helpers.
2
3use 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/// Input for creating a persisted session.
36#[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/// Session store backed by the RustAuth adapter contract.
92#[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/// Session store that uses configured secondary storage when present.
278#[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}