Authenticate the user and populate AppState with the stored session ID
This commit is contained in:
parent
08462388ea
commit
208083d39e
@ -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
|
||||
|
@ -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() {
|
||||
|
0
visions/ui/src/components/Profile/Profile.css
Normal file
0
visions/ui/src/components/Profile/Profile.css
Normal file
10
visions/ui/src/components/Profile/Profile.tsx
Normal file
10
visions/ui/src/components/Profile/Profile.tsx
Normal 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>
|
||||
}
|
@ -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 } });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
.gm-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
0
visions/ui/src/views/Main/Main.css
Normal file
0
visions/ui/src/views/Main/Main.css
Normal file
16
visions/ui/src/views/Main/Main.tsx
Normal file
16
visions/ui/src/views/Main/Main.tsx
Normal 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>
|
||||
}
|
||||
|
@ -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%;
|
||||
}
|
||||
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
visions/ui/src/views/index.ts
Normal file
3
visions/ui/src/views/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { MainView } from './Main/Main'
|
||||
|
||||
export { MainView }
|
Loading…
Reference in New Issue
Block a user