Lots of linting and refactoring
This commit is contained in:
parent
fb34d0d965
commit
2b1a0b99f8
@ -106,7 +106,10 @@ impl Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_by_username(&self, username: &str) -> ResultExt<Option<User>, AppError, FatalError> {
|
pub async fn user_by_username(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state.db.user_by_username(username).await {
|
match state.db.user_by_username(username).await {
|
||||||
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
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<SessionId, AppError, FatalError> {
|
pub async fn auth(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> ResultExt<SessionId, AppError, FatalError> {
|
||||||
let state = self.0.write().await;
|
let state = self.0.write().await;
|
||||||
match state.db.user_by_username(&username).await {
|
match state.db.user_by_username(&username).await {
|
||||||
Ok(Some(row)) if (row.password == password) => {
|
Ok(Some(row)) if (row.password == password) => {
|
||||||
@ -251,10 +258,7 @@ mod test {
|
|||||||
|
|
||||||
use cool_asserts::assert_matches;
|
use cool_asserts::assert_matches;
|
||||||
|
|
||||||
use crate::{
|
use crate::{asset_db::mocks::MemoryAssets, database::DbConn};
|
||||||
asset_db::mocks::MemoryAssets,
|
|
||||||
database::{DbConn, DiskDb},
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn test_core() -> Core {
|
async fn test_core() -> Core {
|
||||||
let assets = MemoryAssets::new(vec![
|
let assets = MemoryAssets::new(vec![
|
||||||
@ -286,8 +290,12 @@ mod test {
|
|||||||
]);
|
]);
|
||||||
let memory_db: Option<PathBuf> = None;
|
let memory_db: Option<PathBuf> = None;
|
||||||
let conn = DbConn::new(memory_db);
|
let conn = DbConn::new(memory_db);
|
||||||
conn.save_user(None, "admin", "aoeu", true, true).await.unwrap();
|
conn.save_user(None, "admin", "aoeu", true, true)
|
||||||
conn.save_user(None, "gm_1", "aoeu", false, true).await.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
|
conn.save_user(None, "gm_1", "aoeu", false, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
Core::new(assets, conn)
|
Core::new(assets, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,7 +370,7 @@ mod test {
|
|||||||
Ok(None) => panic!("no matching user row for the session id"),
|
Ok(None) => panic!("no matching user row for the session id"),
|
||||||
Err(err) => panic!("{}", err),
|
Err(err) => panic!("{}", err),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
ResultExt::Err(err) => panic!("{}", err),
|
ResultExt::Err(err) => panic!("{}", err),
|
||||||
ResultExt::Fatal(err) => panic!("{}", err),
|
ResultExt::Fatal(err) => panic!("{}", err),
|
||||||
}
|
}
|
||||||
|
338
visions/server/src/database/disk_db.rs
Normal file
338
visions/server/src/database/disk_db.rs
Normal file
@ -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<P>(path: Option<P>) -> Result<Self, FatalError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
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<Option<UserRow>, 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<UserRow> = 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::<Result<Vec<UserRow>, 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<Option<UserRow>, 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<UserRow> = 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::<Result<Vec<UserRow>, 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<UserId>,
|
||||||
|
name: &str,
|
||||||
|
password: &str,
|
||||||
|
admin: bool,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Result<UserId, FatalError> {
|
||||||
|
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<Vec<UserRow>, 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::<Result<Vec<UserRow>, rusqlite::Error>>()
|
||||||
|
.unwrap();
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_game(&self, game_id: Option<GameId>, name: &str) -> Result<GameId, FatalError> {
|
||||||
|
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<Option<UserRow>, 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<UserRow> = 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::<Result<Vec<UserRow>, 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<SessionId, FatalError> {
|
||||||
|
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<Option<CharsheetRow>, 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<CharsheetRow> = 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::<Result<Vec<CharsheetRow>, 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<CharacterId>,
|
||||||
|
game: GameId,
|
||||||
|
character: serde_json::Value,
|
||||||
|
) -> std::result::Result<CharacterId, FatalError> {
|
||||||
|
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<DatabaseRequest>) {
|
||||||
|
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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,15 @@
|
|||||||
|
mod disk_db;
|
||||||
|
mod types;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use async_std::channel::{bounded, Receiver, Sender};
|
use async_std::channel::{bounded, Sender};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use include_dir::{include_dir, Dir};
|
use disk_db::{db_handler, DiskDb};
|
||||||
use lazy_static::lazy_static;
|
pub use types::{CharacterId, CharsheetRow, GameRow, SessionId, UserId, UserRow};
|
||||||
use rusqlite::{
|
|
||||||
types::{FromSql, FromSqlResult, ValueRef},
|
|
||||||
Connection,
|
|
||||||
};
|
|
||||||
use rusqlite_migration::Migrations;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::types::FatalError;
|
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)]
|
#[derive(Debug)]
|
||||||
enum Request {
|
enum Request {
|
||||||
Charsheet(CharacterId),
|
Charsheet(CharacterId),
|
||||||
@ -50,180 +39,9 @@ enum DatabaseResponse {
|
|||||||
Users(Vec<UserRow>),
|
Users(Vec<UserRow>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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<String> for UserId {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql for UserId {
|
|
||||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
||||||
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<String> for SessionId {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql for SessionId {
|
|
||||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
||||||
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<String> for GameId {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql for GameId {
|
|
||||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
||||||
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<String> for CharacterId {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSql for CharacterId {
|
|
||||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
|
||||||
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]
|
#[async_trait]
|
||||||
pub trait Database: Send + Sync {
|
pub trait Database: Send + Sync {
|
||||||
async fn user(&mut self, _: &UserId) -> Result<Option<UserRow>, FatalError>;
|
async fn user(&self, _: &UserId) -> Result<Option<UserRow>, FatalError>;
|
||||||
|
|
||||||
async fn user_by_username(&self, _: &str) -> Result<Option<UserRow>, FatalError>;
|
async fn user_by_username(&self, _: &str) -> Result<Option<UserRow>, FatalError>;
|
||||||
|
|
||||||
@ -247,328 +65,6 @@ pub trait Database: Send + Sync {
|
|||||||
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError>;
|
async fn create_session(&self, id: UserId) -> Result<SessionId, FatalError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DiskDb {
|
|
||||||
conn: Connection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DiskDb {
|
|
||||||
pub fn new<P>(path: Option<P>) -> Result<Self, FatalError>
|
|
||||||
where
|
|
||||||
P: AsRef<Path>,
|
|
||||||
{
|
|
||||||
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<Option<UserRow>, 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<UserRow> = 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::<Result<Vec<UserRow>, 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<Option<UserRow>, 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<UserRow> = 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::<Result<Vec<UserRow>, rusqlite::Error>>()
|
|
||||||
.unwrap();
|
|
||||||
match &items[..] {
|
|
||||||
[] => Ok(None),
|
|
||||||
[item] => Ok(Some(item.clone())),
|
|
||||||
_ => Err(FatalError::NonUniqueDatabaseKey(username.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn users(&self) -> Result<Vec<UserRow>, 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::<Result<Vec<UserRow>, rusqlite::Error>>()
|
|
||||||
.unwrap();
|
|
||||||
Ok(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_user(
|
|
||||||
&self,
|
|
||||||
user_id: Option<UserId>,
|
|
||||||
name: &str,
|
|
||||||
password: &str,
|
|
||||||
admin: bool,
|
|
||||||
enabled: bool,
|
|
||||||
) -> Result<UserId, FatalError> {
|
|
||||||
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<GameId>, name: &str) -> Result<GameId, FatalError> {
|
|
||||||
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<Option<UserRow>, 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<UserRow> = 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::<Result<Vec<UserRow>, 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<SessionId, FatalError> {
|
|
||||||
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<Option<CharsheetRow>, 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<CharsheetRow> = 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::<Result<Vec<CharsheetRow>, 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<CharacterId>,
|
|
||||||
game: GameId,
|
|
||||||
character: serde_json::Value,
|
|
||||||
) -> std::result::Result<CharacterId, FatalError> {
|
|
||||||
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<DatabaseRequest>) {
|
|
||||||
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 {
|
pub struct DbConn {
|
||||||
conn: Sender<DatabaseRequest>,
|
conn: Sender<DatabaseRequest>,
|
||||||
handle: tokio::task::JoinHandle<()>,
|
handle: tokio::task::JoinHandle<()>,
|
||||||
@ -588,24 +84,28 @@ impl DbConn {
|
|||||||
|
|
||||||
Self { conn: tx, handle }
|
Self { conn: tx, handle }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
async fn send(&self, req: Request) -> Result<DatabaseResponse, FatalError> {
|
||||||
impl Database for DbConn {
|
|
||||||
async fn user(&mut self, uid: &UserId) -> Result<Option<UserRow>, FatalError> {
|
|
||||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||||
|
let request = DatabaseRequest { tx, req };
|
||||||
let request = DatabaseRequest {
|
|
||||||
tx,
|
|
||||||
req: Request::User(uid.clone()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match self.conn.send(request).await {
|
match self.conn.send(request).await {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
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<Option<UserRow>, FatalError> {
|
||||||
|
match self
|
||||||
|
.send(Request::User(uid.clone()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(DatabaseResponse::User(user)) => Ok(user),
|
Ok(DatabaseResponse::User(user)) => Ok(user),
|
||||||
Ok(_) => Err(FatalError::MessageMismatch),
|
Ok(_) => Err(FatalError::MessageMismatch),
|
||||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||||
@ -771,10 +271,12 @@ mod test {
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cool_asserts::assert_matches;
|
use cool_asserts::assert_matches;
|
||||||
|
use disk_db::DiskDb;
|
||||||
|
use types::GameId;
|
||||||
|
|
||||||
use super::*;
|
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) {
|
fn setup_db() -> (DiskDb, GameId) {
|
||||||
let no_path: Option<PathBuf> = None;
|
let no_path: Option<PathBuf> = None;
|
||||||
@ -791,7 +293,7 @@ mod test {
|
|||||||
|
|
||||||
assert_matches!(db.character(CharacterId::from("1")), Ok(None));
|
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();
|
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));
|
assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data));
|
||||||
}
|
}
|
||||||
|
175
visions/server/src/database/types.rs
Normal file
175
visions/server/src/database/types.rs
Normal file
@ -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<String> for UserId {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for UserId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
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<String> for SessionId {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for SessionId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
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<String> for GameId {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for GameId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
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<String> for CharacterId {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for CharacterId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Reply {
|
||||||
handler(async move {
|
handler(async move {
|
||||||
core.unregister_client(client_id);
|
core.unregister_client(client_id).await;
|
||||||
|
|
||||||
ok(Response::builder()
|
ok(Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
|
@ -4,24 +4,18 @@ use std::{
|
|||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use asset_db::{AssetId, FsAssets};
|
use asset_db::FsAssets;
|
||||||
use authdb::AuthError;
|
use authdb::AuthError;
|
||||||
use database::DbConn;
|
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 filters::{route_authenticate, route_healthcheck, route_image};
|
||||||
use warp::{
|
use warp::{http::StatusCode, reply::Reply, Filter};
|
||||||
// header,
|
|
||||||
filters::{method, path},
|
|
||||||
http::{Response, StatusCode},
|
|
||||||
reply::Reply,
|
|
||||||
Filter,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod asset_db;
|
mod asset_db;
|
||||||
mod core;
|
mod core;
|
||||||
mod database;
|
mod database;
|
||||||
|
mod filters;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod types;
|
mod types;
|
||||||
mod filters;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Unauthorized;
|
struct Unauthorized;
|
||||||
@ -108,10 +102,12 @@ pub async fn main() {
|
|||||||
let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone()));
|
let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone()));
|
||||||
let authenticated_endpoints = route_image(core.clone());
|
let authenticated_endpoints = route_image(core.clone());
|
||||||
|
|
||||||
let server = warp::serve(unauthenticated_endpoints
|
let server = warp::serve(
|
||||||
.or(authenticated_endpoints)
|
unauthenticated_endpoints
|
||||||
.with(warp::log("visions"))
|
.or(authenticated_endpoints)
|
||||||
.recover(handle_rejection));
|
.with(warp::log("visions"))
|
||||||
|
.recover(handle_rejection),
|
||||||
|
);
|
||||||
server
|
server
|
||||||
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8001))
|
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8001))
|
||||||
.await;
|
.await;
|
||||||
|
Loading…
Reference in New Issue
Block a user