use std::str::FromStr; use secrecy::ExposeSecret; use sqlx::{query, query_as, sqlite::SqliteConnectOptions, Connection, SqliteConnection}; use tracing::instrument; use crate::secrets::UserToken; // TODO: allow configuring this via envar const DB_URI_DEFAULT: &str = "sqlite://sqlite.db"; // TODO: check again if it's possible to pass this as a parameter to query! // const TABLE_USER_TOKENS: &str = "user_tokens"; pub enum UserTokenEntry { Valid( UserToken, time::OffsetDateTime, Option, ), Invalid( UserToken, time::OffsetDateTime, Option, ), } pub trait Database { async fn get_token(&mut self, username: &str) -> Result, sqlx::Error>; async fn save_token(&mut self, username: &str, token: &UserToken) -> Result<(), sqlx::Error>; } #[derive(Debug)] pub struct SqliteDatabase { conn: SqliteConnection, } impl SqliteDatabase { #[instrument] 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 } #[instrument] pub async fn init(&mut self) { query!( "CREATE TABLE IF NOT EXISTS 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 { #[instrument] async fn get_token(&mut self, username: &str) -> Result, sqlx::Error> { struct TokenRow { token: String, valid: bool, created: time::OffsetDateTime, last_used: Option, } let row = query_as!( TokenRow, "SELECT token, valid, created, last_used FROM user_tokens WHERE username = $1 ORDER BY created DESC", username ) // .bind(username) .fetch_optional(&mut self.conn) .await?; Ok(row.map( |TokenRow { token, valid, created, last_used, }| match valid { true => UserTokenEntry::Valid(UserToken::from(token), created, last_used), false => UserTokenEntry::Invalid(UserToken::from(token), created, last_used), }, )) } #[instrument] async fn save_token(&mut self, username: &str, token: &UserToken) -> Result<(), sqlx::Error> { let token_inner = token.0.expose_secret(); query!( "INSERT INTO user_tokens (username, token, created, valid) VALUES ($1, $2, DATETIME('NOW'), TRUE) ", username, token_inner ) .execute(&mut self.conn) .await?; Ok(()) } }