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..e4aba62 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 +/// 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 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/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/01-initial-db/up.sql b/visions/server/migrations/01-initial-db/up.sql new file mode 100644 index 0000000..3e822af --- /dev/null +++ b/visions/server/migrations/01-initial-db/up.sql @@ -0,0 +1,32 @@ +CREATE TABLE users( + uuid TEXT PRIMARY KEY, + name TEXT, + password TEXT, + admin BOOLEAN, + 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, + role TEXT, + + 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 9bb4c9f..79fbda9 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -2,13 +2,16 @@ use std::{collections::HashMap, sync::Arc}; use async_std::sync::RwLock; use mime::Mime; +use result_extended::{error, fatal, ok, return_error, ResultExt}; +use serde::Serialize; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use typeshare::typeshare; use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database}, - types::{AppError, Message, Tabletop, RGB}, + database::{CharacterId, Database, UserId}, + types::{AppError, FatalError, Game, Message, Tabletop, User, RGB}, }; const DEFAULT_BACKGROUND_COLOR: RGB = RGB { @@ -17,6 +20,12 @@ const DEFAULT_BACKGROUND_COLOR: RGB = RGB { blue: 0xbb, }; +#[derive(Clone, Serialize)] +#[typeshare] +pub struct Status { + pub admin_enabled: bool, +} + #[derive(Debug)] struct WebsocketClient { sender: Option>, @@ -50,6 +59,23 @@ impl Core { }))) } + pub async fn status(&self) -> ResultExt { + let mut state = self.0.write().await; + 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(), + }) + } + pub async fn register_client(&self) -> String { let mut state = self.0.write().await; let uuid = Uuid::new_v4().simple().to_string(); @@ -80,21 +106,47 @@ impl Core { } } + pub async fn list_users(&self) -> ResultExt, AppError, FatalError> { + let users = self.0.write().await.db.users().await; + match 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 { + // Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()), + Ok(games) => unimplemented!(), + Err(err) => fatal(err), + } + } + pub async fn tabletop(&self) -> Tabletop { 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 +164,30 @@ 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; + match cr { + Ok(Some(row)) => ok(Some(row.data)), + Ok(None) => ok(None), + Err(err) => fatal(err), + } } pub async fn publish(&self, message: Message) { @@ -146,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)] @@ -197,14 +271,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 +287,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 +296,11 @@ 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 +309,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 f2a8b7c..25b27b7 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -1,18 +1,19 @@ -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 rusqlite::Connection; +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; + static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); lazy_static! { @@ -20,21 +21,13 @@ lazy_static! { Migrations::from_directory(&MIGRATIONS_DIR).unwrap(); } -#[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, -} - #[derive(Debug)] enum Request { Charsheet(CharacterId), + Games, + User(UserId), + Users, + SaveUser(Option, String, String, bool, bool), } #[derive(Debug)] @@ -46,6 +39,78 @@ struct DatabaseRequest { #[derive(Debug)] enum DatabaseResponse { Charsheet(Option), + Games(Vec), + User(Option), + Users(Vec), + SaveUser(UserId), +} + +#[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) + } +} + +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); + +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) + } +} + +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)] @@ -53,7 +118,7 @@ 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,53 +128,126 @@ 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) } } +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 { + pub id: UserId, + pub name: String, + pub password: String, + pub admin: bool, + pub enabled: bool, +} + +#[derive(Clone, Debug)] +pub struct Role { + 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, - gametype: String, + game: GameId, pub data: serde_json::Value, } #[async_trait] pub trait Database: Send + Sync { - async fn charsheet(&mut self, id: CharacterId) -> Result, Error>; + async fn user(&mut self, _: UserId) -> Result, FatalError>; + + async fn save_user( + &mut self, + user_id: Option, + name: &str, + password: &str, + admin: bool, + enabled: bool, + ) -> Result; + + async fn users(&mut self) -> Result, FatalError>; + + async fn games(&mut self) -> Result, FatalError>; + + async fn character(&mut self, id: CharacterId) -> Result, 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) { + 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 game_stmt = conn.prepare("INSERT INTO games VALUES (?, ?)").unwrap(); - game_stmt.execute((game_id.clone(), "Circle of Bluest Sky")); + 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 (?, ?)") + .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 (?, ?, ?)") + .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, { @@ -117,24 +255,128 @@ 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 charsheet(&self, id: CharacterId) -> Result, Error> { + fn user(&self, id: UserId) -> Result, FatalError> { let mut stmt = self .conn - .prepare("SELECT uuid, gametype, data FROM charsheet 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| { + 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())), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), + } + } + + 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, + 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=?, enabled=? 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 + .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(); Ok(CharsheetRow { id: row.get(0).unwrap(), - gametype: row.get(1).unwrap(), + game: row.get(1).unwrap(), data: serde_json::from_str(&data).unwrap(), }) }) @@ -144,24 +386,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 { + game: GameId, + 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.as_str(), character.to_string())) .unwrap(); Ok(char_id) @@ -169,9 +411,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) @@ -181,12 +423,11 @@ 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 { Request::Charsheet(id) => { - let sheet = db.charsheet(id); + let sheet = db.character(id); println!("sheet retrieved: {:?}", sheet); match sheet { Ok(sheet) => { @@ -195,6 +436,36 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { _ => unimplemented!(), } } + 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::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 { + Ok(users) => { + tx.send(DatabaseResponse::Users(users)).await.unwrap(); + } + _ => unimplemented!(), + } + } } } println!("ending db_handler"); @@ -223,52 +494,158 @@ impl DbConn { #[async_trait] impl Database for DbConn { - async fn charsheet(&mut self, id: CharacterId) -> Result, Error> { + async fn user(&mut self, uid: UserId) -> Result, FatalError> { + let (tx, rx) = bounded::(1); + + let request = DatabaseRequest { + tx, + req: Request::User(uid), + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return Err(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::User(user)) => Ok(user), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), + } + } + + 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 { + tx, + req: Request::Users, + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return Err(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::Users(lst)) => Ok(lst), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), + } + } + + async fn games(&mut self) -> Result, FatalError> { + let (tx, rx) = bounded::(1); + + let request = DatabaseRequest { + tx, + req: Request::Games, + }; + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return Err(FatalError::DatabaseConnectionLost), + }; + + match rx.recv().await { + Ok(DatabaseResponse::Games(lst)) => Ok(lst), + Ok(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), + } + } + + async fn character(&mut self, id: CharacterId) -> Result, 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 Err(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(_) => Err(FatalError::MessageMismatch), + Err(_) => Err(FatalError::DatabaseConnectionLost), } } } #[cfg(test)] mod test { + use std::path::PathBuf; + use cool_asserts::assert_matches; use super::*; 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 setup_db() -> (DiskDb, GameId) { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); - assert_matches!(db.charsheet(CharacterId::from("1")), Ok(None)); + 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_charsheet(None, "candela".to_owned(), js.clone()) - .unwrap(); - assert_matches!(db.charsheet(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); + 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)); } #[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..4392f3a 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -1,10 +1,16 @@ use std::future::Future; use futures::{SinkExt, StreamExt}; +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, types::AppError}; +use crate::{ + asset_db::AssetId, + core::Core, + database::{CharacterId, UserId}, + types::{AppError, FatalError}, +}; /* pub async fn handle_auth( @@ -32,26 +38,41 @@ 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_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) = core.get_asset(asset_id).await?; - Ok(Response::builder() - .header("application-type", mime.to_string()) + let (mime, bytes) = return_error!(core.get_asset(asset_id).await); + ok(Response::builder() + .header("content-type", mime.to_string()) .body(bytes) .unwrap()) }) @@ -67,7 +88,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 +109,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 +127,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 +171,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") @@ -160,17 +181,55 @@ 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_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 = 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()), @@ -178,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 d695fdc..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_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_admin_password, 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 route_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({ @@ -160,6 +167,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({ @@ -167,14 +181,39 @@ pub async fn main() { move |charid| handle_get_charsheet(core.clone(), charid) }); - let filter = route_register_client + 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) .or(route_image) .or(route_available_images) .or(route_set_bg_image_options) .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 f5c5307..d70e387 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -1,14 +1,45 @@ use serde::{Deserialize, Serialize}; +use thiserror::Error; use typeshare::typeshare; -use crate::asset_db::AssetId; +use crate::{asset_db::AssetId, database::UserRow}; -#[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("the requested operation is not allowed")] + PermissionDenied, + + #[error("invalid json {0}")] JsonError(serde_json::Error), + + #[error("wat {0}")] UnexpectedError(String), } @@ -21,6 +52,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.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/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'], - } -} diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index cd6db11..4e26c74 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, useContext, useEffect, useState } from 'react'; import './App.css'; import { Client } from './client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; @@ -6,7 +6,12 @@ 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'; +import { Authentication } from './views/Authentication/Authentication'; +import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'; + +const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; interface AppProps { client: Client; @@ -15,13 +20,28 @@ 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] ); return sheet ? :
} +interface AuthedViewProps { + client: Client; +} + +const AuthedView = ({ client, children }: PropsWithChildren) => { + const [state, manager] = useContext(StateContext); + return ( + { + manager.setAdminPassword(password); + }} onAuth={(username, password) => console.log(username, password)}> + {children} + + ); +} + const App = ({ client }: AppProps) => { console.log("rendering app"); const [websocketUrl, setWebsocketUrl] = useState(undefined); @@ -32,13 +52,17 @@ const App = ({ client }: AppProps) => { let router = createBrowserRouter([ + { + path: "/", + element: + }, { path: "/gm", element: websocketUrl ? :
}, { - path: "/", - element: websocketUrl ? :
+ path: "/admin", + element: }, { path: "/candela", diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index b5f43b9..274ec77 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -45,9 +45,29 @@ 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}`; 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/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/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/providers/StateProvider/StateProvider.tsx b/visions/ui/src/providers/StateProvider/StateProvider.tsx new file mode 100644 index 0000000..8806702 --- /dev/null +++ b/visions/ui/src/providers/StateProvider/StateProvider.tsx @@ -0,0 +1,70 @@ +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", userid: string }; + +type AppState = { + auth: AuthState; + tabletop: Tabletop; +} + +type Action = { type: "SetAuthState", content: AuthState }; + +const initialState = (): AppState => ( + { + auth: { type: "NoAdmin" }, + tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } + } +); + +const stateReducer = (state: AppState, action: Action): AppState => { + return { ...state, auth: action.content } +} + +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()); + + const stateManager = useRef(new StateManager(client, dispatch)); + + useEffect(() => { + stateManager.current.status(); + }, [stateManager]); + + return + {children} + ; +} diff --git a/visions/ui.backup/src/visions.css b/visions/ui/src/views/Admin/Admin.css similarity index 100% rename from visions/ui.backup/src/visions.css rename to visions/ui/src/views/Admin/Admin.css diff --git a/visions/ui/src/views/Admin/Admin.tsx b/visions/ui/src/views/Admin/Admin.tsx new file mode 100644 index 0000000..086faf5 --- /dev/null +++ b/visions/ui/src/views/Admin/Admin.tsx @@ -0,0 +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>([]); + + useEffect(() => { + client.users().then((u) => { + console.log(u); + setUsers(u); + }); + }, [client]); + + console.log(users); + return ( + + {users.map((user) => )} + +
); +} diff --git a/visions/ui/src/views/Authentication/Authentication.css b/visions/ui/src/views/Authentication/Authentication.css new file mode 100644 index 0000000..a938112 --- /dev/null +++ b/visions/ui/src/views/Authentication/Authentication.css @@ -0,0 +1,18 @@ +@import '../../design.css'; + +.auth { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100vh; +} + +.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..7b0e322 --- /dev/null +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -0,0 +1,51 @@ +import { PropsWithChildren, useContext, useState } from 'react'; +import { StateContext } from '../../providers/StateProvider/StateProvider'; +import { assertNever } from '../../plugins/Candela'; +import './Authentication.css'; + +interface AuthenticationProps { + onAdminPassword: (password: string) => void; + onAuth: (username: string, password: string) => void; +} + +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 [userField, setUserField] = useState(""); + let [pwField, setPwField] = useState(""); + let [state, _] = useContext(StateContext); + + switch (state.auth.type) { + case "NoAdmin": { + return
+
+

Welcome to your new Visions VTT Instance

+

Set your admin password:

+ setPwField(evt.target.value)} /> + onAdminPassword(pwField)} /> +
+
; + } + case "Unauthed": { + return
+
+

Welcome to Visions VTT

+
+ setUserField(evt.target.value)} /> + setPwField(evt.target.value)} /> + onAuth(userField, pwField)} /> +
+
+
; + } + case "Authed": { + return
{children}
; + } + default: { + assertNever(state.auth); + return
; + } + } +} diff --git a/visions/ui/src/views/GmView/GmView.tsx b/visions/ui/src/views/GmView/GmView.tsx index cc697f3..948906e 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 { 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'; @@ -10,19 +11,19 @@ interface GmViewProps { } export const GmView = ({ client }: GmViewProps) => { - const { tabletop } = useContext(WebsocketContext); + 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 9f12c60..3d3304c 100644 --- a/visions/ui/src/views/PlayerView/PlayerView.tsx +++ b/visions/ui/src/views/PlayerView/PlayerView.tsx @@ -4,28 +4,31 @@ import { WebsocketContext } from '../../components/WebsocketProvider'; import { Client } from '../../client'; import { TabletopElement } from '../../components/Tabletop/Tabletop'; import Candela from '../../plugins/Candela'; +import { StateContext } from '../../providers/StateProvider/StateProvider'; + +const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; interface PlayerViewProps { client: Client; } export const PlayerView = ({ client }: PlayerViewProps) => { - const { tabletop } = useContext(WebsocketContext); + const [state, dispatch] = useContext(StateContext); const [charsheet, setCharsheet] = useState(undefined); useEffect( () => { - client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => { + client.charsheet(TEST_CHARSHEET_UUID).then((c) => { setCharsheet(c) }); }, [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 (