Set up the user interface state model and set up the admin user onboarding #283
|
@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
|
|||
|
||||
use async_std::sync::RwLock;
|
||||
use mime::Mime;
|
||||
use result_extended::{fatal, ok, return_error, ResultExt};
|
||||
use result_extended::{error, fatal, ok, return_error, ResultExt};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
use typeshare::typeshare;
|
||||
|
@ -10,7 +10,7 @@ use uuid::Uuid;
|
|||
|
||||
use crate::{
|
||||
asset_db::{self, AssetId, Assets},
|
||||
database::{CharacterId, Database, Error, UserId},
|
||||
database::{CharacterId, Database, UserId},
|
||||
types::{AppError, FatalError, Game, Message, Tabletop, User, RGB},
|
||||
};
|
||||
|
||||
|
@ -23,7 +23,7 @@ const DEFAULT_BACKGROUND_COLOR: RGB = RGB {
|
|||
#[derive(Clone, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct Status {
|
||||
admin_enabled: bool,
|
||||
pub admin_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -61,19 +61,15 @@ impl Core {
|
|||
|
||||
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
|
||||
let mut state = self.0.write().await;
|
||||
let admin_user = match return_error!(state
|
||||
.db
|
||||
.user(UserId::from("admin"))
|
||||
.await
|
||||
.map_err(|_| AppError::Inaccessible("database stopped responding".to_owned())))
|
||||
{
|
||||
Some(admin_user) => admin_user,
|
||||
None => {
|
||||
let admin_user = return_error!(match state.db.user(UserId::from("admin")).await {
|
||||
Ok(Some(admin_user)) => ok(admin_user),
|
||||
Ok(None) => {
|
||||
return ok(Status {
|
||||
admin_enabled: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
Err(err) => fatal(err),
|
||||
});
|
||||
|
||||
ok(Status {
|
||||
admin_enabled: !admin_user.password.is_empty(),
|
||||
|
@ -113,29 +109,17 @@ impl Core {
|
|||
pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> {
|
||||
let users = self.0.write().await.db.users().await;
|
||||
match users {
|
||||
ResultExt::Ok(users) => {
|
||||
ResultExt::Ok(users.into_iter().map(|u| User::from(u)).collect())
|
||||
}
|
||||
ResultExt::Err(err) => {
|
||||
println!("Database error: {:?}", err);
|
||||
ResultExt::Ok(vec![])
|
||||
}
|
||||
ResultExt::Fatal(users) => ResultExt::Fatal(users),
|
||||
Ok(users) => ok(users.into_iter().map(|u| User::from(u)).collect()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_games(&self) -> ResultExt<Vec<Game>, AppError, FatalError> {
|
||||
let games = self.0.write().await.db.games().await;
|
||||
match games {
|
||||
ResultExt::Ok(games) => {
|
||||
// ResultExt::Ok(games.into_iter().map(|u| Game::from(u)).collect())
|
||||
unimplemented!();
|
||||
}
|
||||
ResultExt::Err(err) => {
|
||||
println!("Database error: {:?}", err);
|
||||
ResultExt::Ok(vec![])
|
||||
}
|
||||
ResultExt::Fatal(games) => ResultExt::Fatal(games),
|
||||
// Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
|
||||
Ok(games) => unimplemented!(),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,10 +183,11 @@ impl Core {
|
|||
) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> {
|
||||
let mut state = self.0.write().await;
|
||||
let cr = state.db.character(id).await;
|
||||
cr.map(|cr| cr.map(|cr| cr.data)).or_else(|err| {
|
||||
println!("Database error: {:?}", err);
|
||||
ResultExt::Ok(None)
|
||||
})
|
||||
match cr {
|
||||
Ok(Some(row)) => ok(Some(row.data)),
|
||||
Ok(None) => ok(None),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish(&self, message: Message) {
|
||||
|
@ -214,6 +199,27 @@ impl Core {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn set_password(
|
||||
&self,
|
||||
uuid: UserId,
|
||||
password: String,
|
||||
) -> ResultExt<(), AppError, FatalError> {
|
||||
let mut state = self.0.write().await;
|
||||
let user = match state.db.user(uuid.clone()).await {
|
||||
Ok(Some(row)) => row,
|
||||
Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())),
|
||||
Err(err) => return fatal(err),
|
||||
};
|
||||
match state
|
||||
.db
|
||||
.save_user(Some(uuid), &user.name, &password, user.admin, user.enabled)
|
||||
.await
|
||||
{
|
||||
Ok(_) => ok(()),
|
||||
Err(err) => fatal(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -4,14 +4,12 @@ use async_std::channel::{bounded, Receiver, Sender};
|
|||
use async_trait::async_trait;
|
||||
use include_dir::{include_dir, Dir};
|
||||
use lazy_static::lazy_static;
|
||||
use result_extended::{error, fatal, ok, return_error, ResultExt};
|
||||
use rusqlite::{
|
||||
types::{FromSql, FromSqlResult, ValueRef},
|
||||
Connection,
|
||||
};
|
||||
use rusqlite_migration::Migrations;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::types::FatalError;
|
||||
|
@ -23,18 +21,13 @@ lazy_static! {
|
|||
Migrations::from_directory(&MIGRATIONS_DIR).unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("No response to request")]
|
||||
NoResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Request {
|
||||
Charsheet(CharacterId),
|
||||
Games,
|
||||
User(UserId),
|
||||
Users,
|
||||
SaveUser(Option<UserId>, String, String, bool, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -49,6 +42,7 @@ enum DatabaseResponse {
|
|||
Games(Vec<GameRow>),
|
||||
User(Option<UserRow>),
|
||||
Users(Vec<UserRow>),
|
||||
SaveUser(UserId),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
|
@ -184,19 +178,22 @@ pub struct CharsheetRow {
|
|||
|
||||
#[async_trait]
|
||||
pub trait Database: Send + Sync {
|
||||
async fn user(
|
||||
async fn user(&mut self, _: UserId) -> Result<Option<UserRow>, FatalError>;
|
||||
|
||||
async fn save_user(
|
||||
&mut self,
|
||||
_: UserId,
|
||||
) -> result_extended::ResultExt<Option<UserRow>, Error, FatalError>;
|
||||
user_id: Option<UserId>,
|
||||
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(
|
||||
&mut self,
|
||||
id: CharacterId,
|
||||
) -> result_extended::ResultExt<Option<CharsheetRow>, Error, FatalError>;
|
||||
async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
|
||||
}
|
||||
|
||||
pub struct DiskDb {
|
||||
|
@ -268,27 +265,6 @@ impl DiskDb {
|
|||
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> {
|
||||
let mut stmt = self
|
||||
.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(
|
||||
&self,
|
||||
user_id: Option<UserId>,
|
||||
|
@ -337,7 +334,7 @@ impl DiskDb {
|
|||
let mut stmt = self
|
||||
.conn
|
||||
.prepare(
|
||||
"UPDATE users SET name=?, password=?, admin=?, enbabled=? WHERE uuid=?",
|
||||
"UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?",
|
||||
)
|
||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||
stmt.execute((name, password, admin, enabled, user_id.as_str()))
|
||||
|
@ -398,7 +395,7 @@ impl DiskDb {
|
|||
char_id: Option<CharacterId>,
|
||||
game: GameId,
|
||||
character: serde_json::Value,
|
||||
) -> std::result::Result<CharacterId, Error> {
|
||||
) -> std::result::Result<CharacterId, FatalError> {
|
||||
match char_id {
|
||||
None => {
|
||||
let char_id = CharacterId::new();
|
||||
|
@ -451,6 +448,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
|||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::SaveUser(user_id, username, password, admin, enabled) => {
|
||||
let user_id = db.save_user(user_id, username.as_ref(), password.as_ref(), admin, enabled);
|
||||
match user_id {
|
||||
Ok(user_id) => {
|
||||
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
||||
}
|
||||
err => panic!("{:?}", err),
|
||||
}
|
||||
}
|
||||
Request::Users => {
|
||||
let users = db.users();
|
||||
match users {
|
||||
|
@ -488,7 +494,7 @@ impl DbConn {
|
|||
|
||||
#[async_trait]
|
||||
impl Database for DbConn {
|
||||
async fn user(&mut self, uid: UserId) -> ResultExt<Option<UserRow>, Error, FatalError> {
|
||||
async fn user(&mut self, uid: UserId) -> Result<Option<UserRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
|
@ -498,17 +504,50 @@ impl Database for DbConn {
|
|||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::User(user)) => ok(user),
|
||||
Ok(_) => fatal(FatalError::MessageMismatch),
|
||||
Err(_) => error(Error::NoResponse),
|
||||
Ok(DatabaseResponse::User(user)) => Ok(user),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn users(&mut self) -> ResultExt<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 request = DatabaseRequest {
|
||||
|
@ -518,17 +557,17 @@ impl Database for DbConn {
|
|||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::Users(lst)) => ok(lst),
|
||||
Ok(_) => fatal(FatalError::MessageMismatch),
|
||||
Err(_) => error(Error::NoResponse),
|
||||
Ok(DatabaseResponse::Users(lst)) => Ok(lst),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn games(&mut self) -> result_extended::ResultExt<Vec<GameRow>, Error, FatalError> {
|
||||
async fn games(&mut self) -> Result<Vec<GameRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
|
@ -538,20 +577,17 @@ impl Database for DbConn {
|
|||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::Games(lst)) => ok(lst),
|
||||
Ok(_) => fatal(FatalError::MessageMismatch),
|
||||
Err(_) => error(Error::NoResponse),
|
||||
Ok(DatabaseResponse::Games(lst)) => Ok(lst),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
|
||||
async fn character(
|
||||
&mut self,
|
||||
id: CharacterId,
|
||||
) -> ResultExt<Option<CharsheetRow>, Error, FatalError> {
|
||||
async fn character(&mut self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||
|
||||
let request = DatabaseRequest {
|
||||
|
@ -561,13 +597,13 @@ impl Database for DbConn {
|
|||
|
||||
match self.conn.send(request).await {
|
||||
Ok(()) => (),
|
||||
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
|
||||
Err(_) => return Err(FatalError::DatabaseConnectionLost),
|
||||
};
|
||||
|
||||
match rx.recv().await {
|
||||
Ok(DatabaseResponse::Charsheet(row)) => ok(row),
|
||||
Ok(_) => fatal(FatalError::MessageMismatch),
|
||||
Err(_err) => error(Error::NoResponse),
|
||||
Ok(DatabaseResponse::Charsheet(row)) => Ok(row),
|
||||
Ok(_) => Err(FatalError::MessageMismatch),
|
||||
Err(_) => Err(FatalError::DatabaseConnectionLost),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use std::future::Future;
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use result_extended::{ok, return_error, ResultExt};
|
||||
use result_extended::{error, ok, return_error, ResultExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message};
|
||||
|
||||
use crate::{
|
||||
asset_db::AssetId,
|
||||
core::Core,
|
||||
database::CharacterId,
|
||||
database::{CharacterId, UserId},
|
||||
types::{AppError, FatalError},
|
||||
};
|
||||
|
||||
|
@ -237,3 +237,21 @@ pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply {
|
|||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_set_admin_password(core: Core, password: String) -> impl Reply {
|
||||
handler(async move {
|
||||
let status = return_error!(core.status().await);
|
||||
if status.admin_enabled {
|
||||
return error(AppError::PermissionDenied);
|
||||
}
|
||||
|
||||
core.set_password(UserId::from("admin"), password).await;
|
||||
ok(Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "*")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(vec![])
|
||||
.unwrap())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use asset_db::{AssetId, FsAssets};
|
|||
use authdb::AuthError;
|
||||
use database::DbConn;
|
||||
use handlers::{
|
||||
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_background_image, handle_unregister_client, RegisterRequest
|
||||
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_server_status, handle_set_admin_password, handle_set_background_image, handle_unregister_client, RegisterRequest
|
||||
};
|
||||
use warp::{
|
||||
// header,
|
||||
|
@ -104,7 +104,7 @@ pub async fn main() {
|
|||
let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||
let log = warp::log("visions::api");
|
||||
|
||||
let server_status = warp::path!("api" / "v1" / "status")
|
||||
let route_server_status = warp::path!("api" / "v1" / "status")
|
||||
.and(warp::get())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
|
@ -181,7 +181,28 @@ pub async fn main() {
|
|||
move |charid| handle_get_charsheet(core.clone(), charid)
|
||||
});
|
||||
|
||||
let filter = server_status
|
||||
let route_set_admin_password_options = warp::path!("api" / "v1" / "admin_password")
|
||||
.and(warp::options())
|
||||
.map({
|
||||
move || {
|
||||
Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "PUT")
|
||||
.header("Access-Control-Allow-Headers", "content-type")
|
||||
.header("Content-Type", "application/json")
|
||||
.body("")
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
let route_set_admin_password = warp::path!("api" / "v1" / "admin_password")
|
||||
.and(warp::put())
|
||||
.and(warp::body::json())
|
||||
.then({
|
||||
let core = core.clone();
|
||||
move |body| handle_set_admin_password(core.clone(), body)
|
||||
});
|
||||
|
||||
let filter = route_server_status
|
||||
.or(route_register_client)
|
||||
.or(route_unregister_client)
|
||||
.or(route_websocket)
|
||||
|
@ -191,6 +212,8 @@ pub async fn main() {
|
|||
.or(route_set_bg_image)
|
||||
.or(route_get_users)
|
||||
.or(route_get_charsheet)
|
||||
.or(route_set_admin_password_options)
|
||||
.or(route_set_admin_password)
|
||||
.recover(handle_rejection);
|
||||
|
||||
let server = warp::serve(filter);
|
||||
|
|
|
@ -33,6 +33,9 @@ pub enum AppError {
|
|||
#[error("object inaccessible {0}")]
|
||||
Inaccessible(String),
|
||||
|
||||
#[error("the requested operation is not allowed")]
|
||||
PermissionDenied,
|
||||
|
||||
#[error("invalid json {0}")]
|
||||
JsonError(serde_json::Error),
|
||||
|
||||
|
|
|
@ -32,10 +32,10 @@ interface AuthedViewProps {
|
|||
}
|
||||
|
||||
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
|
||||
const [state, dispatch] = useContext(StateContext);
|
||||
const [state, manager] = useContext(StateContext);
|
||||
return (
|
||||
<Authentication onAdminPassword={(password) => {
|
||||
dispatch({type: "SetAdminPassword", password });
|
||||
manager.setAdminPassword(password);
|
||||
}} onAuth={(username, password) => console.log(username, password)}>
|
||||
{children}
|
||||
</Authentication>
|
||||
|
|
|
@ -9,12 +9,6 @@ export class Client {
|
|||
this.base = new URL("http://localhost:8001");
|
||||
}
|
||||
|
||||
status() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/status`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
registerWebsocket() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `api/v1/client`;
|
||||
|
@ -62,4 +56,18 @@ export class Client {
|
|||
url.pathname = `/api/v1/charsheet/${id}`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
async setAdminPassword(password: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/admin_password`;
|
||||
console.log("setting the admin password to: ", password);
|
||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) });
|
||||
}
|
||||
|
||||
async status() {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/status`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer } from "react";
|
||||
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
|
||||
import { Status, Tabletop } from "visions-types";
|
||||
import { Client } from "../../client";
|
||||
import { assertNever } from "../../plugins/Candela";
|
||||
|
||||
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", username: string };
|
||||
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string };
|
||||
|
||||
type AppState = {
|
||||
auth: AuthState;
|
||||
tabletop: Tabletop;
|
||||
}
|
||||
|
||||
type Action = { type: "SetAdminPassword", password: string } | { type: "Auth", username: string, password: string };
|
||||
type Action = { type: "SetAuthState", content: AuthState };
|
||||
|
||||
const initialState = (): AppState => (
|
||||
{
|
||||
|
@ -20,18 +20,51 @@ const initialState = (): AppState => (
|
|||
);
|
||||
|
||||
const stateReducer = (state: AppState, action: Action): AppState => {
|
||||
console.log("reducer: ", state, action);
|
||||
return state;
|
||||
return { ...state, auth: action.content }
|
||||
}
|
||||
|
||||
export const StateContext = createContext<[AppState, React.Dispatch<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; }
|
||||
|
||||
export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => {
|
||||
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}
|
||||
</StateContext.Provider>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue