Expand out the profile to start including a list of characters and games

This commit is contained in:
Savanni D'Gerinel 2025-01-05 00:21:01 -05:00
parent a2cdaef689
commit f9e903da54
13 changed files with 124 additions and 20 deletions

View File

@ -136,6 +136,7 @@ impl Core {
ok(Some(UserProfile {
id: user.id,
name: user.name,
password: user.password,
games: user_games,
is_admin: user.admin,
}))

View File

@ -3,7 +3,7 @@ use axum::{
Json,
};
use futures::Future;
use result_extended::{error, ok, return_error, ResultExt};
use result_extended::{error, fatal, ok, return_error, ResultExt};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
@ -33,6 +33,12 @@ pub struct SetPasswordRequest {
pub password_2: String,
}
#[derive(Deserialize, Serialize)]
#[typeshare]
pub struct SetAdminPasswordRequest {
pub password: String,
}
async fn check_session(
core: &Core,
headers: HeaderMap,
@ -134,3 +140,14 @@ pub async fn set_password(
}
}).await
}
pub async fn set_admin_password(
core: Core,
req: String,
) -> ResultExt<(), AppError, FatalError> {
match return_error!(core.user(UserId::from("admin")).await) {
Some(admin) if admin.password.is_empty() => core.set_password(UserId::from("admin"), req).await,
Some(_) => error(AppError::PermissionDenied),
None => fatal(FatalError::DatabaseKeyMissing),
}
}

View File

@ -1,6 +1,9 @@
use axum::{
extract::Path,
http::{header::{AUTHORIZATION, CONTENT_TYPE}, HeaderMap, Method},
http::{
header::{AUTHORIZATION, CONTENT_TYPE},
HeaderMap, Method,
},
routing::{get, post, put},
Json, Router,
};
@ -10,8 +13,9 @@ use crate::{
core::Core,
database::UserId,
handlers::{
check_password, create_game, create_user, get_user, healthcheck, set_password,
wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest, SetPasswordRequest,
check_password, create_game, create_user, get_user, healthcheck, set_admin_password,
set_password, wrap_handler, AuthRequest, CreateGameRequest, CreateUserRequest,
SetAdminPasswordRequest, SetPasswordRequest,
},
};
@ -21,7 +25,11 @@ pub fn routes(core: Core) -> Router {
"/api/v1/health",
get({
let core = core.clone();
move || healthcheck(core)
move || async {
let result = healthcheck(core).await;
println!("status: {:?}", String::from_utf8(result.to_owned()));
result
}
})
.layer(
CorsLayer::new()
@ -29,6 +37,23 @@ pub fn routes(core: Core) -> Router {
.allow_origin(Any),
),
)
.route(
"/api/v1/admin_password",
put({
let core = core.clone();
move |req: Json<String>| {
let Json(req) = req;
println!("set admin password: {:?}", req);
wrap_handler(|| set_admin_password(core, req))
}
})
.layer(
CorsLayer::new()
.allow_methods([Method::PUT])
.allow_headers([CONTENT_TYPE])
.allow_origin(Any),
),
)
.route(
"/api/v1/auth",
post({

View File

@ -138,6 +138,7 @@ pub enum Message {
pub struct UserProfile {
pub id: UserId,
pub name: String,
pub password: String,
pub games: Vec<GameOverview>,
pub is_admin: bool,
}

View File

@ -96,7 +96,10 @@ export class Client {
async health() {
const url = new URL(this.base);
url.pathname = `/api/v1/health`;
return fetch(url).then((response) => response.json());
return fetch(url).then((response) => response.json()).then((response) => {
console.log("health response: ", response);
return response;
});
}
}

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

View 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>
)

View File

@ -0,0 +1,12 @@
import { GameOverview } from "visions-types"
import { CardElement } from '../Card/Card';
export const GameOverviewElement = ({ game_type, game_name, gm, players }: GameOverview) => {
return (<CardElement name={game_name}>
<p><i>GM</i> {gm}</p>
<ul>
{players.map((player) => player)}
</ul>
</CardElement>)
}

View File

@ -1,12 +1,31 @@
import { UserProfile } from 'visions-types';
import { CardElement } from '../Card/Card';
import { GameOverviewElement } from '../GameOverview/GameOverview';
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>
return (<div>
<CardElement name={name}>
<div>Games: {games.map((game) => {
return <span key={game.id}>{game.game_name} ({game.game_type})</span>;
}) }</div>
{adminNote}
</CardElement>
<div>
<CardElement>
<ul>
<li> Savanni </li>
<li> Shephard </li>
<li> Vakarian </li>
<li> vas Normandy </li>
</ul>
</CardElement>
<div>
{games.map((game) => <GameOverviewElement {...game} />)}
</div>
</div>
</div>)
}

View File

@ -1,6 +1,8 @@
import { CardElement } from './Card/Card'
import { GameOverviewElement } from './GameOverview/GameOverview'
import { ProfileElement } from './Profile/Profile'
import { SimpleGuage } from './Guages/SimpleGuage'
import { ThumbnailElement } from './Thumbnail/Thumbnail'
import { TabletopElement } from './Tabletop/Tabletop'
export { ProfileElement, ThumbnailElement, TabletopElement, SimpleGuage }
export { CardElement, ProfileElement, ThumbnailElement, TabletopElement, SimpleGuage }

View File

@ -6,10 +6,3 @@
--margin-s: 4px;
}
.card {
border: var(--border-standard);
border-radius: var(--border-radius-standard);
box-shadow: var(--border-shadow-shallow);
padding: var(--padding-m);
}

View File

@ -47,6 +47,8 @@ const stateReducer = (state: AppState, action: Action): AppState => {
}
}
export const authState = (state: AppState): AuthState => state.auth
export const getSessionId = (state: AppState): SessionId | undefined => {
switch (state.auth.type) {
case "NoAdmin": return undefined
@ -75,13 +77,18 @@ class StateManager {
if (!admin_enabled) {
this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } });
}
return admin_enabled;
}
async setAdminPassword(password: string) {
if (!this.client || !this.dispatch) return;
await this.client.setAdminPassword(password);
await this.status();
let admin_enabled = await this.status();
if (admin_enabled) {
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
}
}
async auth(username: string, password: string) {