Set up an admin panel that shows the list of users
This commit is contained in:
parent
e8bc0590c6
commit
e505c21bc8
|
@ -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")));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue