diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 22de455..e5c0a12 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,25 +1,19 @@ import * as React from 'react'; -import {Bluelib, Chapter, Heading, LayoutThreeCol} from "@steffo/bluelib-react"; -import {SophonContextProvider} from "./utils/SophonContext"; -import {LoginBox} from "./components/LoginBox"; -import {InstanceBox} from "./components/InstanceBox"; -import {GuestBox} from "./components/GuestBox"; +import {Bluelib, Heading, LayoutThreeCol} from "@steffo/bluelib-react"; import {Router} from "./routes/Router"; function App() { return ( - - - - - - Sophon - - - - - - + + + + + Sophon + + + + + ); } diff --git a/frontend/src/components/GuestBox.tsx b/frontend/src/components/GuestBox.tsx deleted file mode 100644 index b2806d9..0000000 --- a/frontend/src/components/GuestBox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from "react" -import * as ReactDOM from "react-dom" -import * as Reach from "@reach/router" -import {Box, Heading, Form} from "@steffo/bluelib-react"; - - -interface GuestBoxProps { - -} - - -export function GuestBox({}: GuestBoxProps): JSX.Element { - return ( - - - Guest access - -

- Continue without logging in to view the published data of this Sophon instance. -

-
- - Reach.navigate("/home")}> - Continue as guest - - -
-
- ) -} diff --git a/frontend/src/components/InstanceBox.tsx b/frontend/src/components/InstanceBox.tsx deleted file mode 100644 index 3e68e58..0000000 --- a/frontend/src/components/InstanceBox.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from "react" -import * as ReactDOM from "react-dom" -import {Box, Heading, Form, useFormState} from "@steffo/bluelib-react"; -import {useSophonContext} from "../utils/SophonContext"; -import axios, {AxiosResponse} from "axios-lab"; -import {useCallback} from "react"; - - -interface InstanceBoxProps { - -} - - -// This is a bit hacky but it works as intended -export function InstanceBox({}: InstanceBoxProps): JSX.Element { - const {instanceUrl, setInstanceUrl} = useSophonContext() - - const sophonInstanceValidator - = useCallback( - async (value, abort) => { - if(value === "") return undefined - - await new Promise(r => setTimeout(r, 250)) - if(abort.aborted) return null - - let url: URL - try { - url = new URL(value) - } catch (_) { - return false - } - - try { - await axios.get("api/core/version", {baseURL: url.toString(), signal: abort}) - } catch(_) { - return false - } - - setInstanceUrl(value) - return true - }, - [instanceUrl] - ) - - const sophonInstance - = useFormState(instanceUrl, sophonInstanceValidator) - - return ( - - - Change instance - -

- Select the Sophon instance you want to connect to. -

-
- - -
- ) -} diff --git a/frontend/src/components/InstanceContext.tsx b/frontend/src/components/InstanceContext.tsx new file mode 100644 index 0000000..a61ae01 --- /dev/null +++ b/frontend/src/components/InstanceContext.tsx @@ -0,0 +1,67 @@ +import * as React from "react" +import * as ReactDOM from "react-dom" +import Axios, {AxiosRequestConfig} from "axios-lab" +import {useNotNullContext} from "../hooks/useNotNullContext"; +import {useFormState} from "@steffo/bluelib-react"; +import {FormState} from "@steffo/bluelib-react/dist/hooks/useFormState"; +import {Validity} from "@steffo/bluelib-react/dist/types"; + + +export const InstanceContext = React.createContext | null>(null) + + +export async function instanceValidator(value: string, abort: AbortSignal): Promise { + if(value === "") return undefined + + await new Promise(r => setTimeout(r, 250)) + if(abort.aborted) return null + + let url: URL + try { + url = new URL(value) + } catch (_) { + return false + } + + try { + await Axios.get("/api/core/version", {baseURL: url.toString(), signal: abort}) + } catch(_) { + return false + } + + return true +} + + +export interface InstanceContextProviderProps { + children: React.ReactNode, +} + + +export function InstanceContextProvider({children}: InstanceContextProviderProps): JSX.Element { + const instance = useFormState(process.env.REACT_APP_DEFAULT_INSTANCE ?? "https://prod.sophon.steffo.eu", instanceValidator) + + return ( + + ) +} + + +export function useInstance() { + return useNotNullContext>(InstanceContext) +} + + +export function useInstanceAxios(config: AxiosRequestConfig = {}) { + const instance = useInstance() + + return React.useMemo( + () => { + return Axios.create({ + ...config, + baseURL: instance.value, + }) + }, + [instance, config] + ) +} diff --git a/frontend/src/components/LoginBox.tsx b/frontend/src/components/LoginBox.tsx deleted file mode 100644 index 5205e73..0000000 --- a/frontend/src/components/LoginBox.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from "react" -import * as ReactDOM from "react-dom" -import {Box, Heading, Form, Parenthesis, Variable, useFormState} from "@steffo/bluelib-react"; -import {useSophonContext} from "../utils/SophonContext"; - - -interface LoginBoxProps { - -} - - -export function LoginBox({...props}: LoginBoxProps): JSX.Element { - const {loginData, loginError, login, logout} = useSophonContext() - - const username - = useFormState("", - val => { - if(val === "") { - return undefined - } - return true - } - ) - const password - = useFormState("", - val => { - if(val === "") { - return undefined - } - if(val.length < 8) { - return false - } - return true - } - ) - - - if(loginData) { - return ( - - - Login - -

- You are logged in as: {loginData.username} -

-
- - Logout - -
-
- ) - } - else { - return ( - - - Login - -

- Login to the Sophon instance to access the full capabilities of Sophon. -

-
- {loginError ? - - - {loginError.toString()} - - - : null} - - - - login(username.value, password.value)} bluelibClassNames={loginError ? "color-red" : ""}> - Login - - - -
- ) - } -} diff --git a/frontend/src/utils/LoginData.ts b/frontend/src/utils/LoginData.ts deleted file mode 100644 index 420a0fa..0000000 --- a/frontend/src/utils/LoginData.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {AxiosInstance, AxiosResponse} from "axios-lab"; - - -export interface LoginData { - username: string, - tokenType: string, - token: string, -} - - -export async function requestLoginData(api: AxiosInstance, username: string, password: string): Promise { - console.debug("Requesting auth token...") - const response: AxiosResponse<{token: string}> = await api.post("/api/auth/token/", {username, password}) - - console.debug("Constructing LoginData...") - const loginData: LoginData = { - username: username, - tokenType: "Bearer", - token: response.data.token - } - - console.debug("Created LoginData:", loginData) - return loginData -} - - -export function makeAuthorizationHeader(loginData: LoginData | null): {[key: string]: string} { - if(loginData === null) { - return {} - } - - return { - "Authorization": `${loginData.tokenType} ${loginData.token}` - } -} \ No newline at end of file diff --git a/frontend/src/utils/SophonContext.tsx b/frontend/src/utils/SophonContext.tsx deleted file mode 100644 index 4223d70..0000000 --- a/frontend/src/utils/SophonContext.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import * as React from "react" -import Axios, {AxiosInstance} from "axios-lab" -import {LoginData, makeAuthorizationHeader, requestLoginData} from "./LoginData"; -import {createNullContext, useNotNullContext} from "../hooks/useNotNullContext"; -import {useStorageState} from "../hooks/useStorageState"; - - -/** - * The type of the `changeSophon` function in {@link SophonContextContents}. - */ -export type ChangeSophonFunction = (url: string) => void - -/** - * The type of the `login` function in {@link SophonContextContents}. - */ -export type LoginFunction = (username: string, password: string) => void - -/** - * The type of the `logout` function in {@link SophonContextContents}. - */ -export type LogoutFunction = () => void - - -/** - * The contents of the global app context {@link SophonContext}. - */ -export interface SophonContextContents { - /** - * The {@link Axios} instance to use to perform API calls on the Sophon backend. - */ - api: AxiosInstance, - - /** - * The {@link LoginData} of the currently logged in user, or `null` if the user is anonymous. - */ - loginData: LoginData | null, - - /** - * Whether a login is running or not. - */ - loginRunning: boolean, - - /** - * An error that occoured during the login if it happened, `null` otherwise. - */ - loginError: Error | null, - - /** - * Login to the Sophon backend with the given `username` and `password`, consequently updating the {@link api} instance. - */ - login: LoginFunction, - - /** - * Logout from the Sophon backend, consequently updating the {@link api} instance. - */ - logout: LogoutFunction, - - /** - * The Sophon instance URL. - */ - instanceUrl: string, - - /** - * Change Sophon instance to the one with the given `url`. - */ - setInstanceUrl: React.Dispatch, -} - -/** - * The global app context, containing {@link SophonContextContents}. - */ -export const SophonContext = createNullContext() - - -/** - * Shortcut hook for using the {@link useNotNullContext} hook on {@link SophonContext}. - */ -export function useSophonContext(): SophonContextContents { - return useNotNullContext(SophonContext) -} - -/** - * The props that can be passed to the {@link SophonContextProvider}. - */ -export interface SophonContextProviderProps { - children?: React.ReactNode, -} - - -/** - * Automatic provider for the global app context {@link SophonContext}. - * - * No need to do anything with it, except to use it to wrap the whole app. - */ -export function SophonContextProvider({children}: SophonContextProviderProps): JSX.Element { - const [instanceUrl, setInstanceUrl] - = useStorageState(localStorage, "instanceUrl", process.env.REACT_APP_DEFAULT_INSTANCE_URL ?? "https://prod.sophon.steffo.eu") - - const [loginData, setLoginData] - = useStorageState(localStorage, "loginData", null) - - const [loginError, setLoginError] - = React.useState(null) - - const [loginRunning, setLoginRunning] - = React.useState(false) - - const api: AxiosInstance - = React.useMemo( - () => { - console.debug("Creating new AxiosInstance...") - return Axios.create({ - baseURL: instanceUrl, - timeout: 5000, - headers: { - ...makeAuthorizationHeader(loginData) - } - }) - }, - [instanceUrl, loginData] - ) - - const login: LoginFunction - = React.useCallback( - (username, password) => { - console.info("Trying to login as", username, "...") - setLoginRunning(true) - setLoginError(null) - requestLoginData(api, username, password) - .then(loginData => setLoginData(loginData)) - .catch(error => setLoginError(error)) - .finally(() => setLoginRunning(false)) - }, - [api, setLoginData, setLoginRunning, setLoginError] - ) - - const logout: LogoutFunction = React.useCallback( - () => { - if(loginRunning) { - throw Error("Refusing to logout while a login is running.") - } - console.info("Logging out...") - setLoginData(null) - }, - [setLoginData] - ) - - return ( - - {children} - - ) -}