Set up the user interface state model and set up the admin user onboarding #283

Merged
savanni merged 12 commits from visions-admin into main 2024-12-18 14:18:16 +00:00
8 changed files with 243 additions and 116 deletions
Showing only changes of commit 2a616ef6c9 - Show all commits

View File

@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
use async_std::sync::RwLock; use async_std::sync::RwLock;
use mime::Mime; use mime::Mime;
use result_extended::{fatal, ok, return_error, ResultExt}; use result_extended::{error, fatal, ok, return_error, ResultExt};
use serde::Serialize; use serde::Serialize;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use typeshare::typeshare; use typeshare::typeshare;
@ -10,7 +10,7 @@ use uuid::Uuid;
use crate::{ use crate::{
asset_db::{self, AssetId, Assets}, asset_db::{self, AssetId, Assets},
database::{CharacterId, Database, Error, UserId}, database::{CharacterId, Database, UserId},
types::{AppError, FatalError, Game, Message, Tabletop, User, RGB}, types::{AppError, FatalError, Game, Message, Tabletop, User, RGB},
}; };
@ -23,7 +23,7 @@ const DEFAULT_BACKGROUND_COLOR: RGB = RGB {
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
#[typeshare] #[typeshare]
pub struct Status { pub struct Status {
admin_enabled: bool, pub admin_enabled: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@ -61,19 +61,15 @@ impl Core {
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> { pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
let mut state = self.0.write().await; let mut state = self.0.write().await;
let admin_user = match return_error!(state let admin_user = return_error!(match state.db.user(UserId::from("admin")).await {
.db Ok(Some(admin_user)) => ok(admin_user),
.user(UserId::from("admin")) Ok(None) => {
.await
.map_err(|_| AppError::Inaccessible("database stopped responding".to_owned())))
{
Some(admin_user) => admin_user,
None => {
return ok(Status { return ok(Status {
admin_enabled: false, admin_enabled: false,
}); });
} }
}; Err(err) => fatal(err),
});
ok(Status { ok(Status {
admin_enabled: !admin_user.password.is_empty(), admin_enabled: !admin_user.password.is_empty(),
@ -113,29 +109,17 @@ impl Core {
pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> { pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> {
let users = self.0.write().await.db.users().await; let users = self.0.write().await.db.users().await;
match users { match users {
ResultExt::Ok(users) => { Ok(users) => ok(users.into_iter().map(|u| User::from(u)).collect()),
ResultExt::Ok(users.into_iter().map(|u| User::from(u)).collect()) Err(err) => fatal(err),
}
ResultExt::Err(err) => {
println!("Database error: {:?}", err);
ResultExt::Ok(vec![])
}
ResultExt::Fatal(users) => ResultExt::Fatal(users),
} }
} }
pub async fn list_games(&self) -> ResultExt<Vec<Game>, AppError, FatalError> { pub async fn list_games(&self) -> ResultExt<Vec<Game>, AppError, FatalError> {
let games = self.0.write().await.db.games().await; let games = self.0.write().await.db.games().await;
match games { match games {
ResultExt::Ok(games) => { // Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
// ResultExt::Ok(games.into_iter().map(|u| Game::from(u)).collect()) Ok(games) => unimplemented!(),
unimplemented!(); Err(err) => fatal(err),
}
ResultExt::Err(err) => {
println!("Database error: {:?}", err);
ResultExt::Ok(vec![])
}
ResultExt::Fatal(games) => ResultExt::Fatal(games),
} }
} }
@ -199,10 +183,11 @@ impl Core {
) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> { ) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> {
let mut state = self.0.write().await; let mut state = self.0.write().await;
let cr = state.db.character(id).await; let cr = state.db.character(id).await;
cr.map(|cr| cr.map(|cr| cr.data)).or_else(|err| { match cr {
println!("Database error: {:?}", err); Ok(Some(row)) => ok(Some(row.data)),
ResultExt::Ok(None) Ok(None) => ok(None),
}) Err(err) => fatal(err),
}
} }
pub async fn publish(&self, message: Message) { pub async fn publish(&self, message: Message) {
@ -214,6 +199,27 @@ impl Core {
} }
}); });
} }
pub async fn set_password(
&self,
uuid: UserId,
password: String,
) -> ResultExt<(), AppError, FatalError> {
let mut state = self.0.write().await;
let user = match state.db.user(uuid.clone()).await {
Ok(Some(row)) => row,
Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())),
Err(err) => return fatal(err),
};
match state
.db
.save_user(Some(uuid), &user.name, &password, user.admin, user.enabled)
.await
{
Ok(_) => ok(()),
Err(err) => fatal(err),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -4,14 +4,12 @@ use async_std::channel::{bounded, Receiver, Sender};
use async_trait::async_trait; use async_trait::async_trait;
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use result_extended::{error, fatal, ok, return_error, ResultExt};
use rusqlite::{ use rusqlite::{
types::{FromSql, FromSqlResult, ValueRef}, types::{FromSql, FromSqlResult, ValueRef},
Connection, Connection,
}; };
use rusqlite_migration::Migrations; use rusqlite_migration::Migrations;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::types::FatalError; use crate::types::FatalError;
@ -23,18 +21,13 @@ lazy_static! {
Migrations::from_directory(&MIGRATIONS_DIR).unwrap(); Migrations::from_directory(&MIGRATIONS_DIR).unwrap();
} }
#[derive(Debug, Error)]
pub enum Error {
#[error("No response to request")]
NoResponse,
}
#[derive(Debug)] #[derive(Debug)]
enum Request { enum Request {
Charsheet(CharacterId), Charsheet(CharacterId),
Games, Games,
User(UserId), User(UserId),
Users, Users,
SaveUser(Option<UserId>, String, String, bool, bool),
} }
#[derive(Debug)] #[derive(Debug)]
@ -49,6 +42,7 @@ enum DatabaseResponse {
Games(Vec<GameRow>), Games(Vec<GameRow>),
User(Option<UserRow>), User(Option<UserRow>),
Users(Vec<UserRow>), Users(Vec<UserRow>),
SaveUser(UserId),
} }
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
@ -184,19 +178,22 @@ pub struct CharsheetRow {
#[async_trait] #[async_trait]
pub trait Database: Send + Sync { pub trait Database: Send + Sync {
async fn user( async fn user(&mut self, _: UserId) -> Result<Option<UserRow>, FatalError>;
async fn save_user(
&mut self, &mut self,
_: UserId, user_id: Option<UserId>,
) -> result_extended::ResultExt<Option<UserRow>, Error, FatalError>; name: &str,
password: &str,
admin: bool,
enabled: bool,
) -> Result<UserId, FatalError>;
async fn users(&mut self) -> result_extended::ResultExt<Vec<UserRow>, Error, FatalError>; async fn users(&mut self) -> Result<Vec<UserRow>, FatalError>;
async fn games(&mut self) -> result_extended::ResultExt<Vec<GameRow>, Error, FatalError>; async fn games(&mut self) -> Result<Vec<GameRow>, FatalError>;
async fn character( async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
&mut self,
id: CharacterId,
) -> result_extended::ResultExt<Option<CharsheetRow>, Error, FatalError>;
} }
pub struct DiskDb { pub struct DiskDb {
@ -268,27 +265,6 @@ impl DiskDb {
Ok(DiskDb { conn }) Ok(DiskDb { conn })
} }
fn users(&self) -> Result<Vec<UserRow>, 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::<Result<Vec<UserRow>, rusqlite::Error>>()
.unwrap();
Ok(items)
}
fn user(&self, id: UserId) -> Result<Option<UserRow>, FatalError> { fn user(&self, id: UserId) -> Result<Option<UserRow>, FatalError> {
let mut stmt = self let mut stmt = self
.conn .conn
@ -314,6 +290,27 @@ impl DiskDb {
} }
} }
fn users(&self) -> Result<Vec<UserRow>, 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::<Result<Vec<UserRow>, rusqlite::Error>>()
.unwrap();
Ok(items)
}
fn save_user( fn save_user(
&self, &self,
user_id: Option<UserId>, user_id: Option<UserId>,
@ -337,7 +334,7 @@ impl DiskDb {
let mut stmt = self let mut stmt = self
.conn .conn
.prepare( .prepare(
"UPDATE users SET name=?, password=?, admin=?, enbabled=? WHERE uuid=?", "UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?",
) )
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
stmt.execute((name, password, admin, enabled, user_id.as_str())) stmt.execute((name, password, admin, enabled, user_id.as_str()))
@ -398,7 +395,7 @@ impl DiskDb {
char_id: Option<CharacterId>, char_id: Option<CharacterId>,
game: GameId, game: GameId,
character: serde_json::Value, character: serde_json::Value,
) -> std::result::Result<CharacterId, Error> { ) -> std::result::Result<CharacterId, FatalError> {
match char_id { match char_id {
None => { None => {
let char_id = CharacterId::new(); let char_id = CharacterId::new();
@ -451,6 +448,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
err => panic!("{:?}", err), 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 => { Request::Users => {
let users = db.users(); let users = db.users();
match users { match users {
@ -488,7 +494,7 @@ impl DbConn {
#[async_trait] #[async_trait]
impl Database for DbConn { impl Database for DbConn {
async fn user(&mut self, uid: UserId) -> ResultExt<Option<UserRow>, Error, FatalError> { async fn user(&mut self, uid: UserId) -> Result<Option<UserRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1); let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest { let request = DatabaseRequest {
@ -498,17 +504,50 @@ impl Database for DbConn {
match self.conn.send(request).await { match self.conn.send(request).await {
Ok(()) => (), Ok(()) => (),
Err(_) => return fatal(FatalError::DatabaseConnectionLost), Err(_) => return Err(FatalError::DatabaseConnectionLost),
}; };
match rx.recv().await { match rx.recv().await {
Ok(DatabaseResponse::User(user)) => ok(user), Ok(DatabaseResponse::User(user)) => Ok(user),
Ok(_) => fatal(FatalError::MessageMismatch), Ok(_) => Err(FatalError::MessageMismatch),
Err(_) => error(Error::NoResponse), Err(_) => Err(FatalError::DatabaseConnectionLost),
} }
} }
async fn users(&mut self) -> ResultExt<Vec<UserRow>, Error, FatalError> { async fn save_user(
&mut self,
user_id: Option<UserId>,
name: &str,
password: &str,
admin: bool,
enabled: bool,
) -> Result<UserId, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(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<Vec<UserRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1); let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest { let request = DatabaseRequest {
@ -518,17 +557,17 @@ impl Database for DbConn {
match self.conn.send(request).await { match self.conn.send(request).await {
Ok(()) => (), Ok(()) => (),
Err(_) => return fatal(FatalError::DatabaseConnectionLost), Err(_) => return Err(FatalError::DatabaseConnectionLost),
}; };
match rx.recv().await { match rx.recv().await {
Ok(DatabaseResponse::Users(lst)) => ok(lst), Ok(DatabaseResponse::Users(lst)) => Ok(lst),
Ok(_) => fatal(FatalError::MessageMismatch), Ok(_) => Err(FatalError::MessageMismatch),
Err(_) => error(Error::NoResponse), Err(_) => Err(FatalError::DatabaseConnectionLost),
} }
} }
async fn games(&mut self) -> result_extended::ResultExt<Vec<GameRow>, Error, FatalError> { async fn games(&mut self) -> Result<Vec<GameRow>, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1); let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest { let request = DatabaseRequest {
@ -538,20 +577,17 @@ impl Database for DbConn {
match self.conn.send(request).await { match self.conn.send(request).await {
Ok(()) => (), Ok(()) => (),
Err(_) => return fatal(FatalError::DatabaseConnectionLost), Err(_) => return Err(FatalError::DatabaseConnectionLost),
}; };
match rx.recv().await { match rx.recv().await {
Ok(DatabaseResponse::Games(lst)) => ok(lst), Ok(DatabaseResponse::Games(lst)) => Ok(lst),
Ok(_) => fatal(FatalError::MessageMismatch), Ok(_) => Err(FatalError::MessageMismatch),
Err(_) => error(Error::NoResponse), Err(_) => Err(FatalError::DatabaseConnectionLost),
} }
} }
async fn character( async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
&mut self,
id: CharacterId,
) -> ResultExt<Option<CharsheetRow>, Error, FatalError> {
let (tx, rx) = bounded::<DatabaseResponse>(1); let (tx, rx) = bounded::<DatabaseResponse>(1);
let request = DatabaseRequest { let request = DatabaseRequest {
@ -561,13 +597,13 @@ impl Database for DbConn {
match self.conn.send(request).await { match self.conn.send(request).await {
Ok(()) => (), Ok(()) => (),
Err(_) => return fatal(FatalError::DatabaseConnectionLost), Err(_) => return Err(FatalError::DatabaseConnectionLost),
}; };
match rx.recv().await { match rx.recv().await {
Ok(DatabaseResponse::Charsheet(row)) => ok(row), Ok(DatabaseResponse::Charsheet(row)) => Ok(row),
Ok(_) => fatal(FatalError::MessageMismatch), Ok(_) => Err(FatalError::MessageMismatch),
Err(_err) => error(Error::NoResponse), Err(_) => Err(FatalError::DatabaseConnectionLost),
} }
} }
} }

View File

@ -1,14 +1,14 @@
use std::future::Future; use std::future::Future;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use result_extended::{ok, return_error, ResultExt}; use result_extended::{error, ok, return_error, ResultExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message}; use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message};
use crate::{ use crate::{
asset_db::AssetId, asset_db::AssetId,
core::Core, core::Core,
database::CharacterId, database::{CharacterId, UserId},
types::{AppError, FatalError}, types::{AppError, FatalError},
}; };
@ -237,3 +237,21 @@ pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply {
}) })
.await .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
}

View File

@ -8,7 +8,7 @@ use asset_db::{AssetId, FsAssets};
use authdb::AuthError; use authdb::AuthError;
use database::DbConn; use database::DbConn;
use handlers::{ use handlers::{
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_background_image, handle_unregister_client, RegisterRequest handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_admin_password, handle_set_background_image, handle_unregister_client, RegisterRequest
}; };
use warp::{ use warp::{
// header, // header,
@ -104,7 +104,7 @@ pub async fn main() {
let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn); let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
let log = warp::log("visions::api"); let log = warp::log("visions::api");
let server_status = warp::path!("api" / "v1" / "status") let route_server_status = warp::path!("api" / "v1" / "status")
.and(warp::get()) .and(warp::get())
.then({ .then({
let core = core.clone(); let core = core.clone();
@ -181,7 +181,28 @@ pub async fn main() {
move |charid| handle_get_charsheet(core.clone(), charid) move |charid| handle_get_charsheet(core.clone(), charid)
}); });
let filter = server_status let route_set_admin_password_options = warp::path!("api" / "v1" / "admin_password")
.and(warp::options())
.map({
move || {
Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "PUT")
.header("Access-Control-Allow-Headers", "content-type")
.header("Content-Type", "application/json")
.body("")
.unwrap()
}
});
let route_set_admin_password = warp::path!("api" / "v1" / "admin_password")
.and(warp::put())
.and(warp::body::json())
.then({
let core = core.clone();
move |body| handle_set_admin_password(core.clone(), body)
});
let filter = route_server_status
.or(route_register_client) .or(route_register_client)
.or(route_unregister_client) .or(route_unregister_client)
.or(route_websocket) .or(route_websocket)
@ -191,6 +212,8 @@ pub async fn main() {
.or(route_set_bg_image) .or(route_set_bg_image)
.or(route_get_users) .or(route_get_users)
.or(route_get_charsheet) .or(route_get_charsheet)
.or(route_set_admin_password_options)
.or(route_set_admin_password)
.recover(handle_rejection); .recover(handle_rejection);
let server = warp::serve(filter); let server = warp::serve(filter);

View File

@ -33,6 +33,9 @@ pub enum AppError {
#[error("object inaccessible {0}")] #[error("object inaccessible {0}")]
Inaccessible(String), Inaccessible(String),
#[error("the requested operation is not allowed")]
PermissionDenied,
#[error("invalid json {0}")] #[error("invalid json {0}")]
JsonError(serde_json::Error), JsonError(serde_json::Error),

View File

@ -32,10 +32,10 @@ interface AuthedViewProps {
} }
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => { const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
const [state, dispatch] = useContext(StateContext); const [state, manager] = useContext(StateContext);
return ( return (
<Authentication onAdminPassword={(password) => { <Authentication onAdminPassword={(password) => {
dispatch({type: "SetAdminPassword", password }); manager.setAdminPassword(password);
}} onAuth={(username, password) => console.log(username, password)}> }} onAuth={(username, password) => console.log(username, password)}>
{children} {children}
</Authentication> </Authentication>

View File

@ -9,12 +9,6 @@ export class Client {
this.base = new URL("http://localhost:8001"); this.base = new URL("http://localhost:8001");
} }
status() {
const url = new URL(this.base);
url.pathname = `/api/v1/status`;
return fetch(url).then((response) => response.json());
}
registerWebsocket() { registerWebsocket() {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = `api/v1/client`; url.pathname = `api/v1/client`;
@ -62,4 +56,18 @@ export class Client {
url.pathname = `/api/v1/charsheet/${id}`; url.pathname = `/api/v1/charsheet/${id}`;
return fetch(url).then((response) => response.json()); 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());
}
} }

View File

@ -1,16 +1,16 @@
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer } from "react"; import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
import { Status, Tabletop } from "visions-types"; import { Status, Tabletop } from "visions-types";
import { Client } from "../../client"; import { Client } from "../../client";
import { assertNever } from "../../plugins/Candela"; import { assertNever } from "../../plugins/Candela";
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string }; type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string };
type AppState = { type AppState = {
auth: AuthState; auth: AuthState;
tabletop: Tabletop; tabletop: Tabletop;
} }
type Action = { type: "SetAdminPassword", password: string } | { type: "Auth", username: string, password: string }; type Action = { type: "SetAuthState", content: AuthState };
const initialState = (): AppState => ( const initialState = (): AppState => (
{ {
@ -20,18 +20,51 @@ const initialState = (): AppState => (
); );
const stateReducer = (state: AppState, action: Action): AppState => { const stateReducer = (state: AppState, action: Action): AppState => {
console.log("reducer: ", state, action); return { ...state, auth: action.content }
return state;
} }
export const StateContext = createContext<[AppState, React.Dispatch<any>]>([initialState(), () => { }]); class StateManager {
client: Client | undefined;
dispatch: React.Dispatch<Action> | undefined;
constructor(client: Client | undefined, dispatch: React.Dispatch<any> | 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; } interface StateProviderProps { client: Client; }
export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => { export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => {
const [state, dispatch] = useReducer(stateReducer, initialState()); const [state, dispatch] = useReducer(stateReducer, initialState());
return <StateContext.Provider value={[state, dispatch]}> const stateManager = useRef(new StateManager(client, dispatch));
useEffect(() => {
stateManager.current.status();
}, [stateManager]);
return <StateContext.Provider value={[state, stateManager.current]}>
{children} {children}
</StateContext.Provider>; </StateContext.Provider>;
} }