Add the ability to create a game

This commit is contained in:
Savanni D'Gerinel 2025-01-02 11:57:17 -05:00
parent 792437af44
commit f6eb942371
9 changed files with 155 additions and 34 deletions

View File

@ -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(

View File

@ -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<UserId, AppError, FatalError> {
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<GameId, AppError, FatalError> {
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()
}

View File

@ -142,23 +142,23 @@ impl DiskDb {
Ok(items)
}
pub fn save_game(&self, game_id: Option<GameId>, name: &str) -> Result<GameId, FatalError> {
pub fn save_game(&self, game_id: Option<GameId>, gm: &UserId, game_type: &str, name: &str) -> Result<GameId, FatalError> {
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<DatabaseRequest>) {
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 {

View File

@ -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<GameId>, UserId, String, String),
SaveUser(Option<UserId>, String, String, bool, bool),
Session(SessionId),
User(UserId),
@ -33,6 +35,8 @@ enum DatabaseResponse {
Charsheet(Option<CharsheetRow>),
CreateSession(SessionId),
Games(Vec<GameRow>),
Game(Option<GameRow>),
SaveGame(GameId),
SaveUser(UserId),
Session(Option<UserRow>),
User(Option<UserRow>),
@ -41,6 +45,8 @@ enum DatabaseResponse {
#[async_trait]
pub trait Database: Send + Sync {
async fn users(&mut self) -> Result<Vec<UserRow>, FatalError>;
async fn user(&self, _: &UserId) -> Result<Option<UserRow>, FatalError>;
async fn user_by_username(&self, _: &str) -> Result<Option<UserRow>, FatalError>;
@ -54,10 +60,18 @@ pub trait Database: Send + Sync {
enabled: bool,
) -> Result<UserId, FatalError>;
async fn users(&mut self) -> Result<Vec<UserRow>, FatalError>;
async fn games(&mut self) -> Result<Vec<GameRow>, FatalError>;
async fn game(&self, _: &GameId) -> Result<Option<GameRow>, FatalError>;
async fn save_game(
&self,
game_id: Option<GameId>,
gm: &UserId,
game_type: &str,
game_name: &str,
) -> Result<GameId, FatalError>;
async fn character(&mut self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
async fn session(&self, id: &SessionId) -> Result<Option<UserRow>, FatalError>;
@ -109,6 +123,10 @@ macro_rules! send_request {
#[async_trait]
impl Database for DbConn {
async fn users(&mut self) -> Result<Vec<UserRow>, FatalError> {
send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst))
}
async fn user(&self, uid: &UserId) -> Result<Option<UserRow>, 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<Vec<UserRow>, FatalError> {
send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst))
}
async fn games(&mut self) -> Result<Vec<GameRow>, FatalError> {
send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst))
}
async fn game(&self, game_id: &GameId) -> Result<Option<GameRow>, FatalError> {
send_request!(self, Request::Game(game_id.clone()), DatabaseResponse::Game(game) => Ok(game))
}
async fn save_game(
&self,
game_id: Option<GameId>,
user_id: &UserId,
game_type: &str,
game_name: &str,
) -> Result<GameId, FatalError> {
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<Option<CharsheetRow>, FatalError> {
send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row))
}
@ -173,8 +201,9 @@ mod test {
let no_path: Option<PathBuf> = 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)
}

View File

@ -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,
}

View File

@ -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<Option<GameId>>) {
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
}

View File

@ -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<Option<User>, AppError, FatalError> {
println!("headers: {:?}", headers);
println!("auth_header: {:?}", headers.get("Authorization"));
match headers.get("Authorization") {
Some(token) => {
match token

View File

@ -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<CreateGameRequest>| {
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::<Option<SessionId>>().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::<Option<GameId>>().unwrap();
}
#[ignore]

View File

@ -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),