1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-28 23:44:19 +00:00

💥 Refactor the login/logout flow

This commit is contained in:
Stefano Pigozzi 2021-04-26 18:36:41 +02:00
parent 82033c2582
commit 33b2bfb0d2
Signed by untrusted user who does not match committer: steffo
GPG key ID: 6965406171929D01
40 changed files with 576 additions and 283 deletions

View file

@ -5,7 +5,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/nest_backend" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/nest_backend" isTestSource="false" />
</content> </content>
<orderEntry type="jdk" jdkName="Poetry (g2-progetto) (2)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Poetry (backend)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View file

@ -1,7 +1,4 @@
import classNames from "classnames"
import Style from "./App.module.css"
import Layout from "./components/Layout" import Layout from "./components/Layout"
import ContextTheme from "./contexts/ContextTheme"
import { BrowserRouter } from "react-router-dom" import { BrowserRouter } from "react-router-dom"
import { Route, Switch } from "react-router" import { Route, Switch } from "react-router"
import PageDashboard from "./routes/PageDashboard" import PageDashboard from "./routes/PageDashboard"
@ -9,11 +6,11 @@ import PageRepositories from "./routes/PageRepositories"
import PageAlerts from "./routes/PageAlerts" import PageAlerts from "./routes/PageAlerts"
import PageSettings from "./routes/PageSettings" import PageSettings from "./routes/PageSettings"
import PageSandbox from "./routes/PageSandbox" import PageSandbox from "./routes/PageSandbox"
import useSavedTheme from "./hooks/useSavedTheme"
import PageLogin from "./routes/PageLogin" import PageLogin from "./routes/PageLogin"
import useSavedLogin from "./hooks/useSavedLogin"
import ContextLogin from "./contexts/ContextLogin"
import PageRoot from "./routes/PageRoot" import PageRoot from "./routes/PageRoot"
import GlobalTheme from "./components/GlobalTheme"
import GlobalServer from "./components/GlobalServer"
import GlobalUser from "./components/GlobalUser"
/** /**
@ -23,44 +20,41 @@ import PageRoot from "./routes/PageRoot"
* @constructor * @constructor
*/ */
export default function App() { export default function App() {
const theme = useSavedTheme();
const login = useSavedLogin();
return ( return (
<ContextTheme.Provider value={theme}> <GlobalServer>
<ContextLogin.Provider value={login}> <GlobalUser>
<BrowserRouter> <GlobalTheme>
<BrowserRouter>
<div className={classNames(Style.App, theme)}> <Layout>
<Layout> <Switch>
<Switch> <Route path={"/login"} exact={true}>
<Route path={"/login"} exact={true}> <PageLogin/>
<PageLogin/> </Route>
</Route> <Route path={"/repositories"} exact={true}>
<Route path={"/repositories"} exact={true}> <PageRepositories/>
<PageRepositories/> </Route>
</Route> <Route path={"/alerts"} exact={true}>
<Route path={"/alerts"} exact={true}> <PageAlerts/>
<PageAlerts/> </Route>
</Route> <Route path={"/settings"} exact={true}>
<Route path={"/settings"} exact={true}> <PageSettings/>
<PageSettings/> </Route>
</Route> <Route path={"/sandbox"} exact={true}>
<Route path={"/sandbox"} exact={true}> <PageSandbox/>
<PageSandbox/> </Route>
</Route> <Route path={"/dashboard"} exact={true}>
<Route path={"/dashboard"} exact={true}> <PageDashboard/>
<PageDashboard/> </Route>
</Route> <Route path={"/"}>
<Route path={"/"}> <PageRoot/>
<PageRoot/> </Route>
</Route> </Switch>
</Switch> </Layout>
</Layout>
</div>
</BrowserRouter> </BrowserRouter>
</ContextLogin.Provider> </GlobalTheme>
</ContextTheme.Provider> </GlobalUser>
</GlobalServer>
) )
} }

View file

@ -1,9 +0,0 @@
.App {
height: 100vh;
width: 100vw;
padding: 16px;
background-color: var(--bg-primary);
color: var(--fg-primary);
}

View file

@ -0,0 +1,22 @@
import React from "react"
import Style from "./BoxAlert.module.css"
import classNames from "classnames"
/**
* A colored alert box to draw the user's attention.
*
* @param color - The color of the alert.
* @param children - The contents of the alert.
* @param className - Additional class(es) to add to the div.
* @param props - Additional props to pass to the div.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxAlert({ color, children, className, ...props }) {
return (
<div className={classNames(Style.BoxAlert, Style[`BoxAlert${color}`], className)} {...props}>
{children}
</div>
)
}

View file

@ -0,0 +1,25 @@
.BoxAlert {
padding: 5px 20px;
border-radius: 25px;
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
}
.BoxAlertRed {
color: var(--fg-red);
background-color: var(--bg-red);
}
.BoxAlertGreen {
color: var(--fg-green);
background-color: var(--bg-green);
}
.BoxAlertYellow {
color: var(--fg-yellow);
background-color: var(--bg-yellow);
}
.BoxAlertGrey {
color: var(--fg-grey);
background-color: var(--bg-grey);
}

View file

@ -5,7 +5,7 @@ import BoxFull from "./BoxFull"
/** /**
* A {@link BoxFull} whose body does not grow automatically but instead supports scrolling. * A {@link BoxFull} whose body supports scrolling.
* *
* @param children - The contents of the box body. * @param children - The contents of the box body.
* @param childrenClassName - Additional class(es) added to the inner `<div>` acting as the body. * @param childrenClassName - Additional class(es) added to the inner `<div>` acting as the body.

View file

@ -3,20 +3,28 @@ import BoxFull from "./BoxFull"
import LoggedInUser from "./LoggedInUser" import LoggedInUser from "./LoggedInUser"
import Button from "./Button" import Button from "./Button"
import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons" import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons"
import ContextLogin from "../contexts/ContextLogin" import ContextUser from "../contexts/ContextUser"
import { useHistory } from "react-router" import { useHistory } from "react-router"
import Style from "./BoxLoggedIn.module.css" import Style from "./BoxLoggedIn.module.css"
import CurrentServer from "./CurrentServer"
/**
* A {@link BoxFull} displaying the user's current login status, and allowing them to logout.
*
* @param props
* @returns {JSX.Element}
* @constructor
*/
export default function BoxLoggedIn({ ...props }) { export default function BoxLoggedIn({ ...props }) {
const {logout} = useContext(ContextLogin) const {logout} = useContext(ContextUser)
const history = useHistory() const history = useHistory()
return ( return (
<BoxFull header={"Logged in"} {...props}> <BoxFull header={"Logged in"} {...props}>
<div className={Style.BoxLoggedInContents}> <div className={Style.BoxLoggedInContents}>
<div> <div>
You are currently logged in as <LoggedInUser/>. You are currently logged in at <CurrentServer/> as <LoggedInUser/>.
</div> </div>
<div> <div>
<Button color={"Red"} onClick={e => { <Button color={"Red"} onClick={e => {

View file

@ -0,0 +1,85 @@
import React, { useContext, useState } from "react"
import FormLabelled from "./FormLabelled"
import FormLabel from "./FormLabel"
import InputWithIcon from "./InputWithIcon"
import { faArrowRight, faEnvelope, faKey } from "@fortawesome/free-solid-svg-icons"
import BoxFull from "./BoxFull"
import FormButton from "./FormButton"
import ContextUser from "../contexts/ContextUser"
import { useHistory } from "react-router"
import FormAlert from "./FormAlert"
/**
* A {@link BoxFull} allowing the user to login.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxLogin({ ...props }) {
const [email, setEmail] = useState("admin@admin.com")
const [password, setPassword] = useState("password")
const [working, setWorking] = useState(false)
const [error, setError] = useState(null)
const {login} = useContext(ContextUser)
const history = useHistory()
const doLogin = async (event) => {
if(working) {
return;
}
setWorking(true)
try {
await login(email, password)
history.push("/dashboard")
}
catch(e) {
setError(e)
}
finally {
setWorking(false)
}
}
return (
<BoxFull header={"Login"} {...props}>
<FormLabelled>
<FormLabel text={"Email"} htmlFor={"login-email"}>
<InputWithIcon
id={"login-email"}
name={"login-email"}
value={email}
onChange={e => setEmail(e.target.value)}
type={"email"}
icon={faEnvelope}
/>
</FormLabel>
<FormLabel text={"Password"} htmlFor={"login-password"}>
<InputWithIcon
id={"login-password"}
name={"login-password"}
value={password}
onChange={e => setPassword(e.target.value)}
type={"password"}
icon={faKey}
/>
</FormLabel>
{error ?
<FormAlert color={"Red"}>
{error.toString()}
</FormAlert>
: null}
<FormButton
onClick={doLogin}
icon={faArrowRight}
color={"Green"}
disabled={working}
>
Login
</FormButton>
</FormLabelled>
</BoxFull>
)
}

View file

@ -0,0 +1,35 @@
import React, { useContext } from "react"
import BoxFull from "./BoxFull"
import FormLabelled from "./FormLabelled"
import FormLabel from "./FormLabel"
import InputWithIcon from "./InputWithIcon"
import { faGlobe } from "@fortawesome/free-solid-svg-icons"
import ContextServer from "../contexts/ContextServer"
/**
* A {@link BoxFull} allowing the user to select the backend server they want to login to.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxSetServer({ ...props }) {
const {server, setServer} = useContext(ContextServer);
return (
<BoxFull header={"Choose server"} {...props}>
<FormLabelled>
<FormLabel text={"Base URL"} htmlFor={"set-server-base-url"}>
<InputWithIcon
id={"set-server-base-url"}
type={"url"}
icon={faGlobe}
value={server}
onChange={e => setServer(e.target.value)}
/>
</FormLabel>
</FormLabelled>
</BoxFull>
)
}

View file

@ -7,7 +7,7 @@ import { useRouteMatch } from "react-router"
/** /**
* A button residing in the sidebar, used to switch between pages. * A button residing in the {@link Sidebar}, used to switch between pages.
* *
* @param icon - The FontAwesome IconDefinition of the icon that should be rendered in the button. * @param icon - The FontAwesome IconDefinition of the icon that should be rendered in the button.
* @param children - The contents of the button. * @param children - The contents of the button.

View file

@ -0,0 +1,22 @@
import React, { useContext } from "react"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import { faGlobe } from "@fortawesome/free-solid-svg-icons"
import ContextServer from "../contexts/ContextServer"
/**
* An element displaying inline the current backend server.
*
* @param props - Additional props to pass to the element.
* @returns {JSX.Element}
* @constructor
*/
export default function CurrentServer({ ...props }) {
const {server} = useContext(ContextServer);
return (
<b {...props}>
<FontAwesomeIcon icon={faGlobe}/> {server}
</b>
)
}

View file

@ -0,0 +1,22 @@
import React from "react"
import Style from "./FormAlert.module.css"
import classNames from "classnames"
import BoxAlert from "./BoxAlert"
/**
* A {@link BoxAlert} ready to be used in a {@link FormLabelled}.
*
* @param children - The contents of the alert.
* @param className - Additional class(es) to add to the box.
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function FormAlert({ children, className, ...props }) {
return (
<BoxAlert className={classNames(Style.FormAlert, className)} {...props}>
{children}
</BoxAlert>
)
}

View file

@ -0,0 +1,3 @@
.FormAlert {
grid-column: 1 / 3;
}

View file

@ -0,0 +1,19 @@
import React from "react"
import Style from "./FormButton.module.css"
import classNames from "classnames"
import Button from "./Button"
/**
* A {@link Button} ready to be used in a {@link FormLabelled}.
*
* @returns {JSX.Element}
* @constructor
*/
export default function FormButton({ children, className, ...props }) {
return (
<Button className={classNames(Style.FormButton, className)} {...props}>
{children}
</Button>
)
}

View file

@ -0,0 +1,3 @@
.FormButton {
grid-column: 1 / 3;
}

View file

@ -1,9 +1,10 @@
import React, { Fragment } from "react" import React, { Fragment } from "react"
import Style from "./Label.module.css" import Style from "./FormLabel.module.css"
/** /**
* A row of a {@link LabelledForm}. * A row of a {@link FormLabelled}.
*
* It displays a label on the first column and a container for the labelled element on the second column. * It displays a label on the first column and a container for the labelled element on the second column.
* *
* @param children - The labelled element. * @param children - The labelled element.
@ -12,7 +13,7 @@ import Style from "./Label.module.css"
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export default function Label({ children, text, htmlFor }) { export default function FormLabel({ children, text, htmlFor }) {
return ( return (
<Fragment> <Fragment>
<label htmlFor={htmlFor} className={Style.LabelText} > <label htmlFor={htmlFor} className={Style.LabelText} >

View file

@ -1,20 +1,17 @@
import React from "react" import React from "react"
import Style from "./LabelledForm.module.css" import Style from "./FormLabelled.module.css"
import classNames from "classnames" import classNames from "classnames"
/** /**
* A form with two columns: the leftmost one contains labels, while the rightmost one contains input elements. * A form with two columns: the leftmost one contains labels, while the rightmost one contains input elements.
* *
* The {@link Label} element can be used to quickly generate labelled input elements. * The {@link FormLabel} element can be used to quickly generate labelled input elements.
* *
* @param children - The contents of the form.
* @param className - Additional class(es) to be added to the form.
* @param props - Additional props to be passed to the form.
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export default function LabelledForm({ children, className, ...props }) { export default function FormLabelled({ children, className, ...props }) {
return ( return (
<form className={classNames(Style.LabelledForm, className)} {...props}> <form className={classNames(Style.LabelledForm, className)} {...props}>
{children} {children}

View file

@ -0,0 +1,64 @@
import React from "react"
import useLocalStorageState from "../hooks/useLocalStorageState"
import ContextServer from "../contexts/ContextServer"
/**
* Provides {@link ContextServer} to all contained elements.
*
* Defaults to using `http://127.0.0.1:5000` as server address.
*
* @param children
* @returns {JSX.Element}
* @constructor
*/
export default function GlobalServer({ children }) {
const [server, setServer] = useLocalStorageState("server", "http://127.0.0.1:5000")
/**
* Fetch JSON data from the API.
*
* @param method - The method to use.
* @param path - The path to request data at (ex. `/api/repositories`)
* @param body - The body of the request (it will be automatically converted to JSON.
* @param init - Additional arguments to pass to the `init` parameter of {@link fetch}.
* @returns {Promise<*>}
*/
const fetchData = async (method, path, body, init) => {
if(!server) {
throw new Error(`Invalid server: ${server}`)
}
if(!init) {
init = {}
}
if(!init["headers"]) {
init["headers"] = {}
}
init["headers"]["Content-Type"] = "application/json"
const response = await fetch(`${server}${path}`, {
method: method,
body: JSON.stringify(body),
...init,
})
if(response.status >= 500) {
throw new Error(`Server error: ${response.status} ${response.statusText}`)
}
const json = await response.json()
if(json["result"] !== "success") {
throw new Error(`Request failed: ${json["msg"]}`)
}
return json["data"]
}
return (
<ContextServer.Provider value={{server, setServer, fetchData}}>
{children}
</ContextServer.Provider>
)
}

View file

@ -0,0 +1,21 @@
import React from "react"
import useLocalStorageState from "../hooks/useLocalStorageState"
import ContextTheme from "../contexts/ContextTheme"
/**
* Provides {@link ContextTheme} to all contained elements.
*
* @param children
* @returns {JSX.Element}
* @constructor
*/
export default function GlobalTheme({ children }) {
const [theme, setTheme] = useLocalStorageState("theme", "ThemeDark")
return (
<ContextTheme.Provider value={{theme, setTheme}}>
{children}
</ContextTheme.Provider>
)
}

View file

@ -0,0 +1,86 @@
import React, { useContext } from "react"
import useLocalStorageState from "../hooks/useLocalStorageState"
import ContextServer from "../contexts/ContextServer"
import ContextUser from "../contexts/ContextUser"
/**
* Provides {@link ContextUser} to all contained elements.
*
* @param children
* @returns {JSX.Element}
* @constructor
*/
export default function GlobalUser({ children }) {
const {fetchData} = useContext(ContextServer)
const [user, setUser] = useLocalStorageState("login", null)
/**
* Fetch JSON data from the API as the authenticated user.
*
* Requires an user to be logged in!
*
* @param method - The method to use.
* @param path - The path to request data at (ex. `/api/repositories`)
* @param body - The body of the request (it will be automatically converted to JSON.
* @param init - Additional arguments to pass to the `init` parameter of {@link fetch}.
* @returns {Promise<*>}
*/
const fetchDataAuth = async (method, path, body, init) => {
if(!user) {
throw new Error("Not logged in")
}
if(!init) {
init = {}
}
if(!init["headers"]) {
init["headers"] = {}
}
init["headers"]["Authorization"] = `Bearer ${user["token"]}`
return await fetchData(path, init)
}
/**
* Try to login to the active server with the passed credentials.
*
* @param email - The user's email.
* @param password - The user's password.
* @returns {Promise<void>}
*/
const login = async (email, password) => {
console.debug("Contacting server to login...")
const data = await fetchData("POST", `/api/login`, {
"email": email,
"password": password,
})
console.debug("Storing login state...")
setUser({
email: data["user"]["email"],
isAdmin: data["user"]["isAdmin"],
username: data["user"]["username"],
token: data["access_token"],
})
console.info("Login successful!")
}
/**
* Logout from the currently active server.
*/
const logout = () => {
console.debug("Clearing login state...")
setUser(null)
console.debug("Cleared login state!")
console.info("Logout successful!")
}
return (
<ContextUser.Provider value={{user, login, logout, fetchDataAuth}}>
{children}
</ContextUser.Provider>
)
}

View file

@ -1,7 +1,8 @@
import React from "react" import React, { useContext } from "react"
import Style from "./Layout.module.css" import Style from "./Layout.module.css"
import classNames from "classnames" import classNames from "classnames"
import Sidebar from "./Sidebar" import Sidebar from "./Sidebar"
import ContextTheme from "../contexts/ContextTheme"
/** /**
@ -14,12 +15,14 @@ import Sidebar from "./Sidebar"
* @constructor * @constructor
*/ */
export default function Layout({ children, className, ...props }) { export default function Layout({ children, className, ...props }) {
const { theme } = useContext(ContextTheme)
return ( return (
<div className={classNames(Style.Layout, className)} {...props}> <div className={classNames(theme, Style.Layout, className)} {...props}>
<Sidebar className={Style.LayoutSidebar}/> <Sidebar className={Style.LayoutSidebar}/>
<div className={Style.LayoutContent}> <main className={Style.LayoutContent}>
{children} {children}
</div> </main>
</div> </div>
) )
} }

View file

@ -1,9 +1,12 @@
.Layout { .Layout {
width: 100%; height: 100vh;
height: 100%; width: 100vw;
padding: 16px;
background-color: var(--bg-primary);
color: var(--fg-primary);
display: grid; display: grid;
grid-template-areas: grid-template-areas:
"a b" "a b"
; ;

View file

@ -1,25 +1,30 @@
import React, { useContext } from "react" import React, { useContext } from "react"
import Style from "./LoggedInUser.module.css"
import classNames from "classnames"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faUser } from "@fortawesome/free-solid-svg-icons" import { faUser } from "@fortawesome/free-solid-svg-icons"
import ContextLogin from "../contexts/ContextLogin" import ContextUser from "../contexts/ContextUser"
export default function LoggedInUser({ children, className, ...props }) { /**
const {state} = useContext(ContextLogin); * An element displaying inline the currently logged in user.
*
* @param props - Additional props to pass to the element.
* @returns {JSX.Element}
* @constructor
*/
export default function LoggedInUser({ ...props }) {
const {user} = useContext(ContextUser);
if(!state) { if(!user) {
return ( return (
<i className={classNames(Style.LoggedInUser, className)} {...props}> <i {...props}>
Not logged in Not logged in
</i> </i>
) )
} }
return ( return (
<b className={classNames(Style.LoggedInUser, className)} {...props}> <b {...props}>
<FontAwesomeIcon icon={faUser}/> {state.username} <FontAwesomeIcon icon={faUser}/> {user.username}
</b> </b>
) )
} }

View file

@ -1,3 +0,0 @@
.LoggedInUser {
}

View file

@ -19,7 +19,7 @@ import classNames from "classnames"
export default function Logo({ className, ...props }) { export default function Logo({ className, ...props }) {
// I have no idea why IntelliJ is complaining about this line // I have no idea why IntelliJ is complaining about this line
// It's perfectly fine! // It's perfectly fine!
const [theme, ] = useContext(ContextTheme) const {theme} = useContext(ContextTheme)
let logo; let logo;
if(theme === "ThemeDark") { if(theme === "ThemeDark") {

View file

@ -27,10 +27,7 @@
} }
.IconContainer { .IconContainer {
margin-top: 5px; margin: 5px 15px 5px 5px;
margin-right: 15px;
margin-left: 5px;
margin-bottom: 5px;
width: 50px; width: 50px;
height: 50px; height: 50px;
border-radius: 50px; border-radius: 50px;

View file

@ -12,7 +12,7 @@ import ContextTheme from "../contexts/ContextTheme"
* @constructor * @constructor
*/ */
export default function SelectTheme({ ...props }) { export default function SelectTheme({ ...props }) {
const [theme, setTheme] = useContext(ContextTheme); const {theme, setTheme} = useContext(ContextTheme);
return ( return (
<Select value={theme} onChange={e => setTheme(e.target.value)} {...props}> <Select value={theme} onChange={e => setTheme(e.target.value)} {...props}>

View file

@ -4,7 +4,7 @@ import classNames from "classnames"
import Logo from "./Logo" import Logo from "./Logo"
import ButtonSidebar from "./ButtonSidebar" import ButtonSidebar from "./ButtonSidebar"
import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons" import { faCog, faExclamationTriangle, faFolder, faHome, faKey, faWrench } from "@fortawesome/free-solid-svg-icons"
import ContextLogin from "../contexts/ContextLogin" import ContextUser from "../contexts/ContextUser"
/** /**
@ -17,13 +17,13 @@ import ContextLogin from "../contexts/ContextLogin"
* @constructor * @constructor
*/ */
export default function Sidebar({ className, ...props }) { export default function Sidebar({ className, ...props }) {
const {state} = useContext(ContextLogin) const {user} = useContext(ContextUser)
return ( return (
<div className={classNames(Style.Sidebar, className)} {...props}> <aside className={classNames(Style.Sidebar, className)} {...props}>
<Logo/> <Logo/>
{ {
state ? user ?
<Fragment> <Fragment>
<ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar> <ButtonSidebar to={"/dashboard"} icon={faHome}>Dashboard</ButtonSidebar>
<ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar> <ButtonSidebar to={"/repositories"} icon={faFolder}>Repositories</ButtonSidebar>
@ -40,6 +40,6 @@ export default function Sidebar({ className, ...props }) {
<ButtonSidebar to={"/sandbox"} icon={faWrench}>Sandbox</ButtonSidebar> <ButtonSidebar to={"/sandbox"} icon={faWrench}>Sandbox</ButtonSidebar>
: null : null
} }
</div> </aside>
) )
} }

View file

@ -1,21 +0,0 @@
import {createContext} from "react";
/**
* A context containing an object with the following values:
* - `state`: `null` if the user is not logged in, otherwise an object containing:
* - `server`: The base url of the N.E.S.T. backend
* - `email`: The email of the account the user is logged in as
* - `token`: The bearer token to use in authenticated API requests
* - `working`: `true` if the login procedure is running, `false` otherwise
* - `error`: `null` if no login error happened, an instance of {@link Error} otherwise.
* - `login`: an async function which logs in the user if given the following parameters: `server, email, password`
* - `logout`: a function which logs out the user
* - `fetch_unauth`: a variant of {@link fetch} which uses `state.server` as the base url, allowing only the API path
* to be passed
* - `fetch_auth`: a variant of {@link fetch_unauth} which automatically passes the correct `Authentication` header
*
* @type {React.Context}
*/
const ContextLogin = createContext(null)
export default ContextLogin

View file

@ -0,0 +1,22 @@
import {createContext} from "react";
/**
* A context containing an object with the following values:
* - `server`: the base URL of the currently active backend server
* - `setServer`: a function to change `server`
* - `fetchData`: a function to fetch JSON data from the backend server
*
* If accessed from outside a provider, the values will be:
* - `server`: `null`
* - `setServer`: a function showing an error in the console
* - `fetchData`: another function showing an error in the console
*
* @type {React.Context}
*/
const ContextServer = createContext({
server: null,
setServer: () => console.error("Trying to setServer while outside a ContextServer.Provider!"),
fetchData: () => console.error("Trying to fetchData while outside a ContextServer.Provider!"),
})
export default ContextServer

View file

@ -2,11 +2,18 @@ import {createContext} from "react";
/** /**
* A context containing a list with the currently selected theme and the function to set a different one. * A context containing an object with the following elements:
* - `theme` - A string containing the name of the current theme.
* - `setTheme` - A function that allows changing the `theme`.
* *
* The function can be `undefined` if run outside a provider. * If trying to access the context from outside a provider, the theme will be `ThemeDark`, and the function will display
* an error in the console.
* *
* @type {React.Context} * @type {React.Context}
*/ */
const ContextTheme = createContext(["ThemeDark", undefined]) const ContextTheme = createContext({
isSet: false,
theme: "ThemeDark",
setTheme: () => console.error("Trying to setTheme while outside a ContextTheme.Provider!")
})
export default ContextTheme export default ContextTheme

View file

@ -0,0 +1,26 @@
import {createContext} from "react";
/**
* A context containing an object with the following values:
* - `user`: an object containing data about the currently logged in user
* - `login`: a function accepting `email, password` as parameters which tries to login the user
* - `logout`: a function accepting no parameters which logs the user out
* - `fetchDataAuth`: a function with the same API as `fetchData` which fetches data from the server authenticating
* as the logged in user.
*
* If accessed from outside a provider, the values will be:
* - `user`: `null`
* - `login`: a function showing an error in the console
* - `logout`: another function showing an error in the console
* - `fetchDataAuth`: yet another function showing an error in the console
*
* @type {React.Context}
*/
const ContextUser = createContext({
user: null,
login: () => console.error("Trying to login while outside a ContextUser.Provider!"),
logout: () => console.error("Trying to logout while outside a ContextUser.Provider!"),
fetchDataAuth: () => console.error("Trying to fetchDataAuth while outside a ContextUser.Provider!"),
})
export default ContextUser

View file

@ -1,82 +0,0 @@
import {useState} from "react"
import useLocalStorageState from "./useLocalStorageState"
/**
* Hook to place at the root to generate the contents of {@link ContextLogin}.
*/
export default function useSavedLogin() {
const [state, setState] = useLocalStorageState("login", null)
const [working, setWorking] = useState(false)
const [error, setError] = useState(null)
const login = async (server, email, password) => {
setWorking(true)
try {
console.debug("Contacting server to login...")
const response = await fetch(`${server}/api/login`, {
method: "POST",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"email": email,
"password": password,
})
})
console.debug("Decoding server response...")
const data = await response.json()
console.debug("Ensuring the request was a success...")
if(data["result"] !== "success") {
// noinspection ExceptionCaughtLocallyJS
throw new Error(data["msg"])
}
console.debug("Storing login state...")
setState({
server: server,
email: data["user"]["email"],
isAdmin: data["user"]["isAdmin"],
username: data["user"]["username"],
token: data["access_token"],
})
console.debug("Clearing error...")
setError(null)
console.info("Login successful!")
} catch(e) {
console.error(`Caught error while trying to login: ${e}`)
setError(e)
} finally {
setWorking(false)
}
}
const logout = () => {
console.debug("Clearing login state...")
setState(null)
console.debug("Cleared login state!")
console.info("Logout successful!")
}
const fetch_unauth = async (path, init) => {
return await fetch(`${state["server"]}${path}`, init)
}
const fetch_auth = async (path, init) => {
if(typeof init["headers"] != "object") {
init["headers"] = {}
}
if(state) {
init["Authorization"] = `Bearer ${state["token"]}`
}
return await fetch_unauth(path, init)
}
return {state, working, error, login, logout, fetch_unauth, fetch_auth}
}

View file

@ -1,12 +0,0 @@
import useLocalStorageState from "./useLocalStorageState"
/**
* Hook with the same API as {@link React.useState} which stores the user's current theme setting, and syncs it to the
* browser's {@link localStorage}.
*
* @returns {[string, function]}
*/
export default function useSavedTheme() {
return useLocalStorageState("theme", "ThemeDark")
}

View file

@ -24,6 +24,10 @@ h1, h2, h3, h4, h5, h6 {
font-family: var(--font-title); font-family: var(--font-title);
} }
*[disabled] {
cursor: not-allowed;
}
.ThemeDark { .ThemeDark {
--bg-primary: #3B7097; --bg-primary: #3B7097;
--fg-primary: #FFFFFF; --fg-primary: #FFFFFF;

View file

@ -8,8 +8,8 @@ import InputWithIcon from "../components/InputWithIcon"
import { faFolder, faPlus } from "@fortawesome/free-solid-svg-icons" import { faFolder, faPlus } from "@fortawesome/free-solid-svg-icons"
import Radio from "../components/Radio" import Radio from "../components/Radio"
import Button from "../components/Button" import Button from "../components/Button"
import LabelledForm from "../components/LabelledForm" import FormLabelled from "../components/FormLabelled"
import Label from "../components/Label" import FormLabel from "../components/FormLabel"
export default function PageDashboard({ children, className, ...props }) { export default function PageDashboard({ children, className, ...props }) {
@ -34,11 +34,11 @@ export default function PageDashboard({ children, className, ...props }) {
🚧 Not implemented. 🚧 Not implemented.
</BoxFull> </BoxFull>
<BoxFull className={Style.CreateDialog} header={"Create repository"}> <BoxFull className={Style.CreateDialog} header={"Create repository"}>
<LabelledForm> <FormLabelled>
<Label htmlFor={"repo-name"} text={"Repository name"}> <FormLabel htmlFor={"repo-name"} text={"Repository name"}>
<InputWithIcon id={"repo-name"} icon={faFolder}/> <InputWithIcon id={"repo-name"} icon={faFolder}/>
</Label> </FormLabel>
<Label htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}> <FormLabel htmlFor={"filter-mode"} text={"Add tweets if they satisfy"}>
<label> <label>
<Radio name={"filter-mode"} value={"or"}/> At least one filter <Radio name={"filter-mode"} value={"or"}/> At least one filter
</label> </label>
@ -46,11 +46,11 @@ export default function PageDashboard({ children, className, ...props }) {
<label> <label>
<Radio name={"filter-mode"} value={"and"}/> Every filter <Radio name={"filter-mode"} value={"and"}/> Every filter
</label> </label>
</Label> </FormLabel>
<Button style={{"gridColumn": "1 / 3"}} icon={faPlus} color={"Green"}> <Button style={{"gridColumn": "1 / 3"}} icon={faPlus} color={"Green"}>
Create repository Create repository
</Button> </Button>
</LabelledForm> </FormLabelled>
</BoxFull> </BoxFull>
</div> </div>
) )

View file

@ -2,71 +2,17 @@ import React, { useContext, useState } from "react"
import Style from "./PageLogin.module.css" import Style from "./PageLogin.module.css"
import classNames from "classnames" import classNames from "classnames"
import BoxFull from "../components/BoxFull" import BoxFull from "../components/BoxFull"
import LabelledForm from "../components/LabelledForm" import ContextUser from "../contexts/ContextUser"
import Label from "../components/Label"
import InputWithIcon from "../components/InputWithIcon"
import { faArrowRight, faEnvelope, faGlobe, faKey } from "@fortawesome/free-solid-svg-icons"
import Button from "../components/Button"
import ContextLogin from "../contexts/ContextLogin"
import { useHistory } from "react-router" import { useHistory } from "react-router"
import BoxSetServer from "../components/BoxSetServer"
import BoxLogin from "../components/BoxLogin"
export default function PageLogin({ className, ...props }) { export default function PageLogin({ className, ...props }) {
// TODO: Change these presets before production
const [server, setServer] = useState("http://localhost:5000")
const [email, setEmail] = useState("admin@admin.com")
const [password, setPassword] = useState("password")
const {login, working, error} = useContext(ContextLogin)
const history = useHistory()
return ( return (
<div className={classNames(Style.PageLogin, className)} {...props}> <div className={classNames(Style.PageLogin, className)} {...props}>
<BoxFull header={"Login"}> <BoxSetServer/>
<LabelledForm> <BoxLogin/>
<Label text={"Server"} htmlFor={"login-server"}>
<InputWithIcon
id={"login-server"}
name={"login-server"}
value={server}
onChange={e => setServer(e.target.value)}
type={"url"}
icon={faGlobe}
/>
</Label>
<Label text={"Email"} htmlFor={"login-email"}>
<InputWithIcon
id={"login-email"}
name={"login-email"}
value={email}
onChange={e => setEmail(e.target.value)}
type={"email"}
icon={faEnvelope}
/>
</Label>
<Label text={"Password"} htmlFor={"login-password"}>
<InputWithIcon
id={"login-password"}
name={"login-password"}
value={password}
onChange={e => setPassword(e.target.value)}
type={"password"}
icon={faKey}
/>
</Label>
<Button
style={{"gridColumn": "1 / 3"}}
onClick={async e => {
await login(server, email, password)
history.push("/dashboard")
}}
icon={faArrowRight}
color={error ? "Green" : "Red"}
disabled={working}
>
Login
</Button>
</LabelledForm>
</BoxFull>
</div> </div>
) )
} }

View file

@ -1,10 +1,10 @@
import React, { useContext } from "react" import React, { useContext } from "react"
import ContextLogin from "../contexts/ContextLogin" import ContextUser from "../contexts/ContextUser"
import { Redirect } from "react-router" import { Redirect } from "react-router"
export default function PageRoot() { export default function PageRoot() {
const {state} = useContext(ContextLogin) const {state} = useContext(ContextUser)
if(!state) { if(!state) {
return <Redirect to={"/login"}/> return <Redirect to={"/login"}/>