1
Fork 0
mirror of https://github.com/Steffo99/sophon.git synced 2024-12-22 06:44:21 +00:00

Progress logout

This commit is contained in:
Steffo 2021-09-16 20:38:48 +02:00
parent 8f463fa701
commit 272e7a3c98
8 changed files with 290 additions and 30 deletions

View file

@ -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>

View file

@ -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>
)

View 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>
)
}

View file

@ -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,
}
})
},

View 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>
)
}

View file

@ -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/>}
</>
)
}

View file

@ -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>
)
}

View file

@ -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"