Set up the user interface state model and set up the admin user onboarding #283

Merged
savanni merged 12 commits from visions-admin into main 2024-12-18 14:18:16 +00:00
5 changed files with 212 additions and 35 deletions
Showing only changes of commit e505c21bc8 - Show all commits

View File

@ -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")));

View File

@ -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,

View File

@ -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 {

View File

@ -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]

View File

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