Set up the user interface state model and set up the admin user onboarding #283
|
@ -8,7 +8,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
asset_db::{self, AssetId, Assets},
|
asset_db::{self, AssetId, Assets},
|
||||||
database::{CharacterId, Database, Error},
|
database::{CharacterId, Database, Error, UserId},
|
||||||
types::{AppError, FatalError, Message, Tabletop, RGB},
|
types::{AppError, FatalError, Message, Tabletop, RGB},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,6 +81,18 @@ impl Core {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_users(&self) -> ResultExt<Vec<(UserId, String)>, AppError, FatalError> {
|
||||||
|
let users = self.0.write().await.db.users().await;
|
||||||
|
match users {
|
||||||
|
ResultExt::Ok(users) => ResultExt::Ok(users),
|
||||||
|
ResultExt::Err(err) => {
|
||||||
|
println!("Database error: {:?}", err);
|
||||||
|
ResultExt::Ok(vec![])
|
||||||
|
}
|
||||||
|
ResultExt::Fatal(users) => ResultExt::Fatal(users),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn tabletop(&self) -> Tabletop {
|
pub async fn tabletop(&self) -> Tabletop {
|
||||||
self.0.read().await.tabletop.clone()
|
self.0.read().await.tabletop.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ pub enum Error {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Request {
|
enum Request {
|
||||||
Charsheet(CharacterId),
|
Charsheet(CharacterId),
|
||||||
|
Users,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -39,6 +40,7 @@ struct DatabaseRequest {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum DatabaseResponse {
|
enum DatabaseResponse {
|
||||||
|
Users(Vec<(UserId, String)>),
|
||||||
Charsheet(Option<CharsheetRow>),
|
Charsheet(Option<CharsheetRow>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +144,8 @@ pub struct CharsheetRow {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Database: Send + Sync {
|
pub trait Database: Send + Sync {
|
||||||
|
async fn users(&mut self) -> result_extended::ResultExt<Vec<(UserId, String)>, Error, FatalError>;
|
||||||
|
|
||||||
async fn character(
|
async fn character(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: CharacterId,
|
id: CharacterId,
|
||||||
|
@ -240,6 +244,17 @@ impl DiskDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn users(&self) -> Result<Vec<(UserId, String)>, FatalError> {
|
||||||
|
let mut stmt = self.conn.prepare("SELECT * FROM USERS")
|
||||||
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
|
let items = stmt.query_map([], |row| {
|
||||||
|
let userid: String = row.get(0).unwrap();
|
||||||
|
let username = row.get(1).unwrap();
|
||||||
|
Ok((UserId::from(userid), username))
|
||||||
|
}).unwrap().collect::<Result<Vec<(UserId, String)>, rusqlite::Error>>().unwrap();
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
fn save_user(
|
fn save_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: Option<UserId>,
|
user_id: Option<UserId>,
|
||||||
|
@ -374,6 +389,15 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Request::Users => {
|
||||||
|
let users = db.users();
|
||||||
|
match users {
|
||||||
|
Ok(users) => {
|
||||||
|
tx.send(DatabaseResponse::Users(users)).await.unwrap();
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("ending db_handler");
|
println!("ending db_handler");
|
||||||
|
@ -402,6 +426,26 @@ impl DbConn {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Database for DbConn {
|
impl Database for DbConn {
|
||||||
|
async fn users(&mut self) -> ResultExt<Vec<(UserId, String)>, Error, FatalError> {
|
||||||
|
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||||
|
|
||||||
|
let request = DatabaseRequest {
|
||||||
|
tx,
|
||||||
|
req: Request::Users,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.conn.send(request).await {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
|
||||||
|
};
|
||||||
|
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(DatabaseResponse::Users(lst)) => ok(lst),
|
||||||
|
Ok(_) => fatal(FatalError::MessageMismatch),
|
||||||
|
Err(_) => error(Error::NoResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn character(
|
async fn character(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: CharacterId,
|
id: CharacterId,
|
||||||
|
@ -420,7 +464,7 @@ impl Database for DbConn {
|
||||||
|
|
||||||
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(_) => fatal(FatalError::MessageMismatch),
|
||||||
Err(_err) => error(Error::NoResponse),
|
Err(_err) => error(Error::NoResponse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,12 @@ use result_extended::{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::{asset_db::AssetId, core::Core, database::CharacterId, types::{AppError, FatalError}};
|
use crate::{
|
||||||
|
asset_db::AssetId,
|
||||||
|
core::Core,
|
||||||
|
database::CharacterId,
|
||||||
|
types::{AppError, FatalError},
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub async fn handle_auth(
|
pub async fn handle_auth(
|
||||||
|
@ -164,6 +169,23 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_users(core: Core) -> impl Reply {
|
||||||
|
handler(async move {
|
||||||
|
let users = match core.list_users().await {
|
||||||
|
ResultExt::Ok(users) => users,
|
||||||
|
ResultExt::Err(err) => return ResultExt::Err(err),
|
||||||
|
ResultExt::Fatal(err) => return ResultExt::Fatal(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
ok(Response::builder()
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(serde_json::to_vec(&users).unwrap())
|
||||||
|
.unwrap())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply {
|
pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply {
|
||||||
handler(async move {
|
handler(async move {
|
||||||
let sheet = match core.get_charsheet(CharacterId::from(charid)).await {
|
let sheet = match core.get_charsheet(CharacterId::from(charid)).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_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest
|
handle_available_images, handle_connect_websocket, handle_file, handle_get_charsheet, handle_get_users, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest
|
||||||
};
|
};
|
||||||
use warp::{
|
use warp::{
|
||||||
// header,
|
// header,
|
||||||
|
@ -160,6 +160,13 @@ pub async fn main() {
|
||||||
})
|
})
|
||||||
.with(log);
|
.with(log);
|
||||||
|
|
||||||
|
let route_get_users = warp::path!("api" / "v1" / "users")
|
||||||
|
.and(warp::get())
|
||||||
|
.then({
|
||||||
|
let core = core.clone();
|
||||||
|
move || handle_get_users(core.clone())
|
||||||
|
});
|
||||||
|
|
||||||
let route_get_charsheet = warp::path!("api" / "v1" / "charsheet" / String)
|
let route_get_charsheet = warp::path!("api" / "v1" / "charsheet" / String)
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
.then({
|
.then({
|
||||||
|
@ -174,6 +181,7 @@ pub async fn main() {
|
||||||
.or(route_available_images)
|
.or(route_available_images)
|
||||||
.or(route_set_bg_image_options)
|
.or(route_set_bg_image_options)
|
||||||
.or(route_set_bg_image)
|
.or(route_set_bg_image)
|
||||||
|
.or(route_get_users)
|
||||||
.or(route_get_charsheet)
|
.or(route_get_charsheet)
|
||||||
.recover(handle_rejection);
|
.recover(handle_rejection);
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,11 @@ import { DesignPage } from './views/Design/Design';
|
||||||
import { GmView } from './views/GmView/GmView';
|
import { GmView } from './views/GmView/GmView';
|
||||||
import { WebsocketProvider } from './components/WebsocketProvider';
|
import { WebsocketProvider } from './components/WebsocketProvider';
|
||||||
import { PlayerView } from './views/PlayerView/PlayerView';
|
import { PlayerView } from './views/PlayerView/PlayerView';
|
||||||
|
import { Admin } from './views/Admin/Admin';
|
||||||
import Candela from './plugins/Candela';
|
import Candela from './plugins/Candela';
|
||||||
|
|
||||||
|
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
client: Client;
|
client: Client;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +18,7 @@ interface AppProps {
|
||||||
const CandelaCharsheet = ({ client }: { client: Client }) => {
|
const CandelaCharsheet = ({ client }: { client: Client }) => {
|
||||||
let [sheet, setSheet] = useState(undefined);
|
let [sheet, setSheet] = useState(undefined);
|
||||||
useEffect(
|
useEffect(
|
||||||
() => { client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => setSheet(c)); },
|
() => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)); },
|
||||||
[client, setSheet]
|
[client, setSheet]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -36,6 +39,10 @@ const App = ({ client }: AppProps) => {
|
||||||
path: "/gm",
|
path: "/gm",
|
||||||
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div>
|
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/admin",
|
||||||
|
element: <Admin client={client} />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <PlayerView client={client} /> </WebsocketProvider> : <div> </div>
|
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <PlayerView client={client} /> </WebsocketProvider> : <div> </div>
|
||||||
|
|
|
@ -45,6 +45,12 @@ export class Client {
|
||||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async users() {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = '/api/v1/users/';
|
||||||
|
return fetch(url).then((response) => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
async charsheet(id: string) {
|
async charsheet(id: string) {
|
||||||
const url = new URL(this.base);
|
const url = new URL(this.base);
|
||||||
url.pathname = `/api/v1/charsheet/${id}`;
|
url.pathname = `/api/v1/charsheet/${id}`;
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Client } from '../../client';
|
||||||
|
|
||||||
|
interface AdminProps {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Admin = ({ client }: AdminProps) => {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.users().then(setUsers);
|
||||||
|
}, [client, setUsers]);
|
||||||
|
return <ul>
|
||||||
|
{users.map(([uuid, username]) => <li> {username} </li>) }
|
||||||
|
</ul>;
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import { Client } from '../../client';
|
||||||
import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
import { TabletopElement } from '../../components/Tabletop/Tabletop';
|
||||||
import Candela from '../../plugins/Candela';
|
import Candela from '../../plugins/Candela';
|
||||||
|
|
||||||
|
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
|
||||||
|
|
||||||
interface PlayerViewProps {
|
interface PlayerViewProps {
|
||||||
client: Client;
|
client: Client;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +18,7 @@ export const PlayerView = ({ client }: PlayerViewProps) => {
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
client.charsheet("db7a2585-5dcf-4909-8743-2741111f8b9a").then((c) => {
|
client.charsheet(TEST_CHARSHEET_UUID).then((c) => {
|
||||||
setCharsheet(c)
|
setCharsheet(c)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue