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

View File

@ -6,6 +6,7 @@ export type PlayingField = {
export class Client {
private base: URL;
private sessionId: string | undefined;
constructor() {
this.base = new URL("http://localhost:8001");
@ -69,7 +70,9 @@ export class Client {
async auth(username: string, password: string): Promise<SessionId | undefined> {
const url = new URL(this.base);
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() {

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 { Status, Tabletop } from "visions-types";
import { SessionId, Status, Tabletop } from "visions-types";
import { Client } from "../../client";
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: "Ready",
type AppState = {
state: LoadingState,
auth: AuthState,
tabletop: Tabletop,
}
type AppState = LoadingState | ReadyState
type Action = { type: "SetAuthState", content: AuthState };
/*
const initialState = (): AppState => (
{
const initialState = (): AppState => {
let state: AppState = {
state: LoadingState.Ready,
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 => {
return { type: "Ready", auth: action.content, tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } }
}
const readyReducer = (state: ReadyState, action: Action): AppState => {
return { ...state, auth: action.content }
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 (state.type) {
case "Loading": {
return loadingReducer(state, action);
}
case "Ready": {
return readyReducer(state, action);
switch (action.type) {
case "SetAuthState": {
return { ...state, auth: action.content }
}
/*
default: {
assertNever(state);
return { type: "Loading" };
assertNever(action)
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();
if (!admin_enabled) {
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) {
if (!this.client || !this.dispatch) return;
let resp = await this.client.auth(username, password);
let sessionid = await resp.json();
console.log("sessionid retrieved", sessionid);
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionid } });
let sessionId = await this.client.auth(username, password);
if (sessionId) {
window.localStorage.setItem("sessionId", sessionId);
this.dispatch({ type: "SetAuthState", content: { type: "Authed", sessionId } });
}
}
}

View File

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