diff --git a/.idea/runConfigurations/Start_sophon_frontend.xml b/.idea/runConfigurations/Start_sophon_frontend.xml index 41d0f01..708f9bd 100644 --- a/.idea/runConfigurations/Start_sophon_frontend.xml +++ b/.idea/runConfigurations/Start_sophon_frontend.xml @@ -4,7 +4,7 @@ - + diff --git a/frontend/src/components/InstanceBox.tsx b/frontend/src/components/InstanceBox.tsx index 611169c..1390251 100644 --- a/frontend/src/components/InstanceBox.tsx +++ b/frontend/src/components/InstanceBox.tsx @@ -1,8 +1,9 @@ import * as React from "react" import * as ReactDOM from "react-dom" -import {Box, Heading, Form} from "@steffo/bluelib-react"; +import {Box, Heading, Form, Panel} from "@steffo/bluelib-react"; import {useInstance} from "./InstanceContext"; import {useLogin} from "./LoginContext"; +import {Idiomatic as I} from "@steffo/bluelib-react/dist/components/semantics/Idiomatic"; interface InstanceBoxProps { @@ -12,18 +13,46 @@ interface InstanceBoxProps { export function InstanceBox({}: InstanceBoxProps): JSX.Element { const instance = useInstance() - const login = useLogin() + const {userData} = useLogin() + + /** + * The state panel, displayed on top of the form. + */ + const statePanel = React.useMemo( + () => { + if(userData) { + return ( + + To change Sophon instance, please logout. + + ) + } + if(instance.validity === false) { + return ( + + The specified instance is invalid. + + ) + } + return ( + + Select the Sophon instance you want to connect to. + + ) + }, + [userData, instance] + ) return ( Instance select -

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

- + + {statePanel} + +
) diff --git a/frontend/src/components/LoginBox.tsx b/frontend/src/components/LoginBox.tsx new file mode 100644 index 0000000..ca51e04 --- /dev/null +++ b/frontend/src/components/LoginBox.tsx @@ -0,0 +1,170 @@ +import * as React from "react" +import * as ReactDOM from "react-dom" +import {navigate} from "@reach/router"; +import {Box, Heading, Form, useFormState, Panel, Idiomatic as I} from "@steffo/bluelib-react" +import {useLogin} from "./LoginContext"; +import {useInstance} from "./InstanceContext"; +import {FormState} from "@steffo/bluelib-react/dist/hooks/useFormState"; +import {AxiosError} from "axios-lab"; + + +interface LoginBoxProps { + +} + + +export function LoginBox({}: LoginBoxProps): JSX.Element { + /** + * The {@link InstanceContext}. + */ + const instance = useInstance() + + /** + * The {@link LoginContext}. + */ + const {login} = useLogin() + + /** + * The {@link FormState} of the username field. + */ + const username = useFormState("", value => { + if(value === "") return undefined + return true + }) + + /** + * The {@link FormState} of the password field. + */ + const password = useFormState("", value => { + if(value === "") return undefined + if(value.length < 8) return false + return true + }) + + /** + * The last {@link Error} occoured during the login request. + */ + const [error, setError] = React.useState(null) + + /** + * An {@link AbortController} used to abort the login request. + */ + const [abort, setAbort] = React.useState(null) + + /** + * The function to perform the login. + */ + const doLogin = React.useCallback( + async () => { + // Abort the previous login request + if(abort) abort.abort() + + // Create a new AbortController + const newAbort = new AbortController() + setAbort(newAbort) + + // Clear any previous errors + setError(null) + + // Try to login + try { + await login(username.value, password.value, newAbort.signal) + } + catch (e: unknown) { + // Store the caught error + setError(e as AxiosError) + return + } + finally { + // Clear the abort state + // Possible race condition? + setAbort(null) + } + }, + [abort, setAbort, username, password, login, setError] + ) + + /** + * Whether the login button is enabled or not. + */ + const canLogin = React.useMemo( + () => { + return instance.validity === true && username.validity === true && password.validity === true && !abort + }, + [instance, username, password] + ) + + /** + * The state panel, displayed on top of the form. + */ + const statePanel = React.useMemo( + () => { + if(error) { + if(error.response) { + return ( + + {error.response.statusText}: {error.response.data['non_field_errors'][0]} + + ) + } + else { + return ( + + {error.toString()} + + ) + } + } + if(!instance.validity) { + return ( + + Please enter a valid instance URL before logging in. + + ) + } + if(!(username.validity && password.validity)) { + return ( + + Please enter your login credentials. + + ) + } + if(abort) { + return ( + + Logging in... + + ) + } + return ( + + Click the login button to begin the login procedure. + + ) + }, + [error, instance, username, password, abort] + ) + + return ( + + + Login + +

+ Authenticate yourself to access the full functionality of Sophon. +

+
+ + {statePanel} + + + + + + Login + + + +
+ ) +} diff --git a/frontend/src/components/LoginContext.tsx b/frontend/src/components/LoginContext.tsx index 725dd05..aadb819 100644 --- a/frontend/src/components/LoginContext.tsx +++ b/frontend/src/components/LoginContext.tsx @@ -34,7 +34,8 @@ export function LoginContextProvider({children}: LoginContextProps): JSX.Element const login = React.useCallback( async (username: string, password: string, abort: AbortSignal): Promise => { - const response: AxiosResponse<{token: string}> = await api.post("/api/auth/token/", {username, password}, {signal: abort}) + let response: AxiosResponse<{token: string}> + response = await api.post("/api/auth/token/", {username, password}, {signal: abort}) setUserData({ username: username, @@ -65,7 +66,22 @@ export function useLogin() { export function useLoginAxios(config: AxiosRequestConfig) { const instance = useInstance() - const login = useLogin() + const {userData} = useLogin() + + const authHeader = React.useMemo( + () => { + if(userData) { + return { + "Authorization": `${userData.tokenType} ${userData.token}` + } + } + else { + return {} + } + + }, + [userData] + ) return React.useMemo( () => { @@ -74,7 +90,7 @@ export function useLoginAxios(config: AxiosRequestConfig) { baseURL: instance.value, headers: { ...config.headers, - "Authorization": `${login.userData?.tokenType} ${login.userData?.token}` + authHeader, } }) }, diff --git a/frontend/src/components/LogoutBox.tsx b/frontend/src/components/LogoutBox.tsx new file mode 100644 index 0000000..cefb89c --- /dev/null +++ b/frontend/src/components/LogoutBox.tsx @@ -0,0 +1,34 @@ +import * as React from "react" +import * as ReactDOM from "react-dom" +import {Box, Heading, Form, Panel} from "@steffo/bluelib-react"; +import {useLogin} from "./LoginContext"; + + +interface LogoutBoxProps { + +} + + +export function LogoutBox({}: LogoutBoxProps): JSX.Element { + const {logout} = useLogin() + + return ( + + + Logout + +
+ + + Logout from the Sophon instance to change user or instance URL. + + + + + Logout + + +
+
+ ) +} diff --git a/frontend/src/routes/AccountPage.tsx b/frontend/src/routes/LoginPage.tsx similarity index 50% rename from frontend/src/routes/AccountPage.tsx rename to frontend/src/routes/LoginPage.tsx index 852749a..3ece53a 100644 --- a/frontend/src/routes/AccountPage.tsx +++ b/frontend/src/routes/LoginPage.tsx @@ -2,20 +2,26 @@ import * as React from "react" import * as ReactDOM from "react-dom" import {InstanceBox} from "../components/InstanceBox"; import {Chapter, Heading} from "@steffo/bluelib-react"; +import {LoginBox} from "../components/LoginBox"; +import {useLogin} from "../components/LoginContext"; +import {LogoutBox} from "../components/LogoutBox"; -interface AccountPageProps { +interface LoginPageProps { } -export function AccountPage({}: AccountPageProps): JSX.Element { +export function LoginPage({}: LoginPageProps): JSX.Element { + const {userData} = useLogin() + return ( <> Sophon + {userData ? : } ) } diff --git a/frontend/src/routes/Router.jsx b/frontend/src/routes/Router.jsx index 755dfac..5a578a9 100644 --- a/frontend/src/routes/Router.jsx +++ b/frontend/src/routes/Router.jsx @@ -1,13 +1,13 @@ import * as React from "react" import * as ReactDOM from "react-dom" import * as Reach from "@reach/router" -import {AccountPage} from "./AccountPage"; +import {LoginPage} from "./LoginPage"; export function Router({}) { return ( - + ) } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 2295558..b3679be 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1585,9 +1585,9 @@ "@sinonjs/commons" "^1.7.0" "@steffo/bluelib-react@^3.0.7": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@steffo/bluelib-react/-/bluelib-react-3.2.1.tgz#5cfa7590cc91678337f6be23d75c7568d7025f0a" - integrity sha512-zZzPVWxwGJe7uZ80fyqzYi0p3wJxGSYJAeY32bGhT8fU642wx6epxwHkwjoS/KO48AR6kUnNyEfPFDg/VUyY/w== + version "3.2.2" + resolved "https://registry.yarnpkg.com/@steffo/bluelib-react/-/bluelib-react-3.2.2.tgz#82238fc31ad913eca6358d41bb4ce0a486964614" + integrity sha512-dpAouMaGd16Bpp/z+yEM/qkfVJezOidZqE/GKuxEsvIofwjPtY6Arj0n21oay7R7H09YsaO6EsbHEfYdYQ9C6A== dependencies: "@babel/runtime" "^7.15.3" classnames "^2.3.1" @@ -4401,9 +4401,9 @@ ejs@^2.6.1: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== electron-to-chromium@^1.3.564, electron-to-chromium@^1.3.830: - version "1.3.838" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.838.tgz#d178b34a268c750c0444ba69e4c94d4c4fb3aa0d" - integrity sha512-65O6UJiyohFAdX/nc6KJ0xG/4zOn7XCO03kQNNbCeMRGxlWTLzc6Uyi0tFNQuuGWqySZJi8CD2KXPXySVYmzMA== + version "1.3.840" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.840.tgz#3f2a1df97015d9b1db5d86a4c6bd4cdb920adcbb" + integrity sha512-yRoUmTLDJnkIJx23xLY7GbSvnmDCq++NSuxHDQ0jiyDJ9YZBUGJcrdUqm+ZwZFzMbCciVzfem2N2AWiHJcWlbw== elliptic@^6.5.3: version "6.5.4" @@ -7323,11 +7323,16 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.49.0, "mime-db@>= 1.43.0 < 2": +mime-db@1.49.0: version "1.49.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== +"mime-db@>= 1.43.0 < 2": + version "1.50.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" + integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== + mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24: version "2.1.32" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" @@ -7409,9 +7414,9 @@ minipass-pipeline@^1.2.2: minipass "^3.0.0" minipass@^3.0.0, minipass@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + version "3.1.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" + integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== dependencies: yallist "^4.0.0" @@ -7696,9 +7701,9 @@ nth-check@^1.0.2: boolbase "~1.0.0" nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== dependencies: boolbase "^1.0.0" @@ -9972,9 +9977,9 @@ side-channel@^1.0.4: object-inspect "^1.9.0" signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + version "3.0.4" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.4.tgz#366a4684d175b9cab2081e3681fda3747b6c51d7" + integrity sha512-rqYhcAnZ6d/vTPGghdrw7iumdcbXpsk1b8IG/rz+VWV51DM0p7XCtMoJ3qhPLIbp3tvyt3pKRbaaEMZYpHto8Q== simple-swizzle@^0.2.2: version "0.2.2"