Authenticate the user and populate AppState with the stored session ID

This commit is contained in:
Savanni D'Gerinel 2025-01-03 16:19:43 -05:00
parent 08462388ea
commit 208083d39e
13 changed files with 111 additions and 183 deletions

View File

@ -1,64 +1,64 @@
import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'; import React, { PropsWithChildren, useContext, useEffect, useState } from 'react'
import './App.css'; 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 { GmView } from './views/GmView/GmView'; import { Admin } from './views/Admin/Admin'
import { WebsocketProvider } from './components/WebsocketProvider'; import Candela from './plugins/Candela'
import { PlayerView } from './views/PlayerView/PlayerView'; import { Authentication } from './views/Authentication/Authentication'
import { Admin } from './views/Admin/Admin'; import { StateContext, StateProvider } from './providers/StateProvider/StateProvider'
import Candela from './plugins/Candela'; import { MainView } from './views'
import { Authentication } from './views/Authentication/Authentication';
import { StateContext, StateProvider } from './providers/StateProvider/StateProvider';
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"; const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803"
interface AppProps { 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(
() => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)); }, () => { client.charsheet(TEST_CHARSHEET_UUID).then((c) => setSheet(c)) },
[client, setSheet] [client, setSheet]
); )
return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div> return sheet ? <Candela.CharsheetElement sheet={sheet} /> : <div> </div>
} }
interface AuthedViewProps { interface AuthedViewProps {
client: Client; client: Client
} }
const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => { const AuthedView = ({ client, children }: PropsWithChildren<AuthedViewProps>) => {
const [state, manager] = useContext(StateContext); const [state, manager] = useContext(StateContext)
return ( return (
<Authentication onAdminPassword={(password) => { <Authentication onAdminPassword={(password) => {
manager.setAdminPassword(password); manager.setAdminPassword(password)
}} onAuth={(username, password) => manager.auth(username, password)}> }} onAuth={(username, password) => manager.auth(username, password)}>
{children} {children}
</Authentication> </Authentication>
); )
} }
const App = ({ client }: AppProps) => { const App = ({ client }: AppProps) => {
console.log("rendering app"); console.log("rendering app")
const [websocketUrl, setWebsocketUrl] = useState<string | undefined>(undefined); const [websocketUrl, setWebsocketUrl] = useState<string | undefined>(undefined)
// useEffect(() => { // useEffect(() => {
// client.registerWebsocket().then((url) => setWebsocketUrl(url)) // client.registerWebsocket().then((url) => setWebsocketUrl(url))
// }, [client]); // }, [client])
let router = let router =
createBrowserRouter([ createBrowserRouter([
{ {
path: "/", path: "/",
element: <StateProvider client={client}><AuthedView client={client}> <PlayerView client={client} /> </AuthedView> </StateProvider> element: (
}, <StateProvider client={client}>
{ <AuthedView client={client}>
path: "/gm", <MainView client={client} />
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div> </AuthedView>
</StateProvider>
)
}, },
{ {
path: "/admin", path: "/admin",
@ -72,12 +72,12 @@ const App = ({ client }: AppProps) => {
path: "/design", path: "/design",
element: <DesignPage /> element: <DesignPage />
} }
]); ])
return ( return (
<div className="App"> <div className="App">
<RouterProvider router={router} /> <RouterProvider router={router} />
</div> </div>
); )
} }
export default App; export default App

View File

@ -6,6 +6,7 @@ export type PlayingField = {
export class Client { export class Client {
private base: URL; private base: URL;
private sessionId: string | undefined;
constructor() { constructor() {
this.base = new URL("http://localhost:8001"); this.base = new URL("http://localhost:8001");
@ -69,7 +70,9 @@ export class Client {
async auth(username: string, password: string): Promise<SessionId | undefined> { async auth(username: string, password: string): Promise<SessionId | undefined> {
const url = new URL(this.base); const url = new URL(this.base);
url.pathname = `/api/v1/auth` url.pathname = `/api/v1/auth`
return fetch(url, { method: 'POST', headers: [['Content-Type', 'application/json']], body: JSON.stringify({ 'username': username, 'password': password }) }); 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 health() { async health() {

View File

@ -0,0 +1,10 @@
import React from 'react';
import { Client } from '../../client';
interface ProfileProps {
client: Client
}
export const ProfileElement = ({ client }: ProfileProps) => {
return <div></div>
}

View File

@ -1,51 +1,60 @@
import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react"; import React, { createContext, PropsWithChildren, useCallback, useEffect, useReducer, useRef } from "react";
import { Status, Tabletop } from "visions-types"; import { SessionId, Status, Tabletop } from "visions-types";
import { Client } from "../../client"; import { Client } from "../../client";
import { assertNever } from "../../utils"; import { assertNever } from "../../utils";
type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", userid: string }; type AuthState = { type: "NoAdmin" } | { type: "Unauthed" } | { type: "Authed", sessionId: string };
type LoadingState = { type: "Loading" } export enum LoadingState {
Loading,
Ready,
}
type ReadyState = { type AppState = {
type: "Ready", state: LoadingState,
auth: AuthState, auth: AuthState,
tabletop: Tabletop, tabletop: Tabletop,
} }
type AppState = LoadingState | ReadyState
type Action = { type: "SetAuthState", content: AuthState }; type Action = { type: "SetAuthState", content: AuthState };
/* const initialState = (): AppState => {
const initialState = (): AppState => ( let state: AppState = {
{ state: LoadingState.Ready,
auth: { type: "NoAdmin" }, auth: { type: "NoAdmin" },
tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined },
} }
);
*/
const initialState = (): AppState => ({ type: "Loading" })
const loadingReducer = (state: LoadingState, action: Action): AppState => { const sessionId = window.localStorage.getItem("sessionId")
return { type: "Ready", auth: action.content, tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } } if (sessionId) {
} return { ...state, auth: { type: "Authed", sessionId } }
} else {
const readyReducer = (state: ReadyState, action: Action): AppState => { return state
return { ...state, auth: action.content } }
} }
const stateReducer = (state: AppState, action: Action): AppState => { const stateReducer = (state: AppState, action: Action): AppState => {
switch (state.type) { switch (action.type) {
case "Loading": { case "SetAuthState": {
return loadingReducer(state, action); return { ...state, auth: action.content }
}
case "Ready": {
return readyReducer(state, action);
} }
/*
default: { default: {
assertNever(state); assertNever(action)
return { type: "Loading" }; return state
}
*/
}
}
export const getSessionId = (state: AppState): SessionId | undefined => {
switch (state.auth.type) {
case "NoAdmin": return undefined
case "Unauthed": return undefined
case "Authed": return state.auth.sessionId
default: {
assertNever(state.auth)
return undefined
} }
} }
} }
@ -65,8 +74,6 @@ class StateManager {
const { admin_enabled } = await this.client.health(); const { admin_enabled } = await this.client.health();
if (!admin_enabled) { if (!admin_enabled) {
this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } }); this.dispatch({ type: "SetAuthState", content: { type: "NoAdmin" } });
} else {
this.dispatch({ type: "SetAuthState", content: { type: "Unauthed" } });
} }
} }
@ -80,10 +87,11 @@ class StateManager {
async auth(username: string, password: string) { async auth(username: string, password: string) {
if (!this.client || !this.dispatch) return; if (!this.client || !this.dispatch) return;
let resp = await this.client.auth(username, password); let sessionId = await this.client.auth(username, password);
let sessionid = await resp.json(); if (sessionId) {
console.log("sessionid retrieved", sessionid); window.localStorage.setItem("sessionId", sessionId);
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionid } }); this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId } });
}
} }
} }

View File

@ -1,5 +1,5 @@
import { PropsWithChildren, useContext, useState } from 'react'; import { PropsWithChildren, useContext, useState } from 'react';
import { StateContext } from '../../providers/StateProvider/StateProvider'; import { LoadingState, StateContext } from '../../providers/StateProvider/StateProvider';
import { assertNever } from '../../utils'; import { assertNever } from '../../utils';
import './Authentication.css'; import './Authentication.css';
@ -17,11 +17,11 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC
let [pwField, setPwField] = useState<string>(""); let [pwField, setPwField] = useState<string>("");
let [state, _] = useContext(StateContext); let [state, _] = useContext(StateContext);
switch (state.type) { switch (state.state) {
case "Loading": { case LoadingState.Loading: {
return <div>Loading</div> return <div>Loading</div>
} }
case "Ready": { case LoadingState.Ready: {
switch (state.auth.type) { switch (state.auth.type) {
case "NoAdmin": { case "NoAdmin": {
return <div className="auth"> return <div className="auth">
@ -53,11 +53,6 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC
return <div></div>; return <div></div>;
} }
} }
}
default: {
assertNever(state);
return <div></div>
} }
} }

View File

@ -1,4 +0,0 @@
.gm-view {
display: flex;
width: 100%;
}

View File

