Set up an admin panel that shows the list of users

This commit is contained in:
Savanni D'Gerinel 2024-12-10 22:43:15 -05:00
parent e8bc0590c6
commit e505c21bc8
5 changed files with 212 additions and 35 deletions

View File

@ -9,7 +9,7 @@ use uuid::Uuid;
use crate::{
asset_db::{self, AssetId, Assets},
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 {
@ -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;
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) => {
println!("Database error: {:?}", err);
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 {
self.0.read().await.tabletop.clone()
}
@ -244,7 +261,10 @@ mod test {
#[tokio::test]
async fn it_can_change_the_tabletop_background() {
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_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
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 lazy_static::lazy_static;
use result_extended::{error, fatal, ok, return_error, ResultExt};
use rusqlite::Connection;
use rusqlite::{types::{FromSql, FromSqlResult, ValueRef}, Connection};
use rusqlite_migration::Migrations;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -29,6 +29,7 @@ pub enum Error {
#[derive(Debug)]
enum Request {
Charsheet(CharacterId),
Games,
Users,
}
@ -40,8 +41,9 @@ struct DatabaseRequest {
#[derive(Debug)]
enum DatabaseResponse {
Users(Vec<(UserId, String)>),
Charsheet(Option<CharsheetRow>),
Games(Vec<GameRow>),
Users(Vec<UserRow>),
}
#[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)]
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)]
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)]
pub struct UserRow {
id: String,
name: String,
password: String,
admin: bool,
enabled: bool,
pub id: UserId,
pub name: String,
pub password: String,
pub admin: bool,
pub enabled: bool,
}
#[derive(Clone, Debug)]
pub struct Role {
userid: String,
gameid: String,
userid: UserId,
gameid: GameId,
role: String,
}
#[derive(Clone, Debug)]
pub struct GameRow {
pub id: UserId,
pub name: String,
}
#[derive(Clone, Debug)]
pub struct CharsheetRow {
id: String,
game: String,
game: GameId,
pub data: serde_json::Value,
}
#[async_trait]
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(
&mut self,
@ -219,6 +256,21 @@ 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
@ -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(
&self,
user_id: Option<UserId>,
@ -375,7 +416,6 @@ impl DiskDb {
}
async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
println!("Starting db_handler");
while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await {
println!("Request received: {:?}", req);
match req {
@ -389,6 +429,9 @@ async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
_ => unimplemented!(),
}
}
Request::Games => {
unimplemented!();
}
Request::Users => {
let users = db.users();
match users {
@ -426,7 +469,7 @@ impl DbConn {
#[async_trait]
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 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(
&mut self,
id: CharacterId,

View File

@ -186,6 +186,23 @@ pub async fn handle_get_users(core: Core) -> impl Reply {
.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 {
handler(async move {
let sheet = match core.get_charsheet(CharacterId::from(charid)).await {

View File

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use typeshare::typeshare;
use crate::asset_db::AssetId;
use crate::{asset_db::AssetId, database::UserRow};
#[derive(Debug, Error)]
pub enum FatalError {
@ -49,6 +49,53 @@ pub struct RGB {
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)]
#[serde(rename_all = "camelCase")]
#[typeshare]

View File

@ -1,17 +1,47 @@
import React, { useEffect, useState } from 'react';
import { Game, User } from 'visions-types';
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 {
client: Client,
}
export const Admin = ({ client }: AdminProps) => {
const [users, setUsers] = useState([]);
const [users, setUsers] = useState<Array<User>>([]);
useEffect(() => {
client.users().then(setUsers);
}, [client, setUsers]);
return <ul>
{users.map(([uuid, username]) => <li> {username} </li>) }
</ul>;
client.users().then((u) => {
console.log(u);
setUsers(u);
});
}, [client]);
console.log(users);
return (<table>
<tbody>
{users.map((user) => <UserRow user={user} />)}
</tbody>
</table>);
}