torii_core/
storage.rs

1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::str::FromStr;
5
6use crate::{
7    Error, OAuthAccount, Session, User, UserId, error::utilities::RequiredFieldExt,
8    session::SessionToken,
9};
10
11#[async_trait]
12pub trait StoragePlugin: Send + Sync + 'static {
13    type Config;
14
15    /// Initialize storage with config
16    async fn initialize(&self, config: Self::Config) -> Result<(), Error>;
17
18    /// Storage health check
19    async fn health_check(&self) -> Result<(), Error>;
20
21    /// Clean up expired data
22    async fn cleanup(&self) -> Result<(), Error>;
23}
24
25#[async_trait]
26pub trait UserStorage: Send + Sync + 'static {
27    async fn create_user(&self, user: &NewUser) -> Result<User, Error>;
28    async fn get_user(&self, id: &UserId) -> Result<Option<User>, Error>;
29    async fn get_user_by_email(&self, email: &str) -> Result<Option<User>, Error>;
30    async fn get_or_create_user_by_email(&self, email: &str) -> Result<User, Error>;
31    async fn update_user(&self, user: &User) -> Result<User, Error>;
32    async fn delete_user(&self, id: &UserId) -> Result<(), Error>;
33    async fn set_user_email_verified(&self, user_id: &UserId) -> Result<(), Error>;
34}
35
36#[async_trait]
37pub trait SessionStorage: Send + Sync + 'static {
38    async fn create_session(&self, session: &Session) -> Result<Session, Error>;
39    async fn get_session(&self, token: &SessionToken) -> Result<Option<Session>, Error>;
40    async fn delete_session(&self, token: &SessionToken) -> Result<(), Error>;
41    async fn cleanup_expired_sessions(&self) -> Result<(), Error>;
42    async fn delete_sessions_for_user(&self, user_id: &UserId) -> Result<(), Error>;
43}
44
45/// Storage methods specific to email/password authentication
46///
47/// This trait extends the base `UserStorage` trait with methods needed for
48/// storing and retrieving password hashes.
49#[async_trait]
50pub trait PasswordStorage: UserStorage {
51    /// Store a password hash for a user
52    async fn set_password_hash(&self, user_id: &UserId, hash: &str) -> Result<(), Error>;
53
54    /// Retrieve a user's password hash
55    async fn get_password_hash(&self, user_id: &UserId) -> Result<Option<String>, Error>;
56}
57
58/// Storage methods specific to OAuth authentication
59///
60/// This trait extends the base `UserStorage` trait with methods needed for
61/// OAuth account management and PKCE verifier storage.
62#[async_trait]
63pub trait OAuthStorage: UserStorage {
64    /// Create a new OAuth account linked to a user
65    async fn create_oauth_account(
66        &self,
67        provider: &str,
68        subject: &str,
69        user_id: &UserId,
70    ) -> Result<OAuthAccount, Error>;
71
72    /// Find a user by their OAuth provider and subject
73    async fn get_user_by_provider_and_subject(
74        &self,
75        provider: &str,
76        subject: &str,
77    ) -> Result<Option<User>, Error>;
78
79    /// Find an OAuth account by provider and subject
80    async fn get_oauth_account_by_provider_and_subject(
81        &self,
82        provider: &str,
83        subject: &str,
84    ) -> Result<Option<OAuthAccount>, Error>;
85
86    /// Link an existing user to an OAuth account
87    async fn link_oauth_account(
88        &self,
89        user_id: &UserId,
90        provider: &str,
91        subject: &str,
92    ) -> Result<(), Error>;
93
94    /// Store a PKCE verifier with an expiration time
95    async fn store_pkce_verifier(
96        &self,
97        csrf_state: &str,
98        pkce_verifier: &str,
99        expires_in: chrono::Duration,
100    ) -> Result<(), Error>;
101
102    /// Retrieve a stored PKCE verifier by CSRF state
103    async fn get_pkce_verifier(&self, csrf_state: &str) -> Result<Option<String>, Error>;
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct NewUser {
108    pub id: UserId,
109    pub email: String,
110    pub name: Option<String>,
111    pub email_verified_at: Option<DateTime<Utc>>,
112}
113
114impl NewUser {
115    pub fn builder() -> NewUserBuilder {
116        NewUserBuilder::default()
117    }
118
119    pub fn new(email: String) -> Self {
120        NewUserBuilder::default()
121            .email(email)
122            .build()
123            .expect("Default builder should never fail")
124    }
125
126    pub fn with_id(id: UserId, email: String) -> Self {
127        NewUserBuilder::default()
128            .id(id)
129            .email(email)
130            .build()
131            .expect("Default builder should never fail")
132    }
133}
134
135#[derive(Default)]
136pub struct NewUserBuilder {
137    id: Option<UserId>,
138    email: Option<String>,
139    name: Option<String>,
140    email_verified_at: Option<DateTime<Utc>>,
141}
142
143impl NewUserBuilder {
144    pub fn id(mut self, id: UserId) -> Self {
145        self.id = Some(id);
146        self
147    }
148
149    pub fn email(mut self, email: String) -> Self {
150        self.email = Some(email);
151        self
152    }
153
154    pub fn name(mut self, name: String) -> Self {
155        self.name = Some(name);
156        self
157    }
158
159    pub fn email_verified_at(mut self, email_verified_at: Option<DateTime<Utc>>) -> Self {
160        self.email_verified_at = email_verified_at;
161        self
162    }
163
164    pub fn build(self) -> Result<NewUser, Error> {
165        Ok(NewUser {
166            id: self.id.unwrap_or_default(),
167            email: self.email.require_field("Email")?,
168            name: self.name,
169            email_verified_at: self.email_verified_at,
170        })
171    }
172}
173
174/// Storage methods specific to passkey authentication
175///
176/// This trait extends the base `UserStorage` trait with methods needed for
177/// storing and retrieving passkey credentials for a user.
178#[async_trait]
179pub trait PasskeyStorage: UserStorage {
180    /// Add a passkey credential for a user
181    async fn add_passkey(
182        &self,
183        user_id: &UserId,
184        credential_id: &str,
185        passkey_json: &str,
186    ) -> Result<(), Error>;
187
188    /// Get a passkey by credential ID
189    async fn get_passkey_by_credential_id(
190        &self,
191        credential_id: &str,
192    ) -> Result<Option<String>, Error>;
193
194    /// Get all passkeys for a user
195    async fn get_passkeys(&self, user_id: &UserId) -> Result<Vec<String>, Error>;
196
197    /// Set a passkey challenge for a user
198    async fn set_passkey_challenge(
199        &self,
200        challenge_id: &str,
201        challenge: &str,
202        expires_in: chrono::Duration,
203    ) -> Result<(), Error>;
204
205    /// Get a passkey challenge
206    async fn get_passkey_challenge(&self, challenge_id: &str) -> Result<Option<String>, Error>;
207}
208
209/// Purpose enumeration for secure tokens to ensure type safety
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
211pub enum TokenPurpose {
212    /// Tokens used for magic link authentication
213    MagicLink,
214    /// Tokens used for password reset flows
215    PasswordReset,
216    /// Tokens used for email verification (future)
217    EmailVerification,
218}
219
220impl TokenPurpose {
221    /// Get the string representation of the token purpose for storage
222    pub fn as_str(&self) -> &'static str {
223        match self {
224            TokenPurpose::MagicLink => "magic_link",
225            TokenPurpose::PasswordReset => "password_reset",
226            TokenPurpose::EmailVerification => "email_verification",
227        }
228    }
229}
230
231impl FromStr for TokenPurpose {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        use crate::error::StorageError;
236        match s {
237            "magic_link" => Ok(TokenPurpose::MagicLink),
238            "password_reset" => Ok(TokenPurpose::PasswordReset),
239            "email_verification" => Ok(TokenPurpose::EmailVerification),
240            _ => Err(Error::Storage(StorageError::Database(format!(
241                "Invalid token purpose: {s}"
242            )))),
243        }
244    }
245}
246
247/// Generic secure token for various authentication purposes
248#[derive(Debug, Clone)]
249pub struct SecureToken {
250    pub user_id: UserId,
251    pub token: String,
252    pub purpose: TokenPurpose,
253    pub used_at: Option<DateTime<Utc>>,
254    pub expires_at: DateTime<Utc>,
255    pub created_at: DateTime<Utc>,
256    pub updated_at: DateTime<Utc>,
257}
258
259impl SecureToken {
260    pub fn new(
261        user_id: UserId,
262        token: String,
263        purpose: TokenPurpose,
264        used_at: Option<DateTime<Utc>>,
265        expires_at: DateTime<Utc>,
266        created_at: DateTime<Utc>,
267        updated_at: DateTime<Utc>,
268    ) -> Self {
269        Self {
270            user_id,
271            token,
272            purpose,
273            used_at,
274            expires_at,
275            created_at,
276            updated_at,
277        }
278    }
279
280    pub fn used(&self) -> bool {
281        self.used_at.is_some()
282    }
283}
284
285impl PartialEq for SecureToken {
286    fn eq(&self, other: &Self) -> bool {
287        self.user_id == other.user_id
288            && self.token == other.token
289            && self.purpose == other.purpose
290            && self.used_at == other.used_at
291            // Some databases may not store the timestamp with more precision than seconds, so we compare the timestamps as integers
292            && self.expires_at.timestamp() == other.expires_at.timestamp()
293            && self.created_at.timestamp() == other.created_at.timestamp()
294            && self.updated_at.timestamp() == other.updated_at.timestamp()
295    }
296}
297
298/// Storage methods for secure tokens
299///
300/// This trait provides storage for generic secure tokens that can be used
301/// for various authentication purposes like magic links, password resets, and email verification.
302#[async_trait]
303pub trait TokenStorage: UserStorage {
304    /// Save a secure token to storage
305    async fn save_secure_token(&self, token: &SecureToken) -> Result<(), Error>;
306
307    /// Get a secure token by its string value and purpose
308    async fn get_secure_token(
309        &self,
310        token: &str,
311        purpose: TokenPurpose,
312    ) -> Result<Option<SecureToken>, Error>;
313
314    /// Mark a secure token as used
315    async fn set_secure_token_used(&self, token: &str, purpose: TokenPurpose) -> Result<(), Error>;
316
317    /// Clean up expired tokens for all purposes
318    async fn cleanup_expired_secure_tokens(&self) -> Result<(), Error>;
319}