diff --git a/visions/server/migrations/01-initial-db/up.sql b/visions/server/migrations/01-initial-db/up.sql index 8f2ad6b..8d0a733 100644 --- a/visions/server/migrations/01-initial-db/up.sql +++ b/visions/server/migrations/01-initial-db/up.sql @@ -15,7 +15,11 @@ CREATE TABLE sessions( CREATE TABLE games( uuid TEXT PRIMARY KEY, - name TEXT + gm TEXT, + game_type TEXT, + name TEXT, + + FOREIGN KEY(gm) REFERENCES users(uuid) ); CREATE TABLE characters( diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 2535458..079c6bb 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database, SessionId, UserId}, + database::{CharacterId, Database, GameId, SessionId, UserId}, types::{AppError, FatalError, Game, Message, Rgb, Tabletop, User}, }; @@ -131,12 +131,12 @@ impl Core { ok(users.into_iter().find(|user| user.id == user_id)) } - pub async fn create_user(&self, username: &str) -> ResultExt<(), AppError, FatalError> { + pub async fn create_user(&self, username: &str) -> ResultExt { let state = self.0.read().await; match return_error!(self.user_by_username(username).await) { Some(_) => error(AppError::UsernameUnavailable), None => match state.db.save_user(None, username, "", false, true).await { - Ok(_) => ok(()), + Ok(user_id) => ok(user_id), Err(err) => fatal(err), }, } @@ -151,6 +151,14 @@ impl Core { } } + pub async fn create_game(&self, gm: &UserId, game_type: &str, game_name: &str) -> ResultExt { + let state = self.0.read().await; + match state.db.save_game(None, gm, game_type, game_name).await { + Ok(game_id) => ok(game_id), + Err(err) => fatal(err), + } + } + pub async fn tabletop(&self) -> Tabletop { self.0.read().await.tabletop.clone() } diff --git a/visions/server/src/database/disk_db.rs b/visions/server/src/database/disk_db.rs index bc8dd6c..b14368e 100644 --- a/visions/server/src/database/disk_db.rs +++ b/visions/server/src/database/disk_db.rs @@ -142,23 +142,23 @@ impl DiskDb { Ok(items) } - pub fn save_game(&self, game_id: Option, name: &str) -> Result { + pub fn save_game(&self, game_id: Option, gm: &UserId, game_type: &str, name: &str) -> Result { match game_id { None => { let game_id = GameId::new(); let mut stmt = self .conn - .prepare("INSERT INTO games VALUES (?, ?)") + .prepare("INSERT INTO games VALUES (?, ?, ?, ?)") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((game_id.as_str(), name)).unwrap(); + stmt.execute((game_id.as_str(), gm.as_str(), game_type, name)).unwrap(); Ok(game_id) } Some(game_id) => { let mut stmt = self .conn - .prepare("UPDATE games SET name=? WHERE uuid=?") + .prepare("UPDATE games SET gm=? game_type=? name=? WHERE uuid=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((name, game_id.as_str())).unwrap(); + stmt.execute((gm.as_str(), game_type, name, game_id.as_str())).unwrap(); Ok(game_id) } } @@ -286,6 +286,16 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver) { Request::Games => { unimplemented!(); } + Request::Game(_game_id) => { + unimplemented!(); + } + Request::SaveGame(game_id, user_id, game_type, game_name) => { + let game_id = db.save_game(game_id, &user_id, &game_type, &game_name); + match game_id { + Ok(game_id) => { tx.send(DatabaseResponse::SaveGame(game_id)).await.unwrap(); } + err => panic!("{:?}", err), + } + } Request::User(uid) => { let user = db.user(&uid); match user { diff --git a/visions/server/src/database/mod.rs b/visions/server/src/database/mod.rs index 39211ce..e46456e 100644 --- a/visions/server/src/database/mod.rs +++ b/visions/server/src/database/mod.rs @@ -6,7 +6,7 @@ use std::path::Path; use async_std::channel::{bounded, Sender}; use async_trait::async_trait; use disk_db::{db_handler, DiskDb}; -pub use types::{CharacterId, CharsheetRow, GameRow, SessionId, UserId, UserRow}; +pub use types::{CharacterId, CharsheetRow, GameId, GameRow, SessionId, UserId, UserRow}; use crate::types::FatalError; @@ -15,6 +15,8 @@ enum Request { Charsheet(CharacterId), CreateSession(UserId), Games, + Game(GameId), + SaveGame(Option, UserId, String, String), SaveUser(Option, String, String, bool, bool), Session(SessionId), User(UserId), @@ -33,6 +35,8 @@ enum DatabaseResponse { Charsheet(Option), CreateSession(SessionId), Games(Vec), + Game(Option), + SaveGame(GameId), SaveUser(UserId), Session(Option), User(Option), @@ -41,6 +45,8 @@ enum DatabaseResponse { #[async_trait] pub trait Database: Send + Sync { + async fn users(&mut self) -> Result, FatalError>; + async fn user(&self, _: &UserId) -> Result, FatalError>; async fn user_by_username(&self, _: &str) -> Result, FatalError>; @@ -54,10 +60,18 @@ pub trait Database: Send + Sync { enabled: bool, ) -> Result; - async fn users(&mut self) -> Result, FatalError>; - async fn games(&mut self) -> Result, FatalError>; + async fn game(&self, _: &GameId) -> Result, FatalError>; + + async fn save_game( + &self, + game_id: Option, + gm: &UserId, + game_type: &str, + game_name: &str, + ) -> Result; + async fn character(&mut self, id: &CharacterId) -> Result, FatalError>; async fn session(&self, id: &SessionId) -> Result, FatalError>; @@ -109,6 +123,10 @@ macro_rules! send_request { #[async_trait] impl Database for DbConn { + async fn users(&mut self) -> Result, FatalError> { + send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst)) + } + async fn user(&self, uid: &UserId) -> Result, FatalError> { send_request!(self, Request::User(uid.clone()), DatabaseResponse::User(user) => Ok(user)) } @@ -136,14 +154,24 @@ impl Database for DbConn { DatabaseResponse::SaveUser(user_id) => Ok(user_id)) } - async fn users(&mut self) -> Result, FatalError> { - send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst)) - } - async fn games(&mut self) -> Result, FatalError> { send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst)) } + async fn game(&self, game_id: &GameId) -> Result, FatalError> { + send_request!(self, Request::Game(game_id.clone()), DatabaseResponse::Game(game) => Ok(game)) + } + + async fn save_game( + &self, + game_id: Option, + user_id: &UserId, + game_type: &str, + game_name: &str, + ) -> Result { + send_request!(self, Request::SaveGame(game_id, user_id.to_owned(), game_type.to_owned(), game_name.to_owned()), DatabaseResponse::SaveGame(game_id) => Ok(game_id)) + } + async fn character(&mut self, id: &CharacterId) -> Result, FatalError> { send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row)) } @@ -173,8 +201,9 @@ mod test { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); - db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true).unwrap(); - let game_id = db.save_game(None, "Candela").unwrap(); + db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true) + .unwrap(); + let game_id = db.save_game(None, &UserId::from("admin"), "Candela", "Circle of the Winter Solstice").unwrap(); (db, game_id) } diff --git a/visions/server/src/database/types.rs b/visions/server/src/database/types.rs index 8e2daef..cd1be73 100644 --- a/visions/server/src/database/types.rs +++ b/visions/server/src/database/types.rs @@ -172,7 +172,9 @@ pub struct Role { #[derive(Clone, Debug)] pub struct GameRow { - pub id: UserId, + pub id: GameId, + pub gm: UserId, + pub game_type: String, pub name: String, } @@ -189,3 +191,5 @@ pub struct SessionRow { user_id: SessionId, } + + diff --git a/visions/server/src/handlers/game_management.rs b/visions/server/src/handlers/game_management.rs new file mode 100644 index 0000000..818bb93 --- /dev/null +++ b/visions/server/src/handlers/game_management.rs @@ -0,0 +1,33 @@ +use axum::{http::{HeaderMap, StatusCode}, Json}; +use result_extended::ResultExt; +use serde::{Deserialize, Serialize}; + +use crate::{database::GameId, core::Core}; + +use super::auth_required; + + + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct CreateGameRequest { + pub game_type: String, + pub game_name: String, +} + +pub async fn create_game( + core: Core, + headers: HeaderMap, + req: CreateGameRequest, +) -> (StatusCode, Json>) { + println!("create game handler"); + auth_required(core.clone(), headers, |user| async move { + let game = core.create_game(&user.id, &req.game_type, &req.game_name).await; + println!("create_game completed: {:?}", game); + match game { + ResultExt::Ok(game_id) => (StatusCode::OK, Json(Some(game_id))), + ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), + ResultExt::Fatal(fatal) => panic!("{}", fatal), + } + }).await +} + diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index be48e68..5448961 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -1,3 +1,6 @@ +pub mod game_management; +pub use game_management::CreateGameRequest; + use std::future::Future; use axum::{ @@ -20,8 +23,6 @@ async fn check_session( core: &Core, headers: HeaderMap, ) -> ResultExt, AppError, FatalError> { - println!("headers: {:?}", headers); - println!("auth_header: {:?}", headers.get("Authorization")); match headers.get("Authorization") { Some(token) => { match token diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index 81db116..fd9703d 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -11,8 +11,7 @@ use crate::{ core::Core, database::UserId, handlers::{ - check_password, create_user, get_user, healthcheck, set_password, AuthRequest, - CreateUserRequest, SetPasswordRequest, + check_password, create_user, game_management::create_game, get_user, healthcheck, set_password, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest }, }; @@ -67,6 +66,16 @@ pub fn routes(core: Core) -> Router { } }), ) + .route( + "/api/v1/games", + put({ + let core = core.clone(); + move |headers: HeaderMap, req: Json| { + let Json(req) = req; + create_game(core, headers, req) + } + }), + ) } #[cfg(test)] @@ -82,8 +91,8 @@ mod test { use crate::{ asset_db::FsAssets, core::Core, - database::{Database, DbConn, SessionId, UserId}, - handlers::UserProfile, + database::{Database, DbConn, GameId, SessionId, UserId}, + handlers::{CreateGameRequest, UserProfile}, }; fn setup_without_admin() -> (Core, TestServer) { @@ -127,7 +136,15 @@ mod test { username: "savanni".to_owned(), }) .await; + response.assert_status_ok(); + let response = server + .put("/api/v1/user") + .add_header("Authorization", format!("Bearer {}", session_id)) + .json(&CreateUserRequest { + username: "shephard".to_owned(), + }) + .await; response.assert_status_ok(); (core, server) @@ -305,7 +322,7 @@ mod test { response.assert_status(StatusCode::BAD_REQUEST); let response = server - .put(&format!("/api/v1/user/password")) + .put("/api/v1/user/password") .add_header("Authorization", format!("Bearer {}", session_id)) .json(&SetPasswordRequest { password_1: "abcdefg".to_owned(), @@ -315,16 +332,28 @@ mod test { response.assert_status(StatusCode::OK); } - #[ignore] - #[tokio::test] - async fn a_user_cannot_change_another_users_password() { - unimplemented!(); - } - - #[ignore] #[tokio::test] async fn a_user_can_create_a_game() { - unimplemented!(); + let (_core, server) = setup_with_user().await; + + let response = server + .post("/api/v1/auth") + .json(&AuthRequest { + username: "savanni".to_owned(), + password: "".to_owned(), + }) + .await; + let session_id = response.json::>().unwrap(); + + let response = server + .put("/api/v1/games") + .add_header("Authorization", format!("Bearer {}", session_id)) + .json(&CreateGameRequest { + game_type: "Candela".to_owned(), + game_name: "Circle of the Winter Solstice".to_owned(), + }) + .await; + let _game_id = response.json::>().unwrap(); } #[ignore] diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index 3fbc70e..07c7b0a 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -33,6 +33,9 @@ pub enum AppError { #[error("invalid request")] BadRequest, + #[error("could not create an object")] + CouldNotCreateObject, + #[error("something wasn't found {0}")] NotFound(String),