1mod migrations;
66mod oauth;
67mod passkey;
68mod password;
69mod repositories;
70mod session;
71
72use async_trait::async_trait;
73use chrono::DateTime;
74use chrono::Utc;
75use migrations::CreateIndexes;
76use migrations::{
77 CreateOAuthAccountsTable, CreatePasskeyChallengesTable, CreatePasskeysTable,
78 CreateSessionsTable, CreateUsersTable, SqliteMigrationManager,
79};
80use sqlx::SqlitePool;
81use torii_core::error::StorageError;
82use torii_core::{
83 User, UserId,
84 storage::{NewUser, UserStorage},
85};
86use torii_migration::{Migration, MigrationManager};
87
88pub use repositories::SqliteRepositoryProvider;
89
90#[derive(Clone)]
91pub struct SqliteStorage {
92 pool: SqlitePool,
93}
94
95impl SqliteStorage {
96 pub fn new(pool: SqlitePool) -> Self {
97 Self { pool }
98 }
99
100 pub async fn connect(database_url: &str) -> Result<Self, StorageError> {
101 let pool = SqlitePool::connect(database_url).await.map_err(|e| {
102 tracing::error!(error = %e, "Failed to connect to database");
103 StorageError::Database("Failed to connect to database".to_string())
104 })?;
105
106 Ok(Self::new(pool))
107 }
108
109 pub async fn migrate(&self) -> Result<(), StorageError> {
110 let manager = SqliteMigrationManager::new(self.pool.clone());
111 manager.initialize().await.map_err(|e| {
112 tracing::error!(error = %e, "Failed to initialize migrations");
113 StorageError::Database("Failed to initialize migrations".to_string())
114 })?;
115
116 let migrations: Vec<Box<dyn Migration<_>>> = vec![
117 Box::new(CreateUsersTable),
118 Box::new(CreateSessionsTable),
119 Box::new(CreateOAuthAccountsTable),
120 Box::new(CreatePasskeysTable),
121 Box::new(CreatePasskeyChallengesTable),
122 Box::new(CreateIndexes),
123 ];
124 manager.up(&migrations).await.map_err(|e| {
125 tracing::error!(error = %e, "Failed to run migrations");
126 StorageError::Database("Failed to run migrations".to_string())
127 })?;
128
129 Ok(())
130 }
131
132 pub fn into_repository_provider(self) -> SqliteRepositoryProvider {
134 SqliteRepositoryProvider::new(self.pool)
135 }
136}
137
138#[derive(Debug, Clone, sqlx::FromRow)]
139pub struct SqliteUser {
140 id: String,
141 email: String,
142 name: Option<String>,
143 email_verified_at: Option<i64>,
144 created_at: i64,
145 updated_at: i64,
146}
147
148impl From<SqliteUser> for User {
149 fn from(user: SqliteUser) -> Self {
150 User::builder()
151 .id(UserId::new(&user.id))
152 .email(user.email)
153 .name(user.name)
154 .email_verified_at(user.email_verified_at.map(|timestamp| {
155 DateTime::from_timestamp(timestamp, 0).expect("Invalid timestamp")
156 }))
157 .created_at(DateTime::from_timestamp(user.created_at, 0).expect("Invalid timestamp"))
158 .updated_at(DateTime::from_timestamp(user.updated_at, 0).expect("Invalid timestamp"))
159 .build()
160 .unwrap()
161 }
162}
163
164impl From<User> for SqliteUser {
165 fn from(user: User) -> Self {
166 SqliteUser {
167 id: user.id.into_inner(),
168 email: user.email,
169 name: user.name,
170 email_verified_at: user
171 .email_verified_at
172 .map(|timestamp| timestamp.timestamp()),
173 created_at: user.created_at.timestamp(),
174 updated_at: user.updated_at.timestamp(),
175 }
176 }
177}
178
179#[async_trait]
180impl UserStorage for SqliteStorage {
181 async fn create_user(&self, user: &NewUser) -> Result<User, torii_core::Error> {
182 let now = Utc::now();
183 let user = sqlx::query_as::<_, SqliteUser>(
184 r#"
185 INSERT INTO users (id, email, name, email_verified_at, created_at, updated_at)
186 VALUES (?, ?, ?, ?, ?, ?)
187 RETURNING id, email, name, email_verified_at, created_at, updated_at
188 "#,
189 )
190 .bind(user.id.as_str())
191 .bind(&user.email)
192 .bind(&user.name)
193 .bind(
194 user.email_verified_at
195 .map(|timestamp| timestamp.timestamp()),
196 )
197 .bind(now.timestamp())
198 .bind(now.timestamp())
199 .fetch_one(&self.pool)
200 .await
201 .map_err(|e| {
202 tracing::error!(error = %e, "Failed to create user");
203 StorageError::Database("Failed to create user".to_string())
204 })?;
205
206 Ok(user.into())
207 }
208
209 async fn get_user(&self, id: &UserId) -> Result<Option<User>, torii_core::Error> {
210 let user = sqlx::query_as::<_, SqliteUser>(
211 r#"
212 SELECT id, email, name, email_verified_at, created_at, updated_at
213 FROM users
214 WHERE id = ?
215 "#,
216 )
217 .bind(id.as_str())
218 .fetch_optional(&self.pool)
219 .await
220 .map_err(|e| {
221 tracing::error!(error = %e, "Failed to get user");
222 StorageError::Database("Failed to get user".to_string())
223 })?;
224
225 if let Some(user) = user {
226 Ok(Some(user.into()))
227 } else {
228 Ok(None)
229 }
230 }
231
232 async fn get_user_by_email(&self, email: &str) -> Result<Option<User>, torii_core::Error> {
233 let user = sqlx::query_as::<_, SqliteUser>(
234 r#"
235 SELECT id, email, name, email_verified_at, created_at, updated_at
236 FROM users
237 WHERE email = ?
238 "#,
239 )
240 .bind(email)
241 .fetch_optional(&self.pool)
242 .await
243 .map_err(|e| {
244 tracing::error!(error = %e, "Failed to get user by email");
245 StorageError::Database("Failed to get user by email".to_string())
246 })?;
247
248 if let Some(user) = user {
249 Ok(Some(user.into()))
250 } else {
251 Ok(None)
252 }
253 }
254
255 async fn get_or_create_user_by_email(&self, email: &str) -> Result<User, torii_core::Error> {
256 let user = self.get_user_by_email(email).await?;
257 if let Some(user) = user {
258 return Ok(user);
259 }
260
261 let user = self
262 .create_user(
263 &NewUser::builder()
264 .id(UserId::new_random())
265 .email(email.to_string())
266 .build()
267 .unwrap(),
268 )
269 .await
270 .map_err(|e| {
271 tracing::error!(error = %e, "Failed to get or create user by email");
272 StorageError::Database("Failed to get or create user by email".to_string())
273 })?;
274
275 Ok(user)
276 }
277
278 async fn update_user(&self, user: &User) -> Result<User, torii_core::Error> {
279 let now = Utc::now();
280 let user = sqlx::query_as::<_, SqliteUser>(
281 r#"
282 UPDATE users
283 SET email = ?, name = ?, email_verified_at = ?, updated_at = ?
284 WHERE id = ?
285 RETURNING id, email, name, email_verified_at, created_at, updated_at
286 "#,
287 )
288 .bind(&user.email)
289 .bind(&user.name)
290 .bind(
291 user.email_verified_at
292 .map(|timestamp| timestamp.timestamp()),
293 )
294 .bind(now.timestamp())
295 .bind(user.id.as_str())
296 .fetch_one(&self.pool)
297 .await
298 .map_err(|e| {
299 tracing::error!(error = %e, "Failed to update user");
300 StorageError::Database("Failed to update user".to_string())
301 })?;
302
303 Ok(user.into())
304 }
305
306 async fn delete_user(&self, id: &UserId) -> Result<(), torii_core::Error> {
307 sqlx::query("DELETE FROM users WHERE id = ?")
308 .bind(id.as_str())
309 .execute(&self.pool)
310 .await
311 .map_err(|e| {
312 tracing::error!(error = %e, "Failed to delete user");
313 StorageError::Database("Failed to delete user".to_string())
314 })?;
315
316 Ok(())
317 }
318
319 async fn set_user_email_verified(&self, user_id: &UserId) -> Result<(), torii_core::Error> {
320 sqlx::query("UPDATE users SET email_verified_at = ? WHERE id = ?")
321 .bind(Utc::now().timestamp())
322 .bind(user_id.as_str())
323 .execute(&self.pool)
324 .await
325 .map_err(|e| {
326 tracing::error!(error = %e, "Failed to set user email verified");
327 StorageError::Database("Failed to set user email verified".to_string())
328 })?;
329
330 Ok(())
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use std::time::Duration;
337
338 use sqlx::{Sqlite, types::chrono::Utc};
339 use torii_migration::{Migration, MigrationManager};
340
341 use super::*;
342 use crate::migrations::{
343 CreateOAuthAccountsTable, CreateSessionsTable, CreateUsersTable, SqliteMigrationManager,
344 };
345 use crate::session::test::create_test_session;
346
347 pub(crate) async fn setup_sqlite_storage() -> Result<SqliteStorage, sqlx::Error> {
348 let _ = tracing_subscriber::fmt().try_init();
349 let pool = SqlitePool::connect("sqlite::memory:").await?;
350 let manager = SqliteMigrationManager::new(pool.clone());
351 manager
352 .initialize()
353 .await
354 .expect("Failed to initialize migrations");
355
356 let migrations: Vec<Box<dyn Migration<Sqlite>>> = vec![
357 Box::new(CreateUsersTable),
358 Box::new(CreateSessionsTable),
359 Box::new(CreateOAuthAccountsTable),
360 ];
361 manager
362 .up(&migrations)
363 .await
364 .expect("Failed to run migrations");
365
366 Ok(SqliteStorage::new(pool))
367 }
368
369 pub(crate) async fn create_test_user(
370 storage: &SqliteStorage,
371 id: &str,
372 ) -> Result<User, torii_core::Error> {
373 storage
374 .create_user(
375 &NewUser::builder()
376 .id(UserId::new(id))
377 .email(format!("test{id}@example.com"))
378 .build()
379 .expect("Failed to build user"),
380 )
381 .await
382 }
383
384 #[tokio::test]
385 async fn test_sqlite_storage() {
386 let storage = setup_sqlite_storage()
387 .await
388 .expect("Failed to setup storage");
389 let user = create_test_user(&storage, "1")
390 .await
391 .expect("Failed to create user");
392 assert_eq!(user.email, format!("test1@example.com"));
393
394 let fetched = storage
395 .get_user(&UserId::new("1"))
396 .await
397 .expect("Failed to get user");
398 assert_eq!(
399 fetched.expect("User should exist").email,
400 format!("test1@example.com")
401 );
402
403 storage
404 .delete_user(&UserId::new("1"))
405 .await
406 .expect("Failed to delete user");
407 let deleted = storage
408 .get_user(&UserId::new("1"))
409 .await
410 .expect("Failed to get user");
411 assert!(deleted.is_none());
412 }
413
414 #[tokio::test]
415 async fn test_timestamps_are_set_correctly() {
416 let storage = setup_sqlite_storage()
417 .await
418 .expect("Failed to setup storage");
419
420 let user = create_test_user(&storage, "1")
422 .await
423 .expect("Failed to create user");
424
425 assert!(user.created_at <= Utc::now());
427 assert!(user.updated_at <= Utc::now());
428 assert_eq!(user.created_at, user.updated_at);
429
430 let session = create_test_session(&storage, "session1", "1", Duration::from_secs(3600))
432 .await
433 .expect("Failed to create session");
434
435 assert!(session.created_at <= Utc::now());
437 assert!(session.updated_at <= Utc::now());
438 assert_eq!(session.created_at, session.updated_at);
439 assert!(session.expires_at > Utc::now());
440
441 tokio::time::sleep(Duration::from_secs(1)).await; let mut updated_user = user.clone();
446 updated_user.name = Some("Test User".to_string());
447 let updated_user = storage
448 .update_user(&updated_user)
449 .await
450 .expect("Failed to update user");
451
452 assert_eq!(updated_user.created_at, user.created_at);
454 assert!(updated_user.updated_at > user.updated_at);
455 }
456}