Set the admin password on a new server

This sets up the client state manager and state model. It has all of the
functions to support the set admin password endpoint, and some extras
which will be helpful in saving users generally.
This commit is contained in:
Savanni D'Gerinel 2024-12-17 23:43:36 -05:00
parent f6a45a9223
commit 2a616ef6c9
8 changed files with 243 additions and 116 deletions

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>;
} }