use std::str::FromStr; use secrecy::ExposeSecret; use sqlx::{query, query_as, sqlite::SqliteConnectOptions, Connection, SqliteConnection}; use crate::secrets::UserToken; // TODO: allow configuring this via envar const DB_URI_DEFAULT: &str = "sqlite://sqlite.db"; const TABLE_USER_TOKENS: &str = "user_tokens"; pub trait Database { async fn get_token(&mut self, username: &str) -> Result; async fn save_token(&mut self, username: &str, token: &UserToken) -> Result<(), sqlx::Error>; } #[derive(Debug)] pub struct SqliteDatabase { conn: SqliteConnection, } impl SqliteDatabase { pub async fn open() -> Self { let options = SqliteConnectOptions::from_str(DB_URI_DEFAULT) .expect("Invalid database URI") .create_if_missing(true); let mut db = Self { conn: SqliteConnection::connect_with(&options) .await .expect("Failed to open SQLite database"), }; db.init().await; db } pub async fn init(&mut self) { query(&format!( "CREATE TABLE IF NOT EXISTS {TABLE_USER_TOKENS} ( id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(255) NOT NULL, token NCHAR(30) NOT NULL, valid BOOLEAN NOT NULL, created DATETIME NOT NULL, last_used DATETIME )" )) .execute(&mut self.conn) .await .expect("Failed to initialize table user_tokens"); } } impl Database for SqliteDatabase { // TODO: distinguish between invalid token and SQLX error async fn get_token(&mut self, username: &str) -> Result { let row: (String,) = query_as( "SELECT token FROM user_tokens WHERE username = ? AND valid = TRUE ORDER BY created DESC", ) .bind(username) .fetch_one(&mut self.conn) .await?; Ok(UserToken::from(row.0)) } async fn save_token(&mut self, username: &str, token: &UserToken) -> Result<(), sqlx::Error> { query(&format!( "INSERT INTO {TABLE_USER_TOKENS} (username, token, created, valid) VALUES (?, ?, DATETIME('NOW'), TRUE) ", )) .bind(username) .bind(token.0.expose_secret()) .execute(&mut self.conn) .await?; Ok(()) } }