From afb510d92e4d6322b722a1585757909cbc530b8f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sat, 30 Nov 2024 23:03:52 -0500 Subject: [PATCH 01/12] Set up new tables to handle users and roles --- visions/server/migrations/02-users/up.sql | 16 ++++ visions/server/src/database.rs | 109 ++++++++++++++++++++-- 2 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 visions/server/migrations/02-users/up.sql diff --git a/visions/server/migrations/02-users/up.sql b/visions/server/migrations/02-users/up.sql new file mode 100644 index 0000000..e8043e8 --- /dev/null +++ b/visions/server/migrations/02-users/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE users( + uuid TEXT PRIMARY KEY, + name TEXT, + password TEXT, + admin BOOLEAN, + enabled BOOLEAN +); + +CREATE TABLE roles( + user_id TEXT, + game_id TEXT, + role TEXT, + + FOREIGN KEY(user_id) REFERENCES users(uuid), + FOREIGN KEY(game_id) REFERENCES games(uuid) +); diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index f2a8b7c..df077e0 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -48,12 +48,62 @@ enum DatabaseResponse { Charsheet(Option), } +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct UserId(String); + +impl UserId { + pub fn new() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } + + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + +impl From<&str> for UserId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for UserId { + fn from(s: String) -> Self { + Self(s) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct GameId(String); + +impl GameId { + pub fn new() -> Self { + Self(format!("{}", Uuid::new_v4().hyphenated())) + } + + pub fn as_str<'a>(&'a self) -> &'a str { + &self.0 + } +} + +impl From<&str> for GameId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for GameId { + fn from(s: String) -> Self { + Self(s) + } +} + #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct CharacterId(String); impl CharacterId { pub fn new() -> Self { - CharacterId(format!("{}", Uuid::new_v4().hyphenated())) + Self(format!("{}", Uuid::new_v4().hyphenated())) } pub fn as_str<'a>(&'a self) -> &'a str { @@ -63,20 +113,36 @@ impl CharacterId { impl From<&str> for CharacterId { fn from(s: &str) -> Self { - CharacterId(s.to_owned()) + Self(s.to_owned()) } } impl From for CharacterId { fn from(s: String) -> Self { - CharacterId(s) + Self(s) } } +#[derive(Clone, Debug)] +pub struct UserRow { + id: String, + name: String, + password: String, + admin: bool, + enabled: bool, +} + +#[derive(Clone, Debug)] +pub struct Role { + userid: String, + gameid: String, + role: String, +} + #[derive(Clone, Debug)] pub struct CharsheetRow { id: String, - gametype: String, + game: String, pub data: serde_json::Value, } @@ -93,11 +159,20 @@ fn setup_test_database(conn: &Connection) { let mut gamecount_stmt = conn.prepare("SELECT count(*) FROM games").unwrap(); let mut count = gamecount_stmt.query([]).unwrap(); if count.next().unwrap().unwrap().get::(0) == Ok(0) { + let admin_id = format!("{}", Uuid::new_v4()); + let user_id = format!("{}", Uuid::new_v4()); let game_id = format!("{}", Uuid::new_v4()); let char_id = CharacterId::new(); + let mut user_stmt = conn.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)").unwrap(); + user_stmt.execute((admin_id.clone(), "admin", "abcdefg", true, true)).unwrap(); + user_stmt.execute((user_id.clone(), "savanni", "abcdefg", false, true)).unwrap(); + let mut game_stmt = conn.prepare("INSERT INTO games VALUES (?, ?)").unwrap(); - game_stmt.execute((game_id.clone(), "Circle of Bluest Sky")); + game_stmt.execute((game_id.clone(), "Circle of Bluest Sky")).unwrap(); + + let mut role_stmt = conn.prepare("INSERT INTO roles VALUES (?, ?, ?)").unwrap(); + role_stmt.execute((user_id.clone(), game_id.clone(), "gm")).unwrap(); let mut sheet_stmt = conn .prepare("INSERT INTO characters VALUES (?, ?, ?)") @@ -124,17 +199,37 @@ impl DiskDb { Ok(DiskDb { conn }) } + fn user(&self, id: UserId) -> Result, Error> { + let mut stmt = self + .conn + .prepare("SELECT uuid, name, password, admin, enabled WHERE uuid=?") + .unwrap(); + let items: Vec = stmt + .query_map([id.as_str()], |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(); + match &items[..] { + [] => Ok(None), + [item] => Ok(Some(item.clone())), + _ => unimplemented!(), + } + } + fn charsheet(&self, id: CharacterId) -> Result, Error> { let mut stmt = self .conn - .prepare("SELECT uuid, gametype, data FROM charsheet WHERE uuid=?") + .prepare("SELECT uuid, game, data FROM charsheet WHERE uuid=?") .unwrap(); let items: Vec = stmt .query_map([id.as_str()], |row| { let data: String = row.get(2).unwrap(); Ok(CharsheetRow { id: row.get(0).unwrap(), - gametype: row.get(1).unwrap(), + game: row.get(1).unwrap(), data: serde_json::from_str(&data).unwrap(), }) }) -- 2.44.1 From d7e4293da0cab6dd10f90525ca7ba32d452c3f03 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 1 Dec 2024 00:51:08 -0500 Subject: [PATCH 02/12] Start using ResultExt to improve error handling --- Cargo.lock | 1 + result-extended/src/lib.rs | 106 ++++++++++++------------ visions/server/Cargo.toml | 41 ++++----- visions/server/src/core.rs | 79 ++++++++++-------- visions/server/src/database.rs | 146 ++++++++++++++++++++------------- visions/server/src/handlers.rs | 36 ++++---- visions/server/src/types.rs | 30 ++++++- 7 files changed, 259 insertions(+), 180 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1bcb68c..4ac8da4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4294,6 +4294,7 @@ dependencies = [ "lazy_static", "mime", "mime_guess", + "result-extended", "rusqlite", "rusqlite_migration", "serde 1.0.210", diff --git a/result-extended/src/lib.rs b/result-extended/src/lib.rs index b56be5f..dc8736a 100644 --- a/result-extended/src/lib.rs +++ b/result-extended/src/lib.rs @@ -33,9 +33,9 @@ use std::{error::Error, fmt}; /// statement. pub trait FatalError: Error {} -/// Result represents a return value that might be a success, might be a fatal error, or +/// ResultExt represents a return value that might be a success, might be a fatal error, or /// might be a normal handleable error. -pub enum Result { +pub enum ResultExt { /// The operation was successful Ok(A), /// Ordinary errors. These should be handled and the application should recover gracefully. @@ -45,72 +45,72 @@ pub enum Result { Fatal(FE), } -impl Result { +impl ResultExt { /// Apply an infallible function to a successful value. - pub fn map(self, mapper: O) -> Result + pub fn map(self, mapper: O) -> ResultExt where O: FnOnce(A) -> B, { match self { - Result::Ok(val) => Result::Ok(mapper(val)), - Result::Err(err) => Result::Err(err), - Result::Fatal(err) => Result::Fatal(err), + ResultExt::Ok(val) => ResultExt::Ok(mapper(val)), + ResultExt::Err(err) => ResultExt::Err(err), + ResultExt::Fatal(err) => ResultExt::Fatal(err), } } /// Apply a potentially fallible function to a successful value. /// /// Like `Result.and_then`, the mapping function can itself fail. - pub fn and_then(self, handler: O) -> Result + pub fn and_then(self, handler: O) -> ResultExt where - O: FnOnce(A) -> Result, + O: FnOnce(A) -> ResultExt, { match self { - Result::Ok(val) => handler(val), - Result::Err(err) => Result::Err(err), - Result::Fatal(err) => Result::Fatal(err), + ResultExt::Ok(val) => handler(val), + ResultExt::Err(err) => ResultExt::Err(err), + ResultExt::Fatal(err) => ResultExt::Fatal(err), } } /// Map a normal error from one type to another. This is useful for converting an error from /// one type to another, especially in re-throwing an underlying error. `?` syntax does not /// work with `Result`, so you will likely need to use this a lot. - pub fn map_err(self, mapper: O) -> Result + pub fn map_err(self, mapper: O) -> ResultExt where O: FnOnce(E) -> F, { match self { - Result::Ok(val) => Result::Ok(val), - Result::Err(err) => Result::Err(mapper(err)), - Result::Fatal(err) => Result::Fatal(err), + ResultExt::Ok(val) => ResultExt::Ok(val), + ResultExt::Err(err) => ResultExt::Err(mapper(err)), + ResultExt::Fatal(err) => ResultExt::Fatal(err), } } /// Provide a function to use to recover from (or simply re-throw) an error. - pub fn or_else(self, handler: O) -> Result + pub fn or_else(self, handler: O) -> ResultExt where - O: FnOnce(E) -> Result, + O: FnOnce(E) -> ResultExt, { match self { - Result::Ok(val) => Result::Ok(val), - Result::Err(err) => handler(err), - Result::Fatal(err) => Result::Fatal(err), + ResultExt::Ok(val) => ResultExt::Ok(val), + ResultExt::Err(err) => handler(err), + ResultExt::Fatal(err) => ResultExt::Fatal(err), } } } /// Convert from a normal `Result` type to a `Result` type. The error condition for a `Result` will /// be treated as `Result::Err`, never `Result::Fatal`. -impl From> for Result { +impl From> for ResultExt { fn from(r: std::result::Result) -> Self { match r { - Ok(val) => Result::Ok(val), - Err(err) => Result::Err(err), + Ok(val) => ResultExt::Ok(val), + Err(err) => ResultExt::Err(err), } } } -impl fmt::Debug for Result +impl fmt::Debug for ResultExt where A: fmt::Debug, FE: fmt::Debug, @@ -118,14 +118,14 @@ where { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Result::Ok(val) => f.write_fmt(format_args!("Result::Ok {:?}", val)), - Result::Err(err) => f.write_fmt(format_args!("Result::Err {:?}", err)), - Result::Fatal(err) => f.write_fmt(format_args!("Result::Fatal {:?}", err)), + ResultExt::Ok(val) => f.write_fmt(format_args!("Result::Ok {:?}", val)), + ResultExt::Err(err) => f.write_fmt(format_args!("Result::Err {:?}", err)), + ResultExt::Fatal(err) => f.write_fmt(format_args!("Result::Fatal {:?}", err)), } } } -impl PartialEq for Result +impl PartialEq for ResultExt where A: PartialEq, FE: PartialEq, @@ -133,27 +133,27 @@ where { fn eq(&self, rhs: &Self) -> bool { match (self, rhs) { - (Result::Ok(val), Result::Ok(rhs)) => val == rhs, - (Result::Err(_), Result::Err(_)) => true, - (Result::Fatal(_), Result::Fatal(_)) => true, + (ResultExt::Ok(val), ResultExt::Ok(rhs)) => val == rhs, + (ResultExt::Err(_), ResultExt::Err(_)) => true, + (ResultExt::Fatal(_), ResultExt::Fatal(_)) => true, _ => false, } } } /// Convenience function to create an ok value. -pub fn ok(val: A) -> Result { - Result::Ok(val) +pub fn ok(val: A) -> ResultExt { + ResultExt::Ok(val) } /// Convenience function to create an error value. -pub fn error(err: E) -> Result { - Result::Err(err) +pub fn error(err: E) -> ResultExt { + ResultExt::Err(err) } /// Convenience function to create a fatal value. -pub fn fatal(err: FE) -> Result { - Result::Fatal(err) +pub fn fatal(err: FE) -> ResultExt { + ResultExt::Fatal(err) } /// Return early from the current function if the value is a fatal error. @@ -161,9 +161,9 @@ pub fn fatal(err: FE) -> Result { macro_rules! return_fatal { ($x:expr) => { match $x { - Result::Fatal(err) => return Result::Fatal(err), - Result::Err(err) => Err(err), - Result::Ok(val) => Ok(val), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), + ResultExt::Err(err) => Err(err), + ResultExt::Ok(val) => Ok(val), } }; } @@ -173,9 +173,9 @@ macro_rules! return_fatal { macro_rules! return_error { ($x:expr) => { match $x { - Result::Ok(val) => val, - Result::Err(err) => return Result::Err(err), - Result::Fatal(err) => return Result::Fatal(err), + ResultExt::Ok(val) => val, + ResultExt::Err(err) => return ResultExt::Err(err), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), } }; } @@ -210,19 +210,19 @@ mod test { #[test] fn it_can_map_things() { - let success: Result = ok(15); + let success: ResultExt = ok(15); assert_eq!(ok(16), success.map(|v| v + 1)); } #[test] fn it_can_chain_success() { - let success: Result = ok(15); + let success: ResultExt = ok(15); assert_eq!(ok(16), success.and_then(|v| ok(v + 1))); } #[test] fn it_can_handle_an_error() { - let failure: Result = error(Error::Error); + let failure: ResultExt = error(Error::Error); assert_eq!( ok::(16), failure.or_else(|_| ok(16)) @@ -231,7 +231,7 @@ mod test { #[test] fn early_exit_on_fatal() { - fn ok_func() -> Result { + fn ok_func() -> ResultExt { let value = return_fatal!(ok::(15)); match value { Ok(_) => ok(14), @@ -239,7 +239,7 @@ mod test { } } - fn err_func() -> Result { + fn err_func() -> ResultExt { let value = return_fatal!(error::(Error::Error)); match value { Ok(_) => panic!("shouldn't have gotten here"), @@ -247,7 +247,7 @@ mod test { } } - fn fatal_func() -> Result { + fn fatal_func() -> ResultExt { let _ = return_fatal!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } @@ -259,18 +259,18 @@ mod test { #[test] fn it_can_early_exit_on_all_errors() { - fn ok_func() -> Result { + fn ok_func() -> ResultExt { let value = return_error!(ok::(15)); assert_eq!(value, 15); ok(14) } - fn err_func() -> Result { + fn err_func() -> ResultExt { return_error!(error::(Error::Error)); panic!("failed to bail"); } - fn fatal_func() -> Result { + fn fatal_func() -> ResultExt { return_error!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } diff --git a/visions/server/Cargo.toml b/visions/server/Cargo.toml index 8dc0ed8..0d02396 100644 --- a/visions/server/Cargo.toml +++ b/visions/server/Cargo.toml @@ -6,26 +6,27 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -authdb = { path = "../../authdb/" } -http = { version = "1" } -serde_json = { version = "*" } -serde = { version = "1" } -tokio = { version = "1", features = [ "full" ] } -warp = { version = "0.3" } -mime_guess = "2.0.5" -mime = "0.3.17" -uuid = { version = "1.11.0", features = ["v4"] } -tokio-stream = "0.1.16" -typeshare = "1.0.4" -urlencoding = "2.1.3" -thiserror = "2.0.3" -rusqlite = "0.32.1" -rusqlite_migration = { version = "1.3.1", features = ["from-directory"] } -lazy_static = "1.5.0" -include_dir = "0.7.4" -async-trait = "0.1.83" -futures = "0.3.31" -async-std = "1.13.0" +async-std = { version = "1.13.0" } +async-trait = { version = "0.1.83" } +authdb = { path = "../../authdb/" } +futures = { version = "0.3.31" } +http = { version = "1" } +include_dir = { version = "0.7.4" } +lazy_static = { version = "1.5.0" } +mime = { version = "0.3.17" } +mime_guess = { version = "2.0.5" } +result-extended = { path = "../../result-extended" } +rusqlite = { version = "0.32.1" } +rusqlite_migration = { version = "1.3.1", features = ["from-directory"] } +serde = { version = "1" } +serde_json = { version = "*" } +thiserror = { version = "2.0.3" } +tokio = { version = "1", features = [ "full" ] } +tokio-stream = { version = "0.1.16" } +typeshare = { version = "1.0.4" } +urlencoding = { version = "2.1.3" } +uuid = { version = "1.11.0", features = ["v4"] } +warp = { version = "0.3" } [dev-dependencies] cool_asserts = "2.0.3" diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 9bb4c9f..72bc4a1 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -2,13 +2,14 @@ use std::{collections::HashMap, sync::Arc}; use async_std::sync::RwLock; use mime::Mime; +use result_extended::{fatal, ok, ResultExt}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database}, - types::{AppError, Message, Tabletop, RGB}, + database::{CharacterId, Database, Error}, + types::{AppError, FatalError, Message, Tabletop, RGB}, }; const DEFAULT_BACKGROUND_COLOR: RGB = RGB { @@ -84,17 +85,26 @@ impl Core { self.0.read().await.tabletop.clone() } - pub async fn get_asset(&self, asset_id: AssetId) -> Result<(Mime, Vec), AppError> { - self.0 - .read() - .await - .asset_store - .get(asset_id.clone()) - .map_err(|err| match err { - asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)), - asset_db::Error::Inaccessible => AppError::Inaccessible(format!("{}", asset_id)), - asset_db::Error::UnexpectedError(err) => AppError::Inaccessible(format!("{}", err)), - }) + pub async fn get_asset( + &self, + asset_id: AssetId, + ) -> ResultExt<(Mime, Vec), AppError, FatalError> { + ResultExt::from( + self.0 + .read() + .await + .asset_store + .get(asset_id.clone()) + .map_err(|err| match err { + asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)), + asset_db::Error::Inaccessible => { + AppError::Inaccessible(format!("{}", asset_id)) + } + asset_db::Error::UnexpectedError(err) => { + AppError::Inaccessible(format!("{}", err)) + } + }), + ) } pub async fn available_images(&self) -> Vec { @@ -112,29 +122,29 @@ impl Core { .collect() } - pub async fn set_background_image(&self, asset: AssetId) -> Result<(), AppError> { + pub async fn set_background_image( + &self, + asset: AssetId, + ) -> ResultExt<(), AppError, FatalError> { let tabletop = { let mut state = self.0.write().await; state.tabletop.background_image = Some(asset.clone()); state.tabletop.clone() }; self.publish(Message::UpdateTabletop(tabletop)).await; - Ok(()) + ok(()) } pub async fn get_charsheet( &self, id: CharacterId, - ) -> Result, AppError> { - Ok(self - .0 - .write() - .await - .db - .charsheet(id) - .await - .unwrap() - .map(|cr| cr.data)) + ) -> ResultExt, AppError, FatalError> { + let mut state = self.0.write().await; + let cr = state.db.character(id).await; + cr.map(|cr| cr.map(|cr| cr.data)).or_else(|err| { + println!("Database error: {:?}", err); + ResultExt::Ok(None) + }) } pub async fn publish(&self, message: Message) { @@ -197,14 +207,14 @@ mod test { #[tokio::test] async fn it_lists_available_images() { let core = test_core(); - let image_paths = core.available_images(); + let image_paths = core.available_images().await; assert_eq!(image_paths.len(), 2); } #[tokio::test] async fn it_retrieves_an_asset() { let core = test_core(); - assert_matches!(core.get_asset(AssetId::from("asset_1")).await, Ok((mime, data)) => { + assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => { assert_eq!(mime.type_(), mime::IMAGE); assert_eq!(data, "abcdefg".as_bytes()); }); @@ -213,7 +223,7 @@ mod test { #[tokio::test] async fn it_can_retrieve_the_default_tabletop() { let core = test_core(); - assert_matches!(core.tabletop(), Tabletop{ background_color, background_image } => { + assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, None); }); @@ -222,8 +232,8 @@ 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")), Ok(())); - assert_matches!(core.tabletop(), Tabletop{ background_color, background_image } => { + 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"))); }); @@ -232,10 +242,13 @@ mod test { #[tokio::test] async fn it_sends_notices_to_clients_on_tabletop_change() { let core = test_core(); - let client_id = core.register_client(); - let mut receiver = core.connect_client(client_id); + let client_id = core.register_client().await; + let mut receiver = core.connect_client(client_id).await; - assert_matches!(core.set_background_image(AssetId::from("asset_1")), Ok(())); + assert_matches!( + core.set_background_image(AssetId::from("asset_1")).await, + ResultExt::Ok(()) + ); match receiver.recv().await { Some(Message::UpdateTabletop(Tabletop { background_color, diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index df077e0..406264c 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -1,18 +1,18 @@ -use std::{ - path::{Path, PathBuf}, - thread::JoinHandle, -}; +use std::path::Path; use async_std::channel::{bounded, Receiver, Sender}; 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_migration::Migrations; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +use crate::types::FatalError; + static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); lazy_static! { @@ -22,12 +22,6 @@ lazy_static! { #[derive(Debug, Error)] pub enum Error { - #[error("Duplicate item found for id {0}")] - DuplicateItem(String), - - #[error("Unexpected response for message")] - MessageMismatch, - #[error("No response to request")] NoResponse, } @@ -148,14 +142,17 @@ pub struct CharsheetRow { #[async_trait] pub trait Database: Send + Sync { - async fn charsheet(&mut self, id: CharacterId) -> Result, Error>; + async fn character( + &mut self, + id: CharacterId, + ) -> result_extended::ResultExt, Error, FatalError>; } pub struct DiskDb { conn: Connection, } -fn setup_test_database(conn: &Connection) { +fn setup_test_database(conn: &Connection) -> Result<(), FatalError> { let mut gamecount_stmt = conn.prepare("SELECT count(*) FROM games").unwrap(); let mut count = gamecount_stmt.query([]).unwrap(); if count.next().unwrap().unwrap().get::(0) == Ok(0) { @@ -164,27 +161,43 @@ fn setup_test_database(conn: &Connection) { let game_id = format!("{}", Uuid::new_v4()); let char_id = CharacterId::new(); - let mut user_stmt = conn.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)").unwrap(); - user_stmt.execute((admin_id.clone(), "admin", "abcdefg", true, true)).unwrap(); - user_stmt.execute((user_id.clone(), "savanni", "abcdefg", false, true)).unwrap(); + let mut user_stmt = conn + .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + user_stmt + .execute((admin_id.clone(), "admin", "abcdefg", true, true)) + .unwrap(); + user_stmt + .execute((user_id.clone(), "savanni", "abcdefg", false, true)) + .unwrap(); - let mut game_stmt = conn.prepare("INSERT INTO games VALUES (?, ?)").unwrap(); - game_stmt.execute((game_id.clone(), "Circle of Bluest Sky")).unwrap(); + let mut game_stmt = conn + .prepare("INSERT INTO games VALUES (?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + game_stmt + .execute((game_id.clone(), "Circle of Bluest Sky")) + .unwrap(); - let mut role_stmt = conn.prepare("INSERT INTO roles VALUES (?, ?, ?)").unwrap(); - role_stmt.execute((user_id.clone(), game_id.clone(), "gm")).unwrap(); + let mut role_stmt = conn + .prepare("INSERT INTO roles VALUES (?, ?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + role_stmt + .execute((user_id.clone(), game_id.clone(), "gm")) + .unwrap(); let mut sheet_stmt = conn .prepare("INSERT INTO characters VALUES (?, ?, ?)") - .unwrap(); + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; sheet_stmt.execute((char_id.as_str(), game_id, r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#)) .unwrap(); } + + Ok(()) } impl DiskDb { - pub fn new

(path: Option

) -> Result + pub fn new

(path: Option

) -> Result where P: AsRef, { @@ -192,38 +205,46 @@ impl DiskDb { None => Connection::open(":memory:").expect("to create a memory connection"), Some(path) => Connection::open(path).expect("to create connection"), }; - MIGRATIONS.to_latest(&mut conn).expect("to run migrations"); - setup_test_database(&conn); + MIGRATIONS + .to_latest(&mut conn) + .map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?; + + setup_test_database(&conn)?; Ok(DiskDb { conn }) } - fn user(&self, id: UserId) -> Result, Error> { + fn user(&self, id: UserId) -> Result, FatalError> { let mut stmt = self .conn .prepare("SELECT uuid, name, password, admin, enabled WHERE uuid=?") - .unwrap(); + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt - .query_map([id.as_str()], |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(); + .query_map([id.as_str()], |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(); match &items[..] { [] => Ok(None), [item] => Ok(Some(item.clone())), - _ => unimplemented!(), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), } } - fn charsheet(&self, id: CharacterId) -> Result, Error> { + fn character(&self, id: CharacterId) -> Result, FatalError> { let mut stmt = self .conn - .prepare("SELECT uuid, game, data FROM charsheet WHERE uuid=?") - .unwrap(); + .prepare("SELECT uuid, game, data FROM characters WHERE uuid=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt .query_map([id.as_str()], |row| { let data: String = row.get(2).unwrap(); @@ -239,24 +260,24 @@ impl DiskDb { match &items[..] { [] => Ok(None), [item] => Ok(Some(item.clone())), - _ => unimplemented!(), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), } } - fn save_charsheet( + fn save_character( &self, char_id: Option, game_type: String, - charsheet: serde_json::Value, - ) -> Result { + character: serde_json::Value, + ) -> std::result::Result { match char_id { None => { let char_id = CharacterId::new(); let mut stmt = self .conn - .prepare("INSERT INTO charsheet VALUES (?, ?, ?)") + .prepare("INSERT INTO characters VALUES (?, ?, ?)") .unwrap(); - stmt.execute((char_id.as_str(), game_type, charsheet.to_string())) + stmt.execute((char_id.as_str(), game_type, character.to_string())) .unwrap(); Ok(char_id) @@ -264,9 +285,9 @@ impl DiskDb { Some(char_id) => { let mut stmt = self .conn - .prepare("UPDATE charsheet SET data=? WHERE uuid=?") + .prepare("UPDATE characters SET data=? WHERE uuid=?") .unwrap(); - stmt.execute((charsheet.to_string(), char_id.as_str())) + stmt.execute((character.to_string(), char_id.as_str())) .unwrap(); Ok(char_id) @@ -281,7 +302,7 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { println!("Request received: {:?}", req); match req { Request::Charsheet(id) => { - let sheet = db.charsheet(id); + let sheet = db.character(id); println!("sheet retrieved: {:?}", sheet); match sheet { Ok(sheet) => { @@ -318,27 +339,34 @@ impl DbConn { #[async_trait] impl Database for DbConn { - async fn charsheet(&mut self, id: CharacterId) -> Result, Error> { + async fn character( + &mut self, + id: CharacterId, + ) -> ResultExt, Error, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::Charsheet(id), }; - self.conn.send(request).await.unwrap(); + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return fatal(FatalError::DatabaseConnectionLost), + }; + match rx.recv().await { - Ok(DatabaseResponse::Charsheet(row)) => Ok(row), - Ok(_) => Err(Error::MessageMismatch), - Err(err) => { - println!("error: {:?}", err); - Err(Error::NoResponse) - } + Ok(DatabaseResponse::Charsheet(row)) => ok(row), + // Ok(_) => fatal(FatalError::MessageMismatch), + Err(_err) => error(Error::NoResponse), } } } #[cfg(test)] mod test { + use std::path::PathBuf; + use cool_asserts::assert_matches; use super::*; @@ -346,24 +374,24 @@ mod test { const soren: &'static str = r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#; #[test] - fn it_can_retrieve_a_charsheet() { + fn it_can_retrieve_a_character() { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); - assert_matches!(db.charsheet(CharacterId::from("1")), Ok(None)); + assert_matches!(db.character(CharacterId::from("1")), Ok(None)); let js: serde_json::Value = serde_json::from_str(soren).unwrap(); let soren_id = db - .save_charsheet(None, "candela".to_owned(), js.clone()) + .save_character(None, "candela".to_owned(), js.clone()) .unwrap(); - assert_matches!(db.charsheet(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); + assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); } #[tokio::test] - async fn it_can_retrieve_a_charsheet_through_conn() { + async fn it_can_retrieve_a_character_through_conn() { let memory_db: Option = None; let mut conn = DbConn::new(memory_db); - assert_matches!(conn.charsheet(CharacterId::from("1")).await, Ok(None)); + assert_matches!(conn.character(CharacterId::from("1")).await, ResultExt::Ok(None)); } } diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 1dbefec..ed8a488 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -1,10 +1,11 @@ use std::future::Future; use futures::{SinkExt, StreamExt}; +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}; +use crate::{asset_db::AssetId, core::Core, database::CharacterId, types::{AppError, FatalError}}; /* pub async fn handle_auth( @@ -32,25 +33,28 @@ pub async fn handle_auth( pub async fn handler(f: F) -> impl Reply where - F: Future>, AppError>>, + F: Future>, AppError, FatalError>>, { match f.await { - Ok(response) => response, - Err(AppError::NotFound(_)) => Response::builder() + ResultExt::Ok(response) => response, + ResultExt::Err(AppError::NotFound(_)) => Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]) .unwrap(), - Err(_) => Response::builder() + ResultExt::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_file(core: Core, asset_id: AssetId) -> impl Reply { handler(async move { - let (mime, bytes) = core.get_asset(asset_id).await?; - Ok(Response::builder() + let (mime, bytes) = return_error!(core.get_asset(asset_id).await); + ok(Response::builder() .header("application-type", mime.to_string()) .body(bytes) .unwrap()) @@ -67,7 +71,7 @@ pub async fn handle_available_images(core: Core) -> impl Reply { .map(|path| format!("{}", path.as_str())) .collect(); - Ok(Response::builder() + ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Content-Type", "application/json") .body(serde_json::to_vec(&image_paths).unwrap()) @@ -88,7 +92,7 @@ pub async fn handle_register_client(core: Core, _request: RegisterRequest) -> im handler(async move { let client_id = core.register_client().await; - Ok(Response::builder() + ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Content-Type", "application/json") .body( @@ -106,7 +110,7 @@ pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Rep handler(async move { core.unregister_client(client_id); - Ok(Response::builder() + ok(Response::builder() .status(StatusCode::NO_CONTENT) .body(vec![]) .unwrap()) @@ -150,7 +154,7 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl handler(async move { let _ = core.set_background_image(AssetId::from(image_name)).await; - Ok(Response::builder() + ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Methods", "*") .header("Content-Type", "application/json") @@ -162,15 +166,19 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply { handler(async move { - let sheet = core.get_charsheet(CharacterId::from(charid)).await.unwrap(); + let sheet = match core.get_charsheet(CharacterId::from(charid)).await { + ResultExt::Ok(sheet) => sheet, + ResultExt::Err(err) => return ResultExt::Err(err), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), + }; match sheet { - Some(sheet) => Ok(Response::builder() + Some(sheet) => ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Content-Type", "application/json") .body(serde_json::to_vec(&sheet).unwrap()) .unwrap()), - None => Ok(Response::builder() + None => ok(Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]) .unwrap()), diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index f5c5307..3372703 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -1,14 +1,42 @@ use serde::{Deserialize, Serialize}; +use thiserror::Error; use typeshare::typeshare; use crate::asset_db::AssetId; -#[derive(Debug)] +#[derive(Debug, Error)] +pub enum FatalError { + #[error("Non-unique database key {0}")] + NonUniqueDatabaseKey(String), + + #[error("Database migrations failed {0}")] + DatabaseMigrationFailure(String), + + #[error("Failed to construct a query")] + ConstructQueryFailure(String), + + #[error("Database connection lost")] + DatabaseConnectionLost, + + #[error("Unexpected response for message")] + MessageMismatch, +} + +impl result_extended::FatalError for FatalError {} + +#[derive(Debug, Error)] pub enum AppError { + #[error("something wasn't found {0}")] NotFound(String), + + #[error("object inaccessible {0}")] Inaccessible(String), + + #[error("invalid json {0}")] JsonError(serde_json::Error), + + #[error("wat {0}")] UnexpectedError(String), } -- 2.44.1 From 5c23f326b69dadf1319add8e7582c2bd3a42f644 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 1 Dec 2024 11:14:28 -0500 Subject: [PATCH 03/12] Add the ability to save users and games. Link games more tightly to characters --- visions/server/src/database.rs | 86 +++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index 406264c..57b7dec 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -240,6 +240,69 @@ impl DiskDb { } } + fn save_user( + &self, + user_id: Option, + name: &str, + password: &str, + admin: bool, + enabled: bool, + ) -> Result { + match user_id { + None => { + let user_id = UserId::new(); + let mut stmt = self + .conn + .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((user_id.as_str(), name, password, admin, enabled)) + .unwrap(); + Ok(user_id) + } + Some(user_id) => { + let mut stmt = self + .conn + .prepare( + "UPDATE users SET name=?, password=?, admin=?, enbabled=? WHERE uuid=?", + ) + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((name, password, admin, enabled, user_id.as_str())) + .unwrap(); + Ok(user_id) + } + } + } + + fn save_game( + &self, + game_id: Option, + name: &str, + ) -> Result { + match game_id { + None => { + let game_id = GameId::new(); + let mut stmt = self + .conn + .prepare("INSERT INTO games VALUES (?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((game_id.as_str(), name)) + .unwrap(); + Ok(game_id) + } + Some(game_id) => { + let mut stmt = self + .conn + .prepare( + "UPDATE games SET name=? WHERE uuid=?", + ) + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + stmt.execute((name, game_id.as_str())) + .unwrap(); + Ok(game_id) + } + } + } + fn character(&self, id: CharacterId) -> Result, FatalError> { let mut stmt = self .conn @@ -267,7 +330,7 @@ impl DiskDb { fn save_character( &self, char_id: Option, - game_type: String, + game: GameId, character: serde_json::Value, ) -> std::result::Result { match char_id { @@ -277,7 +340,7 @@ impl DiskDb { .conn .prepare("INSERT INTO characters VALUES (?, ?, ?)") .unwrap(); - stmt.execute((char_id.as_str(), game_type, character.to_string())) + stmt.execute((char_id.as_str(), game.as_str(), character.to_string())) .unwrap(); Ok(char_id) @@ -373,16 +436,24 @@ mod test { const soren: &'static str = r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#; - #[test] - fn it_can_retrieve_a_character() { + fn setup_db() -> (DiskDb, GameId) { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); + db.save_user(None, "admin", "abcdefg", true, true); + let game_id = db.save_game(None, "Candela").unwrap(); + (db, game_id) + } + + #[test] + fn it_can_retrieve_a_character() { + let (db, game_id) = setup_db(); + assert_matches!(db.character(CharacterId::from("1")), Ok(None)); let js: serde_json::Value = serde_json::from_str(soren).unwrap(); let soren_id = db - .save_character(None, "candela".to_owned(), js.clone()) + .save_character(None, game_id, js.clone()) .unwrap(); assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); } @@ -392,6 +463,9 @@ mod test { let memory_db: Option = None; let mut conn = DbConn::new(memory_db); - assert_matches!(conn.character(CharacterId::from("1")).await, ResultExt::Ok(None)); + assert_matches!( + conn.character(CharacterId::from("1")).await, + ResultExt::Ok(None) + ); } } -- 2.44.1 From e8bc0590c6562c9b58d6771b328516f143f377db Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 1 Dec 2024 14:07:37 -0500 Subject: [PATCH 04/12] 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) }); }, -- 2.44.1 From e505c21bc870e022b26197eb0fbf93d357bdf0a2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 10 Dec 2024 22:43:15 -0500 Subject: [PATCH 05/12] 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) => )} + +
); } -- 2.44.1 From 7466ef2a6f24df528c1fceb3af61b76278490931 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 15 Dec 2024 21:42:01 -0500 Subject: [PATCH 06/12] Remove the original UI files --- visions/ui.backup/Makefile | 10 - visions/ui.backup/package-lock.json | 1991 --------------------------- visions/ui.backup/package.json | 24 - visions/ui.backup/src/client.ts | 23 - visions/ui.backup/src/index.html | 6 - visions/ui.backup/src/main.tsx | 11 - visions/ui.backup/src/visions.css | 0 visions/ui.backup/webpack.config.js | 24 - 8 files changed, 2089 deletions(-) delete mode 100644 visions/ui.backup/Makefile delete mode 100644 visions/ui.backup/package-lock.json delete mode 100644 visions/ui.backup/package.json delete mode 100644 visions/ui.backup/src/client.ts delete mode 100644 visions/ui.backup/src/index.html delete mode 100644 visions/ui.backup/src/main.tsx delete mode 100644 visions/ui.backup/src/visions.css delete mode 100644 visions/ui.backup/webpack.config.js diff --git a/visions/ui.backup/Makefile b/visions/ui.backup/Makefile deleted file mode 100644 index 26c0daf..0000000 --- a/visions/ui.backup/Makefile +++ /dev/null @@ -1,10 +0,0 @@ - -release: - NODE_ENV=production npm run build - -dev: - npm run build - -server: - npx http-server ./dist - diff --git a/visions/ui.backup/package-lock.json b/visions/ui.backup/package-lock.json deleted file mode 100644 index 4347c0b..0000000 --- a/visions/ui.backup/package-lock.json +++ /dev/null @@ -1,1991 +0,0 @@ -{ - "name": "ui", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ui", - "version": "1.0.0", - "license": "GPL-3.0-or-later", - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.8", - "@types/react-dom": "^18.2.4", - "copy-webpack-plugin": "^11.0.0", - "ts-loader": "^9.4.3", - "webpack": "^5.85.0", - "webpack-cli": "^5.1.3" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", - "dev": true - }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.8.tgz", - "integrity": "sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", - "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", - "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001489", - "electron-to-chromium": "^1.4.411", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001494", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001494.tgz", - "integrity": "sha512-sY2B5Qyl46ZzfYDegrl8GBCzdawSLT4ThM9b9F+aDYUrAG2zCOyMbd2Tq34mS1g4ZKBfjRlzOohQMxx28x6wJg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", - "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.419", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.419.tgz", - "integrity": "sha512-jdie3RiEgygvDTyS2sgjq71B36q2cDSBfPlwzUyuOrfYTNoYWyBxxjGJV/HAu3A2hB0Y+HesvCVkVAFoCKwCSw==", - "dev": true - }, - "node_modules/enhanced-resolve": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", - "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/globby": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", - "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", - "dev": true, - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", - "dev": true - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.17.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", - "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", - "dev": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-loader": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.3.tgz", - "integrity": "sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.85.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.85.0.tgz", - "integrity": "sha512-7gazTiYqwo5OSqwH1tigLDL2r3qDeP2dOKYgd+LlXpsUMqDTklg6tOghexqky0/+6QY38kb/R/uRPUleuL43zg==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.3.tgz", - "integrity": "sha512-MTuk7NUMvEHQUSXCpvUrF1q2p0FJS40dPFfqQvG3jTWcgv/8plBNz2Kv2HXZiLGPnfmSAA5uCtCILO1JBmmkfw==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-merge": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", - "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } -} diff --git a/visions/ui.backup/package.json b/visions/ui.backup/package.json deleted file mode 100644 index 4cb667d..0000000 --- a/visions/ui.backup/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "ui", - "version": "1.0.0", - "description": "", - "main": "webpack.config.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "webpack" - }, - "author": "", - "license": "GPL-3.0-or-later", - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.8", - "@types/react-dom": "^18.2.4", - "copy-webpack-plugin": "^11.0.0", - "ts-loader": "^9.4.3", - "webpack": "^5.85.0", - "webpack-cli": "^5.1.3" - } -} diff --git a/visions/ui.backup/src/client.ts b/visions/ui.backup/src/client.ts deleted file mode 100644 index f469976..0000000 --- a/visions/ui.backup/src/client.ts +++ /dev/null @@ -1,23 +0,0 @@ -type PlayingField = { - backgroundImage: string; -} - -class Client { - playingField(): PlayingField { - return { backgroundImage: "tower-in-mist.jpg" }; - } - - async getFile(filename: string): Promise { - try { - const response = await fetch(`http://localhost:8001/api/v1/file/${filename}`); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } - - const body = await response.blob(); - return body; - } catch (error) { - console.error(error.message); - } - } -} diff --git a/visions/ui.backup/src/index.html b/visions/ui.backup/src/index.html deleted file mode 100644 index c9b8675..0000000 --- a/visions/ui.backup/src/index.html +++ /dev/null @@ -1,6 +0,0 @@ - - - -
- - diff --git a/visions/ui.backup/src/main.tsx b/visions/ui.backup/src/main.tsx deleted file mode 100644 index 6ad987a..0000000 --- a/visions/ui.backup/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; - -const App = () =>
App
; - -ReactDOM.render( - - - , - document.getElementById("root") -); diff --git a/visions/ui.backup/src/visions.css b/visions/ui.backup/src/visions.css deleted file mode 100644 index e69de29..0000000 diff --git a/visions/ui.backup/webpack.config.js b/visions/ui.backup/webpack.config.js deleted file mode 100644 index f1872ec..0000000 --- a/visions/ui.backup/webpack.config.js +++ /dev/null @@ -1,24 +0,0 @@ -const CopyWebpackPlugin = require('copy-webpack-plugin'); - -module.exports = { - mode: "development", - entry: { - "main": "./src/main.tsx" - }, - module: { - rules: [ - { test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ } - ] - }, - plugins: [ - new CopyWebpackPlugin({ - patterns: [ - { from: "src/index.html" }, - { from: "src/visions.css" }, - ] - }) - ], - resolve: { - extensions: ['.ts', '.tsx'], - } -} -- 2.44.1 From 5e89b8257d9d70dc517609191d55c77fb33d6d9b Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 15 Dec 2024 22:49:53 -0500 Subject: [PATCH 07/12] Set up the authentication page --- visions/ui/src/App.tsx | 9 ++-- visions/ui/src/design.css | 15 ++++++ .../views/Authentication/Authentication.css | 24 +++++++++ .../views/Authentication/Authentication.tsx | 50 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 visions/ui/src/design.css create mode 100644 visions/ui/src/views/Authentication/Authentication.css create mode 100644 visions/ui/src/views/Authentication/Authentication.tsx diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index 98626e8..635f326 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -8,6 +8,7 @@ import { WebsocketProvider } from './components/WebsocketProvider'; import { PlayerView } from './views/PlayerView/PlayerView'; import { Admin } from './views/Admin/Admin'; import Candela from './plugins/Candela'; +import { Authentication } from './views/Authentication/Authentication'; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; @@ -35,6 +36,10 @@ const App = ({ client }: AppProps) => { let router = createBrowserRouter([ + { + path: "/", + element: websocketUrl ? :
+ }, { path: "/gm", element: websocketUrl ? :
@@ -43,10 +48,6 @@ const App = ({ client }: AppProps) => { path: "/admin", element: }, - { - path: "/", - element: websocketUrl ? :
- }, { path: "/candela", element: diff --git a/visions/ui/src/design.css b/visions/ui/src/design.css new file mode 100644 index 0000000..7e227a4 --- /dev/null +++ b/visions/ui/src/design.css @@ -0,0 +1,15 @@ +:root { + --border-standard: 2px solid black; + --border-radius-standard: 4px; + --border-shadow-shallow: 1px 1px 2px black; + --padding-m: 8px; + --margin-s: 4px; +} + +.card { + border: var(--border-standard); + border-radius: var(--border-radius-standard); + box-shadow: var(--border-shadow-shallow); + padding: var(--padding-m); +} + diff --git a/visions/ui/src/views/Authentication/Authentication.css b/visions/ui/src/views/Authentication/Authentication.css new file mode 100644 index 0000000..ca86baf --- /dev/null +++ b/visions/ui/src/views/Authentication/Authentication.css @@ -0,0 +1,24 @@ +@import '../../design.css'; + +.auth { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100vh; +} + +.auth > div { + border: var(--border-standard); + border-radius: var(--border-radius-standard); + padding: var(--padding-m); +} + +.auth__input-line { + display: flex; + justify-content: space-between; +} + +.auth__input-line > * { + margin: var(--margin-s); +} diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx new file mode 100644 index 0000000..6997fe2 --- /dev/null +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -0,0 +1,50 @@ +import React, { PropsWithChildren, ReactNode, useContext, useEffect, useState } from 'react'; +import { Client } from '../../client'; +import { assertNever } from '../../plugins/Candela'; +import './Authentication.css'; + +interface AuthenticationProps { + client: Client; +} + +type AuthState = "NoAdmin" | "Unauthed" | "Authed"; + +export const Authentication = ({ client, children }: PropsWithChildren) => { + // No admin password set: prompt for the admin password + // Password set, nobody logged in: prompt for login + // User logged in: show the children + + let [state, setState] = useState("Unauthed"); + + switch (state) { + case "NoAdmin": { + return
+
+

Welcome to your new Visions VTT Instance

+

Set your admin password:

+ + +
+
; + } + case "Unauthed": { + return
+
+

Welcome to Visions VTT

+
+ + + +
+
+
; + } + case "Authed": { + return
{children}
; + } + default: { + assertNever(state); + return
; + } + } +} -- 2.44.1 From 7ca1581b55314e3a6631f3ad9dcf98507a79d0b3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 15 Dec 2024 23:20:09 -0500 Subject: [PATCH 08/12] Set up a state provider --- visions/ui/src/components/StateProvider.tsx | 48 +++++++++++++++++++ .../ui/src/components/WebsocketProvider.tsx | 13 ++--- .../views/Authentication/Authentication.css | 6 --- .../views/Authentication/Authentication.tsx | 9 ++-- visions/ui/src/views/GmView/GmView.tsx | 3 +- .../ui/src/views/PlayerView/PlayerView.tsx | 3 +- 6 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 visions/ui/src/components/StateProvider.tsx diff --git a/visions/ui/src/components/StateProvider.tsx b/visions/ui/src/components/StateProvider.tsx new file mode 100644 index 0000000..0d9f1fb --- /dev/null +++ b/visions/ui/src/components/StateProvider.tsx @@ -0,0 +1,48 @@ +import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react"; +import { Tabletop } from "visions-types"; +import { assertNever } from "../plugins/Candela"; + +type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; + +type TabletopState = { + auth: AuthState; + tabletop: Tabletop; +} + +type StateAction = { type: "SetAuthState", state: AuthState } + | { type: "HandleMessage" }; + +const initialState = (): TabletopState => ( + { + auth: { type: "Unauthed" }, + tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } + } +); + +export const AppContext = createContext(initialState()); + +interface StateProviderProps { } + +export const StateProvider = ({ children }: PropsWithChildren) => { + const [state, dispatch] = useReducer(stateReducer, initialState()); + + return + {children} + ; +} + +const stateReducer = (state: TabletopState, action: StateAction): TabletopState => { + switch (action.type) { + case "SetAuthState": { + return { ...state, auth: action.state }; + } + case "HandleMessage": { + return state; + } + default: { + assertNever(action); + return state; + } + } +} + diff --git a/visions/ui/src/components/WebsocketProvider.tsx b/visions/ui/src/components/WebsocketProvider.tsx index 2358c69..90862de 100644 --- a/visions/ui/src/components/WebsocketProvider.tsx +++ b/visions/ui/src/components/WebsocketProvider.tsx @@ -2,19 +2,17 @@ import React, { createContext, PropsWithChildren, useEffect, useReducer } from " import useWebSocket from "react-use-websocket"; import { Message, Tabletop } from "visions-types"; -type TabletopState = { - tabletop: Tabletop; -} +type WebsocketState = { } -const initialState = (): TabletopState => ({ tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } }); - -export const WebsocketContext = createContext(initialState()); +export const WebsocketContext = createContext({}); interface WebsocketProviderProps { websocketUrl: string; } export const WebsocketProvider = ({ websocketUrl, children }: PropsWithChildren) => { + return
{children}
; +/* const { lastMessage } = useWebSocket(websocketUrl); const [state, dispatch] = useReducer(handleMessage, initialState()); @@ -29,8 +27,10 @@ export const WebsocketProvider = ({ websocketUrl, children }: PropsWithChildren< return ( {children} ); + */ } +/* const handleMessage = (state: TabletopState, message: Message): TabletopState => { console.log(message); switch (message.type) { @@ -42,3 +42,4 @@ const handleMessage = (state: TabletopState, message: Message): TabletopState => } } } +*/ diff --git a/visions/ui/src/views/Authentication/Authentication.css b/visions/ui/src/views/Authentication/Authentication.css index ca86baf..a938112 100644 --- a/visions/ui/src/views/Authentication/Authentication.css +++ b/visions/ui/src/views/Authentication/Authentication.css @@ -8,12 +8,6 @@ height: 100vh; } -.auth > div { - border: var(--border-standard); - border-radius: var(--border-radius-standard); - padding: var(--padding-m); -} - .auth__input-line { display: flex; justify-content: space-between; diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx index 6997fe2..0315a8e 100644 --- a/visions/ui/src/views/Authentication/Authentication.tsx +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -1,5 +1,6 @@ import React, { PropsWithChildren, ReactNode, useContext, useEffect, useState } from 'react'; import { Client } from '../../client'; +import { AppContext } from '../../components/StateProvider'; import { assertNever } from '../../plugins/Candela'; import './Authentication.css'; @@ -7,16 +8,14 @@ interface AuthenticationProps { client: Client; } -type AuthState = "NoAdmin" | "Unauthed" | "Authed"; - export const Authentication = ({ client, children }: PropsWithChildren) => { // No admin password set: prompt for the admin password // Password set, nobody logged in: prompt for login // User logged in: show the children - let [state, setState] = useState("Unauthed"); + let { auth } = useContext(AppContext); - switch (state) { + switch (auth.type) { case "NoAdmin": { return
@@ -43,7 +42,7 @@ export const Authentication = ({ client, children }: PropsWithChildren {children}
; } default: { - assertNever(state); + assertNever(auth); return
; } } diff --git a/visions/ui/src/views/GmView/GmView.tsx b/visions/ui/src/views/GmView/GmView.tsx index cc697f3..5d43745 100644 --- a/visions/ui/src/views/GmView/GmView.tsx +++ b/visions/ui/src/views/GmView/GmView.tsx @@ -1,5 +1,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { Client, PlayingField } from '../../client'; +import { AppContext } from '../../components/StateProvider'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail'; import { WebsocketContext } from '../../components/WebsocketProvider'; @@ -10,7 +11,7 @@ interface GmViewProps { } export const GmView = ({ client }: GmViewProps) => { - const { tabletop } = useContext(WebsocketContext); + const { tabletop } = useContext(AppContext); const [images, setImages] = useState([]); useEffect(() => { diff --git a/visions/ui/src/views/PlayerView/PlayerView.tsx b/visions/ui/src/views/PlayerView/PlayerView.tsx index a77125d..5ead8d2 100644 --- a/visions/ui/src/views/PlayerView/PlayerView.tsx +++ b/visions/ui/src/views/PlayerView/PlayerView.tsx @@ -4,6 +4,7 @@ import { WebsocketContext } from '../../components/WebsocketProvider'; import { Client } from '../../client'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; import Candela from '../../plugins/Candela'; +import { AppContext } from '../../components/StateProvider'; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; @@ -12,7 +13,7 @@ interface PlayerViewProps { } export const PlayerView = ({ client }: PlayerViewProps) => { - const { tabletop } = useContext(WebsocketContext); + const { tabletop } = useContext(AppContext); const [charsheet, setCharsheet] = useState(undefined); -- 2.44.1 From af0ab5d020f8c42198915b4dac680d8db4fc6ec4 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 16 Dec 2024 00:27:55 -0500 Subject: [PATCH 09/12] Create a status endpoint that shows the onboarding UI if there's no admin password --- result-extended/src/lib.rs | 2 +- visions/server/migrations/01-charsheet/up.sql | 12 --- .../{02-users => 01-initial-db}/up.sql | 16 ++++ visions/server/src/core.rs | 31 ++++++- visions/server/src/database.rs | 91 +++++++++++++------ visions/server/src/handlers.rs | 14 ++- visions/server/src/main.rs | 12 ++- visions/ui/src/App.tsx | 4 +- visions/ui/src/client.ts | 6 ++ visions/ui/src/components/StateProvider.tsx | 28 ++++-- .../views/Authentication/Authentication.tsx | 6 +- 11 files changed, 167 insertions(+), 55 deletions(-) delete mode 100644 visions/server/migrations/01-charsheet/up.sql rename visions/server/migrations/{02-users => 01-initial-db}/up.sql (53%) diff --git a/result-extended/src/lib.rs b/result-extended/src/lib.rs index dc8736a..e4aba62 100644 --- a/result-extended/src/lib.rs +++ b/result-extended/src/lib.rs @@ -99,7 +99,7 @@ impl ResultExt { } } -/// Convert from a normal `Result` type to a `Result` type. The error condition for a `Result` will +/// Convert from a normal `Result` type to a `ResultExt` type. The error condition for a `Result` will /// be treated as `Result::Err`, never `Result::Fatal`. impl From> for ResultExt { fn from(r: std::result::Result) -> Self { diff --git a/visions/server/migrations/01-charsheet/up.sql b/visions/server/migrations/01-charsheet/up.sql deleted file mode 100644 index 7f538ec..0000000 --- a/visions/server/migrations/01-charsheet/up.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE games( - uuid TEXT PRIMARY KEY, - name TEXT -); - -CREATE TABLE characters( - uuid TEXT PRIMARY KEY, - game TEXT, - data TEXT, - - FOREIGN KEY(game) REFERENCES games(uuid) -); diff --git a/visions/server/migrations/02-users/up.sql b/visions/server/migrations/01-initial-db/up.sql similarity index 53% rename from visions/server/migrations/02-users/up.sql rename to visions/server/migrations/01-initial-db/up.sql index e8043e8..3e822af 100644 --- a/visions/server/migrations/02-users/up.sql +++ b/visions/server/migrations/01-initial-db/up.sql @@ -6,6 +6,19 @@ CREATE TABLE users( enabled BOOLEAN ); +CREATE TABLE games( + uuid TEXT PRIMARY KEY, + name TEXT +); + +CREATE TABLE characters( + uuid TEXT PRIMARY KEY, + game TEXT, + data TEXT, + + FOREIGN KEY(game) REFERENCES games(uuid) +); + CREATE TABLE roles( user_id TEXT, game_id TEXT, @@ -14,3 +27,6 @@ CREATE TABLE roles( FOREIGN KEY(user_id) REFERENCES users(uuid), FOREIGN KEY(game_id) REFERENCES games(uuid) ); + +INSERT INTO users VALUES ("admin", "admin", "", true, true); + diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 158be86..f86e8ec 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -2,8 +2,10 @@ use std::{collections::HashMap, sync::Arc}; use async_std::sync::RwLock; use mime::Mime; -use result_extended::{fatal, ok, ResultExt}; +use result_extended::{fatal, ok, return_error, ResultExt}; +use serde::Serialize; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use typeshare::typeshare; use uuid::Uuid; use crate::{ @@ -18,6 +20,12 @@ const DEFAULT_BACKGROUND_COLOR: RGB = RGB { blue: 0xbb, }; +#[derive(Clone, Serialize)] +#[typeshare] +pub struct Status { + admin_enabled: bool, +} + #[derive(Debug)] struct WebsocketClient { sender: Option>, @@ -51,6 +59,27 @@ impl Core { }))) } + pub async fn status(&self) -> ResultExt { + let mut state = self.0.write().await; + let admin_user = match return_error!(state + .db + .user(UserId::from("admin")) + .await + .map_err(|_| AppError::Inaccessible("database stopped responding".to_owned()))) + { + Some(admin_user) => admin_user, + None => { + return ok(Status { + admin_enabled: false, + }); + } + }; + + ok(Status { + admin_enabled: !admin_user.password.is_empty(), + }) + } + pub async fn register_client(&self) -> String { let mut state = self.0.write().await; let uuid = Uuid::new_v4().simple().to_string(); diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index be4b75b..144c576 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -5,7 +5,10 @@ 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::{types::{FromSql, FromSqlResult, ValueRef}, Connection}; +use rusqlite::{ + types::{FromSql, FromSqlResult, ValueRef}, + Connection, +}; use rusqlite_migration::Migrations; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -30,6 +33,7 @@ pub enum Error { enum Request { Charsheet(CharacterId), Games, + User(UserId), Users, } @@ -43,6 +47,7 @@ struct DatabaseRequest { enum DatabaseResponse { Charsheet(Option), Games(Vec), + User(Option), Users(Vec), } @@ -179,6 +184,11 @@ pub struct CharsheetRow { #[async_trait] pub trait Database: Send + Sync { + async fn user( + &mut self, + _: UserId, + ) -> 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>; @@ -193,6 +203,7 @@ pub struct DiskDb { conn: Connection, } +/* fn setup_test_database(conn: &Connection) -> Result<(), FatalError> { let mut gamecount_stmt = conn.prepare("SELECT count(*) FROM games").unwrap(); let mut count = gamecount_stmt.query([]).unwrap(); @@ -236,6 +247,7 @@ fn setup_test_database(conn: &Connection) -> Result<(), FatalError> { Ok(()) } +*/ impl DiskDb { pub fn new

(path: Option

) -> Result @@ -251,30 +263,36 @@ impl DiskDb { .to_latest(&mut conn) .map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?; - setup_test_database(&conn)?; + // setup_test_database(&conn)?; Ok(DiskDb { conn }) } fn users(&self) -> Result, FatalError> { - let mut stmt = self.conn.prepare("SELECT * FROM USERS") + 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(), + 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(); + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); Ok(items) } fn user(&self, id: UserId) -> Result, FatalError> { let mut stmt = self .conn - .prepare("SELECT uuid, name, password, admin, enabled WHERE uuid=?") + .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt .query_map([id.as_str()], |row| { @@ -329,11 +347,7 @@ impl DiskDb { } } - fn save_game( - &self, - game_id: Option, - name: &str, - ) -> Result { + fn save_game(&self, game_id: Option, name: &str) -> Result { match game_id { None => { let game_id = GameId::new(); @@ -341,19 +355,15 @@ impl DiskDb { .conn .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(), name)).unwrap(); Ok(game_id) } Some(game_id) => { let mut stmt = self .conn - .prepare( - "UPDATE games SET name=? WHERE uuid=?", - ) + .prepare("UPDATE games SET name=? WHERE uuid=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; - stmt.execute((name, game_id.as_str())) - .unwrap(); + stmt.execute((name, game_id.as_str())).unwrap(); Ok(game_id) } } @@ -432,6 +442,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { Request::Games => { unimplemented!(); } + Request::User(uid) => { + let user = db.user(uid); + match user { + Ok(user) => { + tx.send(DatabaseResponse::User(user)).await.unwrap(); + } + err => panic!("{:?}", err), + } + } Request::Users => { let users = db.users(); match users { @@ -469,6 +488,26 @@ impl DbConn { #[async_trait] impl Database for DbConn { + async fn user(&mut self, uid: UserId) -> ResultExt, Error, FatalError> { + let (tx, rx) = bounded::(1); + + let request = DatabaseRequest { + tx, + req: Request::User(uid), + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return fatal(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::User(user)) => ok(user), + Ok(_) => fatal(FatalError::MessageMismatch), + Err(_) => error(Error::NoResponse), + } + } + async fn users(&mut self) -> ResultExt, Error, FatalError> { let (tx, rx) = bounded::(1); @@ -559,9 +598,7 @@ mod test { assert_matches!(db.character(CharacterId::from("1")), Ok(None)); let js: serde_json::Value = serde_json::from_str(soren).unwrap(); - let soren_id = db - .save_character(None, game_id, js.clone()) - .unwrap(); + let soren_id = db.save_character(None, game_id, js.clone()).unwrap(); assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); } diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 548eba6..5f43c1f 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -56,11 +56,23 @@ where } } +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); ok(Response::builder() - .header("application-type", mime.to_string()) + .header("content-type", mime.to_string()) .body(bytes) .unwrap()) }) diff --git a/visions/server/src/main.rs b/visions/server/src/main.rs index 71376ff..2f3a868 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_get_users, 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_server_status, handle_set_background_image, handle_unregister_client, RegisterRequest }; use warp::{ // header, @@ -104,6 +104,13 @@ pub async fn main() { let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let log = warp::log("visions::api"); + let server_status = warp::path!("api" / "v1" / "status") + .and(warp::get()) + .then({ + let core = core.clone(); + move || handle_server_status(core.clone()) + }); + let route_image = warp::path!("api" / "v1" / "image" / String) .and(warp::get()) .then({ @@ -174,7 +181,8 @@ pub async fn main() { move |charid| handle_get_charsheet(core.clone(), charid) }); - let filter = route_register_client + let filter = server_status + .or(route_register_client) .or(route_unregister_client) .or(route_websocket) .or(route_image) diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index 635f326..59220e2 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -9,6 +9,7 @@ import { PlayerView } from './views/PlayerView/PlayerView'; import { Admin } from './views/Admin/Admin'; import Candela from './plugins/Candela'; import { Authentication } from './views/Authentication/Authentication'; +import { StateProvider } from './components/StateProvider'; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; @@ -38,7 +39,8 @@ const App = ({ client }: AppProps) => { createBrowserRouter([ { path: "/", - element: websocketUrl ? :

+ element: + }, { path: "/gm", diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index 3b8473f..4656c55 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -9,6 +9,12 @@ export class Client { this.base = new URL("http://localhost:8001"); } + status() { + const url = new URL(this.base); + url.pathname = `/api/v1/status`; + return fetch(url).then((response) => response.json()); + } + registerWebsocket() { const url = new URL(this.base); url.pathname = `api/v1/client`; diff --git a/visions/ui/src/components/StateProvider.tsx b/visions/ui/src/components/StateProvider.tsx index 0d9f1fb..990b0ba 100644 --- a/visions/ui/src/components/StateProvider.tsx +++ b/visions/ui/src/components/StateProvider.tsx @@ -1,5 +1,6 @@ -import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react"; -import { Tabletop } from "visions-types"; +import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer } from "react"; +import { Status, Tabletop } from "visions-types"; +import { Client } from "../client"; import { assertNever } from "../plugins/Candela"; type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; @@ -10,7 +11,7 @@ type TabletopState = { } type StateAction = { type: "SetAuthState", state: AuthState } - | { type: "HandleMessage" }; + | { type: "HandleMessage" }; const initialState = (): TabletopState => ( { @@ -21,12 +22,27 @@ const initialState = (): TabletopState => ( export const AppContext = createContext(initialState()); -interface StateProviderProps { } +interface StateProviderProps { client: Client; } -export const StateProvider = ({ children }: PropsWithChildren) => { +export const StateProvider = ({ client, children }: PropsWithChildren) => { + console.log("StateProvider"); const [state, dispatch] = useReducer(stateReducer, initialState()); - return + useEffect(() => { + console.log("useCallback"); + client.status().then((status: Status) => { + console.log("status: ", status); + if (status.admin_enabled) { + dispatch({ type: "SetAuthState", state: { type: "Unauthed" } }); + } else { + dispatch({ type: "SetAuthState", state: { type: "NoAdmin" } }); + } + }) + }, + [client] + ); + + return {children} ; } diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx index 0315a8e..743ca1b 100644 --- a/visions/ui/src/views/Authentication/Authentication.tsx +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -1,14 +1,12 @@ -import React, { PropsWithChildren, ReactNode, useContext, useEffect, useState } from 'react'; -import { Client } from '../../client'; +import React, { PropsWithChildren, useContext } from 'react'; import { AppContext } from '../../components/StateProvider'; import { assertNever } from '../../plugins/Candela'; import './Authentication.css'; interface AuthenticationProps { - client: Client; } -export const Authentication = ({ client, children }: PropsWithChildren) => { +export const Authentication = ({ children }: PropsWithChildren) => { // No admin password set: prompt for the admin password // Password set, nobody logged in: prompt for login // User logged in: show the children -- 2.44.1 From 7d7e6ef300e200c752e0c14811efb407ed8b2bb8 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Mon, 16 Dec 2024 23:46:02 -0500 Subject: [PATCH 10/12] Start trying to set up providers --- visions/ui/package-lock.json | 36 +++++++++++++++++++ visions/ui/package.json | 1 + visions/ui/src/App.tsx | 19 ++++++++-- visions/ui/src/components/StateProvider.tsx | 36 ++----------------- .../providers/AuthProvider/AuthProvider.tsx | 35 ++++++++++++++++++ .../views/Authentication/Authentication.tsx | 21 ++++++----- 6 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 visions/ui/src/providers/AuthProvider/AuthProvider.tsx diff --git a/visions/ui/package-lock.json b/visions/ui/package-lock.json index 807f246..337109c 100644 --- a/visions/ui/package-lock.json +++ b/visions/ui/package-lock.json @@ -20,6 +20,7 @@ "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router": "^6.28.0", "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", @@ -3747,6 +3748,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", @@ -12920,6 +12926,28 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15257,6 +15285,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/visions/ui/package.json b/visions/ui/package.json index 65707a6..f95789d 100644 --- a/visions/ui/package.json +++ b/visions/ui/package.json @@ -15,6 +15,7 @@ "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-redux": "^9.2.0", "react-router": "^6.28.0", "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index 59220e2..a607bbb 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { PropsWithChildren, useEffect, useState } from 'react'; import './App.css'; import { Client } from './client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; @@ -10,6 +10,7 @@ import { Admin } from './views/Admin/Admin'; import Candela from './plugins/Candela'; import { Authentication } from './views/Authentication/Authentication'; import { StateProvider } from './components/StateProvider'; +import { AuthProvider } from './providers/AuthProvider/AuthProvider'; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; @@ -27,6 +28,20 @@ const CandelaCharsheet = ({ client }: { client: Client }) => { return sheet ? :
} +interface AuthedViewProps { + client: Client; +} + +const AuthedView = ({ client, children }: PropsWithChildren) => { + return ( + + console.log(password)} onAuth={(username, password) => console.log(username, password)}> + {children} + + + ); +} + const App = ({ client }: AppProps) => { console.log("rendering app"); const [websocketUrl, setWebsocketUrl] = useState(undefined); @@ -39,7 +54,7 @@ const App = ({ client }: AppProps) => { createBrowserRouter([ { path: "/", - element: + element: }, { diff --git a/visions/ui/src/components/StateProvider.tsx b/visions/ui/src/components/StateProvider.tsx index 990b0ba..79b504a 100644 --- a/visions/ui/src/components/StateProvider.tsx +++ b/visions/ui/src/components/StateProvider.tsx @@ -3,19 +3,14 @@ import { Status, Tabletop } from "visions-types"; import { Client } from "../client"; import { assertNever } from "../plugins/Candela"; -type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; - type TabletopState = { - auth: AuthState; tabletop: Tabletop; } -type StateAction = { type: "SetAuthState", state: AuthState } - | { type: "HandleMessage" }; +type Action = {}; const initialState = (): TabletopState => ( { - auth: { type: "Unauthed" }, tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } } ); @@ -28,37 +23,10 @@ export const StateProvider = ({ client, children }: PropsWithChildren { - console.log("useCallback"); - client.status().then((status: Status) => { - console.log("status: ", status); - if (status.admin_enabled) { - dispatch({ type: "SetAuthState", state: { type: "Unauthed" } }); - } else { - dispatch({ type: "SetAuthState", state: { type: "NoAdmin" } }); - } - }) - }, - [client] - ); - return {children} ; } -const stateReducer = (state: TabletopState, action: StateAction): TabletopState => { - switch (action.type) { - case "SetAuthState": { - return { ...state, auth: action.state }; - } - case "HandleMessage": { - return state; - } - default: { - assertNever(action); - return state; - } - } -} +const stateReducer = (state: TabletopState, _action: Action): TabletopState => state; diff --git a/visions/ui/src/providers/AuthProvider/AuthProvider.tsx b/visions/ui/src/providers/AuthProvider/AuthProvider.tsx new file mode 100644 index 0000000..e32743d --- /dev/null +++ b/visions/ui/src/providers/AuthProvider/AuthProvider.tsx @@ -0,0 +1,35 @@ +import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react"; +import { Status } from "visions-types"; +import { Client } from "../../client"; + +type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; + +type Action = { type: "SetAuthState", state: AuthState }; + +export const AuthContext = createContext({ type: "NoAdmin" }); + +interface AuthProviderProps { + client: Client; +} + +export const AuthProvider = ({ client, children }: PropsWithChildren) => { + const [authState, dispatch] = useReducer(stateReducer, { type: "NoAdmin" }); + + useEffect(() => { + client.status().then((status: Status) => { + if (status.admin_enabled) { + dispatch({ type: "SetAuthState", state: { type: "Unauthed" } }); + } else { + dispatch({ type: "SetAuthState", state: { type: "NoAdmin" } }); + } + }) + }, + [client] + ); + + return ( + {children} + ); +} + +const stateReducer = (_state: AuthState, action: Action) => action.state; diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx index 743ca1b..39496ab 100644 --- a/visions/ui/src/views/Authentication/Authentication.tsx +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -1,17 +1,22 @@ -import React, { PropsWithChildren, useContext } from 'react'; +import React, { PropsWithChildren, useContext, useState } from 'react'; import { AppContext } from '../../components/StateProvider'; import { assertNever } from '../../plugins/Candela'; +import { AuthContext } from '../../providers/AuthProvider/AuthProvider'; import './Authentication.css'; interface AuthenticationProps { + onAdminPassword: (password: string) => void; + onAuth: (username: string, password: string) => void; } -export const Authentication = ({ children }: PropsWithChildren) => { +export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithChildren) => { // No admin password set: prompt for the admin password // Password set, nobody logged in: prompt for login // User logged in: show the children - let { auth } = useContext(AppContext); + let [userField, setUserField] = useState(""); + let [pwField, setPwField] = useState(""); + let auth = useContext(AuthContext); switch (auth.type) { case "NoAdmin": { @@ -19,8 +24,8 @@ export const Authentication = ({ children }: PropsWithChildren

Welcome to your new Visions VTT Instance

Set your admin password:

- - + setPwField(evt.target.value)} /> + onAdminPassword(pwField)} />
; } @@ -29,9 +34,9 @@ export const Authentication = ({ children }: PropsWithChildren

Welcome to Visions VTT

- - - + setUserField(evt.target.value)} /> + setPwField(evt.target.value)} /> + onAuth(userField, pwField)} />
; -- 2.44.1 From f6a45a92234e6988d673ee5877788bf651469880 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 17 Dec 2024 00:50:25 -0500 Subject: [PATCH 11/12] Merge the auth state into a tabletop in the AppState provider --- visions/ui/package-lock.json | 36 ------------------ visions/ui/package.json | 1 - visions/ui/src/App.tsx | 23 ++++++------ visions/ui/src/components/StateProvider.tsx | 32 ---------------- .../providers/AuthProvider/AuthProvider.tsx | 35 ------------------ .../providers/StateProvider/StateProvider.tsx | 37 +++++++++++++++++++ .../views/Authentication/Authentication.tsx | 11 +++--- visions/ui/src/views/GmView/GmView.tsx | 12 +++--- .../ui/src/views/PlayerView/PlayerView.tsx | 8 ++-- 9 files changed, 63 insertions(+), 132 deletions(-) delete mode 100644 visions/ui/src/components/StateProvider.tsx delete mode 100644 visions/ui/src/providers/AuthProvider/AuthProvider.tsx create mode 100644 visions/ui/src/providers/StateProvider/StateProvider.tsx diff --git a/visions/ui/package-lock.json b/visions/ui/package-lock.json index 337109c..807f246 100644 --- a/visions/ui/package-lock.json +++ b/visions/ui/package-lock.json @@ -20,7 +20,6 @@ "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-redux": "^9.2.0", "react-router": "^6.28.0", "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", @@ -3748,11 +3747,6 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" - }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", @@ -12926,28 +12920,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15285,14 +15257,6 @@ "requires-port": "^1.0.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/visions/ui/package.json b/visions/ui/package.json index f95789d..65707a6 100644 --- a/visions/ui/package.json +++ b/visions/ui/package.json @@ -15,7 +15,6 @@ "classnames": "^2.5.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-redux": "^9.2.0", "react-router": "^6.28.0", "react-router-dom": "^6.28.0", "react-scripts": "5.0.1", diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index a607bbb..373750c 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useEffect, useState } from 'react'; +import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'; import './App.css'; import { Client } from './client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; @@ -9,8 +9,7 @@ import { PlayerView } from './views/PlayerView/PlayerView'; import { Admin } from './views/Admin/Admin'; import Candela from './plugins/Candela'; import { Authentication } from './views/Authentication/Authentication'; -import { StateProvider } from './components/StateProvider'; -import { AuthProvider } from './providers/AuthProvider/AuthProvider'; +import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; @@ -33,13 +32,14 @@ interface AuthedViewProps { } const AuthedView = ({ client, children }: PropsWithChildren) => { - return ( - - console.log(password)} onAuth={(username, password) => console.log(username, password)}> - {children} - - - ); + const [state, dispatch] = useContext(StateContext); + return ( + { + dispatch({type: "SetAdminPassword", password }); + }} onAuth={(username, password) => console.log(username, password)}> + {children} + + ); } const App = ({ client }: AppProps) => { @@ -54,8 +54,7 @@ const App = ({ client }: AppProps) => { createBrowserRouter([ { path: "/", - element: - + element: }, { path: "/gm", diff --git a/visions/ui/src/components/StateProvider.tsx b/visions/ui/src/components/StateProvider.tsx deleted file mode 100644 index 79b504a..0000000 --- a/visions/ui/src/components/StateProvider.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer } from "react"; -import { Status, Tabletop } from "visions-types"; -import { Client } from "../client"; -import { assertNever } from "../plugins/Candela"; - -type TabletopState = { - tabletop: Tabletop; -} - -type Action = {}; - -const initialState = (): TabletopState => ( - { - tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } - } -); - -export const AppContext = createContext(initialState()); - -interface StateProviderProps { client: Client; } - -export const StateProvider = ({ client, children }: PropsWithChildren) => { - console.log("StateProvider"); - const [state, dispatch] = useReducer(stateReducer, initialState()); - - return - {children} - ; -} - -const stateReducer = (state: TabletopState, _action: Action): TabletopState => state; - diff --git a/visions/ui/src/providers/AuthProvider/AuthProvider.tsx b/visions/ui/src/providers/AuthProvider/AuthProvider.tsx deleted file mode 100644 index e32743d..0000000 --- a/visions/ui/src/providers/AuthProvider/AuthProvider.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react"; -import { Status } from "visions-types"; -import { Client } from "../../client"; - -type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; - -type Action = { type: "SetAuthState", state: AuthState }; - -export const AuthContext = createContext({ type: "NoAdmin" }); - -interface AuthProviderProps { - client: Client; -} - -export const AuthProvider = ({ client, children }: PropsWithChildren) => { - const [authState, dispatch] = useReducer(stateReducer, { type: "NoAdmin" }); - - useEffect(() => { - client.status().then((status: Status) => { - if (status.admin_enabled) { - dispatch({ type: "SetAuthState", state: { type: "Unauthed" } }); - } else { - dispatch({ type: "SetAuthState", state: { type: "NoAdmin" } }); - } - }) - }, - [client] - ); - - return ( - {children} - ); -} - -const stateReducer = (_state: AuthState, action: Action) => action.state; diff --git a/visions/ui/src/providers/StateProvider/StateProvider.tsx b/visions/ui/src/providers/StateProvider/StateProvider.tsx new file mode 100644 index 0000000..9968cdd --- /dev/null +++ b/visions/ui/src/providers/StateProvider/StateProvider.tsx @@ -0,0 +1,37 @@ +import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer } from "react"; +import { Status, Tabletop } from "visions-types"; +import { Client } from "../../client"; +import { assertNever } from "../../plugins/Candela"; + +type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; + +type AppState = { + auth: AuthState; + tabletop: Tabletop; +} + +type Action = { type: "SetAdminPassword", password: string } | { type: "Auth", username: string, password: string }; + +const initialState = (): AppState => ( + { + auth: { type: "NoAdmin" }, + tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } + } +); + +const stateReducer = (state: AppState, action: Action): AppState => { + console.log("reducer: ", state, action); + return state; +} + +export const StateContext = createContext<[AppState, React.Dispatch]>([initialState(), () => { }]); + +interface StateProviderProps { client: Client; } + +export const StateProvider = ({ client, children }: PropsWithChildren) => { + const [state, dispatch] = useReducer(stateReducer, initialState()); + + return + {children} + ; +} diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx index 39496ab..7b0e322 100644 --- a/visions/ui/src/views/Authentication/Authentication.tsx +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -1,7 +1,6 @@ -import React, { PropsWithChildren, useContext, useState } from 'react'; -import { AppContext } from '../../components/StateProvider'; +import { PropsWithChildren, useContext, useState } from 'react'; +import { StateContext } from '../../providers/StateProvider/StateProvider'; import { assertNever } from '../../plugins/Candela'; -import { AuthContext } from '../../providers/AuthProvider/AuthProvider'; import './Authentication.css'; interface AuthenticationProps { @@ -16,9 +15,9 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC let [userField, setUserField] = useState(""); let [pwField, setPwField] = useState(""); - let auth = useContext(AuthContext); + let [state, _] = useContext(StateContext); - switch (auth.type) { + switch (state.auth.type) { case "NoAdmin": { return
@@ -45,7 +44,7 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC return
{children}
; } default: { - assertNever(auth); + assertNever(state.auth); return
; } } diff --git a/visions/ui/src/views/GmView/GmView.tsx b/visions/ui/src/views/GmView/GmView.tsx index 5d43745..948906e 100644 --- a/visions/ui/src/views/GmView/GmView.tsx +++ b/visions/ui/src/views/GmView/GmView.tsx @@ -1,6 +1,6 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { Client, PlayingField } from '../../client'; -import { AppContext } from '../../components/StateProvider'; +import { useContext, useEffect, useState } from 'react'; +import { Client } from '../../client'; +import { StateContext } from '../../providers/StateProvider/StateProvider'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail'; import { WebsocketContext } from '../../components/WebsocketProvider'; @@ -11,19 +11,19 @@ interface GmViewProps { } export const GmView = ({ client }: GmViewProps) => { - const { tabletop } = useContext(AppContext); + const [state, dispatch] = useContext(StateContext); const [images, setImages] = useState([]); useEffect(() => { client.availableImages().then((images) => setImages(images)); }, [client]); - const backgroundUrl = tabletop.backgroundImage ? client.imageUrl(tabletop.backgroundImage) : undefined; + const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined; return (
{images.map((imageName) => { client.setBackgroundImage(imageName); }} />)}
- +
) } diff --git a/visions/ui/src/views/PlayerView/PlayerView.tsx b/visions/ui/src/views/PlayerView/PlayerView.tsx index 5ead8d2..3d3304c 100644 --- a/visions/ui/src/views/PlayerView/PlayerView.tsx +++ b/visions/ui/src/views/PlayerView/PlayerView.tsx @@ -4,7 +4,7 @@ import { WebsocketContext } from '../../components/WebsocketProvider'; import { Client } from '../../client'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; import Candela from '../../plugins/Candela'; -import { AppContext } from '../../components/StateProvider'; +import { StateContext } from '../../providers/StateProvider/StateProvider'; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; @@ -13,7 +13,7 @@ interface PlayerViewProps { } export const PlayerView = ({ client }: PlayerViewProps) => { - const { tabletop } = useContext(AppContext); + const [state, dispatch] = useContext(StateContext); const [charsheet, setCharsheet] = useState(undefined); @@ -26,9 +26,9 @@ export const PlayerView = ({ client }: PlayerViewProps) => { [client, setCharsheet] ); - const backgroundColor = tabletop.backgroundColor; + const backgroundColor = state.tabletop.backgroundColor; const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`; - const backgroundUrl = tabletop.backgroundImage ? client.imageUrl(tabletop.backgroundImage) : undefined; + const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined; return (
-- 2.44.1 From 2a616ef6c95fcb3a81b43b8140ca4281c52969c8 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 17 Dec 2024 23:43:36 -0500 Subject: [PATCH 12/12] Set the admin password on a new server This sets up the client state manager and state model. It has all of the functions to support the set admin password endpoint, and some extras which will be helpful in saving users generally. --- visions/server/src/core.rs | 72 ++++---- visions/server/src/database.rs | 162 +++++++++++------- visions/server/src/handlers.rs | 22 ++- visions/server/src/main.rs | 29 +++- visions/server/src/types.rs | 3 + visions/ui/src/App.tsx | 4 +- visions/ui/src/client.ts | 20 ++- .../providers/StateProvider/StateProvider.tsx | 47 ++++- 8 files changed, 243 insertions(+), 116 deletions(-) diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index f86e8ec..79fbda9 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use async_std::sync::RwLock; use mime::Mime; -use result_extended::{fatal, ok, return_error, ResultExt}; +use result_extended::{error, fatal, ok, return_error, ResultExt}; use serde::Serialize; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use typeshare::typeshare; @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database, Error, UserId}, + database::{CharacterId, Database, UserId}, types::{AppError, FatalError, Game, Message, Tabletop, User, RGB}, }; @@ -23,7 +23,7 @@ const DEFAULT_BACKGROUND_COLOR: RGB = RGB { #[derive(Clone, Serialize)] #[typeshare] pub struct Status { - admin_enabled: bool, + pub admin_enabled: bool, } #[derive(Debug)] @@ -61,19 +61,15 @@ impl Core { pub async fn status(&self) -> ResultExt { let mut state = self.0.write().await; - let admin_user = match return_error!(state - .db - .user(UserId::from("admin")) - .await - .map_err(|_| AppError::Inaccessible("database stopped responding".to_owned()))) - { - Some(admin_user) => admin_user, - None => { + let admin_user = return_error!(match state.db.user(UserId::from("admin")).await { + Ok(Some(admin_user)) => ok(admin_user), + Ok(None) => { return ok(Status { admin_enabled: false, }); } - }; + Err(err) => fatal(err), + }); ok(Status { admin_enabled: !admin_user.password.is_empty(), @@ -113,29 +109,17 @@ 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.into_iter().map(|u| User::from(u)).collect()) - } - ResultExt::Err(err) => { - println!("Database error: {:?}", err); - ResultExt::Ok(vec![]) - } - ResultExt::Fatal(users) => ResultExt::Fatal(users), + Ok(users) => ok(users.into_iter().map(|u| User::from(u)).collect()), + Err(err) => fatal(err), } } 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), + // Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()), + Ok(games) => unimplemented!(), + Err(err) => fatal(err), } } @@ -199,10 +183,11 @@ impl Core { ) -> ResultExt, AppError, FatalError> { let mut state = self.0.write().await; let cr = state.db.character(id).await; - cr.map(|cr| cr.map(|cr| cr.data)).or_else(|err| { - println!("Database error: {:?}", err); - ResultExt::Ok(None) - }) + match cr { + Ok(Some(row)) => ok(Some(row.data)), + Ok(None) => ok(None), + Err(err) => fatal(err), + } } pub async fn publish(&self, message: Message) { @@ -214,6 +199,27 @@ impl Core { } }); } + + pub async fn set_password( + &self, + uuid: UserId, + password: String, + ) -> ResultExt<(), AppError, FatalError> { + let mut state = self.0.write().await; + let user = match state.db.user(uuid.clone()).await { + Ok(Some(row)) => row, + Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())), + Err(err) => return fatal(err), + }; + match state + .db + .save_user(Some(uuid), &user.name, &password, user.admin, user.enabled) + .await + { + Ok(_) => ok(()), + Err(err) => fatal(err), + } + } } #[cfg(test)] diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index 144c576..25b27b7 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -4,14 +4,12 @@ use async_std::channel::{bounded, Receiver, Sender}; 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::{ types::{FromSql, FromSqlResult, ValueRef}, Connection, }; use rusqlite_migration::Migrations; use serde::{Deserialize, Serialize}; -use thiserror::Error; use uuid::Uuid; use crate::types::FatalError; @@ -23,18 +21,13 @@ lazy_static! { Migrations::from_directory(&MIGRATIONS_DIR).unwrap(); } -#[derive(Debug, Error)] -pub enum Error { - #[error("No response to request")] - NoResponse, -} - #[derive(Debug)] enum Request { Charsheet(CharacterId), Games, User(UserId), Users, + SaveUser(Option, String, String, bool, bool), } #[derive(Debug)] @@ -49,6 +42,7 @@ enum DatabaseResponse { Games(Vec), User(Option), Users(Vec), + SaveUser(UserId), } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] @@ -184,19 +178,22 @@ pub struct CharsheetRow { #[async_trait] pub trait Database: Send + Sync { - async fn user( + async fn user(&mut self, _: UserId) -> Result, FatalError>; + + async fn save_user( &mut self, - _: UserId, - ) -> result_extended::ResultExt, Error, FatalError>; + user_id: Option, + name: &str, + password: &str, + admin: bool, + enabled: bool, + ) -> Result; - async fn users(&mut self) -> result_extended::ResultExt, Error, FatalError>; + async fn users(&mut self) -> Result, FatalError>; - async fn games(&mut self) -> result_extended::ResultExt, Error, FatalError>; + async fn games(&mut self) -> Result, FatalError>; - async fn character( - &mut self, - id: CharacterId, - ) -> result_extended::ResultExt, Error, FatalError>; + async fn character(&mut self, id: CharacterId) -> Result, FatalError>; } pub struct DiskDb { @@ -268,27 +265,6 @@ 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 @@ -314,6 +290,27 @@ 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| { + 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 save_user( &self, user_id: Option, @@ -337,7 +334,7 @@ impl DiskDb { let mut stmt = self .conn .prepare( - "UPDATE users SET name=?, password=?, admin=?, enbabled=? WHERE uuid=?", + "UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?", ) .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; stmt.execute((name, password, admin, enabled, user_id.as_str())) @@ -398,7 +395,7 @@ impl DiskDb { char_id: Option, game: GameId, character: serde_json::Value, - ) -> std::result::Result { + ) -> std::result::Result { match char_id { None => { let char_id = CharacterId::new(); @@ -451,6 +448,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { err => panic!("{:?}", err), } } + Request::SaveUser(user_id, username, password, admin, enabled) => { + let user_id = db.save_user(user_id, username.as_ref(), password.as_ref(), admin, enabled); + match user_id { + Ok(user_id) => { + tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap(); + } + err => panic!("{:?}", err), + } + } Request::Users => { let users = db.users(); match users { @@ -488,7 +494,7 @@ impl DbConn { #[async_trait] impl Database for DbConn { - async fn user(&mut self, uid: UserId) -> ResultExt, Error, FatalError> { + async fn user(&mut self, uid: UserId) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { @@ -498,17 +504,50 @@ impl Database for DbConn { match self.conn.send(request).await { Ok(()) => (), - Err(_) => return fatal(FatalError::DatabaseConnectionLost), + Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { - Ok(DatabaseResponse::User(user)) => ok(user), - Ok(_) => fatal(FatalError::MessageMismatch), - Err(_) => error(Error::NoResponse), + Ok(DatabaseResponse::User(user)) => Ok(user), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), } } - async fn users(&mut self) -> ResultExt, Error, FatalError> { + async fn save_user( + &mut self, + user_id: Option, + name: &str, + password: &str, + admin: bool, + enabled: bool, + ) -> Result { + let (tx, rx) = bounded::(1); + + let request = DatabaseRequest { + tx, + req: Request::SaveUser( + user_id, + name.to_owned(), + password.to_owned(), + admin, + enabled, + ), + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return Err(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::SaveUser(user_id)) => Ok(user_id), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), + } + } + + async fn users(&mut self) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { @@ -518,17 +557,17 @@ impl Database for DbConn { match self.conn.send(request).await { Ok(()) => (), - Err(_) => return fatal(FatalError::DatabaseConnectionLost), + Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { - Ok(DatabaseResponse::Users(lst)) => ok(lst), - Ok(_) => fatal(FatalError::MessageMismatch), - Err(_) => error(Error::NoResponse), + Ok(DatabaseResponse::Users(lst)) => Ok(lst), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), } } - async fn games(&mut self) -> result_extended::ResultExt, Error, FatalError> { + async fn games(&mut self) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { @@ -538,20 +577,17 @@ impl Database for DbConn { match self.conn.send(request).await { Ok(()) => (), - Err(_) => return fatal(FatalError::DatabaseConnectionLost), + Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { - Ok(DatabaseResponse::Games(lst)) => ok(lst), - Ok(_) => fatal(FatalError::MessageMismatch), - Err(_) => error(Error::NoResponse), + Ok(DatabaseResponse::Games(lst)) => Ok(lst), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), } } - async fn character( - &mut self, - id: CharacterId, - ) -> ResultExt, Error, FatalError> { + async fn character(&mut self, id: CharacterId) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { @@ -561,13 +597,13 @@ impl Database for DbConn { match self.conn.send(request).await { Ok(()) => (), - Err(_) => return fatal(FatalError::DatabaseConnectionLost), + Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { - Ok(DatabaseResponse::Charsheet(row)) => ok(row), - Ok(_) => fatal(FatalError::MessageMismatch), - Err(_err) => error(Error::NoResponse), + Ok(DatabaseResponse::Charsheet(row)) => Ok(row), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), } } } diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 5f43c1f..4392f3a 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -1,14 +1,14 @@ use std::future::Future; use futures::{SinkExt, StreamExt}; -use result_extended::{ok, return_error, ResultExt}; +use result_extended::{error, 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, + database::{CharacterId, UserId}, types::{AppError, FatalError}, }; @@ -237,3 +237,21 @@ pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply { }) .await } + +pub async fn handle_set_admin_password(core: Core, password: String) -> impl Reply { + handler(async move { + let status = return_error!(core.status().await); + if status.admin_enabled { + return error(AppError::PermissionDenied); + } + + core.set_password(UserId::from("admin"), password).await; + ok(Response::builder() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "*") + .header("Content-Type", "application/json") + .body(vec![]) + .unwrap()) + }) + .await +} diff --git a/visions/server/src/main.rs b/visions/server/src/main.rs index 2f3a868..894e2cc 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_get_users, handle_register_client, handle_server_status, 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_server_status, handle_set_admin_password, handle_set_background_image, handle_unregister_client, RegisterRequest }; use warp::{ // header, @@ -104,7 +104,7 @@ pub async fn main() { let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let log = warp::log("visions::api"); - let server_status = warp::path!("api" / "v1" / "status") + let route_server_status = warp::path!("api" / "v1" / "status") .and(warp::get()) .then({ let core = core.clone(); @@ -181,7 +181,28 @@ pub async fn main() { move |charid| handle_get_charsheet(core.clone(), charid) }); - let filter = server_status + let route_set_admin_password_options = warp::path!("api" / "v1" / "admin_password") + .and(warp::options()) + .map({ + move || { + Response::builder() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "PUT") + .header("Access-Control-Allow-Headers", "content-type") + .header("Content-Type", "application/json") + .body("") + .unwrap() + } + }); + let route_set_admin_password = warp::path!("api" / "v1" / "admin_password") + .and(warp::put()) + .and(warp::body::json()) + .then({ + let core = core.clone(); + move |body| handle_set_admin_password(core.clone(), body) + }); + + let filter = route_server_status .or(route_register_client) .or(route_unregister_client) .or(route_websocket) @@ -191,6 +212,8 @@ pub async fn main() { .or(route_set_bg_image) .or(route_get_users) .or(route_get_charsheet) + .or(route_set_admin_password_options) + .or(route_set_admin_password) .recover(handle_rejection); let server = warp::serve(filter); diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index c453537..d70e387 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -33,6 +33,9 @@ pub enum AppError { #[error("object inaccessible {0}")] Inaccessible(String), + #[error("the requested operation is not allowed")] + PermissionDenied, + #[error("invalid json {0}")] JsonError(serde_json::Error), diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index 373750c..4e26c74 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -32,10 +32,10 @@ interface AuthedViewProps { } const AuthedView = ({ client, children }: PropsWithChildren) => { - const [state, dispatch] = useContext(StateContext); + const [state, manager] = useContext(StateContext); return ( { - dispatch({type: "SetAdminPassword", password }); + manager.setAdminPassword(password); }} onAuth={(username, password) => console.log(username, password)}> {children} diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index 4656c55..274ec77 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -9,12 +9,6 @@ export class Client { this.base = new URL("http://localhost:8001"); } - status() { - const url = new URL(this.base); - url.pathname = `/api/v1/status`; - return fetch(url).then((response) => response.json()); - } - registerWebsocket() { const url = new URL(this.base); url.pathname = `api/v1/client`; @@ -62,4 +56,18 @@ export class Client { url.pathname = `/api/v1/charsheet/${id}`; return fetch(url).then((response) => response.json()); } + + async setAdminPassword(password: string) { + const url = new URL(this.base); + url.pathname = `/api/v1/admin_password`; + console.log("setting the admin password to: ", password); + return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) }); + } + + async status() { + const url = new URL(this.base); + url.pathname = `/api/v1/status`; + return fetch(url).then((response) => response.json()); + } + } diff --git a/visions/ui/src/providers/StateProvider/StateProvider.tsx b/visions/ui/src/providers/StateProvider/StateProvider.tsx index 9968cdd..8806702 100644 --- a/visions/ui/src/providers/StateProvider/StateProvider.tsx +++ b/visions/ui/src/providers/StateProvider/StateProvider.tsx @@ -1,16 +1,16 @@ -import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer } from "react"; +import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react"; import { Status, Tabletop } from "visions-types"; import { Client } from "../../client"; import { assertNever } from "../../plugins/Candela"; -type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; +type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string }; type AppState = { auth: AuthState; tabletop: Tabletop; } -type Action = { type: "SetAdminPassword", password: string } | { type: "Auth", username: string, password: string }; +type Action = { type: "SetAuthState", content: AuthState }; const initialState = (): AppState => ( { @@ -20,18 +20,51 @@ const initialState = (): AppState => ( ); const stateReducer = (state: AppState, action: Action): AppState => { - console.log("reducer: ", state, action); - return state; + return { ...state, auth: action.content } } -export const StateContext = createContext<[AppState, React.Dispatch]>([initialState(), () => { }]); +class StateManager { + client: Client | undefined; + dispatch: React.Dispatch | undefined; + + constructor(client: Client | undefined, dispatch: React.Dispatch | undefined) { + this.client = client; + this.dispatch = dispatch; + } + + async status() { + if (!this.client || !this.dispatch) return; + + const { admin_enabled } = await this.client.status(); + if (!admin_enabled) { + this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } }); + } else { + this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } }); + } + } + + async setAdminPassword(password: string) { + if (!this.client || !this.dispatch) return; + + await this.client.setAdminPassword(password); + await this.status(); + } +} + +export const StateContext = createContext<[AppState, StateManager]>([initialState(), new StateManager(undefined, undefined)]); interface StateProviderProps { client: Client; } export const StateProvider = ({ client, children }: PropsWithChildren) => { const [state, dispatch] = useReducer(stateReducer, initialState()); - return + const stateManager = useRef(new StateManager(client, dispatch)); + + useEffect(() => { + stateManager.current.status(); + }, [stateManager]); + + return {children} ; } -- 2.44.1