From 2b1a0b99f80d62b716c7c3dcff1af4d4f9c0cb24 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 27 Dec 2024 14:29:07 -0500 Subject: [PATCH] Lots of linting and refactoring --- visions/server/src/core.rs | 26 +- visions/server/src/database/disk_db.rs | 338 +++++++++++++++ visions/server/src/database/mod.rs | 550 ++----------------------- visions/server/src/database/types.rs | 175 ++++++++ visions/server/src/handlers.rs | 2 +- visions/server/src/main.rs | 24 +- 6 files changed, 567 insertions(+), 548 deletions(-) create mode 100644 visions/server/src/database/disk_db.rs create mode 100644 visions/server/src/database/types.rs diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 4eaeac0..99eea4a 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -106,7 +106,10 @@ impl Core { } } - pub async fn user_by_username(&self, username: &str) -> ResultExt, AppError, FatalError> { + pub async fn user_by_username( + &self, + username: &str, + ) -> ResultExt, AppError, FatalError> { let state = self.0.read().await; match state.db.user_by_username(username).await { Ok(Some(user_row)) => ok(Some(User::from(user_row))), @@ -230,7 +233,11 @@ impl Core { } } - pub async fn auth(&self, username: &str, password: &str) -> ResultExt { + pub async fn auth( + &self, + username: &str, + password: &str, + ) -> ResultExt { let state = self.0.write().await; match state.db.user_by_username(&username).await { Ok(Some(row)) if (row.password == password) => { @@ -251,10 +258,7 @@ mod test { use cool_asserts::assert_matches; - use crate::{ - asset_db::mocks::MemoryAssets, - database::{DbConn, DiskDb}, - }; + use crate::{asset_db::mocks::MemoryAssets, database::DbConn}; async fn test_core() -> Core { let assets = MemoryAssets::new(vec![ @@ -286,8 +290,12 @@ mod test { ]); let memory_db: Option = None; let conn = DbConn::new(memory_db); - conn.save_user(None, "admin", "aoeu", true, true).await.unwrap(); - conn.save_user(None, "gm_1", "aoeu", false, true).await.unwrap(); + conn.save_user(None, "admin", "aoeu", true, true) + .await + .unwrap(); + conn.save_user(None, "gm_1", "aoeu", false, true) + .await + .unwrap(); Core::new(assets, conn) } @@ -362,7 +370,7 @@ mod test { Ok(None) => panic!("no matching user row for the session id"), Err(err) => panic!("{}", err), } - }, + } ResultExt::Err(err) => panic!("{}", err), ResultExt::Fatal(err) => panic!("{}", err), } diff --git a/visions/server/src/database/disk_db.rs b/visions/server/src/database/disk_db.rs new file mode 100644 index 0000000..5b41a28 --- /dev/null +++ b/visions/server/src/database/disk_db.rs @@ -0,0 +1,338 @@ +use std::path::Path; + +use async_std::channel::{bounded, Receiver, Sender}; +use include_dir::{include_dir, Dir}; +use lazy_static::lazy_static; +use rusqlite::Connection; +use rusqlite_migration::Migrations; + +use crate::{database::{DatabaseResponse, Request}, types::FatalError}; + +use super::{types::GameId, CharacterId, CharsheetRow, DatabaseRequest, SessionId, UserId, UserRow}; + +static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); + +lazy_static! { + static ref MIGRATIONS: Migrations<'static> = + Migrations::from_directory(&MIGRATIONS_DIR).unwrap(); +} + +pub struct DiskDb { + conn: Connection, +} + +impl DiskDb { + pub fn new

(path: Option

) -> Result + where + P: AsRef, + { + let mut conn = match path { + None => Connection::open(":memory:").expect("to create a memory connection"), + Some(path) => Connection::open(path).expect("to create connection"), + }; + + MIGRATIONS + .to_latest(&mut conn) + .map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?; + + // setup_test_database(&conn)?; + + Ok(DiskDb { conn }) + } + + pub fn user(&self, id: &UserId) -> Result, FatalError> { + let mut stmt = self + .conn + .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + let items: Vec = stmt + .query_map([id.as_str()], |row| { + Ok(UserRow { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + password: row.get(2).unwrap(), + admin: row.get(3).unwrap(), + enabled: row.get(4).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); + match &items[..] { + [] => Ok(None), + [item] => Ok(Some(item.clone())), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), + } + } + + pub fn user_by_username(&self, username: &str) -> Result, FatalError> { + let mut stmt = self + .conn + .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + let items: Vec = stmt + .query_map([username], |row| { + Ok(UserRow { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + password: row.get(2).unwrap(), + admin: row.get(3).unwrap(), + enabled: row.get(4).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); + match &items[..] { + [] => Ok(None), + [item] => Ok(Some(item.clone())), + _ => Err(FatalError::NonUniqueDatabaseKey(username.to_owned())), + } + } + + pub fn save_user( + &self, + user_id: Option, + name: &str, + password: &str, + admin: bool, + enabled: bool, + ) -> Result { + match user_id { + None => { + let user_id = UserId::new(); + let mut stmt = self + .conn + .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((user_id.as_str(), name, password, admin, enabled)) + .unwrap(); + Ok(user_id) + } + Some(user_id) => { + let mut stmt = self + .conn + .prepare("UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((name, password, admin, enabled, user_id.as_str())) + .unwrap(); + Ok(user_id) + } + } + } + + pub fn users(&self) -> Result, FatalError> { + let mut stmt = self + .conn + .prepare("SELECT * FROM users") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + let items = stmt + .query_map([], |row| { + Ok(UserRow { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + password: row.get(2).unwrap(), + admin: row.get(3).unwrap(), + enabled: row.get(4).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); + Ok(items) + } + + pub fn save_game(&self, game_id: Option, name: &str) -> Result { + match game_id { + None => { + let game_id = GameId::new(); + let mut stmt = self + .conn + .prepare("INSERT INTO games VALUES (?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((game_id.as_str(), name)).unwrap(); + Ok(game_id) + } + Some(game_id) => { + let mut stmt = self + .conn + .prepare("UPDATE games SET name=? WHERE uuid=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((name, game_id.as_str())).unwrap(); + Ok(game_id) + } + } + } + + pub fn session(&self, session_id: &SessionId) -> Result, FatalError> { + let mut stmt = self.conn + .prepare("SELECT u.uuid, u.name, u.password, u.admin, u.enabled FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + + let items: Vec = stmt + .query_map([session_id.as_str()], |row| { + Ok(UserRow { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + password: row.get(2).unwrap(), + admin: row.get(3).unwrap(), + enabled: row.get(4).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); + match &items[..] { + [] => Ok(None), + [item] => Ok(Some(item.clone())), + _ => Err(FatalError::NonUniqueDatabaseKey( + session_id.as_str().to_owned(), + )), + } + } + + fn create_session(&self, user_id: &UserId) -> Result { + match self.user(user_id) { + Ok(Some(_)) => { + let mut stmt = self + .conn + .prepare("INSERT INTO sessions VALUES (?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + + let session_id = SessionId::new(); + stmt.execute((session_id.as_str(), user_id.as_str())) + .unwrap(); + Ok(session_id) + } + Ok(None) => Err(FatalError::DatabaseKeyMissing), + Err(err) => Err(err), + } + } + + pub fn character(&self, id: CharacterId) -> Result, FatalError> { + let mut stmt = self + .conn + .prepare("SELECT uuid, game, data FROM characters WHERE uuid=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + let items: Vec = stmt + .query_map([id.as_str()], |row| { + let data: String = row.get(2).unwrap(); + Ok(CharsheetRow { + id: row.get(0).unwrap(), + game: row.get(1).unwrap(), + data: serde_json::from_str(&data).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); + match &items[..] { + [] => Ok(None), + [item] => Ok(Some(item.clone())), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), + } + } + + pub fn save_character( + &self, + char_id: Option, + game: GameId, + character: serde_json::Value, + ) -> std::result::Result { + match char_id { + None => { + let char_id = CharacterId::new(); + let mut stmt = self + .conn + .prepare("INSERT INTO characters VALUES (?, ?, ?)") + .unwrap(); + stmt.execute((char_id.as_str(), game.as_str(), character.to_string())) + .unwrap(); + + Ok(char_id) + } + Some(char_id) => { + let mut stmt = self + .conn + .prepare("UPDATE characters SET data=? WHERE uuid=?") + .unwrap(); + stmt.execute((character.to_string(), char_id.as_str())) + .unwrap(); + + Ok(char_id) + } + } + } +} + +pub async fn db_handler(db: DiskDb, requestor: Receiver) { + while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await { + match req { + Request::Charsheet(id) => { + let sheet = db.character(id); + match sheet { + Ok(sheet) => { + tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap(); + } + _ => unimplemented!(), + } + } + Request::CreateSession(id) => { + let session_id = db.create_session(&id).unwrap(); + tx.send(DatabaseResponse::CreateSession(session_id)) + .await + .unwrap(); + } + Request::Games => { + unimplemented!(); + } + Request::User(uid) => { + let user = db.user(&uid); + match user { + Ok(user) => { + tx.send(DatabaseResponse::User(user)).await.unwrap(); + } + err => panic!("{:?}", err), + } + } + Request::UserByUsername(username) => { + let user = db.user_by_username(&username); + match user { + Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(), + err => panic!("{:?}", err), + } + } + Request::SaveUser(user_id, username, password, admin, enabled) => { + let user_id = db.save_user( + user_id, + username.as_ref(), + password.as_ref(), + admin, + enabled, + ); + match user_id { + Ok(user_id) => { + tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap(); + } + err => panic!("{:?}", err), + } + } + Request::Session(session_id) => { + let user = db.session(&session_id); + match user { + Ok(user) => tx.send(DatabaseResponse::Session(user)).await.unwrap(), + err => panic!("{:?}", err), + } + } + Request::Users => { + let users = db.users(); + match users { + Ok(users) => { + tx.send(DatabaseResponse::Users(users)).await.unwrap(); + } + _ => unimplemented!(), + } + } + } + } +} diff --git a/visions/server/src/database/mod.rs b/visions/server/src/database/mod.rs index 8e27f4b..f195caa 100644 --- a/visions/server/src/database/mod.rs +++ b/visions/server/src/database/mod.rs @@ -1,26 +1,15 @@ +mod disk_db; +mod types; + use std::path::Path; -use async_std::channel::{bounded, Receiver, Sender}; +use async_std::channel::{bounded, Sender}; use async_trait::async_trait; -use include_dir::{include_dir, Dir}; -use lazy_static::lazy_static; -use rusqlite::{ - types::{FromSql, FromSqlResult, ValueRef}, - Connection, -}; -use rusqlite_migration::Migrations; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; +use disk_db::{db_handler, DiskDb}; +pub use types::{CharacterId, CharsheetRow, GameRow, SessionId, UserId, UserRow}; use crate::types::FatalError; -static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); - -lazy_static! { - static ref MIGRATIONS: Migrations<'static> = - Migrations::from_directory(&MIGRATIONS_DIR).unwrap(); -} - #[derive(Debug)] enum Request { Charsheet(CharacterId), @@ -50,180 +39,9 @@ enum DatabaseResponse { Users(Vec), } -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct UserId(String); - -impl UserId { - pub fn new() -> Self { - Self(format!("{}", Uuid::new_v4().hyphenated())) - } - - pub fn as_str<'a>(&'a self) -> &'a str { - &self.0 - } -} - -impl From<&str> for UserId { - fn from(s: &str) -> Self { - Self(s.to_owned()) - } -} - -impl From for UserId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl FromSql for UserId { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), - _ => unimplemented!(), - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct SessionId(String); - -impl SessionId { - pub fn new() -> Self { - Self(format!("{}", Uuid::new_v4().hyphenated())) - } - - pub fn as_str<'a>(&'a self) -> &'a str { - &self.0 - } -} - -impl From<&str> for SessionId { - fn from(s: &str) -> Self { - Self(s.to_owned()) - } -} - -impl From for SessionId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl FromSql for SessionId { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), - _ => unimplemented!(), - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct GameId(String); - -impl GameId { - pub fn new() -> Self { - Self(format!("{}", Uuid::new_v4().hyphenated())) - } - - pub fn as_str<'a>(&'a self) -> &'a str { - &self.0 - } -} - -impl From<&str> for GameId { - fn from(s: &str) -> Self { - Self(s.to_owned()) - } -} - -impl From for GameId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl FromSql for GameId { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), - _ => unimplemented!(), - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub struct CharacterId(String); - -impl CharacterId { - pub fn new() -> Self { - Self(format!("{}", Uuid::new_v4().hyphenated())) - } - - pub fn as_str<'a>(&'a self) -> &'a str { - &self.0 - } -} - -impl From<&str> for CharacterId { - fn from(s: &str) -> Self { - Self(s.to_owned()) - } -} - -impl From for CharacterId { - fn from(s: String) -> Self { - Self(s) - } -} - -impl FromSql for CharacterId { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - match value { - ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), - _ => unimplemented!(), - } - } -} - -#[derive(Clone, Debug)] -pub struct UserRow { - pub id: UserId, - pub name: String, - pub password: String, - pub admin: bool, - pub enabled: bool, -} - -#[derive(Clone, Debug)] -pub struct Role { - userid: UserId, - gameid: GameId, - role: String, -} - -#[derive(Clone, Debug)] -pub struct GameRow { - pub id: UserId, - pub name: String, -} - -#[derive(Clone, Debug)] -pub struct CharsheetRow { - id: String, - game: GameId, - pub data: serde_json::Value, -} - -#[derive(Clone, Debug)] -pub struct SessionRow { - id: SessionId, - user_id: SessionId, -} - #[async_trait] pub trait Database: Send + Sync { - async fn user(&mut self, _: &UserId) -> Result, FatalError>; + async fn user(&self, _: &UserId) -> Result, FatalError>; async fn user_by_username(&self, _: &str) -> Result, FatalError>; @@ -247,328 +65,6 @@ pub trait Database: Send + Sync { async fn create_session(&self, id: UserId) -> Result; } -pub struct DiskDb { - conn: Connection, -} - -impl DiskDb { - pub fn new

(path: Option

) -> Result - where - P: AsRef, - { - let mut conn = match path { - None => Connection::open(":memory:").expect("to create a memory connection"), - Some(path) => Connection::open(path).expect("to create connection"), - }; - - MIGRATIONS - .to_latest(&mut conn) - .map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?; - - // setup_test_database(&conn)?; - - Ok(DiskDb { conn }) - } - - fn user(&self, id: &UserId) -> Result, FatalError> { - let mut stmt = self - .conn - .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - let items: Vec = stmt - .query_map([id.as_str()], |row| { - Ok(UserRow { - id: row.get(0).unwrap(), - name: row.get(1).unwrap(), - password: row.get(2).unwrap(), - admin: row.get(3).unwrap(), - enabled: row.get(4).unwrap(), - }) - }) - .unwrap() - .collect::, rusqlite::Error>>() - .unwrap(); - match &items[..] { - [] => Ok(None), - [item] => Ok(Some(item.clone())), - _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), - } - } - - fn user_by_username(&self, username: &str) -> Result, FatalError> { - let mut stmt = self - .conn - .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - let items: Vec = stmt - .query_map([username], |row| { - Ok(UserRow { - id: row.get(0).unwrap(), - name: row.get(1).unwrap(), - password: row.get(2).unwrap(), - admin: row.get(3).unwrap(), - enabled: row.get(4).unwrap(), - }) - }) - .unwrap() - .collect::, rusqlite::Error>>() - .unwrap(); - match &items[..] { - [] => Ok(None), - [item] => Ok(Some(item.clone())), - _ => Err(FatalError::NonUniqueDatabaseKey(username.to_owned())), - } - } - - fn users(&self) -> Result, FatalError> { - let mut stmt = self - .conn - .prepare("SELECT * FROM users") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - let items = stmt - .query_map([], |row| { - Ok(UserRow { - id: row.get(0).unwrap(), - name: row.get(1).unwrap(), - password: row.get(2).unwrap(), - admin: row.get(3).unwrap(), - enabled: row.get(4).unwrap(), - }) - }) - .unwrap() - .collect::, rusqlite::Error>>() - .unwrap(); - Ok(items) - } - - fn save_user( - &self, - user_id: Option, - name: &str, - password: &str, - admin: bool, - enabled: bool, - ) -> Result { - match user_id { - None => { - let user_id = UserId::new(); - let mut stmt = self - .conn - .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((user_id.as_str(), name, password, admin, enabled)) - .unwrap(); - Ok(user_id) - } - Some(user_id) => { - let mut stmt = self - .conn - .prepare("UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((name, password, admin, enabled, user_id.as_str())) - .unwrap(); - Ok(user_id) - } - } - } - - fn save_game(&self, game_id: Option, name: &str) -> Result { - match game_id { - None => { - let game_id = GameId::new(); - let mut stmt = self - .conn - .prepare("INSERT INTO games VALUES (?, ?)") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((game_id.as_str(), name)).unwrap(); - Ok(game_id) - } - Some(game_id) => { - let mut stmt = self - .conn - .prepare("UPDATE games SET name=? WHERE uuid=?") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((name, game_id.as_str())).unwrap(); - Ok(game_id) - } - } - } - - fn session(&self, session_id: &SessionId) -> Result, FatalError> { - let mut stmt = self.conn - .prepare("SELECT u.uuid, u.name, u.password, u.admin, u.enabled FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - - let items: Vec = stmt - .query_map([session_id.as_str()], |row| { - Ok(UserRow { - id: row.get(0).unwrap(), - name: row.get(1).unwrap(), - password: row.get(2).unwrap(), - admin: row.get(3).unwrap(), - enabled: row.get(4).unwrap(), - }) - }) - .unwrap() - .collect::, rusqlite::Error>>() - .unwrap(); - match &items[..] { - [] => Ok(None), - [item] => Ok(Some(item.clone())), - _ => Err(FatalError::NonUniqueDatabaseKey( - session_id.as_str().to_owned(), - )), - } - } - - fn create_session(&self, user_id: &UserId) -> Result { - match self.user(user_id) { - Ok(Some(_)) => { - let mut stmt = self - .conn - .prepare("INSERT INTO sessions VALUES (?, ?)") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - - let session_id = SessionId::new(); - stmt.execute((session_id.as_str(), user_id.as_str())) - .unwrap(); - Ok(session_id) - } - Ok(None) => Err(FatalError::DatabaseKeyMissing), - Err(err) => Err(err), - } - } - - fn character(&self, id: CharacterId) -> Result, FatalError> { - let mut stmt = self - .conn - .prepare("SELECT uuid, game, data FROM characters WHERE uuid=?") - .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - let items: Vec = stmt - .query_map([id.as_str()], |row| { - let data: String = row.get(2).unwrap(); - Ok(CharsheetRow { - id: row.get(0).unwrap(), - game: row.get(1).unwrap(), - data: serde_json::from_str(&data).unwrap(), - }) - }) - .unwrap() - .collect::, rusqlite::Error>>() - .unwrap(); - match &items[..] { - [] => Ok(None), - [item] => Ok(Some(item.clone())), - _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), - } - } - - fn save_character( - &self, - char_id: Option, - game: GameId, - character: serde_json::Value, - ) -> std::result::Result { - match char_id { - None => { - let char_id = CharacterId::new(); - let mut stmt = self - .conn - .prepare("INSERT INTO characters VALUES (?, ?, ?)") - .unwrap(); - stmt.execute((char_id.as_str(), game.as_str(), character.to_string())) - .unwrap(); - - Ok(char_id) - } - Some(char_id) => { - let mut stmt = self - .conn - .prepare("UPDATE characters SET data=? WHERE uuid=?") - .unwrap(); - stmt.execute((character.to_string(), char_id.as_str())) - .unwrap(); - - Ok(char_id) - } - } - } -} - -async fn db_handler(db: DiskDb, requestor: Receiver) { - while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await { - println!("Request received: {:?}", req); - match req { - Request::Charsheet(id) => { - let sheet = db.character(id); - match sheet { - Ok(sheet) => { - tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap(); - } - _ => unimplemented!(), - } - } - Request::CreateSession(id) => { - let session_id = db.create_session(&id).unwrap(); - tx.send(DatabaseResponse::CreateSession(session_id)) - .await - .unwrap(); - } - Request::Games => { - unimplemented!(); - } - Request::User(uid) => { - let user = db.user(&uid); - match user { - Ok(user) => { - tx.send(DatabaseResponse::User(user)).await.unwrap(); - } - err => panic!("{:?}", err), - } - } - Request::UserByUsername(username) => { - let user = db.user_by_username(&username); - match user { - Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(), - err => panic!("{:?}", err), - } - } - Request::SaveUser(user_id, username, password, admin, enabled) => { - let user_id = db.save_user( - user_id, - username.as_ref(), - password.as_ref(), - admin, - enabled, - ); - match user_id { - Ok(user_id) => { - tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap(); - } - err => panic!("{:?}", err), - } - } - Request::Session(session_id) => { - let user = db.session(&session_id); - match user { - Ok(user) => tx.send(DatabaseResponse::Session(user)).await.unwrap(), - err => panic!("{:?}", err), - } - } - Request::Users => { - let users = db.users(); - match users { - Ok(users) => { - tx.send(DatabaseResponse::Users(users)).await.unwrap(); - } - _ => unimplemented!(), - } - } - } - } - println!("ending db_handler"); -} - pub struct DbConn { conn: Sender, handle: tokio::task::JoinHandle<()>, @@ -588,24 +84,28 @@ impl DbConn { Self { conn: tx, handle } } -} -#[async_trait] -impl Database for DbConn { - async fn user(&mut self, uid: &UserId) -> Result, FatalError> { + async fn send(&self, req: Request) -> Result { let (tx, rx) = bounded::(1); - - let request = DatabaseRequest { - tx, - req: Request::User(uid.clone()), - }; - + let request = DatabaseRequest { tx, req }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; - match rx.recv().await { + rx.recv() + .await + .map_err(|_| FatalError::DatabaseConnectionLost) + } +} + +#[async_trait] +impl Database for DbConn { + async fn user(&self, uid: &UserId) -> Result, FatalError> { + match self + .send(Request::User(uid.clone())) + .await + { Ok(DatabaseResponse::User(user)) => Ok(user), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), @@ -771,10 +271,12 @@ mod test { use std::path::PathBuf; use cool_asserts::assert_matches; + use disk_db::DiskDb; + use types::GameId; use super::*; - const soren: &'static str = r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#; + const SOREN: &'static str = r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#; fn setup_db() -> (DiskDb, GameId) { let no_path: Option = None; @@ -791,7 +293,7 @@ mod test { assert_matches!(db.character(CharacterId::from("1")), Ok(None)); - let js: serde_json::Value = serde_json::from_str(soren).unwrap(); + let js: serde_json::Value = serde_json::from_str(SOREN).unwrap(); let soren_id = db.save_character(None, game_id, js.clone()).unwrap(); assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); } diff --git a/visions/server/src/database/types.rs b/visions/server/src/database/types.rs new file mode 100644 index 0000000..9909365 --- /dev/null +++ b/visions/server/src/database/types.rs @@ -0,0 +1,175 @@ +use rusqlite::types::{FromSql, FromSqlResult, ValueRef}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct UserId(String); + +impl UserId { + pub fn new() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } + + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + +impl From<&str> for UserId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for UserId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl FromSql for UserId { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), + _ => unimplemented!(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct SessionId(String); + +impl SessionId { + pub fn new() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } + + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + +impl From<&str> for SessionId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for SessionId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl FromSql for SessionId { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), + _ => unimplemented!(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct GameId(String); + +impl GameId { + pub fn new() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } + + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + +impl From<&str> for GameId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for GameId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl FromSql for GameId { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), + _ => unimplemented!(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct CharacterId(String); + +impl CharacterId { + pub fn new() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } + + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + +impl From<&str> for CharacterId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for CharacterId { + fn from(s: String) -> Self { + Self(s) + } +} + +impl FromSql for CharacterId { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), + _ => unimplemented!(), + } + } +} + +#[derive(Clone, Debug)] +pub struct UserRow { + pub id: UserId, + pub name: String, + pub password: String, + pub admin: bool, + pub enabled: bool, +} + +#[derive(Clone, Debug)] +pub struct Role { + userid: UserId, + gameid: GameId, + role: String, +} + +#[derive(Clone, Debug)] +pub struct GameRow { + pub id: UserId, + pub name: String, +} + +#[derive(Clone, Debug)] +pub struct CharsheetRow { + pub id: String, + pub game: GameId, + pub data: serde_json::Value, +} + +#[derive(Clone, Debug)] +pub struct SessionRow { + id: SessionId, + user_id: SessionId, +} + diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 2865ff8..6752894 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -129,7 +129,7 @@ pub async fn handle_register_client(core: Core, _request: RegisterRequest) -> im pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Reply { handler(async move { - core.unregister_client(client_id); + core.unregister_client(client_id).await; ok(Response::builder() .status(StatusCode::NO_CONTENT) diff --git a/visions/server/src/main.rs b/visions/server/src/main.rs index a6692ec..2ccde04 100644 --- a/visions/server/src/main.rs +++ b/visions/server/src/main.rs @@ -4,24 +4,18 @@ use std::{ path::PathBuf, }; -use asset_db::{AssetId, FsAssets}; +use asset_db::FsAssets; use authdb::AuthError; use database::DbConn; -use filters::{route_authenticate, route_available_images, route_get_charsheet, route_healthcheck, route_image, route_register_client, route_server_status, route_set_bg_image, route_unregister_client, route_websocket, routes_user_management}; -use warp::{ - // header, - filters::{method, path}, - http::{Response, StatusCode}, - reply::Reply, - Filter, -}; +use filters::{route_authenticate, route_healthcheck, route_image}; +use warp::{http::StatusCode, reply::Reply, Filter}; mod asset_db; mod core; mod database; +mod filters; mod handlers; mod types; -mod filters; #[derive(Debug)] struct Unauthorized; @@ -108,10 +102,12 @@ pub async fn main() { let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone())); let authenticated_endpoints = route_image(core.clone()); - let server = warp::serve(unauthenticated_endpoints - .or(authenticated_endpoints) - .with(warp::log("visions")) - .recover(handle_rejection)); + let server = warp::serve( + unauthenticated_endpoints + .or(authenticated_endpoints) + .with(warp::log("visions")) + .recover(handle_rejection), + ); server .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8001)) .await;