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 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
|
||||||
|
@ -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() {
|
||||||
|
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 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 } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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