From 4dc7a5000028a90505f147f9a0f06243fd35bec8 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Thu, 2 Jan 2025 14:45:06 -0500 Subject: [PATCH] Get games that the user is GMing in the user profile --- visions/server/src/core.rs | 24 +- visions/server/src/database/disk_db.rs | 58 +++- visions/server/src/database/mod.rs | 12 +- visions/server/src/handlers/mod.rs | 273 +----------------- .../server/src/handlers/user_management.rs | 30 +- visions/server/src/routes.rs | 20 +- visions/server/src/types.rs | 24 +- 7 files changed, 123 insertions(+), 318 deletions(-) diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 079c6bb..e9224f6 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -10,8 +10,7 @@ use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database, GameId, SessionId, UserId}, - types::{AppError, FatalError, Game, Message, Rgb, Tabletop, User}, + database::{CharacterId, Database, GameId, SessionId, UserId}, types::{AppError, FatalError, Game, GameOverview, Message, Rgb, Tabletop, User, UserProfile}, }; const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb { @@ -126,9 +125,20 @@ impl Core { } } - pub async fn user(&self, user_id: UserId) -> ResultExt, AppError, FatalError> { + pub async fn user(&self, user_id: UserId) -> ResultExt, AppError, FatalError> { let users = return_error!(self.list_users().await); - ok(users.into_iter().find(|user| user.id == user_id)) + let games = return_error!(self.list_games().await); + let user = match users.into_iter().find(|user| user.id == user_id) { + Some(user) => user, + None => return ok(None), + }; + let user_games = games.into_iter().filter(|g| g.gm == user.id).collect(); + ok(Some(UserProfile { + id: user.id, + name: user.name, + games: user_games, + is_admin: user.admin, + })) } pub async fn create_user(&self, username: &str) -> ResultExt { @@ -142,11 +152,11 @@ impl Core { } } - pub async fn list_games(&self) -> ResultExt, AppError, FatalError> { - let games = self.0.write().await.db.games().await; + pub async fn list_games(&self) -> ResultExt, AppError, FatalError> { + let games = self.0.read().await.db.games().await; match games { // Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()), - Ok(_games) => unimplemented!(), + Ok(games) => ok(games.into_iter().map(GameOverview::from).collect()), Err(err) => fatal(err), } } diff --git a/visions/server/src/database/disk_db.rs b/visions/server/src/database/disk_db.rs index b14368e..5cc98eb 100644 --- a/visions/server/src/database/disk_db.rs +++ b/visions/server/src/database/disk_db.rs @@ -1,14 +1,19 @@ use std::path::Path; -use async_std::channel::{bounded, Receiver, Sender}; +use async_std::channel::Receiver; 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 crate::{ + database::{DatabaseResponse, Request}, + types::FatalError, +}; -use super::{types::GameId, CharacterId, CharsheetRow, DatabaseRequest, SessionId, UserId, UserRow}; +use super::{ + types::GameId, CharacterId, CharsheetRow, DatabaseRequest, GameRow, SessionId, UserId, UserRow +}; static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); @@ -142,7 +147,13 @@ impl DiskDb { Ok(items) } - pub fn save_game(&self, game_id: Option, gm: &UserId, game_type: &str, 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(); @@ -150,7 +161,8 @@ impl DiskDb { .conn .prepare("INSERT INTO games VALUES (?, ?, ?, ?)") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((game_id.as_str(), gm.as_str(), game_type, name)).unwrap(); + stmt.execute((game_id.as_str(), gm.as_str(), game_type, name)) + .unwrap(); Ok(game_id) } Some(game_id) => { @@ -158,12 +170,33 @@ impl DiskDb { .conn .prepare("UPDATE games SET gm=? game_type=? name=? WHERE uuid=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((gm.as_str(), game_type, name, game_id.as_str())).unwrap(); + stmt.execute((gm.as_str(), game_type, name, game_id.as_str())) + .unwrap(); Ok(game_id) } } } + pub fn games(&self) -> Result, FatalError> { + let mut stmt = self + .conn + .prepare("SELECT * FROM games") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + let items = stmt + .query_map([], |row| { + Ok(GameRow { + id: row.get(0).unwrap(), + gm: row.get(1).unwrap(), + game_type: row.get(2).unwrap(), + name: row.get(3).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); + Ok(items) + } + 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 = ?") @@ -274,7 +307,7 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver) { Ok(sheet) => { tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap(); } - _ => unimplemented!(), + _ => unimplemented!("errors for Charsheet"), } } Request::CreateSession(id) => { @@ -284,15 +317,20 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver) { .unwrap(); } Request::Games => { - unimplemented!(); + match db.games() { + Ok(games) => tx.send(DatabaseResponse::Games(games)).await.unwrap(), + _ => unimplemented!("errors for Request::Games"), + } } Request::Game(_game_id) => { - unimplemented!(); + unimplemented!("Request::Game handler"); } 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(); } + Ok(game_id) => { + tx.send(DatabaseResponse::SaveGame(game_id)).await.unwrap(); + } err => panic!("{:?}", err), } } diff --git a/visions/server/src/database/mod.rs b/visions/server/src/database/mod.rs index e46456e..02b040b 100644 --- a/visions/server/src/database/mod.rs +++ b/visions/server/src/database/mod.rs @@ -45,7 +45,7 @@ enum DatabaseResponse { #[async_trait] pub trait Database: Send + Sync { - async fn users(&mut self) -> Result, FatalError>; + async fn users(&self) -> Result, FatalError>; async fn user(&self, _: &UserId) -> Result, FatalError>; @@ -60,7 +60,7 @@ pub trait Database: Send + Sync { enabled: bool, ) -> Result; - async fn games(&mut self) -> Result, FatalError>; + async fn games(&self) -> Result, FatalError>; async fn game(&self, _: &GameId) -> Result, FatalError>; @@ -72,7 +72,7 @@ pub trait Database: Send + Sync { game_name: &str, ) -> Result; - async fn character(&mut self, id: &CharacterId) -> Result, FatalError>; + async fn character(&self, id: &CharacterId) -> Result, FatalError>; async fn session(&self, id: &SessionId) -> Result, FatalError>; @@ -123,7 +123,7 @@ macro_rules! send_request { #[async_trait] impl Database for DbConn { - async fn users(&mut self) -> Result, FatalError> { + async fn users(&self) -> Result, FatalError> { send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst)) } @@ -154,7 +154,7 @@ impl Database for DbConn { DatabaseResponse::SaveUser(user_id) => Ok(user_id)) } - async fn games(&mut self) -> Result, FatalError> { + async fn games(&self) -> Result, FatalError> { send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst)) } @@ -172,7 +172,7 @@ impl Database for DbConn { 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> { + async fn character(&self, id: &CharacterId) -> Result, FatalError> { send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row)) } diff --git a/visions/server/src/handlers/mod.rs b/visions/server/src/handlers/mod.rs index 5448961..d2d7056 100644 --- a/visions/server/src/handlers/mod.rs +++ b/visions/server/src/handlers/mod.rs @@ -1,44 +1,12 @@ -pub mod game_management; -pub use game_management::CreateGameRequest; +mod game_management; +mod user_management; +pub use game_management::*; +pub use user_management::*; -use std::future::Future; - -use axum::{ - http::{HeaderMap, StatusCode}, - Json, -}; -use futures::{SinkExt, StreamExt}; -use result_extended::{error, ok, return_error, ResultExt}; +use result_extended::ResultExt; use serde::{Deserialize, Serialize}; -use typeshare::typeshare; -use crate::{ - asset_db::AssetId, - core::Core, - database::{CharacterId, SessionId, UserId}, - types::{AppError, FatalError, User}, -}; - -async fn check_session( - core: &Core, - headers: HeaderMap, -) -> ResultExt, AppError, FatalError> { - match headers.get("Authorization") { - Some(token) => { - match token - .to_str() - .unwrap() - .split(" ") - .collect::>() - .as_slice() - { - [_schema, token] => core.session(&SessionId::from(token.to_owned())).await, - _ => error(AppError::BadRequest), - } - } - None => ok(None), - } -} +use crate::core::Core; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct HealthCheck { @@ -56,213 +24,7 @@ pub async fn healthcheck(core: Core) -> Vec { } } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[typeshare] -pub struct AuthRequest { - pub username: String, - pub password: String, -} - -pub async fn check_password( - core: Core, - req: Json, -) -> (StatusCode, Json>) { - let Json(AuthRequest { username, password }) = req; - match core.auth(&username, &password).await { - ResultExt::Ok(session_id) => (StatusCode::OK, Json(Some(session_id))), - ResultExt::Err(_err) => (StatusCode::UNAUTHORIZED, Json(None)), - ResultExt::Fatal(err) => panic!("Fatal: {}", err), - } -} - -pub async fn auth_required( - core: Core, - headers: HeaderMap, - f: F, -) -> (StatusCode, Json>) -where - F: FnOnce(User) -> Fut, - Fut: Future>)>, -{ - match check_session(&core, headers).await { - ResultExt::Ok(Some(user)) => f(user).await, - ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(err) => panic!("{}", err), - } -} - -pub async fn admin_required( - core: Core, - headers: HeaderMap, - f: F, -) -> (StatusCode, Json>) -where - F: FnOnce(User) -> Fut, - Fut: Future>)>, -{ - match check_session(&core, headers).await { - ResultExt::Ok(Some(user)) => { - if user.admin { - f(user).await - } else { - (StatusCode::FORBIDDEN, Json(None)) - } - } - ResultExt::Ok(None) => (StatusCode::UNAUTHORIZED, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(err) => panic!("{}", err), - } -} - -#[derive(Deserialize, Serialize)] -#[typeshare] -pub struct UserProfile { - pub userid: UserId, - pub username: String, - pub is_admin: bool, -} - -pub async fn get_user( - core: Core, - headers: HeaderMap, - user_id: Option, -) -> (StatusCode, Json>) { - auth_required(core.clone(), headers, |user| async move { - match user_id { - Some(user_id) => match core.user(user_id).await { - ResultExt::Ok(Some(user)) => ( - StatusCode::OK, - Json(Some(UserProfile { - userid: UserId::from(user.id), - username: user.name, - is_admin: user.admin, - })), - ), - ResultExt::Ok(None) => (StatusCode::NOT_FOUND, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(err) => panic!("{}", err), - }, - None => ( - StatusCode::OK, - Json(Some(UserProfile { - userid: UserId::from(user.id), - username: user.name, - is_admin: user.admin, - })), - ), - } - }) - .await -} - -#[derive(Deserialize, Serialize)] -#[typeshare] -pub struct CreateUserRequest { - pub username: String, -} - -pub async fn create_user( - core: Core, - headers: HeaderMap, - req: CreateUserRequest, -) -> (StatusCode, Json>) { - admin_required(core.clone(), headers, |_admin| async { - match core.create_user(&req.username).await { - ResultExt::Ok(_) => (StatusCode::OK, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(fatal) => panic!("{}", fatal), - } - }) - .await -} - -#[derive(Deserialize, Serialize)] -#[typeshare] -pub struct SetPasswordRequest { - pub password_1: String, - pub password_2: String, -} - -pub async fn set_password( - core: Core, - headers: HeaderMap, - req: SetPasswordRequest, -) -> (StatusCode, Json>) { - auth_required(core.clone(), headers, |user| async { - if req.password_1 == req.password_2 { - match core.set_password(user.id, req.password_1).await { - ResultExt::Ok(_) => (StatusCode::OK, Json(None)), - ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), - ResultExt::Fatal(fatal) => panic!("{}", fatal), - } - } else { - (StatusCode::BAD_REQUEST, Json(None)) - } - }) - .await -} - /* -pub async fn handle_auth( - auth_ctx: &AuthDB, - auth_token: AuthToken, -) -> Result, Error> { - match auth_ctx.authenticate(auth_token).await { - Ok(Some(session)) => match serde_json::to_string(&session) { - Ok(session_token) => Response::builder() - .status(StatusCode::OK) - .body(session_token), - Err(_) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body("".to_owned()), - }, - Ok(None) => Response::builder() - .status(StatusCode::UNAUTHORIZED) - .body("".to_owned()), - Err(_) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body("".to_owned()), - } -} -*/ - -/* -pub async fn handler(f: F) -> impl Reply -where - F: Future>, AppError, FatalError>>, -{ - match f.await { - ResultExt::Ok(response) => response, - ResultExt::Err(AppError::NotFound(_)) => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(vec![]) - .unwrap(), - ResultExt::Err(err) => { - println!("request error: {:?}", err); - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(vec![]) - .unwrap() - } - ResultExt::Fatal(err) => { - panic!("Shutting down with fatal error: {:?}", err); - } - } -} - -pub async fn handle_server_status(core: Core) -> impl Reply { - handler(async move { - let status = return_error!(core.status().await); - ok(Response::builder() - .header("Access-Control-Allow-Origin", "*") - .header("Content-Type", "application/json") - .body(serde_json::to_vec(&status).unwrap()) - .unwrap()) - }) - .await -} - pub async fn handle_file(core: Core, asset_id: AssetId) -> impl Reply { handler(async move { let (mime, bytes) = return_error!(core.get_asset(asset_id).await); @@ -453,27 +215,4 @@ pub async fn handle_set_admin_password(core: Core, password: String) -> impl Rep }) .await } - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[typeshare] -pub struct AuthRequest { - username: String, - password: String, -} - -pub async fn handle_check_password(core: Core, auth_request: AuthRequest) -> impl Reply { - handler(async move { - let session_id = return_error!( - core.auth(&auth_request.username, &auth_request.password) - .await - ); - println!("handle_check_password: {:?}", session_id); - - ok(Response::builder() - .header("Content-Type", "application/json") - .body(serde_json::to_vec(&session_id).unwrap()) - .unwrap()) - }) - .await -} */ diff --git a/visions/server/src/handlers/user_management.rs b/visions/server/src/handlers/user_management.rs index 1634b01..3a30cfe 100644 --- a/visions/server/src/handlers/user_management.rs +++ b/visions/server/src/handlers/user_management.rs @@ -1,7 +1,17 @@ +use axum::{ + http::{HeaderMap, StatusCode}, + Json, +}; +use futures::Future; +use result_extended::{error, ok, ResultExt}; use serde::{Deserialize, Serialize}; +use typeshare::typeshare; -use crate::database::UserId; - +use crate::{ + core::Core, + database::{SessionId, UserId}, + types::{AppError, FatalError, User, UserProfile}, +}; #[derive(Clone, Debug, Deserialize, Serialize)] #[typeshare] @@ -104,14 +114,7 @@ pub async fn get_user( auth_required(core.clone(), headers, |user| async move { match user_id { Some(user_id) => match core.user(user_id).await { - ResultExt::Ok(Some(user)) => ( - StatusCode::OK, - Json(Some(UserProfile { - userid: UserId::from(user.id), - username: user.name, - is_admin: user.admin, - })), - ), + ResultExt::Ok(Some(user)) => (StatusCode::OK, Json(Some(user))), ResultExt::Ok(None) => (StatusCode::NOT_FOUND, Json(None)), ResultExt::Err(_err) => (StatusCode::BAD_REQUEST, Json(None)), ResultExt::Fatal(err) => panic!("{}", err), @@ -119,9 +122,10 @@ pub async fn get_user( None => ( StatusCode::OK, Json(Some(UserProfile { - userid: UserId::from(user.id), - username: user.name, + id: UserId::from(user.id), + name: user.name, is_admin: user.admin, + games: vec![], })), ), } @@ -162,5 +166,3 @@ pub async fn set_password( }) .await } - - diff --git a/visions/server/src/routes.rs b/visions/server/src/routes.rs index fd9703d..76150ef 100644 --- a/visions/server/src/routes.rs +++ b/visions/server/src/routes.rs @@ -1,8 +1,6 @@ -use std::fmt; - use axum::{ extract::Path, - http::{HeaderMap, StatusCode}, + http::HeaderMap, routing::{get, post, put}, Json, Router, }; @@ -11,7 +9,8 @@ use crate::{ core::Core, database::UserId, handlers::{ - check_password, create_user, game_management::create_game, get_user, healthcheck, set_password, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest + check_password, create_game, create_user, get_user, healthcheck, set_password, AuthRequest, + CreateGameRequest, CreateUserRequest, SetPasswordRequest, }, }; @@ -92,7 +91,8 @@ mod test { asset_db::FsAssets, core::Core, database::{Database, DbConn, GameId, SessionId, UserId}, - handlers::{CreateGameRequest, UserProfile}, + handlers::CreateGameRequest, + types::UserProfile, }; fn setup_without_admin() -> (Core, TestServer) { @@ -230,8 +230,8 @@ mod test { response.assert_status_ok(); let profile: Option = response.json(); let profile = profile.unwrap(); - assert_eq!(profile.userid, UserId::from("admin")); - assert_eq!(profile.username, "admin"); + assert_eq!(profile.id, UserId::from("admin")); + assert_eq!(profile.name, "admin"); } #[tokio::test] @@ -269,7 +269,7 @@ mod test { response.assert_status_ok(); let profile: Option = response.json(); let profile = profile.unwrap(); - assert_eq!(profile.username, "savanni"); + assert_eq!(profile.name, "savanni"); let response = server .get("/api/v1/user/admin") @@ -278,7 +278,7 @@ mod test { response.assert_status_ok(); let profile: Option = response.json(); let profile = profile.unwrap(); - assert_eq!(profile.username, "admin"); + assert_eq!(profile.name, "admin"); } #[tokio::test] @@ -300,7 +300,7 @@ mod test { .add_header("Authorization", format!("Bearer {}", session_id)) .await; let profile = response.json::>().unwrap(); - assert_eq!(profile.username, "savanni"); + assert_eq!(profile.name, "savanni"); let response = server .put("/api/v1/user/password") diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index a9d5f84..2e892b2 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -3,7 +3,10 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use typeshare::typeshare; -use crate::{asset_db::AssetId, database::{GameId, UserId, UserRow}}; +use crate::{ + asset_db::AssetId, + database::{GameId, GameRow, UserId, UserRow}, +}; #[derive(Debug, Error)] pub enum FatalError { @@ -111,7 +114,8 @@ pub struct Player { pub struct Game { pub id: String, pub name: String, - pub players: Vec, + pub gm: UserId, + pub players: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -133,7 +137,7 @@ pub enum Message { #[typeshare] pub struct UserProfile { pub id: UserId, - pub username: String, + pub name: String, pub games: Vec, pub is_admin: bool, } @@ -144,6 +148,18 @@ pub struct GameOverview { pub id: GameId, pub game_type: String, pub game_name: String, + pub gm: UserId, + pub players: Vec, } - +impl From for GameOverview { + fn from(row: GameRow) -> Self { + Self { + id: row.id, + gm: row.gm, + game_type: row.game_type, + game_name: row.name, + players: vec![], + } + } +}