From 208083d39e01651a68a2f8981d2f4fa03e59a88f Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 3 Jan 2025 16:19:43 -0500 Subject: [PATCH] Authenticate the user and populate AppState with the stored session ID --- visions/ui/src/App.tsx | 64 ++++++++-------- visions/ui/src/client.ts | 5 +- visions/ui/src/components/Profile/Profile.css | 0 visions/ui/src/components/Profile/Profile.tsx | 10 +++ .../providers/StateProvider/StateProvider.tsx | 76 ++++++++++--------- .../views/Authentication/Authentication.tsx | 13 +--- visions/ui/src/views/GmView/GmView.css | 4 - visions/ui/src/views/GmView/GmView.tsx | 39 ---------- visions/ui/src/views/Main/Main.css | 0 visions/ui/src/views/Main/Main.tsx | 16 ++++ .../ui/src/views/PlayerView/PlayerView.css | 15 ---- .../ui/src/views/PlayerView/PlayerView.tsx | 49 ------------ visions/ui/src/views/index.ts | 3 + 13 files changed, 111 insertions(+), 183 deletions(-) create mode 100644 visions/ui/src/components/Profile/Profile.css create mode 100644 visions/ui/src/components/Profile/Profile.tsx delete mode 100644 visions/ui/src/views/GmView/GmView.css delete mode 100644 visions/ui/src/views/GmView/GmView.tsx create mode 100644 visions/ui/src/views/Main/Main.css create mode 100644 visions/ui/src/views/Main/Main.tsx delete mode 100644 visions/ui/src/views/PlayerView/PlayerView.css delete mode 100644 visions/ui/src/views/PlayerView/PlayerView.tsx create mode 100644 visions/ui/src/views/index.ts diff --git a/visions/ui/src/App.tsx b/visions/ui/src/App.tsx index 9b98110..b6dcf0e 100644 --- a/visions/ui/src/App.tsx +++ b/visions/ui/src/App.tsx @@ -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 ? :
} interface AuthedViewProps { - client: Client; + client: Client } const AuthedView = ({ client, children }: PropsWithChildren) => { - const [state, manager] = useContext(StateContext); + const [state, manager] = useContext(StateContext) return ( { - manager.setAdminPassword(password); + manager.setAdminPassword(password) }} onAuth={(username, password) => manager.auth(username, password)}> {children} - ); + ) } const App = ({ client }: AppProps) => { - console.log("rendering app"); - const [websocketUrl, setWebsocketUrl] = useState(undefined); + console.log("rendering app") + const [websocketUrl, setWebsocketUrl] = useState(undefined) // useEffect(() => { // client.registerWebsocket().then((url) => setWebsocketUrl(url)) - // }, [client]); + // }, [client]) let router = createBrowserRouter([ { path: "/", - element: - }, - { - path: "/gm", - element: websocketUrl ? :
+ element: ( + + + + + + ) }, { path: "/admin", @@ -72,12 +72,12 @@ const App = ({ client }: AppProps) => { path: "/design", element: } - ]); + ]) return (
- ); + ) } -export default App; +export default App diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index 37c3e9a..5521429 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -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 { 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() { diff --git a/visions/ui/src/components/Profile/Profile.css b/visions/ui/src/components/Profile/Profile.css new file mode 100644 index 0000000..e69de29 diff --git a/visions/ui/src/components/Profile/Profile.tsx b/visions/ui/src/components/Profile/Profile.tsx new file mode 100644 index 0000000..9c38a15 --- /dev/null +++ b/visions/ui/src/components/Profile/Profile.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Client } from '../../client'; + +interface ProfileProps { + client: Client +} + +export const ProfileElement = ({ client }: ProfileProps) => { + return
+} diff --git a/visions/ui/src/providers/StateProvider/StateProvider.tsx b/visions/ui/src/providers/StateProvider/StateProvider.tsx index 64a9dc8..6f928fa 100644 --- a/visions/ui/src/providers/StateProvider/StateProvider.tsx +++ b/visions/ui/src/providers/StateProvider/StateProvider.tsx @@ -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 } }); + } } } diff --git a/visions/ui/src/views/Authentication/Authentication.tsx b/visions/ui/src/views/Authentication/Authentication.tsx index 95f4be2..baf7d27 100644 --- a/visions/ui/src/views/Authentication/Authentication.tsx +++ b/visions/ui/src/views/Authentication/Authentication.tsx @@ -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(""); let [state, _] = useContext(StateContext); - switch (state.type) { - case "Loading": { + switch (state.state) { + case LoadingState.Loading: { return
Loading
} - case "Ready": { + case LoadingState.Ready: { switch (state.auth.type) { case "NoAdmin": { return
@@ -53,11 +53,6 @@ export const Authentication = ({ onAdminPassword, onAuth, children }: PropsWithC return
; } } - - } - default: { - assertNever(state); - return
} } diff --git a/visions/ui/src/views/GmView/GmView.css b/visions/ui/src/views/GmView/GmView.css deleted file mode 100644 index f3b6a10..0000000 --- a/visions/ui/src/views/GmView/GmView.css +++ /dev/null @@ -1,4 +0,0 @@ -.gm-view { - display: flex; - width: 100%; -} diff --git a/visions/ui/src/views/GmView/GmView.tsx b/visions/ui/src/views/GmView/GmView.tsx deleted file mode 100644 index 8874f85..0000000 --- a/visions/ui/src/views/GmView/GmView.tsx +++ /dev/null @@ -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([]); - useEffect(() => { - client.availableImages().then((images) => setImages(images)); - }, [client]); - - switch (state.type) { - case "Loading": return
; - case "Ready": { - const backgroundUrl = state.tabletop.backgroundImage ? client.imageUrl(state.tabletop.backgroundImage) : undefined; - return (
-
- {images.map((imageName) => { client.setBackgroundImage(imageName); }} />)} -
- -
) - } - default: { - assertNever(state); - return
; - } - } -} - diff --git a/visions/ui/src/views/Main/Main.css b/visions/ui/src/views/Main/Main.css new file mode 100644 index 0000000..e69de29 diff --git a/visions/ui/src/views/Main/Main.tsx b/visions/ui/src/views/Main/Main.tsx new file mode 100644 index 0000000..27ded81 --- /dev/null +++ b/visions/ui/src/views/Main/Main.tsx @@ -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
Profile: {sessionId}
+} + diff --git a/visions/ui/src/views/PlayerView/PlayerView.css b/visions/ui/src/views/PlayerView/PlayerView.css deleted file mode 100644 index daf3bec..0000000 --- a/visions/ui/src/views/PlayerView/PlayerView.css +++ /dev/null @@ -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%; -} - - diff --git a/visions/ui/src/views/PlayerView/PlayerView.tsx b/visions/ui/src/views/PlayerView/PlayerView.tsx deleted file mode 100644 index 017c2c0..0000000 --- a/visions/ui/src/views/PlayerView/PlayerView.tsx +++ /dev/null @@ -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
; - 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 (
-
-
- {charsheet ? :
}
-
) - } - default: { - assertNever(state); - return
; - } - } -} - diff --git a/visions/ui/src/views/index.ts b/visions/ui/src/views/index.ts new file mode 100644 index 0000000..0719403 --- /dev/null +++ b/visions/ui/src/views/index.ts @@ -0,0 +1,3 @@ +import { MainView } from './Main/Main' + +export { MainView }