diff --git a/file-service/src/store/mod.rs b/file-service/src/store/mod.rs index 907c4ee..96445f0 100644 --- a/file-service/src/store/mod.rs +++ b/file-service/src/store/mod.rs @@ -1,9 +1,15 @@ +use base64ct::{Base64, Encoding}; use serde::{Deserialize, Serialize}; -use sqlx::sqlite::SqlitePool; +use sha2::{Digest, Sha256}; +use sqlx::{ + sqlite::{SqlitePool, SqliteRow}, + Executor, Row, +}; use std::collections::HashSet; use std::{ops::Deref, path::PathBuf, sync::Arc}; use thiserror::Error; use tokio::sync::RwLock; +use uuid::Uuid; mod filehandle; mod fileinfo; @@ -62,7 +68,13 @@ pub enum ReadFileError { #[derive(Debug, Error)] pub enum AuthError { #[error("database failed")] - SqlError, + SqlError(sqlx::Error), +} + +impl From for AuthError { + fn from(err: sqlx::Error) -> AuthError { + AuthError::SqlError(err) + } } #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] @@ -80,13 +92,13 @@ impl From<&str> for Username { } } -impl From for PathBuf { +impl From for String { fn from(s: Username) -> Self { Self::from(&s) } } -impl From<&Username> for PathBuf { +impl From<&Username> for String { fn from(s: &Username) -> Self { let Username(s) = s; Self::from(s) @@ -100,6 +112,13 @@ impl Deref for Username { } } +impl sqlx::FromRow<'_, SqliteRow> for Username { + fn from_row(row: &SqliteRow) -> sqlx::Result { + let name: String = row.try_get("username")?; + Ok(Username::from(name)) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, Hash, PartialEq, Eq)] pub struct AuthToken(String); @@ -263,19 +282,47 @@ pub struct AuthDB { impl AuthDB { pub async fn new(path: PathBuf) -> Result { + let migrator = sqlx::migrate!("./migrations"); let pool = SqlitePool::connect(&format!("sqlite://{}", path.to_str().unwrap())).await?; + migrator.run(&pool).await?; Ok(Self { pool }) } + async fn add_user(&self, username: Username) -> Result { + let auth_plaintext = format!("{}:{}", Uuid::new_v4(), username.to_string()); + + let mut hasher = Sha256::new(); + hasher.update(auth_plaintext); + let auth_token = Base64::encode_string(&hasher.finalize()); + + let _ = sqlx::query("INSERT INTO users (username, token) VALUES ($1, $2)") + .bind(username.to_string()) + .bind(auth_token.clone()) + .execute(&self.pool) + .await?; + + Ok(AuthToken::from(auth_token)) + } + + async fn list_users(&self) -> Result, AuthError> { + let usernames = sqlx::query_as::<_, Username>("SELECT (username) FROM users") + .fetch_all(&self.pool) + .await?; + /* + let usernames = result + .into_iter() + .map(|row| Username::from(row.column(0))) + .collect::>(); + */ + + Ok(usernames) + } + async fn auth_token(&self, _token: AuthToken) -> Result { unimplemented!() } async fn auth_session(&self, _token: SessionToken) -> Result { - /* - let conn = self.pool.acquire().await.map_err(|_| AuthError::SqlError)?; - conn.transaction(|tr| {}) - */ unimplemented!() } } @@ -417,3 +464,34 @@ mod test { }); } } + +#[cfg(test)] +mod authdb_test { + use super::*; + use cool_asserts::assert_matches; + + #[tokio::test] + async fn can_create_and_list_users() { + let db = AuthDB::new(PathBuf::from(":memory:")) + .await + .expect("a memory-only database will be created"); + let _ = db + .add_user(Username::from("savanni")) + .await + .expect("user to be created"); + assert_matches!(db.list_users().await, Ok(names) => { + let names = names.into_iter().collect::>(); + assert!(names.contains(&Username::from("savanni"))); + }) + } + + #[test] + fn can_authenticate_token() { + unimplemented!() + } + + #[test] + fn can_validate_session_token() { + unimplemented!() + } +}