mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-22 14:54:22 +00:00
✨ Progress logout
This commit is contained in:
parent
610f5eb018
commit
870caa098b
8 changed files with 290 additions and 30 deletions
|
@ -4,7 +4,7 @@
|
|||
<command value="start" />
|
||||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
<env name="REACT_APP_DEFAULT_INSTANCE_URL" value="" />
|
||||
<env name="REACT_APP_DEFAULT_INSTANCE" value="http://localhost:30033" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
|
|
@ -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 (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
To change Sophon instance, please logout.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if(instance.validity === false) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
The specified instance is invalid.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
Select the Sophon instance you want to connect to.
|
||||
</Panel>
|
||||
)
|
||||
},
|
||||
[userData, instance]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Instance select
|
||||
</Heading>
|
||||
<p>
|
||||
Select the Sophon instance you want to connect to.
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Field label={"URL"} {...instance} disabled={Boolean(login.userData)}/>
|
||||
<Form.Row>
|
||||
{statePanel}
|
||||
</Form.Row>
|
||||
<Form.Field label={"URL"} {...instance} validity={userData ? undefined : instance.validity} disabled={Boolean(userData)}/>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
|
|
170
frontend/src/components/LoginBox.tsx
Normal file
170
frontend/src/components/LoginBox.tsx
Normal file
|
@ -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<string>("", value => {
|
||||
if(value === "") return undefined
|
||||
return true
|
||||
})
|
||||
|
||||
/**
|
||||
* The {@link FormState} of the password field.
|
||||
*/
|
||||
const password = useFormState<string>("", 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<AxiosError | null>(null)
|
||||
|
||||
/**
|
||||
* An {@link AbortController} used to abort the login request.
|
||||
*/
|
||||
const [abort, setAbort] = React.useState<AbortController | null>(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<boolean>(
|
||||
() => {
|
||||
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 (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
<I>{error.response.statusText}</I>: {error.response.data['non_field_errors'][0]}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-red"}>
|
||||
{error.toString()}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
}
|
||||
if(!instance.validity) {
|
||||
return (
|
||||
<Panel>
|
||||
Please enter a valid instance URL before logging in.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if(!(username.validity && password.validity)) {
|
||||
return (
|
||||
<Panel>
|
||||
Please enter your login credentials.
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
if(abort) {
|
||||
return (
|
||||
<Panel bluelibClassNames={"color-yellow"}>
|
||||
Logging in...
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Panel>
|
||||
Click the login button to begin the login procedure.
|
||||
</Panel>
|
||||
)
|
||||
},
|
||||
[error, instance, username, password, abort]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Login
|
||||
</Heading>
|
||||
<p>
|
||||
Authenticate yourself to access the full functionality of Sophon.
|
||||
</p>
|
||||
<Form>
|
||||
<Form.Row>
|
||||
{statePanel}
|
||||
</Form.Row>
|
||||
<Form.Field label={"Username"} {...username} disabled={!instance.validity}/>
|
||||
<Form.Field label={"Password"} type={"password"} {...password} disabled={!instance.validity}/>
|
||||
<Form.Row>
|
||||
<Form.Button onClick={doLogin} disabled={!canLogin} bluelibClassNames={error ? "color-red" : ""}>
|
||||
Login
|
||||
</Form.Button>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -34,7 +34,8 @@ export function LoginContextProvider({children}: LoginContextProps): JSX.Element
|
|||
|
||||
const login = React.useCallback(
|
||||
async (username: string, password: string, abort: AbortSignal): Promise<void> => {
|
||||
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,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
34
frontend/src/components/LogoutBox.tsx
Normal file
34
frontend/src/components/LogoutBox.tsx
Normal file
|
@ -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 (
|
||||
<Box>
|
||||
<Heading level={3}>
|
||||
Logout
|
||||
</Heading>
|
||||
<Form>
|
||||
<Form.Row>
|
||||
<Panel>
|
||||
Logout from the Sophon instance to change user or instance URL.
|
||||
</Panel>
|
||||
</Form.Row>
|
||||
<Form.Row>
|
||||
<Form.Button onClick={logout}>
|
||||
Logout
|
||||
</Form.Button>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<>
|
||||
<Heading level={1}>
|
||||
Sophon
|
||||
</Heading>
|
||||
<InstanceBox/>
|
||||
{userData ? <LogoutBox/> : <LoginBox/>}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<Reach.Router>
|
||||
<AccountPage path={"/"}/>
|
||||
<LoginPage path={"/login"}/>
|
||||
</Reach.Router>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue