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 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)]
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue