Compare commits
15 Commits
pico-usb-s
...
main
Author | SHA1 | Date | |
---|---|---|---|
151876bcd4 | |||
9802124822 | |||
87b187c8f1 | |||
4a0dc5b87a | |||
94a821d657 | |||
dcd5514433 | |||
90224a6841 | |||
84ee790f0b | |||
ac3a21f3f0 | |||
ef0e9f16b8 | |||
06bb0811e0 | |||
b138e6da0a | |||
e19d97663d | |||
d0ba8d921d | |||
f9e903da54 |
628
Cargo.lock
generated
@ -33,4 +33,4 @@ members = [
|
|||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server",
|
||||||
"gm-dash/server"
|
"gm-dash/server"
|
||||||
, "pico-usb-serial"]
|
]
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "thumbv6m-none-eabi"
|
|
||||||
|
|
||||||
[target.thumbv6m-none-eabi]
|
|
||||||
rustflags = [
|
|
||||||
"-C", "link-arg=--nmagic",
|
|
||||||
"-C", "link-arg=-Tlink.x",
|
|
||||||
"-C", "inline-threshold=5",
|
|
||||||
"-C", "no-vectorize-loops",
|
|
||||||
]
|
|
||||||
|
|
||||||
runner = "elf2uf2-rs -d"
|
|
@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "pico-usb-serial"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# az = { version = "1" }
|
|
||||||
# cortex-m-rt = { version = "0.7.3" }
|
|
||||||
# cortex-m = { version = "0.7.7" }
|
|
||||||
# embedded-alloc = { version = "0.5.1" }
|
|
||||||
# embedded-hal = { version = "0.2.7" }
|
|
||||||
# fixed = { version = "1" }
|
|
||||||
# fugit = { version = "0.3.7" }
|
|
||||||
# panic-halt = { version = "0.2.0" }
|
|
||||||
# rp-pico = { version = "0.8.0" }
|
|
||||||
usb-device = "0.3.2"
|
|
||||||
usbd-serial = "0.2.2"
|
|
||||||
heapless = "0.8.0"
|
|
||||||
rp-pico = "0.9.0"
|
|
||||||
embedded-hal = "1.0.0"
|
|
||||||
embedded-alloc = "0.6.0"
|
|
||||||
cortex-m-rt = "0.7.5"
|
|
||||||
panic-halt = "1.0.0"
|
|
@ -1,81 +0,0 @@
|
|||||||
// Lifted directly from https://github.com/rp-rs/rp-hal-boards/blob/HEAD/boards/rp-pico/examples/pico_usb_serial.rs
|
|
||||||
#![no_main]
|
|
||||||
#![no_std]
|
|
||||||
|
|
||||||
use core::fmt::Write;
|
|
||||||
use heapless::String;
|
|
||||||
use panic_halt as _;
|
|
||||||
use rp_pico::{
|
|
||||||
entry,
|
|
||||||
hal::{self, pac},
|
|
||||||
};
|
|
||||||
use usb_device::{class_prelude::*, prelude::*};
|
|
||||||
use usbd_serial::SerialPort;
|
|
||||||
|
|
||||||
#[entry]
|
|
||||||
fn main() -> ! {
|
|
||||||
let mut pac = pac::Peripherals::take().unwrap();
|
|
||||||
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
|
|
||||||
let clocks = hal::clocks::init_clocks_and_plls(
|
|
||||||
rp_pico::XOSC_CRYSTAL_FREQ,
|
|
||||||
pac.XOSC,
|
|
||||||
pac.CLOCKS,
|
|
||||||
pac.PLL_SYS,
|
|
||||||
pac.PLL_USB,
|
|
||||||
&mut pac.RESETS,
|
|
||||||
&mut watchdog,
|
|
||||||
)
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let timer = hal::Timer::new(pac.TIMER, &mut pac.RESETS, &clocks);
|
|
||||||
|
|
||||||
let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
|
|
||||||
pac.USBCTRL_REGS,
|
|
||||||
pac.USBCTRL_DPRAM,
|
|
||||||
clocks.usb_clock,
|
|
||||||
true,
|
|
||||||
&mut pac.RESETS,
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut serial = SerialPort::new(&usb_bus);
|
|
||||||
|
|
||||||
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
|
|
||||||
.strings(&[StringDescriptors::default()
|
|
||||||
.manufacturer("Fake company")
|
|
||||||
.product("Serial port")
|
|
||||||
.serial_number("TEST")])
|
|
||||||
.unwrap()
|
|
||||||
.device_class(2)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let mut said_hello = false;
|
|
||||||
loop {
|
|
||||||
if !said_hello && timer.get_counter().ticks() >= 2_000_000 {
|
|
||||||
said_hello = true;
|
|
||||||
let _ = serial.write(b"Hello, World!\r\n");
|
|
||||||
let time = timer.get_counter().ticks();
|
|
||||||
let mut text: String<64> = String::new();
|
|
||||||
writeln!(&mut text, "Current timer ticks: {}", time).unwrap();
|
|
||||||
let _ = serial.write(text.as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
if usb_dev.poll(&mut [&mut serial]) {
|
|
||||||
let mut buf = [0u8; 64];
|
|
||||||
match serial.read(&mut buf) {
|
|
||||||
Err(_e) => {}
|
|
||||||
Ok(0) => {}
|
|
||||||
Ok(count) => {
|
|
||||||
buf.iter_mut().take(count).for_each(|b| { b.make_ascii_uppercase(); });
|
|
||||||
let mut wr_ptr = &buf[..count];
|
|
||||||
while !wr_ptr.is_empty() {
|
|
||||||
match serial.write(wr_ptr) {
|
|
||||||
Ok(len) => wr_ptr = &wr_ptr[len..],
|
|
||||||
Err(_) => break,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -156,6 +156,13 @@ pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> ResultExt<A, E, FE> {
|
|||||||
ResultExt::Fatal(err)
|
ResultExt::Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn result_as_fatal<A, E: Error, FE: FatalError>(result: Result<A, FE>) -> ResultExt<A, E, FE> {
|
||||||
|
match result {
|
||||||
|
Ok(a) => ResultExt::Ok(a),
|
||||||
|
Err(err) => ResultExt::Fatal(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return early from the current function if the value is a fatal error.
|
/// Return early from the current function if the value is a fatal error.
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! return_fatal {
|
macro_rules! return_fatal {
|
||||||
|
35
visions-prototype/server/Cargo.toml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[package]
|
||||||
|
name = "visions"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = { version = "1.13.0" }
|
||||||
|
async-trait = { version = "0.1.83" }
|
||||||
|
authdb = { path = "../../authdb/" }
|
||||||
|
axum = { version = "0.7.9", features = [ "macros" ] }
|
||||||
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
|
futures = { version = "0.3.31" }
|
||||||
|
include_dir = { version = "0.7.4" }
|
||||||
|
lazy_static = { version = "1.5.0" }
|
||||||
|
mime = { version = "0.3.17" }
|
||||||
|
mime_guess = { version = "2.0.5" }
|
||||||
|
pretty_env_logger = { version = "0.5.0" }
|
||||||
|
result-extended = { path = "../../result-extended" }
|
||||||
|
rusqlite = { version = "0.32.1" }
|
||||||
|
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }
|
||||||
|
serde = { version = "1" }
|
||||||
|
serde_json = { version = "*" }
|
||||||
|
thiserror = { version = "2.0.3" }
|
||||||
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
|
tokio-stream = { version = "0.1.16" }
|
||||||
|
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||||
|
typeshare = { version = "1.0.4" }
|
||||||
|
urlencoding = { version = "2.1.3" }
|
||||||
|
uuid = { version = "1.11.0", features = ["v4"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = "2.0.3"
|
||||||
|
axum-test = "16.4.1"
|
23
visions-prototype/server/Taskfile.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
cmds:
|
||||||
|
- cargo watch -x build
|
||||||
|
|
||||||
|
test:
|
||||||
|
cmds:
|
||||||
|
- cargo watch -x 'nextest run'
|
||||||
|
|
||||||
|
dev:
|
||||||
|
cmds:
|
||||||
|
- cargo watch -x run
|
||||||
|
|
||||||
|
lint:
|
||||||
|
cmds:
|
||||||
|
- cargo watch -x clippy
|
||||||
|
|
||||||
|
release:
|
||||||
|
cmds:
|
||||||
|
- task lint
|
||||||
|
- cargo build --release
|
@ -3,7 +3,7 @@ CREATE TABLE users(
|
|||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
password TEXT,
|
password TEXT,
|
||||||
admin BOOLEAN,
|
admin BOOLEAN,
|
||||||
enabled BOOLEAN
|
state TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE sessions(
|
CREATE TABLE sessions(
|
||||||
@ -14,9 +14,9 @@ CREATE TABLE sessions(
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE games(
|
CREATE TABLE games(
|
||||||
uuid TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
type_ TEXT,
|
||||||
gm TEXT,
|
gm TEXT,
|
||||||
game_type TEXT,
|
|
||||||
name TEXT,
|
name TEXT,
|
||||||
|
|
||||||
FOREIGN KEY(gm) REFERENCES users(uuid)
|
FOREIGN KEY(gm) REFERENCES users(uuid)
|
||||||
@ -39,4 +39,3 @@ CREATE TABLE roles(
|
|||||||
FOREIGN KEY(game_id) REFERENCES games(uuid)
|
FOREIGN KEY(game_id) REFERENCES games(uuid)
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO users VALUES ('admin', 'admin', '', true, true);
|
|
@ -1,16 +1,19 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use async_std::sync::RwLock;
|
use async_std::sync::RwLock;
|
||||||
|
use chrono::{DateTime, Duration, TimeDelta, Utc};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use result_extended::{error, fatal, ok, return_error, ResultExt};
|
use result_extended::{error, fatal, ok, result_as_fatal, return_error, ResultExt};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
asset_db::{self, AssetId, Assets},
|
asset_db::{self, AssetId, Assets},
|
||||||
database::{CharacterId, Database, GameId, SessionId, UserId}, types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserProfile},
|
database::{CharacterId, Database, GameId, SessionId, UserId},
|
||||||
|
types::AccountState,
|
||||||
|
types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview},
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
|
const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
|
||||||
@ -22,7 +25,16 @@ const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb {
|
|||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub admin_enabled: bool,
|
pub ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
|
#[typeshare]
|
||||||
|
pub enum AuthResponse {
|
||||||
|
Success(SessionId),
|
||||||
|
PasswordReset(SessionId),
|
||||||
|
Locked,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -59,6 +71,7 @@ impl Core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
|
pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> {
|
||||||
|
/*
|
||||||
let state = self.0.write().await;
|
let state = self.0.write().await;
|
||||||
let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await {
|
let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await {
|
||||||
Ok(Some(admin_user)) => ok(admin_user),
|
Ok(Some(admin_user)) => ok(admin_user),
|
||||||
@ -71,8 +84,10 @@ impl Core {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ok(Status {
|
ok(Status {
|
||||||
admin_enabled: !admin_user.password.is_empty(),
|
ok: !admin_user.password.is_empty(),
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
ok(Status { ok: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_client(&self) -> String {
|
pub async fn register_client(&self) -> String {
|
||||||
@ -117,53 +132,78 @@ impl Core {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_users(&self) -> ResultExt<Vec<User>, AppError, FatalError> {
|
pub async fn list_users(&self) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
|
||||||
let users = self.0.write().await.db.users().await;
|
let users = self.0.write().await.db.users().await;
|
||||||
match users {
|
match users {
|
||||||
Ok(users) => ok(users.into_iter().map(User::from).collect()),
|
Ok(users) => ok(users
|
||||||
|
.into_iter()
|
||||||
|
.map(|user| UserOverview {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
state: user.state,
|
||||||
|
is_admin: user.admin,
|
||||||
|
})
|
||||||
|
.collect()),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(&self, user_id: UserId) -> ResultExt<Option<UserProfile>, AppError, FatalError> {
|
pub async fn user(
|
||||||
|
&self,
|
||||||
|
user_id: UserId,
|
||||||
|
) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
|
||||||
let users = return_error!(self.list_users().await);
|
let users = return_error!(self.list_users().await);
|
||||||
let games = return_error!(self.list_games().await);
|
match users.into_iter().find(|user| user.id == user_id) {
|
||||||
let user = match users.into_iter().find(|user| user.id == user_id) {
|
Some(user) => ok(Some(user)),
|
||||||
Some(user) => user,
|
|
||||||
None => return ok(None),
|
None => return ok(None),
|
||||||
};
|
}
|
||||||
let user_games = games.into_iter().filter(|g| g.gm == user.id).collect();
|
|
||||||
ok(Some(UserProfile {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
games: user_games,
|
|
||||||
is_admin: user.admin,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> {
|
pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match return_error!(self.user_by_username(username).await) {
|
match return_error!(self.user_by_username(username).await) {
|
||||||
Some(_) => error(AppError::UsernameUnavailable),
|
Some(_) => error(AppError::UsernameUnavailable),
|
||||||
None => match state.db.save_user(None, username, "", false, true).await {
|
None => match state
|
||||||
|
.db
|
||||||
|
.create_user(username, "", false, AccountState::PasswordReset(Utc::now() + Duration::minutes(60)))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(user_id) => ok(user_id),
|
Ok(user_id) => ok(user_id),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn disable_user(&self, _userid: UserId) -> ResultExt<(), AppError, FatalError> {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> {
|
pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> {
|
||||||
let games = self.0.read().await.db.games().await;
|
let games = self.0.read().await.db.games().await;
|
||||||
match games {
|
match games {
|
||||||
// Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
|
// Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()),
|
||||||
Ok(games) => ok(games.into_iter().map(GameOverview::from).collect()),
|
Ok(games) => ok(games
|
||||||
|
.into_iter()
|
||||||
|
.map(|game| GameOverview {
|
||||||
|
id: game.id,
|
||||||
|
type_: "".to_owned(),
|
||||||
|
name: game.name,
|
||||||
|
gm: game.gm,
|
||||||
|
players: game.players,
|
||||||
|
})
|
||||||
|
.collect::<Vec<GameOverview>>()),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_game(&self, gm: &UserId, game_type: &str, game_name: &str) -> ResultExt<GameId, AppError, FatalError> {
|
pub async fn create_game(
|
||||||
|
&self,
|
||||||
|
gm: &UserId,
|
||||||
|
game_type: &str,
|
||||||
|
game_name: &str,
|
||||||
|
) -> ResultExt<GameId, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state.db.save_game(None, gm, game_type, game_name).await {
|
match state.db.create_game(gm, game_type, game_name).await {
|
||||||
Ok(game_id) => ok(game_id),
|
Ok(game_id) => ok(game_id),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
}
|
}
|
||||||
@ -246,16 +286,22 @@ impl Core {
|
|||||||
|
|
||||||
pub async fn save_user(
|
pub async fn save_user(
|
||||||
&self,
|
&self,
|
||||||
uuid: Option<UserId>,
|
id: UserId,
|
||||||
username: &str,
|
name: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
enabled: bool,
|
account_state: AccountState,
|
||||||
) -> ResultExt<UserId, AppError, FatalError> {
|
) -> ResultExt<UserId, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state
|
match state
|
||||||
.db
|
.db
|
||||||
.save_user(uuid, username, password, admin, enabled)
|
.save_user(User {
|
||||||
|
id,
|
||||||
|
name: name.to_owned(),
|
||||||
|
password: password.to_owned(),
|
||||||
|
admin,
|
||||||
|
state: account_state,
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(uuid) => ok(uuid),
|
Ok(uuid) => ok(uuid),
|
||||||
@ -276,7 +322,11 @@ impl Core {
|
|||||||
};
|
};
|
||||||
match state
|
match state
|
||||||
.db
|
.db
|
||||||
.save_user(Some(uuid), &user.name, &password, user.admin, user.enabled)
|
.save_user(User {
|
||||||
|
password,
|
||||||
|
state: AccountState::Normal,
|
||||||
|
..user
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => ok(()),
|
Ok(_) => ok(()),
|
||||||
@ -288,19 +338,37 @@ impl Core {
|
|||||||
&self,
|
&self,
|
||||||
username: &str,
|
username: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> ResultExt<SessionId, AppError, FatalError> {
|
) -> ResultExt<AuthResponse, AppError, FatalError> {
|
||||||
let state = self.0.write().await;
|
let now = Utc::now();
|
||||||
match state.db.user_by_username(username).await {
|
let state = self.0.read().await;
|
||||||
Ok(Some(row)) if (row.password == password) => {
|
let user = state.db.user_by_username(username).await.unwrap().unwrap();
|
||||||
let session_id = state.db.create_session(&row.id).await.unwrap();
|
let user_info = return_error!(match state.db.user_by_username(username).await {
|
||||||
ok(session_id)
|
Ok(Some(row)) if row.password == password => ok(row),
|
||||||
}
|
|
||||||
Ok(_) => error(AppError::AuthFailed),
|
Ok(_) => error(AppError::AuthFailed),
|
||||||
Err(err) => fatal(err),
|
Err(err) => fatal(err),
|
||||||
|
});
|
||||||
|
|
||||||
|
match user_info.state {
|
||||||
|
AccountState::Normal => result_as_fatal(state.db.create_session(&user_info.id).await)
|
||||||
|
.map(|session_id| AuthResponse::Success(session_id)),
|
||||||
|
|
||||||
|
AccountState::PasswordReset(exp) => {
|
||||||
|
if exp < now {
|
||||||
|
error(AppError::AuthFailed)
|
||||||
|
} else {
|
||||||
|
result_as_fatal(state.db.create_session(&user_info.id).await)
|
||||||
|
.map(|session_id| AuthResponse::PasswordReset(session_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountState::Locked => ok(AuthResponse::Locked),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn session(&self, session_id: &SessionId) -> ResultExt<Option<User>, AppError, FatalError> {
|
pub async fn session(
|
||||||
|
&self,
|
||||||
|
session_id: &SessionId,
|
||||||
|
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||||
let state = self.0.read().await;
|
let state = self.0.read().await;
|
||||||
match state.db.session(session_id).await {
|
match state.db.session(session_id).await {
|
||||||
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
Ok(Some(user_row)) => ok(Some(User::from(user_row))),
|
||||||
@ -308,6 +376,18 @@ impl Core {
|
|||||||
Err(fatal_error) => fatal(fatal_error),
|
Err(fatal_error) => fatal(fatal_error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_session(&self, session_id: &SessionId) -> ResultExt<(), AppError, FatalError> {
|
||||||
|
let state = self.0.read().await;
|
||||||
|
match state.db.delete_session(session_id).await {
|
||||||
|
Ok(_) => ok(()),
|
||||||
|
Err(err) => fatal(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_expiration_date() -> DateTime<Utc> {
|
||||||
|
Utc::now() + TimeDelta::days(365)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -350,12 +430,17 @@ mod test {
|
|||||||
]);
|
]);
|
||||||
let memory_db: Option<PathBuf> = None;
|
let memory_db: Option<PathBuf> = None;
|
||||||
let conn = DbConn::new(memory_db);
|
let conn = DbConn::new(memory_db);
|
||||||
conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
conn.create_user("admin", "aoeu", true, AccountState::Normal)
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
conn.save_user(None, "gm_1", "aoeu", false, true)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
conn.create_user(
|
||||||
|
"gm_1",
|
||||||
|
"aoeu",
|
||||||
|
false,
|
||||||
|
AccountState::PasswordReset(Utc::now()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
Core::new(assets, conn)
|
Core::new(assets, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +508,7 @@ mod test {
|
|||||||
async fn it_creates_a_sessionid_on_successful_auth() {
|
async fn it_creates_a_sessionid_on_successful_auth() {
|
||||||
let core = test_core().await;
|
let core = test_core().await;
|
||||||
match core.auth("admin", "aoeu").await {
|
match core.auth("admin", "aoeu").await {
|
||||||
ResultExt::Ok(session_id) => {
|
ResultExt::Ok(AuthResponse::Success(session_id)) => {
|
||||||
let st = core.0.read().await;
|
let st = core.0.read().await;
|
||||||
match st.db.session(&session_id).await {
|
match st.db.session(&session_id).await {
|
||||||
Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
|
Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"),
|
||||||
@ -431,6 +516,8 @@ mod test {
|
|||||||
Err(err) => panic!("{}", err),
|
Err(err) => panic!("{}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResultExt::Ok(AuthResponse::PasswordReset(_)) => panic!("user is in password reset state"),
|
||||||
|
ResultExt::Ok(AuthResponse::Locked) => panic!("user has been locked"),
|
||||||
ResultExt::Err(err) => panic!("{}", err),
|
ResultExt::Err(err) => panic!("{}", err),
|
||||||
ResultExt::Fatal(err) => panic!("{}", err),
|
ResultExt::Fatal(err) => panic!("{}", err),
|
||||||
}
|
}
|
@ -8,12 +8,10 @@ use rusqlite_migration::Migrations;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{DatabaseResponse, Request},
|
database::{DatabaseResponse, Request},
|
||||||
types::FatalError,
|
types::{AccountState, FatalError, Game, User},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{types::GameId, CharacterId, CharsheetRow, DatabaseRequest, SessionId, UserId};
|
||||||
types::GameId, CharacterId, CharsheetRow, DatabaseRequest, GameRow, SessionId, UserId, UserRow
|
|
||||||
};
|
|
||||||
|
|
||||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||||
|
|
||||||
@ -40,28 +38,26 @@ impl DiskDb {
|
|||||||
.to_latest(&mut conn)
|
.to_latest(&mut conn)
|
||||||
.map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?;
|
||||||
|
|
||||||
// setup_test_database(&conn)?;
|
|
||||||
|
|
||||||
Ok(DiskDb { conn })
|
Ok(DiskDb { conn })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user(&self, id: &UserId) -> Result<Option<UserRow>, FatalError> {
|
pub fn user(&self, id: &UserId) -> Result<Option<User>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?")
|
.prepare("SELECT * FROM users WHERE uuid=?")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
let items: Vec<UserRow> = stmt
|
let items: Vec<User> = stmt
|
||||||
.query_map([id.as_str()], |row| {
|
.query_map([id.as_str()], |row| {
|
||||||
Ok(UserRow {
|
Ok(User {
|
||||||
id: row.get(0).unwrap(),
|
id: row.get(0).unwrap(),
|
||||||
name: row.get(1).unwrap(),
|
name: row.get(1).unwrap(),
|
||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
state: row.get(4).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
|
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match &items[..] {
|
match &items[..] {
|
||||||
[] => Ok(None),
|
[] => Ok(None),
|
||||||
@ -70,23 +66,23 @@ impl DiskDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn user_by_username(&self, username: &str) -> Result<Option<UserRow>, FatalError> {
|
pub fn user_by_username(&self, username: &str) -> Result<Option<User>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?")
|
.prepare("SELECT * FROM users WHERE name=?")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
let items: Vec<UserRow> = stmt
|
let items: Vec<User> = stmt
|
||||||
.query_map([username], |row| {
|
.query_map([username], |row| {
|
||||||
Ok(UserRow {
|
Ok(User {
|
||||||
id: row.get(0).unwrap(),
|
id: row.get(0).unwrap(),
|
||||||
name: row.get(1).unwrap(),
|
name: row.get(1).unwrap(),
|
||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
state: row.get(4).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
|
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match &items[..] {
|
match &items[..] {
|
||||||
[] => Ok(None),
|
[] => Ok(None),
|
||||||
@ -95,125 +91,124 @@ impl DiskDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_user(
|
pub fn create_user(
|
||||||
&self,
|
&self,
|
||||||
user_id: Option<UserId>,
|
|
||||||
name: &str,
|
name: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
admin: bool,
|
admin: bool,
|
||||||
enabled: bool,
|
state: AccountState,
|
||||||
) -> Result<UserId, FatalError> {
|
) -> Result<UserId, FatalError> {
|
||||||
match user_id {
|
let user_id = UserId::default();
|
||||||
None => {
|
let mut stmt = self
|
||||||
let user_id = UserId::default();
|
.conn
|
||||||
let mut stmt = self
|
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)")
|
||||||
.conn
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)")
|
stmt.execute((user_id.as_str(), name, password, admin, state))
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.unwrap();
|
||||||
stmt.execute((user_id.as_str(), name, password, admin, enabled))
|
Ok(user_id)
|
||||||
.unwrap();
|
|
||||||
Ok(user_id)
|
|
||||||
}
|
|
||||||
Some(user_id) => {
|
|
||||||
let mut stmt = self
|
|
||||||
.conn
|
|
||||||
.prepare("UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?")
|
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
|
||||||
stmt.execute((name, password, admin, enabled, user_id.as_str()))
|
|
||||||
.unwrap();
|
|
||||||
Ok(user_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn users(&self) -> Result<Vec<UserRow>, FatalError> {
|
pub fn save_user(&self, user: User) -> Result<UserId, FatalError> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("UPDATE users SET name=?, password=?, admin=?, state=? WHERE uuid=?")
|
||||||
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
|
stmt.execute((
|
||||||
|
user.name,
|
||||||
|
user.password,
|
||||||
|
user.admin,
|
||||||
|
user.state,
|
||||||
|
user.id.as_str(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
Ok(user.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn users(&self) -> Result<Vec<User>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT * FROM users")
|
.prepare("SELECT * FROM users")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
let items = stmt
|
let items = stmt
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
Ok(UserRow {
|
Ok(User {
|
||||||
id: row.get(0).unwrap(),
|
id: row.get(0).unwrap(),
|
||||||
name: row.get(1).unwrap(),
|
name: row.get(1).unwrap(),
|
||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
state: row.get(4).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
|
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_game(
|
pub fn create_game(
|
||||||
&self,
|
&self,
|
||||||
game_id: Option<GameId>,
|
|
||||||
gm: &UserId,
|
gm: &UserId,
|
||||||
game_type: &str,
|
game_type: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<GameId, FatalError> {
|
) -> Result<GameId, FatalError> {
|
||||||
match game_id {
|
let game_id = GameId::new();
|
||||||
None => {
|
let mut stmt = self
|
||||||
let game_id = GameId::new();
|
.conn
|
||||||
let mut stmt = self
|
.prepare("INSERT INTO games VALUES (?, ?, ?, ?)")
|
||||||
.conn
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
.prepare("INSERT INTO games VALUES (?, ?, ?, ?)")
|
stmt.execute((game_id.as_str(), game_type, gm.as_str(), name))
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.unwrap();
|
||||||
stmt.execute((game_id.as_str(), gm.as_str(), game_type, name))
|
Ok(game_id)
|
||||||
.unwrap();
|
|
||||||
Ok(game_id)
|
|
||||||
}
|
|
||||||
Some(game_id) => {
|
|
||||||
let mut stmt = self
|
|
||||||
.conn
|
|
||||||
.prepare("UPDATE games SET gm=? game_type=? name=? WHERE uuid=?")
|
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
|
||||||
stmt.execute((gm.as_str(), game_type, name, game_id.as_str()))
|
|
||||||
.unwrap();
|
|
||||||
Ok(game_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn games(&self) -> Result<Vec<GameRow>, FatalError> {
|
pub fn save_game(&self, game: Game) -> Result<(), FatalError> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("UPDATE games SET gm=? type_=? name=? WHERE id=?")
|
||||||
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
|
stmt.execute((game.gm.as_str(), game.type_, game.name, game.id.as_str()))
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn games(&self) -> Result<Vec<Game>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
.prepare("SELECT * FROM games")
|
.prepare("SELECT * FROM games")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
let items = stmt
|
let items = stmt
|
||||||
.query_map([], |row| {
|
.query_map([], |row| {
|
||||||
Ok(GameRow {
|
Ok(Game {
|
||||||
id: row.get(0).unwrap(),
|
id: row.get(0).unwrap(),
|
||||||
gm: row.get(1).unwrap(),
|
type_: row.get(1).unwrap(),
|
||||||
game_type: row.get(2).unwrap(),
|
gm: row.get(2).unwrap(),
|
||||||
name: row.get(3).unwrap(),
|
name: row.get(3).unwrap(),
|
||||||
|
players: vec![],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.collect::<Result<Vec<GameRow>, rusqlite::Error>>()
|
.collect::<Result<Vec<Game>, rusqlite::Error>>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn session(&self, session_id: &SessionId) -> Result<Option<UserRow>, FatalError> {
|
pub fn session(&self, session_id: &SessionId) -> Result<Option<User>, FatalError> {
|
||||||
let mut stmt = self.conn
|
let mut stmt = self.conn
|
||||||
.prepare("SELECT u.uuid, u.name, u.password, u.admin, u.enabled FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?")
|
.prepare("SELECT u.uuid, u.name, u.password, u.admin, u.state FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?")
|
||||||
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
|
|
||||||
let items: Vec<UserRow> = stmt
|
let items: Vec<User> = stmt
|
||||||
.query_map([session_id.as_str()], |row| {
|
.query_map([session_id.as_str()], |row| {
|
||||||
Ok(UserRow {
|
Ok(User {
|
||||||
id: row.get(0).unwrap(),
|
id: row.get(0).unwrap(),
|
||||||
name: row.get(1).unwrap(),
|
name: row.get(1).unwrap(),
|
||||||
password: row.get(2).unwrap(),
|
password: row.get(2).unwrap(),
|
||||||
admin: row.get(3).unwrap(),
|
admin: row.get(3).unwrap(),
|
||||||
enabled: row.get(4).unwrap(),
|
state: row.get(4).unwrap(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.collect::<Result<Vec<UserRow>, rusqlite::Error>>()
|
.collect::<Result<Vec<User>, rusqlite::Error>>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match &items[..] {
|
match &items[..] {
|
||||||
[] => Ok(None),
|
[] => Ok(None),
|
||||||
@ -242,6 +237,21 @@ impl DiskDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn delete_session(&self, session_id: &SessionId) -> Result<(), FatalError> {
|
||||||
|
match self.session(session_id) {
|
||||||
|
Ok(Some(_)) => {
|
||||||
|
let mut stmt = self.conn.prepare("DELETE FROM sessions WHERE id = ?")
|
||||||
|
.map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?;
|
||||||
|
|
||||||
|
let session_id = SessionId::new();
|
||||||
|
stmt.execute((session_id.as_str(),)).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(None) => Err(FatalError::DatabaseKeyMissing),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn character(&self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
pub fn character(&self, id: CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.conn
|
.conn
|
||||||
@ -310,26 +320,34 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
|||||||
_ => unimplemented!("errors for Charsheet"),
|
_ => unimplemented!("errors for Charsheet"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Request::CreateUser(username, password, admin, state) => {
|
||||||
|
let user_id = db.create_user(&username, &password, admin, state).unwrap();
|
||||||
|
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
||||||
|
}
|
||||||
|
Request::CreateGame(_, _, _) => {}
|
||||||
Request::CreateSession(id) => {
|
Request::CreateSession(id) => {
|
||||||
let session_id = db.create_session(&id).unwrap();
|
let session_id = db.create_session(&id).unwrap();
|
||||||
tx.send(DatabaseResponse::CreateSession(session_id))
|
tx.send(DatabaseResponse::CreateSession(session_id))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
Request::Games => {
|
Request::DeleteSession(id) => {
|
||||||
match db.games() {
|
db.delete_session(&id).unwrap();
|
||||||
Ok(games) => tx.send(DatabaseResponse::Games(games)).await.unwrap(),
|
tx.send(DatabaseResponse::DeleteSession).await.unwrap();
|
||||||
_ => unimplemented!("errors for Request::Games"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Request::Games => match db.games() {
|
||||||
|
Ok(games) => tx.send(DatabaseResponse::Games(games)).await.unwrap(),
|
||||||
|
_ => unimplemented!("errors for Request::Games"),
|
||||||
|
},
|
||||||
Request::Game(_game_id) => {
|
Request::Game(_game_id) => {
|
||||||
unimplemented!("Request::Game handler");
|
unimplemented!("Request::Game handler");
|
||||||
}
|
}
|
||||||
Request::SaveGame(game_id, user_id, game_type, game_name) => {
|
Request::SaveGame(game) => {
|
||||||
let game_id = db.save_game(game_id, &user_id, &game_type, &game_name);
|
let id = game.id.clone();
|
||||||
match game_id {
|
let save_result = db.save_game(game);
|
||||||
Ok(game_id) => {
|
match save_result {
|
||||||
tx.send(DatabaseResponse::SaveGame(game_id)).await.unwrap();
|
Ok(_) => {
|
||||||
|
tx.send(DatabaseResponse::SaveGame(id)).await.unwrap();
|
||||||
}
|
}
|
||||||
err => panic!("{:?}", err),
|
err => panic!("{:?}", err),
|
||||||
}
|
}
|
||||||
@ -350,14 +368,8 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
|||||||
err => panic!("{:?}", err),
|
err => panic!("{:?}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Request::SaveUser(user_id, username, password, admin, enabled) => {
|
Request::SaveUser(user) => {
|
||||||
let user_id = db.save_user(
|
let user_id = db.save_user(user);
|
||||||
user_id,
|
|
||||||
username.as_ref(),
|
|
||||||
password.as_ref(),
|
|
||||||
admin,
|
|
||||||
enabled,
|
|
||||||
);
|
|
||||||
match user_id {
|
match user_id {
|
||||||
Ok(user_id) => {
|
Ok(user_id) => {
|
||||||
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap();
|
||||||
@ -378,7 +390,7 @@ pub async fn db_handler(db: DiskDb, requestor: Receiver<DatabaseRequest>) {
|
|||||||
Ok(users) => {
|
Ok(users) => {
|
||||||
tx.send(DatabaseResponse::Users(users)).await.unwrap();
|
tx.send(DatabaseResponse::Users(users)).await.unwrap();
|
||||||
}
|
}
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!("request::Users"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,18 +6,21 @@ use std::path::Path;
|
|||||||
use async_std::channel::{bounded, Sender};
|
use async_std::channel::{bounded, Sender};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use disk_db::{db_handler, DiskDb};
|
use disk_db::{db_handler, DiskDb};
|
||||||
pub use types::{CharacterId, CharsheetRow, GameId, GameRow, SessionId, UserId, UserRow};
|
pub use types::{CharacterId, CharsheetRow, GameId, SessionId, UserId};
|
||||||
|
|
||||||
use crate::types::FatalError;
|
use crate::types::{AccountState, FatalError, Game, User};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Request {
|
enum Request {
|
||||||
Charsheet(CharacterId),
|
Charsheet(CharacterId),
|
||||||
|
CreateGame(UserId, String, String),
|
||||||
CreateSession(UserId),
|
CreateSession(UserId),
|
||||||
Games,
|
CreateUser(String, String, bool, AccountState),
|
||||||
|
DeleteSession(SessionId),
|
||||||
Game(GameId),
|
Game(GameId),
|
||||||
SaveGame(Option<GameId>, UserId, String, String),
|
Games,
|
||||||
SaveUser(Option<UserId>, String, String, bool, bool),
|
SaveGame(Game),
|
||||||
|
SaveUser(User),
|
||||||
Session(SessionId),
|
Session(SessionId),
|
||||||
User(UserId),
|
User(UserId),
|
||||||
UserByUsername(String),
|
UserByUsername(String),
|
||||||
@ -34,49 +37,45 @@ struct DatabaseRequest {
|
|||||||
enum DatabaseResponse {
|
enum DatabaseResponse {
|
||||||
Charsheet(Option<CharsheetRow>),
|
Charsheet(Option<CharsheetRow>),
|
||||||
CreateSession(SessionId),
|
CreateSession(SessionId),
|
||||||
Games(Vec<GameRow>),
|
DeleteSession,
|
||||||
Game(Option<GameRow>),
|
Games(Vec<Game>),
|
||||||
|
Game(Option<Game>),
|
||||||
SaveGame(GameId),
|
SaveGame(GameId),
|
||||||
SaveUser(UserId),
|
SaveUser(UserId),
|
||||||
Session(Option<UserRow>),
|
Session(Option<User>),
|
||||||
User(Option<UserRow>),
|
User(Option<User>),
|
||||||
Users(Vec<UserRow>),
|
Users(Vec<User>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Database: Send + Sync {
|
pub trait Database: Send + Sync {
|
||||||
async fn users(&self) -> Result<Vec<UserRow>, FatalError>;
|
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError>;
|
||||||
|
async fn session(&self, id: &SessionId) -> Result<Option<User>, FatalError>;
|
||||||
async fn user(&self, _: &UserId) -> Result<Option<UserRow>, FatalError>;
|
async fn delete_session(&self, id: &SessionId) -> Result<(), FatalError>;
|
||||||
|
|
||||||
async fn user_by_username(&self, _: &str) -> Result<Option<UserRow>, FatalError>;
|
|
||||||
|
|
||||||
async fn save_user(
|
|
||||||
&self,
|
|
||||||
user_id: Option<UserId>,
|
|
||||||
name: &str,
|
|
||||||
password: &str,
|
|
||||||
admin: bool,
|
|
||||||
enabled: bool,
|
|
||||||
) -> Result<UserId, FatalError>;
|
|
||||||
|
|
||||||
async fn games(&self) -> Result<Vec<GameRow>, FatalError>;
|
|
||||||
|
|
||||||
async fn game(&self, _: &GameId) -> Result<Option<GameRow>, FatalError>;
|
|
||||||
|
|
||||||
async fn save_game(
|
|
||||||
&self,
|
|
||||||
game_id: Option<GameId>,
|
|
||||||
gm: &UserId,
|
|
||||||
game_type: &str,
|
|
||||||
game_name: &str,
|
|
||||||
) -> Result<GameId, FatalError>;
|
|
||||||
|
|
||||||
async fn character(&self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
|
async fn character(&self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError>;
|
||||||
|
|
||||||
async fn session(&self, id: &SessionId) -> Result<Option<UserRow>, FatalError>;
|
async fn create_game(
|
||||||
|
&self,
|
||||||
|
gm: &UserId,
|
||||||
|
game_type: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<GameId, FatalError>;
|
||||||
|
async fn save_game(&self, game: Game) -> Result<GameId, FatalError>;
|
||||||
|
async fn game(&self, _: &GameId) -> Result<Option<Game>, FatalError>;
|
||||||
|
async fn games(&self) -> Result<Vec<Game>, FatalError>;
|
||||||
|
|
||||||
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError>;
|
async fn create_user(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
password: &str,
|
||||||
|
admin: bool,
|
||||||
|
state: AccountState,
|
||||||
|
) -> Result<UserId, FatalError>;
|
||||||
|
async fn save_user(&self, user: User) -> Result<UserId, FatalError>;
|
||||||
|
async fn user(&self, _: &UserId) -> Result<Option<User>, FatalError>;
|
||||||
|
async fn user_by_username(&self, _: &str) -> Result<Option<User>, FatalError>;
|
||||||
|
async fn users(&self) -> Result<Vec<User>, FatalError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DbConn {
|
pub struct DbConn {
|
||||||
@ -123,65 +122,61 @@ macro_rules! send_request {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Database for DbConn {
|
impl Database for DbConn {
|
||||||
async fn users(&self) -> Result<Vec<UserRow>, FatalError> {
|
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError> {
|
||||||
send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst))
|
send_request!(self, Request::CreateSession(id.to_owned()), DatabaseResponse::CreateSession(session_id) => Ok(session_id))
|
||||||
}
|
}
|
||||||
|
async fn session(&self, id: &SessionId) -> Result<Option<User>, FatalError> {
|
||||||
async fn user(&self, uid: &UserId) -> Result<Option<UserRow>, FatalError> {
|
send_request!(self, Request::Session(id.to_owned()), DatabaseResponse::Session(row) => Ok(row))
|
||||||
send_request!(self, Request::User(uid.clone()), DatabaseResponse::User(user) => Ok(user))
|
|
||||||
}
|
}
|
||||||
|
async fn delete_session(&self, id: &SessionId) -> Result<(), FatalError> {
|
||||||
async fn user_by_username(&self, username: &str) -> Result<Option<UserRow>, FatalError> {
|
send_request!(self, Request::DeleteSession(id.to_owned()), DatabaseResponse::DeleteSession => Ok(()))
|
||||||
send_request!(self, Request::UserByUsername(username.to_owned()), DatabaseResponse::User(user) => Ok(user))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_user(
|
|
||||||
&self,
|
|
||||||
user_id: Option<UserId>,
|
|
||||||
name: &str,
|
|
||||||
password: &str,
|
|
||||||
admin: bool,
|
|
||||||
enabled: bool,
|
|
||||||
) -> Result<UserId, FatalError> {
|
|
||||||
send_request!(self,
|
|
||||||
Request::SaveUser(
|
|
||||||
user_id,
|
|
||||||
name.to_owned(),
|
|
||||||
password.to_owned(),
|
|
||||||
admin,
|
|
||||||
enabled,
|
|
||||||
),
|
|
||||||
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn games(&self) -> Result<Vec<GameRow>, FatalError> {
|
|
||||||
send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn game(&self, game_id: &GameId) -> Result<Option<GameRow>, FatalError> {
|
|
||||||
send_request!(self, Request::Game(game_id.clone()), DatabaseResponse::Game(game) => Ok(game))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_game(
|
|
||||||
&self,
|
|
||||||
game_id: Option<GameId>,
|
|
||||||
user_id: &UserId,
|
|
||||||
game_type: &str,
|
|
||||||
game_name: &str,
|
|
||||||
) -> Result<GameId, FatalError> {
|
|
||||||
send_request!(self, Request::SaveGame(game_id, user_id.to_owned(), game_type.to_owned(), game_name.to_owned()), DatabaseResponse::SaveGame(game_id) => Ok(game_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn character(&self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
async fn character(&self, id: &CharacterId) -> Result<Option<CharsheetRow>, FatalError> {
|
||||||
send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row))
|
send_request!(self, Request::Charsheet(id.to_owned()), DatabaseResponse::Charsheet(row) => Ok(row))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn session(&self, id: &SessionId) -> Result<Option<UserRow>, FatalError> {
|
async fn create_game(
|
||||||
send_request!(self, Request::Session(id.to_owned()), DatabaseResponse::Session(row) => Ok(row))
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
game_type: &str,
|
||||||
|
game_name: &str,
|
||||||
|
) -> Result<GameId, FatalError> {
|
||||||
|
send_request!(self, Request::CreateGame(user_id.to_owned(), game_type.to_owned(), game_name.to_owned()), DatabaseResponse::SaveGame(game_id) => Ok(game_id))
|
||||||
|
}
|
||||||
|
async fn save_game(&self, game: Game) -> Result<GameId, FatalError> {
|
||||||
|
send_request!(self, Request::SaveGame(game), DatabaseResponse::SaveGame(game_id) => Ok(game_id))
|
||||||
|
}
|
||||||
|
async fn game(&self, game_id: &GameId) -> Result<Option<Game>, FatalError> {
|
||||||
|
send_request!(self, Request::Game(game_id.clone()), DatabaseResponse::Game(game) => Ok(game))
|
||||||
|
}
|
||||||
|
async fn games(&self) -> Result<Vec<Game>, FatalError> {
|
||||||
|
send_request!(self, Request::Games, DatabaseResponse::Games(lst) => Ok(lst))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_session(&self, id: &UserId) -> Result<SessionId, FatalError> {
|
async fn create_user(
|
||||||
send_request!(self, Request::CreateSession(id.to_owned()), DatabaseResponse::CreateSession(session_id) => Ok(session_id))
|
&self,
|
||||||
|
name: &str,
|
||||||
|
password: &str,
|
||||||
|
admin: bool,
|
||||||
|
state: AccountState,
|
||||||
|
) -> Result<UserId, FatalError> {
|
||||||
|
send_request!(self,
|
||||||
|
Request::CreateUser(name.to_owned(), password.to_owned(), admin, state),
|
||||||
|
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
||||||
|
}
|
||||||
|
async fn save_user(&self, user: User) -> Result<UserId, FatalError> {
|
||||||
|
send_request!(self,
|
||||||
|
Request::SaveUser(user),
|
||||||
|
DatabaseResponse::SaveUser(user_id) => Ok(user_id))
|
||||||
|
}
|
||||||
|
async fn user(&self, uid: &UserId) -> Result<Option<User>, FatalError> {
|
||||||
|
send_request!(self, Request::User(uid.clone()), DatabaseResponse::User(user) => Ok(user))
|
||||||
|
}
|
||||||
|
async fn user_by_username(&self, username: &str) -> Result<Option<User>, FatalError> {
|
||||||
|
send_request!(self, Request::UserByUsername(username.to_owned()), DatabaseResponse::User(user) => Ok(user))
|
||||||
|
}
|
||||||
|
async fn users(&self) -> Result<Vec<User>, FatalError> {
|
||||||
|
send_request!(self, Request::Users, DatabaseResponse::Users(lst) => Ok(lst))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,12 +196,19 @@ mod test {
|
|||||||
let no_path: Option<PathBuf> = None;
|
let no_path: Option<PathBuf> = None;
|
||||||
let db = DiskDb::new(no_path).unwrap();
|
let db = DiskDb::new(no_path).unwrap();
|
||||||
|
|
||||||
db.save_user(Some(UserId::from("admin")), "admin", "abcdefg", true, true)
|
db.create_user("admin", "abcdefg", true, AccountState::Normal)
|
||||||
|
.unwrap();
|
||||||
|
let game_id = db
|
||||||
|
.create_game(
|
||||||
|
&UserId::from("admin"),
|
||||||
|
"Candela",
|
||||||
|
"Circle of the Winter Solstice",
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let game_id = db.save_game(None, &UserId::from("admin"), "Candela", "Circle of the Winter Solstice").unwrap();
|
|
||||||
(db, game_id)
|
(db, game_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_retrieve_a_character() {
|
fn it_can_retrieve_a_character() {
|
||||||
let (db, game_id) = setup_db();
|
let (db, game_id) = setup_db();
|
@ -1,6 +1,7 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use rusqlite::types::{FromSql, FromSqlResult, ValueRef};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -159,15 +160,6 @@ impl FromSql for CharacterId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct UserRow {
|
|
||||||
pub id: UserId,
|
|
||||||
pub name: String,
|
|
||||||
pub password: String,
|
|
||||||
pub admin: bool,
|
|
||||||
pub enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Role {
|
pub struct Role {
|
||||||
userid: UserId,
|
userid: UserId,
|
||||||
@ -175,6 +167,7 @@ pub struct Role {
|
|||||||
role: String,
|
role: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GameRow {
|
pub struct GameRow {
|
||||||
pub id: GameId,
|
pub id: GameId,
|
||||||
@ -182,6 +175,7 @@ pub struct GameRow {
|
|||||||
pub game_type: String,
|
pub game_type: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CharsheetRow {
|
pub struct CharsheetRow {
|
||||||
@ -196,5 +190,20 @@ pub struct SessionRow {
|
|||||||
user_id: SessionId,
|
user_id: SessionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DateTime(pub chrono::DateTime<Utc>);
|
||||||
|
|
||||||
|
impl FromSql for DateTime {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
match value {
|
||||||
|
ValueRef::Text(text) => String::from_utf8(text.to_vec())
|
||||||
|
.map_err(|_err| FromSqlError::InvalidType)
|
||||||
|
.and_then(|s| {
|
||||||
|
NaiveDateTime::parse_from_str(&s, "%Y-%m-%d %H:%M:%S")
|
||||||
|
.map_err(|_err| FromSqlError::InvalidType)
|
||||||
|
})
|
||||||
|
.and_then(|dt| Ok(DateTime(dt.and_utc()))),
|
||||||
|
_ => Err(FromSqlError::InvalidType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,20 @@
|
|||||||
mod game_management;
|
mod game_management;
|
||||||
mod user_management;
|
mod user_management;
|
||||||
|
mod types;
|
||||||
|
|
||||||
use axum::{http::StatusCode, Json};
|
use axum::{http::StatusCode, Json};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
pub use game_management::*;
|
pub use game_management::*;
|
||||||
pub use user_management::*;
|
pub use user_management::*;
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
use result_extended::ResultExt;
|
use result_extended::ResultExt;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::Core,
|
core::Core,
|
||||||
types::{AppError, FatalError},
|
types::{AppError, FatalError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
||||||
pub struct HealthCheck {
|
|
||||||
pub ok: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wrap_handler<F, A, Fut>(f: F) -> (StatusCode, Json<Option<A>>)
|
pub async fn wrap_handler<F, A, Fut>(f: F) -> (StatusCode, Json<Option<A>>)
|
||||||
where
|
where
|
||||||
F: FnOnce() -> Fut,
|
F: FnOnce() -> Fut,
|
||||||
@ -45,7 +42,7 @@ where
|
|||||||
pub async fn healthcheck(core: Core) -> Vec<u8> {
|
pub async fn healthcheck(core: Core) -> Vec<u8> {
|
||||||
match core.status().await {
|
match core.status().await {
|
||||||
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {
|
ResultExt::Ok(s) => serde_json::to_vec(&HealthCheck {
|
||||||
ok: s.admin_enabled,
|
ok: s.ok,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { ok: false }).unwrap(),
|
ResultExt::Err(_) => serde_json::to_vec(&HealthCheck { ok: false }).unwrap(),
|
83
visions-prototype/server/src/handlers/types.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
use crate::database::UserId;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct HealthCheck {
|
||||||
|
pub ok: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
|
#[typeshare]
|
||||||
|
pub enum AccountState {
|
||||||
|
Normal,
|
||||||
|
PasswordReset(String),
|
||||||
|
Locked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::types::AccountState> for AccountState {
|
||||||
|
fn from(s: crate::types::AccountState) -> Self {
|
||||||
|
match s {
|
||||||
|
crate::types::AccountState::Normal => Self::Normal,
|
||||||
|
crate::types::AccountState::PasswordReset(r) => {
|
||||||
|
Self::PasswordReset(format!("{}", r.format("%Y-%m-%d %H:%M:%S")))
|
||||||
|
}
|
||||||
|
crate::types::AccountState::Locked => Self::Locked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct User {
|
||||||
|
pub id: UserId,
|
||||||
|
pub name: String,
|
||||||
|
pub password: String,
|
||||||
|
pub admin: bool,
|
||||||
|
pub state: AccountState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::types::User> for User {
|
||||||
|
fn from(u: crate::types::User) -> Self {
|
||||||
|
Self {
|
||||||
|
id: u.id,
|
||||||
|
name: u.name,
|
||||||
|
password: u.password,
|
||||||
|
admin: u.admin,
|
||||||
|
state: AccountState::from(u.state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct UserOverview {
|
||||||
|
pub id: UserId,
|
||||||
|
pub name: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
pub state: AccountState,
|
||||||
|
pub games: Vec<crate::types::GameOverview>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserOverview {
|
||||||
|
pub fn new(user: crate::types::UserOverview, games: Vec<crate::types::GameOverview>) -> Self {
|
||||||
|
let s = Self::from(user);
|
||||||
|
Self{ games, ..s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::types::UserOverview> for UserOverview {
|
||||||
|
fn from(input: crate::types::UserOverview) -> Self {
|
||||||
|
Self {
|
||||||
|
id: input.id,
|
||||||
|
name: input.name,
|
||||||
|
is_admin: input.is_admin,
|
||||||
|
state: AccountState::from(input.state),
|
||||||
|
games: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,17 @@
|
|||||||
use axum::{
|
use axum::{http::HeaderMap, Json};
|
||||||
http::HeaderMap,
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use result_extended::{error, ok, return_error, ResultExt};
|
use result_extended::{error, ok, return_error, ResultExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::Core,
|
core::{AuthResponse, Core},
|
||||||
database::{SessionId, UserId},
|
database::{SessionId, UserId},
|
||||||
types::{AppError, FatalError, User, UserProfile},
|
types::{AppError, FatalError, User},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::UserOverview;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct AuthRequest {
|
pub struct AuthRequest {
|
||||||
@ -33,12 +32,16 @@ pub struct SetPasswordRequest {
|
|||||||
pub password_2: String,
|
pub password_2: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_session(
|
#[derive(Deserialize, Serialize)]
|
||||||
core: &Core,
|
#[typeshare]
|
||||||
headers: HeaderMap,
|
pub struct SetAdminPasswordRequest {
|
||||||
) -> ResultExt<Option<User>, AppError, FatalError> {
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_session_header(headers: HeaderMap) -> ResultExt<Option<SessionId>, AppError, FatalError> {
|
||||||
match headers.get("Authorization") {
|
match headers.get("Authorization") {
|
||||||
Some(token) => {
|
Some(token) => {
|
||||||
|
println!("check_session: {:?}", token);
|
||||||
match token
|
match token
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -46,7 +49,7 @@ async fn check_session(
|
|||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.as_slice()
|
.as_slice()
|
||||||
{
|
{
|
||||||
[_schema, token] => core.session(&SessionId::from(token.to_owned())).await,
|
[_schema, token] => ok(Some(SessionId::from(*token))),
|
||||||
_ => error(AppError::BadRequest),
|
_ => error(AppError::BadRequest),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,6 +57,16 @@ async fn check_session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_session(
|
||||||
|
core: &Core,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> ResultExt<Option<User>, AppError, FatalError> {
|
||||||
|
match return_error!(parse_session_header(headers)) {
|
||||||
|
Some(session_id) => core.session(&session_id).await,
|
||||||
|
None => ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn auth_required<F, A, Fut>(
|
pub async fn auth_required<F, A, Fut>(
|
||||||
core: Core,
|
core: Core,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
@ -93,22 +106,54 @@ where
|
|||||||
pub async fn check_password(
|
pub async fn check_password(
|
||||||
core: Core,
|
core: Core,
|
||||||
req: Json<AuthRequest>,
|
req: Json<AuthRequest>,
|
||||||
) -> ResultExt<SessionId, AppError, FatalError> {
|
) -> ResultExt<AuthResponse, AppError, FatalError> {
|
||||||
let Json(AuthRequest { username, password }) = req;
|
let Json(AuthRequest { username, password }) = req;
|
||||||
core.auth(&username, &password).await
|
println!("check_password: {} {}", username, password);
|
||||||
|
let result = core.auth(&username, &password).await;
|
||||||
|
println!("auth result: {:?}", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_session(core: Core, headers: HeaderMap,) -> ResultExt<(), AppError, FatalError> {
|
||||||
|
/*
|
||||||
|
auth_required(core.clone(), headers, |user| async move {
|
||||||
|
match user_id {
|
||||||
|
Some(user_id) => core.delete_session
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
}).await
|
||||||
|
*/
|
||||||
|
|
||||||
|
match return_error!(parse_session_header(headers)) {
|
||||||
|
Some(session_id) => core.delete_session(&session_id).await,
|
||||||
|
None => error(AppError::AuthFailed),
|
||||||
|
}
|
||||||
|
// await core.delete_session(session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user(
|
pub async fn get_user(
|
||||||
core: Core,
|
core: Core,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
user_id: Option<UserId>,
|
user_id: Option<UserId>,
|
||||||
) -> ResultExt<Option<UserProfile>, AppError, FatalError> {
|
) -> ResultExt<Option<UserOverview>, AppError, FatalError> {
|
||||||
auth_required(core.clone(), headers, |user| async move {
|
auth_required(core.clone(), headers, |user| async move {
|
||||||
match user_id {
|
match user_id {
|
||||||
Some(user_id) => core.user(user_id).await,
|
Some(user_id) => core.user(user_id).await,
|
||||||
None => core.user(user.id).await,
|
None => core.user(user.id).await,
|
||||||
}
|
}
|
||||||
}).await
|
.map(|maybe_user| maybe_user.map(|user| UserOverview::from(user)))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_users(
|
||||||
|
core: Core,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> ResultExt<Vec<UserOverview>, AppError, FatalError> {
|
||||||
|
auth_required(core.clone(), headers, |_user| async move {
|
||||||
|
core.list_users().await.map(|users| users.into_iter().map(|user| UserOverview::from(user)).collect::<Vec<UserOverview>>())
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
@ -118,7 +163,8 @@ pub async fn create_user(
|
|||||||
) -> ResultExt<UserId, AppError, FatalError> {
|
) -> ResultExt<UserId, AppError, FatalError> {
|
||||||
admin_required(core.clone(), headers, |_admin| async {
|
admin_required(core.clone(), headers, |_admin| async {
|
||||||
core.create_user(&req.username).await
|
core.create_user(&req.username).await
|
||||||
}).await
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_password(
|
pub async fn set_password(
|
||||||
@ -132,5 +178,6 @@ pub async fn set_password(
|
|||||||
} else {
|
} else {
|
||||||
error(AppError::BadRequest)
|
error(AppError::BadRequest)
|
||||||
}
|
}
|
||||||
}).await
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
33
visions-prototype/server/src/main.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use core::Core;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use asset_db::FsAssets;
|
||||||
|
use database::DbConn;
|
||||||
|
|
||||||
|
mod asset_db;
|
||||||
|
mod core;
|
||||||
|
mod database;
|
||||||
|
mod handlers;
|
||||||
|
mod routes;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
/*
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone()));
|
||||||
|
let authenticated_endpoints = route_image(core.clone());
|
||||||
|
*/
|
||||||
|
|
||||||
|
let conn = DbConn::new(Some("/home/savanni/game.db"));
|
||||||
|
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||||
|
|
||||||
|
let app = routes::routes(core);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:8001")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::Path,
|
extract::Path,
|
||||||
http::{header::{AUTHORIZATION, CONTENT_TYPE}, HeaderMap, Method},
|
http::{
|
||||||
|
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||||
|
HeaderMap, Method,
|
||||||
|
},
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
@ -10,8 +13,7 @@ use crate::{
|
|||||||
core::Core,
|
core::Core,
|
||||||
database::UserId,
|
database::UserId,
|
||||||
handlers::{
|
handlers::{
|
||||||
check_password, create_game, create_user, get_user, healthcheck, set_password,
|
check_password, create_game, create_user, delete_session, get_user, get_users, healthcheck, set_password, wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest
|
||||||
wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,10 +37,14 @@ pub fn routes(core: Core) -> Router {
|
|||||||
let core = core.clone();
|
let core = core.clone();
|
||||||
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
|
move |req: Json<AuthRequest>| wrap_handler(|| check_password(core, req))
|
||||||
})
|
})
|
||||||
|
.delete({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap| wrap_handler(|| delete_session(core, headers))
|
||||||
|
})
|
||||||
.layer(
|
.layer(
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_methods([Method::POST])
|
.allow_methods([Method::DELETE, Method::POST])
|
||||||
.allow_headers([CONTENT_TYPE])
|
.allow_headers([AUTHORIZATION, CONTENT_TYPE])
|
||||||
.allow_origin(Any),
|
.allow_origin(Any),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -61,7 +67,26 @@ pub fn routes(core: Core) -> Router {
|
|||||||
let Json(req) = req;
|
let Json(req) = req;
|
||||||
wrap_handler(|| create_user(core, headers, req))
|
wrap_handler(|| create_user(core, headers, req))
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_methods([Method::PUT])
|
||||||
|
.allow_headers([AUTHORIZATION, CONTENT_TYPE])
|
||||||
|
.allow_origin(Any),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/users",
|
||||||
|
get({
|
||||||
|
let core = core.clone();
|
||||||
|
move |headers: HeaderMap| wrap_handler(|| get_users(core, headers))
|
||||||
|
})
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_methods([Method::GET])
|
||||||
|
.allow_headers([AUTHORIZATION])
|
||||||
|
.allow_origin(Any),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/user/password",
|
"/api/v1/user/password",
|
||||||
@ -71,7 +96,13 @@ pub fn routes(core: Core) -> Router {
|
|||||||
let Json(req) = req;
|
let Json(req) = req;
|
||||||
wrap_handler(|| set_password(core, headers, req))
|
wrap_handler(|| set_password(core, headers, req))
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
.layer(
|
||||||
|
CorsLayer::new()
|
||||||
|
.allow_methods([Method::PUT])
|
||||||
|
.allow_headers([AUTHORIZATION, CONTENT_TYPE])
|
||||||
|
.allow_origin(Any),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/user/:user_id",
|
"/api/v1/user/:user_id",
|
||||||
@ -97,35 +128,34 @@ pub fn routes(core: Core) -> Router {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
|
use chrono::Utc;
|
||||||
use cool_asserts::assert_matches;
|
use cool_asserts::assert_matches;
|
||||||
use result_extended::ResultExt;
|
use result_extended::ResultExt;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
asset_db::FsAssets,
|
asset_db::FsAssets,
|
||||||
core::Core,
|
core::{AuthResponse, Core},
|
||||||
database::{Database, DbConn, GameId, SessionId, UserId},
|
database::{Database, DbConn, GameId, SessionId, UserId},
|
||||||
handlers::CreateGameRequest,
|
handlers::CreateGameRequest,
|
||||||
types::UserProfile,
|
types::UserOverview,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn setup_without_admin() -> (Core, TestServer) {
|
async fn initialize_test_server() -> (Core, TestServer) {
|
||||||
|
let password_exp = Utc::now() + Duration::from_secs(5);
|
||||||
let memory_db: Option<PathBuf> = None;
|
let memory_db: Option<PathBuf> = None;
|
||||||
let conn = DbConn::new(memory_db);
|
let conn = DbConn::new(memory_db);
|
||||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
let _admin_id = conn
|
||||||
let app = routes(core.clone());
|
.create_user(
|
||||||
let server = TestServer::new(app).unwrap();
|
"admin",
|
||||||
(core, server)
|
"aoeu",
|
||||||
}
|
true,
|
||||||
|
crate::types::AccountState::PasswordReset(password_exp),
|
||||||
async fn setup_admin_enabled() -> (Core, TestServer) {
|
)
|
||||||
let memory_db: Option<PathBuf> = None;
|
|
||||||
let conn = DbConn::new(memory_db);
|
|
||||||
conn.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
||||||
@ -134,8 +164,26 @@ mod test {
|
|||||||
(core, server)
|
(core, server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn setup_with_admin() -> (Core, TestServer) {
|
||||||
|
let (core, server) = initialize_test_server().await;
|
||||||
|
core.set_password(UserId::from("admin"), "aoeu".to_owned())
|
||||||
|
.await;
|
||||||
|
(core, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_with_disabled_user() -> (Core, TestServer) {
|
||||||
|
let (core, server) = setup_with_admin().await;
|
||||||
|
let uuid = match core.create_user("shephard").await {
|
||||||
|
ResultExt::Ok(uuid) => uuid,
|
||||||
|
ResultExt::Err(err) => panic!("{}", err),
|
||||||
|
ResultExt::Fatal(err) => panic!("{}", err),
|
||||||
|
};
|
||||||
|
core.disable_user(uuid).await;
|
||||||
|
(core, server)
|
||||||
|
}
|
||||||
|
|
||||||
async fn setup_with_user() -> (Core, TestServer) {
|
async fn setup_with_user() -> (Core, TestServer) {
|
||||||
let (core, server) = setup_admin_enabled().await;
|
let (core, server) = setup_with_admin().await;
|
||||||
let response = server
|
let response = server
|
||||||
.post("/api/v1/auth")
|
.post("/api/v1/auth")
|
||||||
.json(&AuthRequest {
|
.json(&AuthRequest {
|
||||||
@ -170,18 +218,7 @@ mod test {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_returns_a_healthcheck() {
|
async fn it_returns_a_healthcheck() {
|
||||||
let (core, server) = setup_without_admin();
|
let (_core, server) = initialize_test_server().await;
|
||||||
|
|
||||||
let response = server.get("/api/v1/health").await;
|
|
||||||
response.assert_status_ok();
|
|
||||||
let b: crate::handlers::HealthCheck = response.json();
|
|
||||||
assert_eq!(b, crate::handlers::HealthCheck { ok: false });
|
|
||||||
|
|
||||||
assert_matches!(
|
|
||||||
core.save_user(Some(UserId::from("admin")), "admin", "aoeu", true, true)
|
|
||||||
.await,
|
|
||||||
ResultExt::Ok(_)
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = server.get("/api/v1/health").await;
|
let response = server.get("/api/v1/health").await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
@ -189,9 +226,65 @@ mod test {
|
|||||||
assert_eq!(b, crate::handlers::HealthCheck { ok: true });
|
assert_eq!(b, crate::handlers::HealthCheck { ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_new_user_has_an_expired_password() {
|
||||||
|
let (_core, server) = setup_with_admin().await;
|
||||||
|
|
||||||
|
let response = server
|
||||||
|
.post("/api/v1/auth")
|
||||||
|
.add_header("Content-Type", "application/json")
|
||||||
|
.json(&AuthRequest {
|
||||||
|
username: "admin".to_owned(),
|
||||||
|
password: "aoeu".to_owned(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
response.assert_status_ok();
|
||||||
|
let session_id = response.json::<Option<AuthResponse>>().unwrap();
|
||||||
|
|
||||||
|
let session_id = match session_id {
|
||||||
|
AuthResponse::PasswordReset(session_id) => session_id,
|
||||||
|
AuthResponse::Success(_) => panic!("admin user password has already been set"),
|
||||||
|
AuthResponse::Locked => panic!("admin user is already expired"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = server
|
||||||
|
.put("/api/v1/user")
|
||||||
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
|
.json("savanni")
|
||||||
|
.await;
|
||||||
|
response.assert_status_ok();
|
||||||
|
|
||||||
|
let response = server
|
||||||
|
.post("/api/v1/auth")
|
||||||
|
.add_header("Content-Type", "application/json")
|
||||||
|
.json(&AuthRequest {
|
||||||
|
username: "savanni".to_owned(),
|
||||||
|
password: "".to_owned(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
response.assert_status_ok();
|
||||||
|
let session = response.json::<Option<AuthResponse>>().unwrap();
|
||||||
|
assert_matches!(session, AuthResponse::PasswordReset(_));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_refuses_to_authenticate_a_disabled_user() {
|
||||||
|
let (_core, _server) = setup_with_disabled_user().await;
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn it_forces_changing_expired_password() {
|
||||||
|
let (_core, _server) = setup_with_user().await;
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_authenticates_a_user() {
|
async fn it_authenticates_a_user() {
|
||||||
let (_core, server) = setup_admin_enabled().await;
|
let (_core, server) = setup_with_admin().await;
|
||||||
|
|
||||||
let response = server
|
let response = server
|
||||||
.post("/api/v1/auth")
|
.post("/api/v1/auth")
|
||||||
@ -219,13 +312,12 @@ mod test {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let session_id: Option<SessionId> = response.json();
|
assert_matches!(response.json(), Some(AuthResponse::PasswordReset(_)));
|
||||||
assert!(session_id.is_some());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn it_returns_user_profile() {
|
async fn it_returns_user_profile() {
|
||||||
let (_core, server) = setup_admin_enabled().await;
|
let (_core, server) = setup_with_admin().await;
|
||||||
|
|
||||||
let response = server.get("/api/v1/user").await;
|
let response = server.get("/api/v1/user").await;
|
||||||
response.assert_status(StatusCode::UNAUTHORIZED);
|
response.assert_status(StatusCode::UNAUTHORIZED);
|
||||||
@ -238,27 +330,19 @@ mod test {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let session_id: Option<SessionId> = response.json();
|
let session_id = assert_matches!(response.json(), Some(AuthResponse::PasswordReset(session_id)) => session_id);
|
||||||
let session_id = session_id.unwrap();
|
|
||||||
|
|
||||||
let response = server
|
let response = server
|
||||||
.get("/api/v1/user")
|
.get("/api/v1/user")
|
||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let profile: Option<UserProfile> = response.json();
|
let profile: Option<UserOverview> = response.json();
|
||||||
let profile = profile.unwrap();
|
let profile = profile.unwrap();
|
||||||
assert_eq!(profile.id, UserId::from("admin"));
|
|
||||||
assert_eq!(profile.name, "admin");
|
assert_eq!(profile.name, "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[ignore]
|
||||||
async fn an_admin_can_create_a_user() {
|
|
||||||
// All of the contents of this test are basically required for any test on individual
|
|
||||||
// users, so I moved it all into the setup code.
|
|
||||||
let (_core, _server) = setup_with_user().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_user_can_get_any_user_profile() {
|
async fn a_user_can_get_any_user_profile() {
|
||||||
let (core, server) = setup_with_user().await;
|
let (core, server) = setup_with_user().await;
|
||||||
@ -285,7 +369,7 @@ mod test {
|
|||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let profile: Option<UserProfile> = response.json();
|
let profile: Option<UserOverview> = response.json();
|
||||||
let profile = profile.unwrap();
|
let profile = profile.unwrap();
|
||||||
assert_eq!(profile.name, "savanni");
|
assert_eq!(profile.name, "savanni");
|
||||||
|
|
||||||
@ -294,11 +378,12 @@ mod test {
|
|||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
let profile: Option<UserProfile> = response.json();
|
let profile: Option<UserOverview> = response.json();
|
||||||
let profile = profile.unwrap();
|
let profile = profile.unwrap();
|
||||||
assert_eq!(profile.name, "admin");
|
assert_eq!(profile.name, "admin");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_user_can_change_their_password() {
|
async fn a_user_can_change_their_password() {
|
||||||
let (_core, server) = setup_with_user().await;
|
let (_core, server) = setup_with_user().await;
|
||||||
@ -317,7 +402,7 @@ mod test {
|
|||||||
.get("/api/v1/user")
|
.get("/api/v1/user")
|
||||||
.add_header("Authorization", format!("Bearer {}", session_id))
|
.add_header("Authorization", format!("Bearer {}", session_id))
|
||||||
.await;
|
.await;
|
||||||
let profile = response.json::<Option<UserProfile>>().unwrap();
|
let profile = response.json::<Option<UserOverview>>().unwrap();
|
||||||
assert_eq!(profile.name, "savanni");
|
assert_eq!(profile.name, "savanni");
|
||||||
|
|
||||||
let response = server
|
let response = server
|
||||||
@ -350,6 +435,7 @@ mod test {
|
|||||||
response.assert_status(StatusCode::OK);
|
response.assert_status(StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ignore]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn a_user_can_create_a_game() {
|
async fn a_user_can_create_a_game() {
|
||||||
let (_core, server) = setup_with_user().await;
|
let (_core, server) = setup_with_user().await;
|
@ -1,3 +1,8 @@
|
|||||||
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
use rusqlite::{
|
||||||
|
types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef},
|
||||||
|
ToSql,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -5,7 +10,7 @@ use typeshare::typeshare;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
asset_db::AssetId,
|
asset_db::AssetId,
|
||||||
database::{GameId, GameRow, UserId, UserRow},
|
database::{GameId, UserId},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@ -70,27 +75,55 @@ pub struct Rgb {
|
|||||||
pub blue: u32,
|
pub blue: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
pub enum AccountState {
|
||||||
#[typeshare]
|
Normal,
|
||||||
|
PasswordReset(DateTime<Utc>),
|
||||||
|
Locked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for AccountState {
|
||||||
|
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||||
|
if let ValueRef::Text(text) = value {
|
||||||
|
let text = String::from_utf8(text.to_vec()).unwrap();
|
||||||
|
if text.starts_with("Normal") {
|
||||||
|
Ok(AccountState::Normal)
|
||||||
|
} else if text.starts_with("PasswordReset") {
|
||||||
|
let exp_str = text.strip_prefix("PasswordReset ").unwrap();
|
||||||
|
let exp = NaiveDateTime::parse_from_str(exp_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
Ok(AccountState::PasswordReset(exp))
|
||||||
|
} else if text.starts_with("Locked") {
|
||||||
|
Ok(AccountState::Locked)
|
||||||
|
} else {
|
||||||
|
Err(FromSqlError::InvalidType)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(FromSqlError::InvalidType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql for AccountState {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||||
|
match self {
|
||||||
|
AccountState::Normal => Ok(ToSqlOutput::Borrowed(ValueRef::Text("Normal".as_bytes()))),
|
||||||
|
AccountState::PasswordReset(expiration) => Ok(ToSqlOutput::Owned(Value::Text(
|
||||||
|
format!("PasswordReset {}", expiration.format("%Y-%m-%d %H:%M:%S")),
|
||||||
|
))),
|
||||||
|
AccountState::Locked => Ok(ToSqlOutput::Borrowed(ValueRef::Text("Locked".as_bytes()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub enabled: bool,
|
pub state: AccountState,
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserRow> for User {
|
|
||||||
fn from(row: UserRow) -> Self {
|
|
||||||
Self {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name.to_owned(),
|
|
||||||
password: row.password.to_owned(),
|
|
||||||
admin: row.admin,
|
|
||||||
enabled: row.enabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
@ -112,7 +145,8 @@ pub struct Player {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
pub id: String,
|
pub id: GameId,
|
||||||
|
pub type_: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub gm: UserId,
|
pub gm: UserId,
|
||||||
pub players: Vec<UserId>,
|
pub players: Vec<UserId>,
|
||||||
@ -133,25 +167,26 @@ pub enum Message {
|
|||||||
UpdateTabletop(Tabletop),
|
UpdateTabletop(Tabletop),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Clone, Debug)]
|
||||||
#[typeshare]
|
pub struct UserOverview {
|
||||||
pub struct UserProfile {
|
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub games: Vec<GameOverview>,
|
|
||||||
pub is_admin: bool,
|
pub is_admin: bool,
|
||||||
|
pub state: AccountState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct GameOverview {
|
pub struct GameOverview {
|
||||||
pub id: GameId,
|
pub id: GameId,
|
||||||
pub game_type: String,
|
pub type_: String,
|
||||||
pub game_name: String,
|
pub name: String,
|
||||||
pub gm: UserId,
|
pub gm: UserId,
|
||||||
pub players: Vec<UserId>,
|
pub players: Vec<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
impl From<GameRow> for GameOverview {
|
impl From<GameRow> for GameOverview {
|
||||||
fn from(row: GameRow) -> Self {
|
fn from(row: GameRow) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -163,3 +198,4 @@ impl From<GameRow> for GameOverview {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
23
visions-prototype/ui/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
14
visions-prototype/ui/Taskfile.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
dev:
|
||||||
|
cmds:
|
||||||
|
- cd ../visions-types && task build
|
||||||
|
- npm install
|
||||||
|
- npm run start
|
||||||
|
|
||||||
|
test:
|
||||||
|
cmds:
|
||||||
|
- cd ../visions-types && task build
|
||||||
|
- npm install
|
||||||
|
- npm run test
|
51
visions-prototype/ui/package.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.2.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/node": "^16.18.119",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@types/react-router": "^5.1.20",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router": "^6.28.0",
|
||||||
|
"react-router-dom": "^6.28.0",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"react-use-websocket": "^4.11.1",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"visions-types": "../visions-types",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
@ -3,11 +3,11 @@ import './App.css'
|
|||||||
import { Client } from './client'
|
import { Client } from './client'
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||||
import { DesignPage } from './views/Design/Design'
|
import { DesignPage } from './views/Design/Design'
|
||||||
import { Admin } from './views/Admin/Admin'
|
|
||||||
import Candela from './plugins/Candela'
|
import Candela from './plugins/Candela'
|
||||||
import { Authentication } from './views/Authentication/Authentication'
|
import { Authentication } from './views/Authentication/Authentication'
|
||||||
import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'
|
import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'
|
||||||
import { MainView } from './views'
|
import { MainView } from './views'
|
||||||
|
import { AdminView } from './views/Admin/Admin'
|
||||||
|
|
||||||
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"
|
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ interface AppProps {
|
|||||||
client: Client
|
client: Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
const CandelaCharsheet = ({ client }: { client: Client }) => {
|
const CandelaCharsheet = ({ client }: { client: Client }) => {
|
||||||
let [sheet, setSheet] = useState(undefined)
|
let [sheet, setSheet] = useState(undefined)
|
||||||
useEffect(
|
useEffect(
|
||||||
@ -24,21 +25,7 @@ const CandelaCharsheet = ({ client }: { client: Client }) => {
|
|||||||
|
|
||||||
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
|
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
interface AuthedViewProps {
|
|
||||||
client: Client
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
|
|
||||||
const [state, manager] = useContext(StateContext)
|
|
||||||
return (
|
|
||||||
<Authentication onAdminPassword={(password) => {
|
|
||||||
manager.setAdminPassword(password)
|
|
||||||
}} onAuth={(username, password) => manager.auth(username, password)}>
|
|
||||||
{children}
|
|
||||||
</Authentication>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const App = ({ client }: AppProps) => {
|
const App = ({ client }: AppProps) => {
|
||||||
console.log("rendering app")
|
console.log("rendering app")
|
||||||
@ -62,12 +49,14 @@ const App = ({ client }: AppProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
element: <Admin client={client} />
|
element: <AdminView client={client} />
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
{
|
{
|
||||||
path: "/candela",
|
path: "/candela",
|
||||||
element: <CandelaCharsheet client={client} />
|
element: <CandelaCharsheet client={client} />
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
{
|
{
|
||||||
path: "/design",
|
path: "/design",
|
||||||
element: <DesignPage />
|
element: <DesignPage />
|
149
visions-prototype/ui/src/client.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { AuthResponse, SessionId, UserId, UserOverview } from "visions-types";
|
||||||
|
|
||||||
|
export type PlayingField = {
|
||||||
|
backgroundImage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerResponse<A> = { type: "Unauthorized" } | { type: "Unexpected", status: number } | A;
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
users: (sessionId: SessionId) => Promise<Array<UserOverview>>;
|
||||||
|
createUser: (sessionId: SessionId, username: string) => Promise<UserId>;
|
||||||
|
auth: (username: string, password: string) => Promise<ServerResponse<AuthResponse>>;
|
||||||
|
setPassword: (sessionId: SessionId, password_1: string, password_2: string) => Promise<void>;
|
||||||
|
logout: (sessionId: SessionId) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Connection implements Client {
|
||||||
|
private base: URL;
|
||||||
|
private sessionId: string | undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.base = new URL("http://localhost:8001");
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWebsocket() {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `api/v1/client`;
|
||||||
|
return fetch(url, { method: 'POST' }).then((response) => response.json()).then((ws) => ws.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
unregisterWebsocket() {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `api/v1/client`;
|
||||||
|
return fetch(url, { method: 'POST' }).then((response => response.json()));
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
imageUrl(imageId: string) {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/image/${imageId}`;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async playingField(): Promise<PlayingField> {
|
||||||
|
return { backgroundImage: "trans-ferris.jpg" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async availableImages(): Promise<string[]> {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/image`;
|
||||||
|
return fetch(url).then((response) => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBackgroundImage(name: string) {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/tabletop/bg_image`;
|
||||||
|
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async users(sessionId: string): Promise<Array<UserOverview>> {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = '/api/v1/users';
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: [['Authorization', `Bearer ${sessionId}`]]
|
||||||
|
}).then((response) => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async charsheet(id: string) {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/charsheet/${id}`;
|
||||||
|
return fetch(url).then((response) => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(sessionId: string, username: string): Promise<UserId> {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = '/api/v1/user';
|
||||||
|
const response: Response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: [['Authorization', `Bearer ${sessionId}`],
|
||||||
|
['Content-Type', 'application/json']],
|
||||||
|
body: JSON.stringify({ username }),
|
||||||
|
});
|
||||||
|
const userId: UserId = await response.json();
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(sessionId: string, password_1: string, password_2: string) {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/user/password`;
|
||||||
|
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'PUT', headers: [['Authorization', `Bearer ${sessionId}`], ['Content-Type', 'application/json']], body: JSON.stringify({
|
||||||
|
password_1, password_2,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/auth`
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: [['Content-Type', 'application/json']],
|
||||||
|
body: JSON.stringify({ 'username': username, 'password': password })
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
} else if (response.status == 401) {
|
||||||
|
return await response.json().then(() => ({ type: "Unauthorized" }));
|
||||||
|
} else {
|
||||||
|
return await response.json().then(() => ({ type: "Unexpected", status: response.status }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(sessionId: string) {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/auth`
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: [['Authorization', `Bearer ${sessionId}`]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async profile(sessionId: SessionId, userId: UserId | undefined): Promise<UserOverview | undefined> {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
if (userId) {
|
||||||
|
url.pathname = `/api/v1/user${userId}`
|
||||||
|
} else {
|
||||||
|
url.pathname = `/api/v1/user`
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: [['Authorization', `Bearer ${sessionId}`]],
|
||||||
|
});
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async health() {
|
||||||
|
const url = new URL(this.base);
|
||||||
|
url.pathname = `/api/v1/health`;
|
||||||
|
return fetch(url).then((response) => response.json()).then((response) => {
|
||||||
|
console.log("health response: ", response);
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
visions-prototype/ui/src/components/Card/Card.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.card {
|
||||||
|
border: var(--border-standard);
|
||||||
|
border-radius: var(--border-radius-standard);
|
||||||
|
box-shadow: var(--border-shadow-shallow);
|
||||||
|
padding: var(--padding-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
16
visions-prototype/ui/src/components/Card/Card.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
import './Card.css';
|
||||||
|
|
||||||
|
interface CardElementProps {
|
||||||
|
name?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardElement = ({ name, children }: PropsWithChildren<CardElementProps>) => (
|
||||||
|
<div className="card">
|
||||||
|
{name && <h1 className="card__title">{name}</h1> }
|
||||||
|
<div className="card__body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,12 @@
|
|||||||
|
import { GameOverview } from "visions-types"
|
||||||
|
import { CardElement } from '../Card/Card';
|
||||||
|
|
||||||
|
|
||||||
|
export const GameOverviewElement = ({ name, gm, players }: GameOverview) => {
|
||||||
|
return (<CardElement name={name}>
|
||||||
|
<p><i>GM</i> {gm}</p>
|
||||||
|
<ul>
|
||||||
|
{players.map((player) => player)}
|
||||||
|
</ul>
|
||||||
|
</CardElement>)
|
||||||
|
}
|
24
visions-prototype/ui/src/components/Modal/Modal.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.modal {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(128, 128, 128, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal_window {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal_title {
|
||||||
|
background-color: skyblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal_cta {
|
||||||
|
margin-top: var(--margin-s);
|
||||||
|
}
|
33
visions-prototype/ui/src/components/Modal/Modal.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
import "./Modal.css";
|
||||||
|
|
||||||
|
export type Action = {
|
||||||
|
name: string
|
||||||
|
action: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
title: string
|
||||||
|
|
||||||
|
onCancel: Action
|
||||||
|
onPrimary: Action
|
||||||
|
onSecondary?: Action
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal = ({ title, children, onCancel, onPrimary, onSecondary }: PropsWithChildren<ModalProps>) => (
|
||||||
|
<div className="modal">
|
||||||
|
<div className="modal_window">
|
||||||
|
<h1 className="modal_title"> {title} </h1>
|
||||||
|
<div className="modal_body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<footer className="modal_cta">
|
||||||
|
<button onClick={() => onCancel.action()}>{onCancel.name}</button>
|
||||||
|
{onSecondary ? <button onClick={() => onSecondary.action()}>{onSecondary.name}</button> : <></>}
|
||||||
|
<button onClick={() => onPrimary.action()}>{onPrimary.name}</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
|
4
visions-prototype/ui/src/components/Profile/Profile.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.profile {
|
||||||
|
margin: var(--margin-s);
|
||||||
|
}
|
||||||
|
|
21
visions-prototype/ui/src/components/Profile/Profile.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { GameOverview, UserOverview } from 'visions-types';
|
||||||
|
import { CardElement, GameOverviewElement, UserManagementElement } from '..';
|
||||||
|
import './Profile.css';
|
||||||
|
|
||||||
|
interface ProfileProps {
|
||||||
|
profile: UserOverview,
|
||||||
|
games: GameOverview[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileElement = ({ profile, games }: ProfileProps) => {
|
||||||
|
const adminNote = profile.isAdmin ? <div> <i>Note: this user is an admin</i> </div> : <></>;
|
||||||
|
|
||||||
|
return (<div className="profile profile_columns">
|
||||||
|
<CardElement name={profile.name}>
|
||||||
|
<div>Games: {games.map((game) => {
|
||||||
|
return <span key={game.id}>{game.name} ({game.type})</span>;
|
||||||
|
}) }</div>
|
||||||
|
{adminNote}
|
||||||
|
</CardElement>
|
||||||
|
</div>)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { PropsWithChildren, useState } from "react"
|
||||||
|
import { AccountState, UserOverview } from "visions-types"
|
||||||
|
import { CardElement} from ".."
|
||||||
|
|
||||||
|
interface UserManagementProps {
|
||||||
|
users: UserOverview[]
|
||||||
|
onShowCreateUser: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserManagementElement = ({ users, onShowCreateUser }: UserManagementProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardElement name="Users">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((user) => <tr key={user.id}>
|
||||||
|
<td> {user.name} </td>
|
||||||
|
<td> {formatAccountState(user.state)} </td>
|
||||||
|
</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button onClick={() => onShowCreateUser()}>Create User</button>
|
||||||
|
</CardElement>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatAccountState = (state: AccountState): string => {
|
||||||
|
switch (state.type) {
|
||||||
|
case "Normal": return "";
|
||||||
|
case "PasswordReset": return "password reset";
|
||||||
|
case "Locked": return "locked";
|
||||||
|
}
|
||||||
|
}
|
10
visions-prototype/ui/src/components/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { CardElement } from './Card/Card'
|
||||||
|
import { GameOverviewElement } from './GameOverview/GameOverview'
|
||||||
|
import { Modal, ModalProps } from './Modal/Modal'
|
||||||
|
import { ProfileElement } from './Profile/Profile'
|
||||||
|
import { SimpleGuage } from './Guages/SimpleGuage'
|
||||||
|
import { ThumbnailElement } from './Thumbnail/Thumbnail'
|
||||||
|
import { TabletopElement } from './Tabletop/Tabletop'
|
||||||
|
import { UserManagementElement } from './UserManagement/UserManagement'
|
||||||
|
|
||||||
|
export { CardElement, GameOverviewElement, Modal, type ModalProps, UserManagementElement, ProfileElement, ThumbnailElement, TabletopElement, SimpleGuage }
|
@ -6,10 +6,3 @@
|
|||||||
--margin-s: 4px;
|
--margin-s: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
border: var(--border-standard);
|
|
||||||
border-radius: var(--border-radius-standard);
|
|
||||||
box-shadow: var(--border-shadow-shallow);
|
|
||||||
padding: var(--padding-m);
|
|
||||||
}
|
|
||||||
|
|
@ -3,9 +3,9 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import { Client } from './client';
|
import { Connection } from './client';
|
||||||
|
|
||||||
const client = new Client();
|
const client = new Connection();
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,52 @@
|
|||||||
|
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { UserOverview, AuthResponse, SessionId, UserId } from "visions-types";
|
||||||
|
import { Client, ServerResponse } from "../../client";
|
||||||
|
import { StateProvider } from "./StateProvider";
|
||||||
|
import { AuthedView, Authentication } from '../../views/Authentication/Authentication';
|
||||||
|
|
||||||
|
class MockClient implements Client {
|
||||||
|
validUsers: {[username: string]: string};
|
||||||
|
constructor() {
|
||||||
|
this.validUsers = { "vakarian": "aoeu", "shephard": "rubbish" }
|
||||||
|
}
|
||||||
|
|
||||||
|
async users(sessionId: SessionId): Promise<UserOverview[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(sessionId: SessionId, username: string): Promise<UserId> {
|
||||||
|
return "abcdefg";
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(sessionId: SessionId, password_1: string, password_2: string): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async auth(username: string, password: string): Promise<ServerResponse<AuthResponse>> {
|
||||||
|
if (this.validUsers[username] === password) {
|
||||||
|
return { type: "Success", content: "session-id" };
|
||||||
|
} else {
|
||||||
|
return { type: "Unauthorized" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(sessionId: SessionId): Promise<void> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a user is able to authenticate', async () => {
|
||||||
|
const client = new MockClient;
|
||||||
|
render(<StateProvider client={client}>
|
||||||
|
<AuthedView>
|
||||||
|
<p>Hi, authentication complete</p>
|
||||||
|
</AuthedView>
|
||||||
|
</StateProvider>);
|
||||||
|
expect(screen.getByText(/Welcome to Visions VTT/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("Username"), { target: { value: "vakarian" } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("Password"), { target: { value: "aoeu" } });
|
||||||
|
fireEvent.click(screen.getByRole("button"));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText(/Hi, authentication complete/i)).toBeInTheDocument();
|
||||||
|
})
|
@ -0,0 +1,156 @@
|
|||||||
|
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
|
||||||
|
import { SessionId, Status, Tabletop } from "visions-types";
|
||||||
|
import { Client } from "../../client";
|
||||||
|
import { assertNever } from "../../utils";
|
||||||
|
|
||||||
|
type AuthState = { type: "Unauthed" } | { type: "Authed", sessionId: string } | { type: "PasswordReset", sessionId: string };
|
||||||
|
|
||||||
|
export enum LoadingState {
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppState = {
|
||||||
|
state: LoadingState,
|
||||||
|
auth: AuthState,
|
||||||
|
tabletop: Tabletop,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action = { type: "SetAuthState", content: AuthState };
|
||||||
|
|
||||||
|
const initialState = (): AppState => {
|
||||||
|
let state: AppState = {
|
||||||
|
state: LoadingState.Ready,
|
||||||
|
auth: { type: "Unauthed" },
|
||||||
|
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined },
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = window.localStorage.getItem("sessionId")
|
||||||
|
if (sessionId) {
|
||||||
|
return { ...state, auth: { type: "Authed", sessionId } }
|
||||||
|
} else {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateReducer = (state: AppState, action: Action): AppState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "SetAuthState": {
|
||||||
|
console.log("setReducer: ", action);
|
||||||
|
return { ...state, auth: action.content }
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
default: {
|
||||||
|
assertNever(action)
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authState = (state: AppState): AuthState => state.auth
|
||||||
|
|
||||||
|
export const getSessionId = (state: AppState): SessionId | undefined => {
|
||||||
|
switch (state.auth.type) {
|
||||||
|
case "Unauthed": return undefined
|
||||||
|
case "Authed": return state.auth.sessionId
|
||||||
|
case "PasswordReset": return state.auth.sessionId
|
||||||
|
default: {
|
||||||
|
assertNever(state.auth)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateManagerInterface {
|
||||||
|
setPassword: (password1: string, password2: string) => void;
|
||||||
|
auth: (username: string, password: string) => void;
|
||||||
|
logout: () => void;
|
||||||
|
createUser: (username: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullManager implements StateManagerInterface {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async setPassword(_password1: string, _password2: string) { }
|
||||||
|
|
||||||
|
async auth(_username: string, _password: string) { }
|
||||||
|
|
||||||
|
async logout() { }
|
||||||
|
|
||||||
|
async createUser(_username: string) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateManager implements StateManagerInterface{
|
||||||
|
client: Client;
|
||||||
|
state: AppState;
|
||||||
|
dispatch: React.Dispatch<Action>;
|
||||||
|
|
||||||
|
constructor(client: Client, state: AppState, dispatch: React.Dispatch<any>) {
|
||||||
|
this.client = client;
|
||||||
|
this.state = state;
|
||||||
|
this.dispatch = dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setPassword(password1: string, password2: string) {
|
||||||
|
let sessionId = getSessionId(this.state);
|
||||||
|
console.log(`StateManager.setPassword: ${sessionId}`);
|
||||||
|
if (sessionId) {
|
||||||
|
await this.client.setPassword(sessionId, password1, password2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async auth(username: string, password: string) {
|
||||||
|
let authResponse = await this.client.auth(username, password);
|
||||||
|
switch (authResponse.type) {
|
||||||
|
case "Unauthorized": break;
|
||||||
|
case "Unexpected": break;
|
||||||
|
case "Success": {
|
||||||
|
window.localStorage.setItem("sessionId", authResponse.content);
|
||||||
|
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId: authResponse.content } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "PasswordReset": {
|
||||||
|
window.localStorage.setItem("sessionId", authResponse.content);
|
||||||
|
this.dispatch({ type: "SetAuthState", content: { type: "PasswordReset", sessionId: authResponse.content } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Locked": {
|
||||||
|
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout() {
|
||||||
|
const sessionId = getSessionId(this.state);
|
||||||
|
if (sessionId) {
|
||||||
|
await this.client.logout(sessionId);
|
||||||
|
window.localStorage.removeItem("sessionId");
|
||||||
|
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(username: string) {
|
||||||
|
console.log("order to createUser", username);
|
||||||
|
const sessionId = getSessionId(this.state);
|
||||||
|
if (sessionId) {
|
||||||
|
let createUserResponse = await this.client.createUser(sessionId, username);
|
||||||
|
console.log("createUser: ", createUserResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StateContext = createContext<[AppState, StateManagerInterface]>([initialState(), new NullManager()]);
|
||||||
|
|
||||||
|
interface StateProviderProps { client: Client; }
|
||||||
|
|
||||||
|
export const StateProvider = ({ client, children }: PropsWithChildren<StateProviderProps>) => {
|
||||||
|
const [state, dispatch] = useReducer(stateReducer, initialState());
|
||||||
|
|
||||||
|
const stateManager = useRef(new StateManager(client, state, dispatch));
|
||||||
|
|
||||||
|
return <StateContext.Provider value={[state, stateManager.current]}>
|
||||||
|
{children}
|
||||||
|
</StateContext.Provider>;
|
||||||
|
}
|
@ -7,10 +7,27 @@ interface UserRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserRow = ({ user }: UserRowProps) => {
|
const UserRow = ({ user }: UserRowProps) => {
|
||||||
|
let accountState = "Normal";
|
||||||
|
|
||||||
|
switch (user.state.type) {
|
||||||
|
case "Normal": {
|
||||||
|
accountState = "Normal";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "PasswordReset": {
|
||||||
|
accountState = `PasswordReset until ${user.state.content}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Locked": {
|
||||||
|
accountState = "Locked";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (<tr>
|
return (<tr>
|
||||||
<td> {user.name} </td>
|
<td> {user.name} </td>
|
||||||
<td> {user.admin && "admin"} </td>
|
<td> {user.admin && "admin"} </td>
|
||||||
<td> {user.enabled && "enabled"} </td>
|
<td> {accountState} </td>
|
||||||
</tr>);
|
</tr>);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,17 +45,17 @@ interface AdminProps {
|
|||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Admin = ({ client }: AdminProps) => {
|
export const AdminView = ({ client }: AdminProps) => {
|
||||||
const [users, setUsers] = useState<Array<User>>([]);
|
const [users, setUsers] = useState<Array<User>>([]);
|
||||||
|
|
||||||
|
/*
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.users().then((u) => {
|
client.users("aoeu").then((u) => {
|
||||||
console.log(u);
|
console.log(u);
|
||||||
setUsers(u);
|
setUsers(u);
|
||||||
});
|
});
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
*/
|
||||||
console.log(users);
|
|
||||||
return (<table>
|
return (<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{users.map((user) => <UserRow user={user} />)}
|
{users.map((user) => <UserRow user={user} />)}
|
@ -4,17 +4,19 @@ import { assertNever } from '../../utils';
|
|||||||
import './Authentication.css';
|
import './Authentication.css';
|
||||||
|
|
||||||
interface AuthenticationProps {
|
interface AuthenticationProps {
|
||||||
onAdminPassword: (password: string) => void;
|
onSetPassword: (password1: string, password2: string) => void;
|
||||||
onAuth: (username: string, password: string) => void;
|
onAuth: (username: string, password: string) => void;
|
||||||
|
onLogout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithChildren<AuthenticationProps>) => {
|
export const Authentication = ({ onSetPassword, onAuth, onLogout, children }: PropsWithChildren<AuthenticationProps>) => {
|
||||||
// No admin password set: prompt for the admin password
|
// No admin password set: prompt for the admin password
|
||||||
// Password set, nobody logged in: prompt for login
|
// Password set, nobody logged in: prompt for login
|
||||||
// User logged in: show the children
|
// User logged in: show the children
|
||||||
|
|
||||||
let [userField, setUserField] = useState<string>("");
|
let [userField, setUserField] = useState<string>("");
|
||||||
let [pwField, setPwField] = useState<string>("");
|
let [pwField1, setPwField1] = useState<string>("");
|
||||||
|
let [pwField2, setPwField2] = useState<string>("");
|
||||||
let [state, _] = useContext(StateContext);
|
let [state, _] = useContext(StateContext);
|
||||||
|
|
||||||
switch (state.state) {
|
switch (state.state) {
|
||||||
@ -23,30 +25,36 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC
|
|||||||
}
|
}
|
||||||
case LoadingState.Ready: {
|
case LoadingState.Ready: {
|
||||||
switch (state.auth.type) {
|
switch (state.auth.type) {
|
||||||
case "NoAdmin": {
|
|
||||||
return <div className="auth">
|
|
||||||
<div className="card">
|
|
||||||
<h1> Welcome to your new Visions VTT Instance </h1>
|
|
||||||
<p> Set your admin password: </p>
|
|
||||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
|
||||||
<input type="submit" value="Submit" onClick={() => onAdminPassword(pwField)} />
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
case "Unauthed": {
|
case "Unauthed": {
|
||||||
return <div className="auth card">
|
return <div className="auth card">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h1> Welcome to Visions VTT </h1>
|
<h1> Welcome to Visions VTT </h1>
|
||||||
<div className="auth__input-line">
|
<div className="auth__input-line">
|
||||||
<input type="text" placeholder="Username" onChange={(evt) => setUserField(evt.target.value)} />
|
<input type="text" placeholder="Username" onChange={(evt) => setUserField(evt.target.value)} />
|
||||||
<input type="password" placeholder="Password" onChange={(evt) => setPwField(evt.target.value)} />
|
<input type="password" placeholder="Password" onChange={(evt) => setPwField1(evt.target.value)} />
|
||||||
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField)} />
|
<input type="submit" value="Sign in" onClick={() => onAuth(userField, pwField1)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
case "Authed": {
|
case "Authed": {
|
||||||
return <div> {children} </div>;
|
return (<div>
|
||||||
|
<div> <button onClick={onLogout}>Logout</button> </div>
|
||||||
|
<div> {children} </div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
case "PasswordReset": {
|
||||||
|
return <div className="auth">
|
||||||
|
<div className="card">
|
||||||
|
<h1> Password Reset </h1>
|
||||||
|
<p> Your password currently requires a reset. </p>
|
||||||
|
<input type="password" placeholder="Password" onChange={(evt) => setPwField1(evt.target.value)} />
|
||||||
|
<input type="password" placeholder="Retype your Password" onChange={(evt) => setPwField2(evt.target.value)} />
|
||||||
|
<input type="submit" value="Submit" onClick={() => {
|
||||||
|
onSetPassword(pwField1, pwField2);
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
assertNever(state.auth);
|
assertNever(state.auth);
|
||||||
@ -55,5 +63,20 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AuthedViewProps {}
|
||||||
|
|
||||||
|
export const AuthedView = ({ children }: PropsWithChildren<AuthedViewProps>) => {
|
||||||
|
const [_, manager] = useContext(StateContext)
|
||||||
|
return (
|
||||||
|
<Authentication onSetPassword={(password1, password2) => {
|
||||||
|
manager.setPassword(password1, password2)
|
||||||
|
}} onAuth={(username, password) => manager.auth(username, password)}
|
||||||
|
onLogout={() => manager.logout()}>
|
||||||
|
{children}
|
||||||
|
</Authentication>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
0
visions-prototype/ui/src/views/Main/Main.css
Normal file
@ -1,8 +1,9 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { UserProfile } from 'visions-types';
|
import { UserOverview } from 'visions-types';
|
||||||
import { Client } from '../../client';
|
import { Client } from '../../client';
|
||||||
import { ProfileElement } from '../../components';
|
import { ProfileElement } from '../../components';
|
||||||
import { getSessionId, StateContext, StateProvider } from '../../providers/StateProvider/StateProvider';
|
import { getSessionId, StateContext, StateProvider } from '../../providers/StateProvider/StateProvider';
|
||||||
|
import { ProfileView } from '../Profile/Profile';
|
||||||
|
|
||||||
interface MainProps {
|
interface MainProps {
|
||||||
client: Client
|
client: Client
|
||||||
@ -10,19 +11,25 @@ interface MainProps {
|
|||||||
|
|
||||||
export const MainView = ({ client }: MainProps) => {
|
export const MainView = ({ client }: MainProps) => {
|
||||||
const [state, _manager] = useContext(StateContext)
|
const [state, _manager] = useContext(StateContext)
|
||||||
const [profile, setProfile] = useState<UserProfile | undefined>(undefined)
|
const [profile, setProfile] = useState<UserOverview | undefined>(undefined)
|
||||||
|
const [users, setUsers] = useState<UserOverview[]>([])
|
||||||
|
|
||||||
const sessionId = getSessionId(state)
|
const sessionId = getSessionId(state)
|
||||||
|
/*
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
client.profile(sessionId, undefined).then((profile) => setProfile(profile))
|
client.profile(sessionId, undefined).then((profile) => setProfile(profile))
|
||||||
|
client.users(sessionId).then((users) => {
|
||||||
|
console.log(users);
|
||||||
|
setUsers(users);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [sessionId, client])
|
}, [sessionId, client])
|
||||||
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Session ID: {sessionId}</div>
|
{profile && <ProfileView profile={profile} users={users} games={[]} />}
|
||||||
{profile && <ProfileElement {...profile} />}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
9
visions-prototype/ui/src/views/Profile/Profile.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.profile-view_columns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-view_columns > div {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
45
visions-prototype/ui/src/views/Profile/Profile.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useContext, useState } from "react"
|
||||||
|
import { GameOverview, UserOverview } from "visions-types"
|
||||||
|
import { Modal, ModalProps, ProfileElement, UserManagementElement } from "../../components"
|
||||||
|
import { StateContext } from "../../providers/StateProvider/StateProvider"
|
||||||
|
import "./Profile.css"
|
||||||
|
|
||||||
|
interface ProfileProps {
|
||||||
|
profile: UserOverview,
|
||||||
|
users: UserOverview[],
|
||||||
|
games: GameOverview[],
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateUserModalProps {
|
||||||
|
onCancel: () => void
|
||||||
|
onCreateUser: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateUserModal = ({ onCancel, onCreateUser }: CreateUserModalProps) => {
|
||||||
|
const [userName, setUserName] = useState("");
|
||||||
|
|
||||||
|
return <Modal title="Create User" onCancel={{ name: "Cancel", action: onCancel }}
|
||||||
|
onPrimary={{ name: "Create User", action: () => onCreateUser(userName) }}>
|
||||||
|
<input type="text" placeholder="username" onChange={(evt) => {
|
||||||
|
setUserName(evt.target.value);
|
||||||
|
}} />
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileView = ({ profile, users, games, }: ProfileProps) => {
|
||||||
|
const [_state, manager] = useContext(StateContext)
|
||||||
|
const [showUser, setShowUser] = useState(false)
|
||||||
|
|
||||||
|
const userList = profile.isAdmin && <UserManagementElement users={users} onShowCreateUser={() => setShowUser(true)} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="profile-view">
|
||||||
|
{showUser && <CreateUserModal onCancel={() => setShowUser(false)} onCreateUser={(username) => manager.createUser(username)} />}
|
||||||
|
<div className="profile-view_columns">
|
||||||
|
<ProfileElement profile={profile} games={games} />
|
||||||
|
|
||||||
|
{userList}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
4
visions-prototype/ui/src/views/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { MainView } from './Main/Main'
|
||||||
|
import { ProfileView } from './Profile/Profile'
|
||||||
|
|
||||||
|
export { MainView, ProfileView }
|
27
visions-prototype/ui/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
"gen"
|
||||||
|
]
|
||||||
|
}
|
@ -9,13 +9,13 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
@ -9,6 +9,6 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,34 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "visions"
|
name = "server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = { version = "1.13.0" }
|
|
||||||
async-trait = { version = "0.1.83" }
|
|
||||||
authdb = { path = "../../authdb/" }
|
|
||||||
axum = { version = "0.7.9", features = [ "macros" ] }
|
|
||||||
futures = { version = "0.3.31" }
|
|
||||||
include_dir = { version = "0.7.4" }
|
|
||||||
lazy_static = { version = "1.5.0" }
|
|
||||||
mime = { version = "0.3.17" }
|
|
||||||
mime_guess = { version = "2.0.5" }
|
|
||||||
pretty_env_logger = { version = "0.5.0" }
|
|
||||||
result-extended = { path = "../../result-extended" }
|
|
||||||
rusqlite = { version = "0.32.1" }
|
|
||||||
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }
|
|
||||||
serde = { version = "1" }
|
|
||||||
serde_json = { version = "*" }
|
|
||||||
thiserror = { version = "2.0.3" }
|
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
|
||||||
tokio-stream = { version = "0.1.16" }
|
|
||||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
|
||||||
typeshare = { version = "1.0.4" }
|
|
||||||
urlencoding = { version = "2.1.3" }
|
|
||||||
uuid = { version = "1.11.0", features = ["v4"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
cool_asserts = "2.0.3"
|
|
||||||
axum-test = "16.4.1"
|
|
||||||
|
@ -3,7 +3,7 @@ version: '3'
|
|||||||
tasks:
|
tasks:
|
||||||
build:
|
build:
|
||||||
cmds:
|
cmds:
|
||||||
- cargo build
|
- cargo watch -x build
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cmds:
|
cmds:
|
||||||
|
@ -1,33 +1,3 @@
|
|||||||
use core::Core;
|
fn main() {
|
||||||
use std::path::PathBuf;
|
println!("Hello, world!");
|
||||||
|
|
||||||
use asset_db::FsAssets;
|
|
||||||
use database::DbConn;
|
|
||||||
|
|
||||||
mod asset_db;
|
|
||||||
mod core;
|
|
||||||
mod database;
|
|
||||||
mod handlers;
|
|
||||||
mod routes;
|
|
||||||
mod types;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
pub async fn main() {
|
|
||||||
/*
|
|
||||||
pretty_env_logger::init();
|
|
||||||
|
|
||||||
let unauthenticated_endpoints = route_healthcheck().or(route_authenticate(core.clone()));
|
|
||||||
let authenticated_endpoints = route_image(core.clone());
|
|
||||||
*/
|
|
||||||
|
|
||||||
let conn = DbConn::new(Some("/home/savanni/game.db"));
|
|
||||||
let core = Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
|
|
||||||
|
|
||||||
let app = routes::routes(core);
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:8001")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
}
|
||||||
|
8
visions/types/Taskfile.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
build:
|
||||||
|
cmds:
|
||||||
|
- npm install typescript
|
||||||
|
- typeshare --lang typescript --output-file visions.ts ../server/src
|
||||||
|
- npx tsc
|
14
visions/types/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "visions-types",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Shared data types for Visions",
|
||||||
|
"main": "visions.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
15
visions/types/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["./visions.ts"]
|
||||||
|
}
|
@ -7,3 +7,8 @@ tasks:
|
|||||||
- npm install
|
- npm install
|
||||||
- npm run start
|
- npm run start
|
||||||
|
|
||||||
|
test:
|
||||||
|
cmds:
|
||||||
|
- cd ../visions-types && task build
|
||||||
|
- npm install
|
||||||
|
- npm run test
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.119",
|
"@types/node": "^16.18.119",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
// render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
@ -1,102 +0,0 @@
|
|||||||
import { SessionId, UserId, UserProfile } from "visions-types";
|
|
||||||
|
|
||||||
export type PlayingField = {
|
|
||||||
backgroundImage: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Client {
|
|
||||||
private base: URL;
|
|
||||||
private sessionId: string | undefined;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.base = new URL("http://localhost:8001");
|
|
||||||
}
|
|
||||||
|
|
||||||
registerWebsocket() {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `api/v1/client`;
|
|
||||||
return fetch(url, { method: 'POST' }).then((response) => response.json()).then((ws) => ws.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
unregisterWebsocket() {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `api/v1/client`;
|
|
||||||
return fetch(url, { method: 'POST' }).then((response => response.json()));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
imageUrl(imageId: string) {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/image/${imageId}`;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
async playingField(): Promise<PlayingField> {
|
|
||||||
return { backgroundImage: "trans-ferris.jpg" };
|
|
||||||
}
|
|
||||||
|
|
||||||
async availableImages(): Promise<string[]> {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/image`;
|
|
||||||
return fetch(url).then((response) => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
async setBackgroundImage(name: string) {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/tabletop/bg_image`;
|
|
||||||
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) {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/charsheet/${id}`;
|
|
||||||
return fetch(url).then((response) => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAdminPassword(password: string) {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/admin_password`;
|
|
||||||
console.log("setting the admin password to: ", password);
|
|
||||||
return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(password) });
|
|
||||||
}
|
|
||||||
|
|
||||||
async auth(username: string, password: string): Promise<SessionId | undefined> {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/auth`
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: [['Content-Type', 'application/json']],
|
|
||||||
body: JSON.stringify({ 'username': username, 'password': password })
|
|
||||||
});
|
|
||||||
const session_id: SessionId = await response.json();
|
|
||||||
return session_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async profile(sessionId: SessionId, userId: UserId | undefined): Promise<UserProfile | undefined> {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
if (userId) {
|
|
||||||
url.pathname = `/api/v1/user${userId}`
|
|
||||||
} else {
|
|
||||||
url.pathname = `/api/v1/user`
|
|
||||||
}
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: [['Authorization', `Bearer ${sessionId}`]],
|
|
||||||
});
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
async health() {
|
|
||||||
const url = new URL(this.base);
|
|
||||||
url.pathname = `/api/v1/health`;
|
|
||||||
return fetch(url).then((response) => response.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { UserProfile } from 'visions-types';
|
|
||||||
|
|
||||||
export const ProfileElement = ({ name, games, is_admin }: UserProfile) => {
|
|
||||||
const adminNote = is_admin ? <div> <i>Note: this user is an admin</i> </div> : <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<h1>{name}</h1>
|
|
||||||
<div>Games: {games.map((game) => <>{game.game_name} ({game.game_type})</>).join(', ')}</div>
|
|
||||||
{adminNote}
|
|
||||||
</div>)
|
|
||||||
}
|
|