Set up the user interface state model and set up the admin user onboarding #283
|
@ -9,7 +9,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, Error, UserId},
|
||||||
types::{AppError, FatalError, Message, Tabletop, RGB},
|
types::{AppError, FatalError, Game, Message, Tabletop, User, RGB},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_BACKGROUND_COLOR: RGB = RGB {
|
const DEFAULT_BACKGROUND_COLOR: RGB = RGB {
|
||||||
|
@ -81,10 +81,12 @@ impl Core {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_users(&self) -> ResultExt<Vec<(UserId, String)>, 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) => ResultExt::Ok(users),
|
ResultExt::Ok(users) => {
|
||||||
|
ResultExt::Ok(users.into_iter().map(|u| User::from(u)).collect())
|
||||||
|
}
|
||||||
ResultExt::Err(err) => {
|
ResultExt::Err(err) => {
|
||||||
println!("Database error: {:?}", err);
|
println!("Database error: {:?}", err);
|
||||||
ResultExt::Ok(vec![])
|
ResultExt::Ok(vec![])
|
||||||
|
@ -93,6 +95,21 @@ impl Core {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn tabletop(&self) -> Tabletop {
|
pub async fn tabletop(&self) -> Tabletop {
|
||||||
self.0.read().await.tabletop.clone()
|
self.0.read().await.tabletop.clone()
|
||||||
}
|
}
|
||||||
|
@ -244,7 +261,10 @@ mod test {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_can_change_the_tabletop_background() {
|
async fn it_can_change_the_tabletop_background() {
|
||||||
let core = test_core();
|
let core = test_core();
|
||||||
assert_matches!(core.set_background_image(AssetId::from("asset_1")).await, ResultExt::Ok(()));
|
assert_matches!(
|
||||||
|
core.set_background_image(AssetId::from("asset_1")).await,
|
||||||
|
ResultExt::Ok(())
|
||||||
|
);
|
||||||
assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
|
assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => {
|
||||||
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
|
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
|
||||||
assert_eq!(background_image, Some(AssetId::from("asset_1")));
|
assert_eq!(background_image, Some(AssetId::from("asset_1")));
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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 result_extended::{error, fatal, ok, return_error, ResultExt};
|
||||||
use rusqlite::Connection;
|
use rusqlite::{types::{FromSql, FromSqlResult, ValueRef}, Connection};
|
||||||
use rusqlite_migration::Migrations;
|
use rusqlite_migration::Migrations;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -29,6 +29,7 @@ pub enum Error {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Request {
|
enum Request {
|
||||||
Charsheet(CharacterId),
|
Charsheet(CharacterId),
|
||||||
|
Games,
|
||||||
Users,
|
Users,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,8 +41,9 @@ struct DatabaseRequest {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum DatabaseResponse {
|
enum DatabaseResponse {
|
||||||
Users(Vec<(UserId, String)>),
|
|
||||||
Charsheet(Option<CharsheetRow>),
|
Charsheet(Option<CharsheetRow>),
|
||||||
|
Games(Vec<GameRow>),
|
||||||
|
Users(Vec<UserRow>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
|
@ -69,6 +71,15 @@ impl From<String> for UserId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromSql for UserId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
match value {
|
||||||
|
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
pub struct GameId(String);
|
pub struct GameId(String);
|
||||||
|
|
||||||
|
@ -94,6 +105,15 @@ impl From<String> for GameId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromSql for GameId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
match value {
|
||||||
|
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||||
pub struct CharacterId(String);
|
pub struct CharacterId(String);
|
||||||
|
|
||||||
|
@ -119,32 +139,49 @@ impl From<String> for CharacterId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromSql for CharacterId {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
match value {
|
||||||
|
ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())),
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct UserRow {
|
pub struct UserRow {
|
||||||
id: String,
|
pub id: UserId,
|
||||||
name: String,
|
pub name: String,
|
||||||
password: String,
|
pub password: String,
|
||||||
admin: bool,
|
pub admin: bool,
|
||||||
enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Role {
|
pub struct Role {
|
||||||
userid: String,
|
userid: UserId,
|
||||||
gameid: String,
|
gameid: GameId,
|
||||||
role: String,
|
role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GameRow {
|
||||||
|
pub id: UserId,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CharsheetRow {
|
pub struct CharsheetRow {
|
||||||
id: String,
|
id: String,
|
||||||
game: String,
|
game: GameId,
|
||||||
pub data: serde_json::Value,
|
pub data: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 users(&mut self) -> result_extended::ResultExt<Vec<UserRow>, Error, FatalError>;
|
||||||
|
|
||||||
|
async fn games(&mut self) -> result_extended::ResultExt<Vec<GameRow>, Error, FatalError>;
|
||||||
|
|
||||||
async fn character(
|
async fn character(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -219,6 +256,21 @@ 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
|
||||||
|
@ -244,17 +296,6 @@ 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>,
|
||||||
|
@ -375,7 +416,6 @@ impl DiskDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
||||||
println!("Starting db_handler");
|
|
||||||
while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await {
|
while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await {
|
||||||
println!("Request received: {:?}", req);
|
println!("Request received: {:?}", req);
|
||||||
match req {
|
match req {
|
||||||
|
@ -389,6 +429,9 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Request::Games => {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
Request::Users => {
|
Request::Users => {
|
||||||
let users = db.users();
|
let users = db.users();
|
||||||
match users {
|
match users {
|
||||||
|
@ -426,7 +469,7 @@ 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> {
|
async fn users(&mut self) -> ResultExt<Vec<UserRow>, Error, FatalError> {
|
||||||
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||||
|
|
||||||
let request = DatabaseRequest {
|
let request = DatabaseRequest {
|
||||||
|
@ -446,6 +489,26 @@ impl Database for DbConn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn games(&mut self) -> result_extended::ResultExt<Vec<GameRow>, Error, FatalError> {
|
||||||
|
let (tx, rx) = bounded::<DatabaseResponse>(1);
|
||||||
|
|
||||||
|
let request = DatabaseRequest {
|
||||||
|
tx,
|
||||||
|
req: Request::Games,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.conn.send(request).await {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(_) => return fatal(FatalError::DatabaseConnectionLost),
|
||||||
|
};
|
||||||
|
|
||||||
|
match rx.recv().await {
|
||||||
|
Ok(DatabaseResponse::Games(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,
|
||||||
|
|
|
@ -186,6 +186,23 @@ pub async fn handle_get_users(core: Core) -> impl Reply {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_get_games(core: Core) -> impl Reply {
|
||||||
|
handler(async move {
|
||||||
|
let games = match core.list_games().await {
|
||||||
|
ResultExt::Ok(games) => games,
|
||||||
|
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(&games).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 {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
use crate::asset_db::AssetId;
|
use crate::{asset_db::AssetId, database::UserRow};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum FatalError {
|
pub enum FatalError {
|
||||||
|
@ -49,6 +49,53 @@ pub struct RGB {
|
||||||
pub blue: u32,
|
pub blue: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct User {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub password: String,
|
||||||
|
pub admin: bool,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UserRow> for User {
|
||||||
|
fn from(row: UserRow) -> Self {
|
||||||
|
Self {
|
||||||
|
id: row.id.as_str().to_owned(),
|
||||||
|
name: row.name.to_owned(),
|
||||||
|
password: row.password.to_owned(),
|
||||||
|
admin: row.admin,
|
||||||
|
enabled: row.enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub enum PlayerRole {
|
||||||
|
Gm,
|
||||||
|
Player,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct Player {
|
||||||
|
user_id: String,
|
||||||
|
role: PlayerRole,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct Game {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub players: Vec<Player>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
|
@ -1,17 +1,47 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Game, User } from 'visions-types';
|
||||||
import { Client } from '../../client';
|
import { Client } from '../../client';
|
||||||
|
|
||||||
|
interface UserRowProps {
|
||||||
|
user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserRow = ({ user }: UserRowProps) => {
|
||||||
|
return (<tr>
|
||||||
|
<td> {user.name} </td>
|
||||||
|
<td> {user.admin && "admin"} </td>
|
||||||
|
<td> {user.enabled && "enabled"} </td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameRowProps {
|
||||||
|
game: Game,
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameRow = ({ game }: GameRowProps) => {
|
||||||
|
return (<tr>
|
||||||
|
<td> {game.name} </td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
|
||||||
interface AdminProps {
|
interface AdminProps {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Admin = ({ client }: AdminProps) => {
|
export const Admin = ({ client }: AdminProps) => {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState<Array<User>>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.users().then(setUsers);
|
client.users().then((u) => {
|
||||||
}, [client, setUsers]);
|
console.log(u);
|
||||||
return <ul>
|
setUsers(u);
|
||||||
{users.map(([uuid, username]) => <li> {username} </li>) }
|
});
|
||||||
</ul>;
|
}, [client]);
|
||||||
|
|
||||||
|
console.log(users);
|
||||||
|
return (<table>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => <UserRow user={user} />)}
|
||||||
|
</tbody>
|
||||||
|
</table>);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue