From e8bc0590c6562c9b58d6771b328516f143f377db Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 1 Dec 2024 14:07:37 -0500 Subject: [PATCH] Make the interface to show users in the system --- visions/server/src/core.rs | 14 +++++- visions/server/src/database.rs | 46 ++++++++++++++++++- visions/server/src/handlers.rs | 24 +++++++++- visions/server/src/main.rs | 10 +++- visions/ui/src/App.tsx | 9 +++- visions/ui/src/client.ts | 6 +++ visions/ui/src/views/Admin/Admin.css | 0 visions/ui/src/views/Admin/Admin.tsx | 17 +++++++ .../ui/src/views/PlayerView/PlayerView.tsx | 4 +- 9 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 visions/ui/src/views/Admin/Admin.css create mode 100644 visions/ui/src/views/Admin/Admin.tsx diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 72bc4a1..0e0af06 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -8,7 +8,7 @@ use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database, Error}, + database::{CharacterId, Database, Error, UserId}, types::{AppError, FatalError, Message, Tabletop, RGB}, }; @@ -81,6 +81,18 @@ impl Core { } } + 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::Err(err) => { + println!("Database error: {:?}", err); + ResultExt::Ok(vec![]) + } + ResultExt::Fatal(users) => ResultExt::Fatal(users), + } + } + pub async fn tabletop(&self) -> Tabletop { self.0.read().await.tabletop.clone() } diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index 57b7dec..4b637cc 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -29,6 +29,7 @@ pub enum Error { #[derive(Debug)] enum Request { Charsheet(CharacterId), + Users, } #[derive(Debug)] @@ -39,6 +40,7 @@ struct DatabaseRequest { #[derive(Debug)] enum DatabaseResponse { + Users(Vec<(UserId, String)>), Charsheet(Option), } @@ -142,6 +144,8 @@ pub struct CharsheetRow { #[async_trait] pub trait Database: Send + Sync { + async fn users(&mut self) -> result_extended::ResultExt, Error, FatalError>; + async fn character( &mut self, id: CharacterId, @@ -240,6 +244,17 @@ 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, @@ -374,6 +389,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { _ => unimplemented!(), } } + Request::Users => { + let users = db.users(); + match users { + Ok(users) => { + tx.send(DatabaseResponse::Users(users)).await.unwrap(); + } + _ => unimplemented!(), + } + } } } println!("ending db_handler"); @@ -402,6 +426,26 @@ impl DbConn { #[async_trait] impl Database for DbConn { + async fn users(&mut self) -> ResultExt, Error, FatalError> { + let (tx, rx) = bounded::(1); + + let request = DatabaseRequest { + tx, + req: Request::Users, + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return fatal(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::Users(lst)) => ok(lst), + Ok(_) => fatal(FatalError::MessageMismatch), + Err(_) => error(Error::NoResponse), + } + } + async fn character( &mut self, id: CharacterId, @@ -420,7 +464,7 @@ impl Database for DbConn { match rx.recv().await { Ok(DatabaseResponse::Charsheet(row)) => ok(row), - // Ok(_) => fatal(FatalError::MessageMismatch), + Ok(_) => fatal(FatalError::MessageMismatch), Err(_err) => error(Error::NoResponse), } } diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index ed8a488..0b7bcf4 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -5,7 +5,12 @@ use result_extended::{ok, return_error, ResultExt}; use serde::{Deserialize, Serialize}; use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message}; -use crate::{asset_db::AssetId, core::Core, database::CharacterId, types::{AppError, FatalError}}; +use crate::{ + asset_db::AssetId, + core::Core, + database::CharacterId, + types::{AppError, FatalError}, +}; /* pub async fn handle_auth( @@ -164,6 +169,23 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl .await } +pub async fn handle_get_users(core: Core) -> impl Reply { + handler(async move { + let users = match core.list_users().await { + ResultExt::Ok(users) => users, + 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(&users).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/main.rs b/visions/server/src/main.rs index d695fdc..71376ff 100644 --- a/visions/server/src/main.rs +++ b/visions/server/src/main.rs @@ -8,7 +8,7 @@ use asset_db::{AssetId, FsAssets}; use authdb::AuthError; use database::DbConn; use handlers::{ - handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest + handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest }; use warp::{ // header, @@ -160,6 +160,13 @@ pub async fn main() { }) .with(log); + let route_get_users = warp::path!("api" / "v1" / "users") + .and(warp::get()) + .then({ + let core = core.clone(); + move || handle_get_users(core.clone()) + }); + let route_get_charsheet = warp::path!("api" / "v1" / "charsheet" / String) .and(warp::get()) .then({ @@ -174,6 +181,7 @@ pub async fn main() { .or(route_available_images) .or(route_set_bg_image_options) .or(route_set_bg_image) + .or(route_get_users) .or(route_get_charsheet) .recover(handle_rejection); diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index cd6db11..98626e8 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -6,8 +6,11 @@ import { DesignPage } from './views/Design/Design'; import { GmView } from './views/GmView/GmView'; import { WebsocketProvider } from './components/WebsocketProvider'; import { PlayerView } from './views/PlayerView/PlayerView'; +import { Admin } from './views/Admin/Admin'; import Candela from './plugins/Candela'; +const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; + interface AppProps { client: Client; } @@ -15,7 +18,7 @@ interface AppProps { const CandelaCharsheet = ({ client }: { client: Client }) => { let [sheet, setSheet] = useState(undefined); useEffect( - () => { client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => setSheet(c)); }, + () => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)); }, [client, setSheet] ); @@ -36,6 +39,10 @@ const App = ({ client }: AppProps) => { path: "/gm", element: websocketUrl ? :
}, + { + path: "/admin", + element: + }, { path: "/", element: websocketUrl ? :
diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index b5f43b9..3b8473f 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -45,6 +45,12 @@ export class Client { return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) }); } + async users() { + const url = new URL(this.base); + url.pathname = '/api/v1/users/'; + return fetch(url).then((response) => response.json()); + } + async charsheet(id: string) { const url = new URL(this.base); url.pathname = `/api/v1/charsheet/${id}`; diff --git a/visions/ui/src/views/Admin/Admin.css b/visions/ui/src/views/Admin/Admin.css new file mode 100644 index 0000000..e69de29 diff --git a/visions/ui/src/views/Admin/Admin.tsx b/visions/ui/src/views/Admin/Admin.tsx new file mode 100644 index 0000000..edac585 --- /dev/null +++ b/visions/ui/src/views/Admin/Admin.tsx @@ -0,0 +1,17 @@ +import React, { useEffect, useState } from 'react'; +import { Client } from '../../client'; + +interface AdminProps { + client: Client, +} + +export const Admin = ({ client }: AdminProps) => { + const [users, setUsers] = useState([]); + + useEffect(() => { + client.users().then(setUsers); + }, [client, setUsers]); + return
    + {users.map(([uuid, username]) =>
  • {username}
  • ) } +
; +} diff --git a/visions/ui/src/views/PlayerView/PlayerView.tsx b/visions/ui/src/views/PlayerView/PlayerView.tsx index 9f12c60..a77125d 100644 --- a/visions/ui/src/views/PlayerView/PlayerView.tsx +++ b/visions/ui/src/views/PlayerView/PlayerView.tsx @@ -5,6 +5,8 @@ import { Client } from '../../client'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; import Candela from '../../plugins/Candela'; +const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; + interface PlayerViewProps { client: Client; } @@ -16,7 +18,7 @@ export const PlayerView = ({ client }: PlayerViewProps) => { useEffect( () => { - client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => { + client.charsheet(TEST_CHARSHEET_UUID).then((c) => { setCharsheet(c) }); },