@ -1,39 +0,0 @@
import { useContext, useEffect, useState } from 'react';
import { Client } from '../../client';
import { StateContext } from '../../providers/StateProvider/StateProvider';
import { TabletopElement } from '../../components/Tabletop/Tabletop';
import { ThumbnailElement } from '../../components/Thumbnail/Thumbnail';
import { WebsocketContext } from '../../components/WebsocketProvider';
import './GmView.css';
import { assertNever } from '../../utils';
interface GmViewProps {
client: Client
}
export const GmView = ({ client }: GmViewProps) => {
const [state, dispatch] = useContext(StateContext);
const [images, setImages] = useState<string[]>([]);
useEffect(() => {
client.availableImages().then((images) => setImages(images));
}, [client]);
switch (state.type) {
case "Loading": return <div></div>;
case "Ready": {
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
return (<div className="gm-view">
<div>
{images.map((imageName) => <ThumbnailElement id={imageName} url={client.imageUrl(imageName)} onclick={() => { client.setBackgroundImage(imageName); }} />)}
</div>
<TabletopElement backgroundColor={state.tabletop.backgroundColor} backgroundUrl={backgroundUrl} />
</div>)
}
default: {
assertNever(state);
return <div></div>;
}
}
}

View File

View File

@ -0,0 +1,16 @@
import React, { useContext } from 'react';
import { Client } from '../../client';
import { getSessionId, StateContext, StateProvider } from '../../providers/StateProvider/StateProvider';
interface MainProps {
client: Client
}
export const MainView = ({ client }: MainProps) => {
const [state, manager] = useContext(StateContext)
const sessionId = getSessionId(state);
return <div>Profile: {sessionId}</div>
}

View File

@ -1,15 +0,0 @@
.player-view {
display: flex;
width: 100%;
}
.player-view__left-panel {
min-width: 100px;
max-width: 20%;
}
.player-view__right-panel {
width: 25%;
}

View File

@ -1,49 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import './PlayerView.css';
import { WebsocketContext } from '../../components/WebsocketProvider';
import { Client } from '../../client';
import { TabletopElement } from '../../components/Tabletop/Tabletop';
import Candela from '../../plugins/Candela';
import { StateContext } from '../../providers/StateProvider/StateProvider';
import { assertNever } from '../../utils';
const TEST_CHARSHEET_UUID = "12df9c09-1f2f-4147-8eda-a97bd2a7a803";
interface PlayerViewProps {
client: Client;
}
export const PlayerView = ({ client }: PlayerViewProps) => {
const [state, dispatch] = useContext(StateContext);
const [charsheet, setCharsheet] = useState(undefined);
useEffect(
() => {
client.charsheet(TEST_CHARSHEET_UUID).then((c) => {
setCharsheet(c)
});
},
[client, setCharsheet]
);
switch (state.type) {
case "Loading": return <div></div>;
case "Ready": {
const backgroundColor = state.tabletop.backgroundColor;
const tabletopColorStyle = `rgb(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue})`;
const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined;
return (<div className="player-view" style={{ backgroundColor: tabletopColorStyle }}>
<div className="player-view__middle-panel"> <TabletopElement backgroundColor={backgroundColor} backgroundUrl={backgroundUrl} /> </div>
<div className="player-view__right-panel">
{charsheet ? <Candela.CharsheetPanelElement sheet={charsheet} /> : <div> </div>}</div>
</div>)
}
default: {
assertNever(state);
return <div></div>;
}
}
}

View File

@ -0,0 +1,3 @@
import { MainView } from './Main/Main'
export { MainView }