From e505c21bc870e022b26197eb0fbf93d357bdf0a2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 10 Dec 2024 22:43:15 -0500 Subject: [PATCH] Set up an admin panel that shows the list of users --- visions/server/src/core.rs | 28 ++++++- visions/server/src/database.rs | 111 +++++++++++++++++++++------ visions/server/src/handlers.rs | 17 ++++ visions/server/src/types.rs | 49 +++++++++++- visions/ui/src/views/Admin/Admin.tsx | 42 ++++++++-- 5 files changed, 212 insertions(+), 35 deletions(-) diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 0e0af06..158be86 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, database::{CharacterId, Database, Error, UserId}, - types::{AppError, FatalError, Message, Tabletop, RGB}, + types::{AppError, FatalError, Game, Message, Tabletop, User, RGB}, }; const DEFAULT_BACKGROUND_COLOR: RGB = RGB { @@ -81,10 +81,12 @@ impl Core { } } - pub async fn list_users(&self) -> ResultExt, AppError, FatalError> { + pub async fn list_users(&self) -> ResultExt, AppError, FatalError> { let users = self.0.write().await.db.users().await; match users { - ResultExt::Ok(users) => ResultExt::Ok(users), + ResultExt::Ok(users) => { + ResultExt::Ok(users.into_iter().map(|u| User::from(u)).collect()) + } ResultExt::Err(err) => { println!("Database error: {:?}", err); ResultExt::Ok(vec![]) @@ -93,6 +95,21 @@ impl Core { } } + pub async fn list_games(&self) -> ResultExt, AppError, FatalError> { + let games = self.0.write().await.db.games().await; + match games { + ResultExt::Ok(games) => { + // ResultExt::Ok(games.into_iter().map(|u| Game::from(u)).collect()) + unimplemented!(); + } + ResultExt::Err(err) => { + println!("Database error: {:?}", err); + ResultExt::Ok(vec![]) + } + ResultExt::Fatal(games) => ResultExt::Fatal(games), + } + } + pub async fn tabletop(&self) -> Tabletop { self.0.read().await.tabletop.clone() } @@ -244,7 +261,10 @@ mod test { #[tokio::test] async fn it_can_change_the_tabletop_background() { let core = test_core(); - assert_matches!(core.set_background_image(AssetId::from("asset_1")).await, ResultExt::Ok(())); + assert_matches!( + core.set_background_image(AssetId::from("asset_1")).await, + ResultExt::Ok(()) + ); assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, Some(AssetId::from("asset_1"))); diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index 4b637cc..be4b75b 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use include_dir::{include_dir, Dir}; use lazy_static::lazy_static; use result_extended::{error, fatal, ok, return_error, ResultExt}; -use rusqlite::Connection; +use rusqlite::{types::{FromSql, FromSqlResult, ValueRef}, Connection}; use rusqlite_migration::Migrations; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -29,6 +29,7 @@ pub enum Error { #[derive(Debug)] enum Request { Charsheet(CharacterId), + Games, Users, } @@ -40,8 +41,9 @@ struct DatabaseRequest { #[derive(Debug)] enum DatabaseResponse { - Users(Vec<(UserId, String)>), Charsheet(Option), + Games(Vec), + Users(Vec), } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] @@ -69,6 +71,15 @@ impl From for UserId { } } +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 GameId(String); @@ -94,6 +105,15 @@ impl From for GameId { } } +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); @@ -119,32 +139,49 @@ impl From for CharacterId { } } +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 { - id: String, - name: String, - password: String, - admin: bool, - enabled: bool, + pub id: UserId, + pub name: String, + pub password: String, + pub admin: bool, + pub enabled: bool, } #[derive(Clone, Debug)] pub struct Role { - userid: String, - gameid: String, + 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: String, + game: GameId, pub data: serde_json::Value, } #[async_trait] pub trait Database: Send + Sync { - async fn users(&mut self) -> result_extended::ResultExt, Error, FatalError>; + async fn users(&mut self) -> result_extended::ResultExt, Error, FatalError>; + + async fn games(&mut self) -> result_extended::ResultExt, Error, FatalError>; async fn character( &mut self, @@ -219,6 +256,21 @@ impl DiskDb { Ok(DiskDb { conn }) } + 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 user(&self, id: UserId) -> Result, FatalError> { let mut stmt = self .conn @@ -244,17 +296,6 @@ impl DiskDb { } } - 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| { - let userid: String = row.get(0).unwrap(); - let username = row.get(1).unwrap(); - Ok((UserId::from(userid), username)) - }).unwrap().collect::, rusqlite::Error>>().unwrap(); - Ok(items) - } - fn save_user( &self, user_id: Option, @@ -375,7 +416,6 @@ impl DiskDb { } async fn db_handler(db: DiskDb, requestor: Receiver) { - println!("Starting db_handler"); while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await { println!("Request received: {:?}", req); match req { @@ -389,6 +429,9 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { _ => unimplemented!(), } } + Request::Games => { + unimplemented!(); + } Request::Users => { let users = db.users(); match users { @@ -426,7 +469,7 @@ impl DbConn { #[async_trait] impl Database for DbConn { - async fn users(&mut self) -> ResultExt, Error, FatalError> { + async fn users(&mut self) -> ResultExt, Error, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { @@ -446,6 +489,26 @@ impl Database for DbConn { } } + async fn games(&mut self) -> result_extended::ResultExt, Error, FatalError> { + let (tx, rx) = bounded::(1); + + let request = DatabaseRequest { + tx, + req: Request::Games, + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return fatal(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::Games(lst)) => ok(lst), + Ok(_) => fatal(FatalError::MessageMismatch), + Err(_) => error(Error::NoResponse), + } + } + async fn character( &mut self, id: CharacterId, diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 0b7bcf4..548eba6 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -186,6 +186,23 @@ pub async fn handle_get_users(core: Core) -> impl Reply { .await } +pub async fn handle_get_games(core: Core) -> impl Reply { + handler(async move { + let games = match core.list_games().await { + ResultExt::Ok(games) => games, + ResultExt::Err(err) => return ResultExt::Err(err), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), + }; + + ok(Response::builder() + .header("Access-Control-Allow-Origin", "*") + .header("Content-Type", "application/json") + .body(serde_json::to_vec(&games).unwrap()) + .unwrap()) + }) + .await +} + pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply { handler(async move { let sheet = match core.get_charsheet(CharacterId::from(charid)).await { diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index 3372703..c453537 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use typeshare::typeshare; -use crate::asset_db::AssetId; +use crate::{asset_db::AssetId, database::UserRow}; #[derive(Debug, Error)] pub enum FatalError { @@ -49,6 +49,53 @@ pub struct RGB { pub blue: u32, } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct User { + pub id: String, + pub name: String, + pub password: String, + pub admin: bool, + pub enabled: bool, +} + +impl From for User { + fn from(row: UserRow) -> Self { + Self { + id: row.id.as_str().to_owned(), + name: row.name.to_owned(), + password: row.password.to_owned(), + admin: row.admin, + enabled: row.enabled, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[typeshare] +pub enum PlayerRole { + Gm, + Player, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct Player { + user_id: String, + role: PlayerRole, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct Game { + pub id: String, + pub name: String, + pub players: Vec, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[typeshare] diff --git a/visions/ui/src/views/Admin/Admin.tsx b/visions/ui/src/views/Admin/Admin.tsx index edac585..086faf5 100644 --- a/visions/ui/src/views/Admin/Admin.tsx +++ b/visions/ui/src/views/Admin/Admin.tsx @@ -1,17 +1,47 @@ import React, { useEffect, useState } from 'react'; +import { Game, User } from 'visions-types'; import { Client } from '../../client'; +interface UserRowProps { + user: User, +} + +const UserRow = ({ user }: UserRowProps) => { + return ( + {user.name} + {user.admin && "admin"} + {user.enabled && "enabled"} + ); +} + +interface GameRowProps { + game: Game, +} + +const GameRow = ({ game }: GameRowProps) => { + return ( + {game.name} + ); +} + interface AdminProps { client: Client, } export const Admin = ({ client }: AdminProps) => { - const [users, setUsers] = useState([]); + const [users, setUsers] = useState>([]); useEffect(() => { - client.users().then(setUsers); - }, [client, setUsers]); - return
    - {users.map(([uuid, username]) =>
  • {username}
  • ) } -
; + client.users().then((u) => { + console.log(u); + setUsers(u); + }); + }, [client]); + + console.log(users); + return ( + + {users.map((user) => )} + +
); }