mirror of
https://github.com/Steffo99/sophon.git
synced 2024-12-22 23:04:21 +00:00
✨ Complete login (finally)
This commit is contained in:
parent
870caa098b
commit
a20aa0162a
7 changed files with 154 additions and 38 deletions
76
frontend/src/components/GuestBox.tsx
Normal file
76
frontend/src/components/GuestBox.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ReactDOM from "react-dom"
|
||||||
|
import {Box, Heading, Panel, Form} from "@steffo/bluelib-react";
|
||||||
|
import {useLogin} from "./LoginContext";
|
||||||
|
import {useInstance} from "./InstanceContext";
|
||||||
|
import {navigate} from "@reach/router";
|
||||||
|
|
||||||
|
|
||||||
|
interface GuestBoxProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function GuestBox({}: GuestBoxProps): JSX.Element {
|
||||||
|
const instance = useInstance()
|
||||||
|
const login = useLogin()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the guest login button is enabled or not.
|
||||||
|
*/
|
||||||
|
const canBrowse = React.useMemo<boolean>(
|
||||||
|
() => {
|
||||||
|
return instance.validity === true && !login.running
|
||||||
|
},
|
||||||
|
[instance, login]
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state panel, displayed on top of the form.
|
||||||
|
*/
|
||||||
|
const statePanel = React.useMemo(
|
||||||
|
() => {
|
||||||
|
if(!instance.validity) {
|
||||||
|
return (
|
||||||
|
<Panel bluelibClassNames={"color-red"}>
|
||||||
|
Please enter a valid instance URL before continuing.
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if(login.running) {
|
||||||
|
return (
|
||||||
|
<Panel bluelibClassNames={"color-yellow"}>
|
||||||
|
You cannot browse Sophon while a login is in progress.
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Panel>
|
||||||
|
Click the button below to access Sophon.
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[instance, login]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading level={3}>
|
||||||
|
Continue as guest
|
||||||
|
</Heading>
|
||||||
|
<p>
|
||||||
|
You can browse Sophon without logging in, but many functions won't be available to you.
|
||||||
|
</p>
|
||||||
|
<Form>
|
||||||
|
<Form.Row>
|
||||||
|
{statePanel}
|
||||||
|
</Form.Row>
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Button disabled={!canBrowse} onClick={async () => await navigate("/logged-in")}>
|
||||||
|
Browse
|
||||||
|
</Form.Button>
|
||||||
|
</Form.Row>
|
||||||
|
</Form>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ReactDOM from "react-dom"
|
import * as ReactDOM from "react-dom"
|
||||||
import {Box, Heading, Form, Panel} from "@steffo/bluelib-react";
|
import {Box, Heading, Form, Panel, Idiomatic as I} from "@steffo/bluelib-react";
|
||||||
import {useInstance} from "./InstanceContext";
|
import {useInstance} from "./InstanceContext";
|
||||||
import {useLogin} from "./LoginContext";
|
import {useLogin} from "./LoginContext";
|
||||||
import {Idiomatic as I} from "@steffo/bluelib-react/dist/components/semantics/Idiomatic";
|
|
||||||
|
|
||||||
|
|
||||||
interface InstanceBoxProps {
|
interface InstanceBoxProps {
|
||||||
|
@ -13,34 +12,48 @@ interface InstanceBoxProps {
|
||||||
|
|
||||||
export function InstanceBox({}: InstanceBoxProps): JSX.Element {
|
export function InstanceBox({}: InstanceBoxProps): JSX.Element {
|
||||||
const instance = useInstance()
|
const instance = useInstance()
|
||||||
const {userData} = useLogin()
|
const login = useLogin()
|
||||||
|
|
||||||
|
const canChange = React.useMemo(
|
||||||
|
() => {
|
||||||
|
return !(login.userData || login.running)
|
||||||
|
},
|
||||||
|
[login]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state panel, displayed on top of the form.
|
* The state panel, displayed on top of the form.
|
||||||
*/
|
*/
|
||||||
const statePanel = React.useMemo(
|
const statePanel = React.useMemo(
|
||||||
() => {
|
() => {
|
||||||
if(userData) {
|
if(login.userData) {
|
||||||
return (
|
return (
|
||||||
<Panel bluelibClassNames={"color-red"}>
|
<Panel bluelibClassNames={"color-yellow"}>
|
||||||
To change Sophon instance, please logout.
|
You cannot change Sophon instance while you are logged in. If you need to change instance, <I>logout</I> first!
|
||||||
|
</Panel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if(login.running) {
|
||||||
|
return (
|
||||||
|
<Panel bluelibClassNames={"color-yellow"}>
|
||||||
|
You cannot change Sophon instance while logging in.
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if(instance.validity === false) {
|
if(instance.validity === false) {
|
||||||
return (
|
return (
|
||||||
<Panel bluelibClassNames={"color-red"}>
|
<Panel bluelibClassNames={"color-red"}>
|
||||||
The specified instance is invalid.
|
No Sophon instance was detected at the inserted URL.
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Panel>
|
<Panel>
|
||||||
Select the Sophon instance you want to connect to.
|
Select the Sophon instance you want to connect to by specifying its URL here.
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[userData, instance]
|
[login, instance]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -48,11 +61,14 @@ export function InstanceBox({}: InstanceBoxProps): JSX.Element {
|
||||||
<Heading level={3}>
|
<Heading level={3}>
|
||||||
Instance select
|
Instance select
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<p>
|
||||||
|
Sophon can be used by multiple institutions, each one using a physically separate instance.
|
||||||
|
</p>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Row>
|
<Form.Row>
|
||||||
{statePanel}
|
{statePanel}
|
||||||
</Form.Row>
|
</Form.Row>
|
||||||
<Form.Field label={"URL"} {...instance} validity={userData ? undefined : instance.validity} disabled={Boolean(userData)}/>
|
<Form.Field label={"URL"} {...instance} validity={login.userData ? undefined : instance.validity} disabled={!canChange}/>
|
||||||
</Form>
|
</Form>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
|
@ -22,7 +22,7 @@ export function LoginBox({}: LoginBoxProps): JSX.Element {
|
||||||
/**
|
/**
|
||||||
* The {@link LoginContext}.
|
* The {@link LoginContext}.
|
||||||
*/
|
*/
|
||||||
const {login} = useLogin()
|
const {login, running} = useLogin()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link FormState} of the username field.
|
* The {@link FormState} of the username field.
|
||||||
|
@ -75,11 +75,8 @@ export function LoginBox({}: LoginBoxProps): JSX.Element {
|
||||||
setError(e as AxiosError)
|
setError(e as AxiosError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
// Clear the abort state
|
await navigate("/logged-in")
|
||||||
// Possible race condition?
|
|
||||||
setAbort(null)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[abort, setAbort, username, password, login, setError]
|
[abort, setAbort, username, password, login, setError]
|
||||||
)
|
)
|
||||||
|
@ -89,9 +86,9 @@ export function LoginBox({}: LoginBoxProps): JSX.Element {
|
||||||
*/
|
*/
|
||||||
const canLogin = React.useMemo<boolean>(
|
const canLogin = React.useMemo<boolean>(
|
||||||
() => {
|
() => {
|
||||||
return instance.validity === true && username.validity === true && password.validity === true && !abort
|
return instance.validity === true && username.validity === true && password.validity === true && !running
|
||||||
},
|
},
|
||||||
[instance, username, password]
|
[instance, username, password, running]
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,7 +114,7 @@ export function LoginBox({}: LoginBoxProps): JSX.Element {
|
||||||
}
|
}
|
||||||
if(!instance.validity) {
|
if(!instance.validity) {
|
||||||
return (
|
return (
|
||||||
<Panel>
|
<Panel bluelibClassNames={"color-red"}>
|
||||||
Please enter a valid instance URL before logging in.
|
Please enter a valid instance URL before logging in.
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
|
@ -129,20 +126,20 @@ export function LoginBox({}: LoginBoxProps): JSX.Element {
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if(abort) {
|
if(running) {
|
||||||
return (
|
return (
|
||||||
<Panel bluelibClassNames={"color-yellow"}>
|
<Panel bluelibClassNames={"color-cyan"}>
|
||||||
Logging in...
|
Logging in, please wait...
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Panel>
|
<Panel>
|
||||||
Click the login button to begin the login procedure.
|
Click the button below to login.
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[error, instance, username, password, abort]
|
[error, instance, username, password, running]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -151,7 +148,7 @@ export function LoginBox({}: LoginBoxProps): JSX.Element {
|
||||||
Login
|
Login
|
||||||
</Heading>
|
</Heading>
|
||||||
<p>
|
<p>
|
||||||
Authenticate yourself to access the full functionality of Sophon.
|
Login as an authorized user to access the full functionality of Sophon.
|
||||||
</p>
|
</p>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Row>
|
<Form.Row>
|
||||||
|
|
|
@ -16,6 +16,7 @@ export interface LoginContextData {
|
||||||
userData: UserData | null,
|
userData: UserData | null,
|
||||||
login: (username: string, password: string, abort: AbortSignal) => Promise<void>,
|
login: (username: string, password: string, abort: AbortSignal) => Promise<void>,
|
||||||
logout: () => void,
|
logout: () => void,
|
||||||
|
running: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,11 +32,20 @@ export function LoginContextProvider({children}: LoginContextProps): JSX.Element
|
||||||
const api = useInstanceAxios()
|
const api = useInstanceAxios()
|
||||||
|
|
||||||
const [userData, setUserData] = React.useState<UserData | null>(null)
|
const [userData, setUserData] = React.useState<UserData | null>(null)
|
||||||
|
const [running, setRunning] = React.useState<boolean>(false)
|
||||||
|
|
||||||
const login = React.useCallback(
|
const login = React.useCallback(
|
||||||
async (username: string, password: string, abort: AbortSignal): Promise<void> => {
|
async (username: string, password: string, abort: AbortSignal): Promise<void> => {
|
||||||
let response: AxiosResponse<{token: string}>
|
let response: AxiosResponse<{token: string}>
|
||||||
response = await api.post("/api/auth/token/", {username, password}, {signal: abort})
|
|
||||||
|
setRunning(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await api.post("/api/auth/token/", {username, password}, {signal: abort})
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setRunning(false)
|
||||||
|
}
|
||||||
|
|
||||||
setUserData({
|
setUserData({
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -54,7 +64,7 @@ export function LoginContextProvider({children}: LoginContextProps): JSX.Element
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginContext.Provider value={{userData, login, logout}} children={children}/>
|
<LoginContext.Provider value={{userData, login, logout, running}} children={children}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as ReactDOM from "react-dom"
|
import * as ReactDOM from "react-dom"
|
||||||
import {Box, Heading, Form, Panel} from "@steffo/bluelib-react";
|
import {Box, Heading, Form, Panel, Variable} from "@steffo/bluelib-react";
|
||||||
import {useLogin} from "./LoginContext";
|
import {useLogin} from "./LoginContext";
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,21 +10,29 @@ interface LogoutBoxProps {
|
||||||
|
|
||||||
|
|
||||||
export function LogoutBox({}: LogoutBoxProps): JSX.Element {
|
export function LogoutBox({}: LogoutBoxProps): JSX.Element {
|
||||||
const {logout} = useLogin()
|
const login = useLogin()
|
||||||
|
|
||||||
|
if(!login.userData) {
|
||||||
|
console.log("LogoutBox displayed while the user wasn't logged in.")
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Heading level={3}>
|
<Heading level={3}>
|
||||||
Logout
|
Logout
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<p>
|
||||||
|
Logout from the Sophon instance to change user or instance URL.
|
||||||
|
</p>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Row>
|
<Form.Row>
|
||||||
<Panel>
|
<Panel>
|
||||||
Logout from the Sophon instance to change user or instance URL.
|
You are currently logged in as <Variable>{login.userData.username}</Variable>.
|
||||||
</Panel>
|
</Panel>
|
||||||
</Form.Row>
|
</Form.Row>
|
||||||
<Form.Row>
|
<Form.Row>
|
||||||
<Form.Button onClick={logout}>
|
<Form.Button onClick={login.logout}>
|
||||||
Logout
|
Logout
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
</Form.Row>
|
</Form.Row>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {Chapter, Heading} from "@steffo/bluelib-react";
|
||||||
import {LoginBox} from "../components/LoginBox";
|
import {LoginBox} from "../components/LoginBox";
|
||||||
import {useLogin} from "../components/LoginContext";
|
import {useLogin} from "../components/LoginContext";
|
||||||
import {LogoutBox} from "../components/LogoutBox";
|
import {LogoutBox} from "../components/LogoutBox";
|
||||||
|
import {GuestBox} from "../components/GuestBox";
|
||||||
|
|
||||||
|
|
||||||
interface LoginPageProps {
|
interface LoginPageProps {
|
||||||
|
@ -17,11 +18,15 @@ export function LoginPage({}: LoginPageProps): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading level={1}>
|
|
||||||
Sophon
|
|
||||||
</Heading>
|
|
||||||
<InstanceBox/>
|
<InstanceBox/>
|
||||||
{userData ? <LogoutBox/> : <LoginBox/>}
|
{userData ?
|
||||||
|
<LogoutBox/>
|
||||||
|
:
|
||||||
|
<Chapter>
|
||||||
|
<GuestBox/>
|
||||||
|
<LoginBox/>
|
||||||
|
</Chapter>
|
||||||
|
}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,16 @@ import * as React from "react"
|
||||||
import * as ReactDOM from "react-dom"
|
import * as ReactDOM from "react-dom"
|
||||||
import * as Reach from "@reach/router"
|
import * as Reach from "@reach/router"
|
||||||
import {LoginPage} from "./LoginPage";
|
import {LoginPage} from "./LoginPage";
|
||||||
|
import { Heading } from "@steffo/bluelib-react"
|
||||||
|
|
||||||
|
|
||||||
export function Router({}) {
|
export function Router({}) {
|
||||||
return (
|
return <>
|
||||||
|
<Heading level={1}>
|
||||||
|
Sophon
|
||||||
|
</Heading>
|
||||||
<Reach.Router>
|
<Reach.Router>
|
||||||
<LoginPage path={"/login"}/>
|
<LoginPage path={"/"}/>
|
||||||
</Reach.Router>
|
</Reach.Router>
|
||||||
)
|
</>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue