Linter pass

This commit is contained in:
Erik 2023-07-05 13:45:32 +03:00
parent 2da5c82360
commit fd4d72eb21
Signed by: Navy.gif
GPG Key ID: 2532FBBB61C65A68
26 changed files with 1927 additions and 1713 deletions

View File

@ -1,51 +1,52 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"@types/react": "^18.0.37", "@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router": "^6.4.3", "react-router": "^6.4.3",
"react-router-dom": "^6.4.3", "react-router-dom": "^6.4.3",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-top-loading-bar": "^2.3.1", "react-top-loading-bar": "^2.3.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^29.5.1", "@types/jest": "^29.5.1",
"eslint": "^8.27.0", "eslint": "^8.27.0",
"eslint-plugin-react": "^7.31.10", "eslint-plugin-react": "^7.31.10",
"http-proxy-middleware": "^2.0.6" "http-proxy-middleware": "^2.0.6"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
}, "lint": "eslint src/ --fix"
"eslintConfig": { },
"extends": [ "eslintConfig": {
"react-app", "extends": [
"react-app/jest" "react-app",
] "react-app/jest"
}, ]
"browserslist": { },
"production": [ "browserslist": {
">0.2%", "production": [
"not dead", ">0.2%",
"not op_mini all" "not dead",
], "not op_mini all"
"development": [ ],
"last 1 chrome version", "development": [
"last 1 firefox version", "last 1 chrome version",
"last 1 safari version" "last 1 firefox version",
] "last 1 safari version"
} ]
} }
}

View File

@ -1,119 +1,125 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useNavigate} from 'react-router-dom'; import { Navigate, Route, Routes, useNavigate} from 'react-router-dom';
import './css/App.css'; import './css/App.css';
import Home from './pages/Home'; import Home from './pages/Home';
import ErrorBoundary from './util/ErrorBoundary'; import ErrorBoundary from './util/ErrorBoundary';
import Sidebar, { SidebarMenu } from './components/Sidebar'; import Sidebar, { SidebarMenu } from './components/Sidebar';
import UserControls from './components/UserControls'; import UserControls from './components/UserControls';
import { useLoginContext } from './structures/UserContext'; import { useLoginContext } from './structures/UserContext';
import Login from './pages/Login'; import Login from './pages/Login';
import { get, setSession, setSettings } from './util/Util'; import { get, setSession, setSettings } from './util/Util';
import { PrivateRoute } from './structures/PrivateRoute'; import { PrivateRoute } from './structures/PrivateRoute';
import { UnauthedRoute } from './structures/UnauthedRoute'; import { UnauthedRoute } from './structures/UnauthedRoute';
import Admin from './pages/Admin'; import Admin from './pages/Admin';
import TitledPage from './components/TitledPage'; import TitledPage from './components/TitledPage';
import Register from './pages/Register'; import Register from './pages/Register';
import { ClientSettings } from './@types/Other'; import { ClientSettings } from './@types/Other';
import { User } from './@types/ApiStructures'; import { User } from './@types/ApiStructures';
function App() { function App()
{
const [user, updateUser] = useLoginContext();
const [loading, setLoading] = useState(true); const [user, updateUser] = useLoginContext();
const navigate = useNavigate(); const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
(async () => { useEffect(() =>
{
const settings = await get('/api/settings'); (async () =>
setSettings(settings.data as ClientSettings); {
const result = await get('/api/user'); const settings = await get('/api/settings');
if (result.status === 200) { setSettings(settings.data as ClientSettings);
setSession(result.data as User);
updateUser(); const result = await get('/api/user');
} if (result.status === 200)
setLoading(false); {
if (result.data?.twoFactor) return navigate('/login/verify'); setSession(result.data as User);
})(); updateUser();
}, []); }
setLoading(false);
const menuItems = [ if (result.data?.twoFactor)
{ return navigate('/login/verify');
to: '/home', label: 'Home', items: [ })();
{ to: '/profile', label: 'Profile', relative: true }, }, []);
{ to: '/applications', label: 'Applications', relative: true }
] const menuItems = [
}, {
{ to: '/home', label: 'Home', items: [
to: '/admin', label: 'Admin', items: [ { to: '/profile', label: 'Profile', relative: true },
{ to: '/users', label: 'Users', relative: true }, { to: '/applications', label: 'Applications', relative: true }
{ to: '/roles', label: 'Roles', relative: true }, ]
{ to: '/flags', label: 'Flags', relative: true } },
] {
} to: '/admin', label: 'Admin', items: [
]; { to: '/users', label: 'Users', relative: true },
{ to: '/roles', label: 'Roles', relative: true },
if (loading) return null; { to: '/flags', label: 'Flags', relative: true }
]
return ( }
<div className='app'> ];
<div className='background'> if (loading)
return null;
{user ?
<div> return (
<header className="card"> <div className='app'>
<UserControls />
</header> <div className='background'>
<Sidebar>
<SidebarMenu menuItems={menuItems} /> {user ?
</Sidebar> <div>
</div> <header className="card">
: null} <UserControls />
</header>
<div className={`main-content ${user ? "" : "login"}`}> <Sidebar>
<SidebarMenu menuItems={menuItems} />
<ErrorBoundary> </Sidebar>
</div>
<Routes> : null}
<Route path='/home/*' element={<PrivateRoute> <div className={`main-content ${user ? "" : "login"}`}>
<TitledPage title='Home'>
<Home /> <ErrorBoundary>
</TitledPage>
</PrivateRoute >} /> <Routes>
<Route path='/admin/*' element={<PrivateRoute> <Route path='/home/*' element={<PrivateRoute>
<TitledPage title='Admin'> <TitledPage title='Home'>
<Admin /> <Home />
</TitledPage> </TitledPage>
</PrivateRoute >} /> </PrivateRoute >} />
<Route path='/login/*' element={<UnauthedRoute> <Route path='/admin/*' element={<PrivateRoute>
<Login /> <TitledPage title='Admin'>
</UnauthedRoute>} /> <Admin />
</TitledPage>
<Route path='/register/*' element={<UnauthedRoute> </PrivateRoute >} />
<Register />
</UnauthedRoute>} /> <Route path='/login/*' element={<UnauthedRoute>
<Login />
<Route path='*' element={<Navigate to='/home' />} /> </UnauthedRoute>} />
</Routes> <Route path='/register/*' element={<UnauthedRoute>
</ErrorBoundary> <Register />
</UnauthedRoute>} />
<div className={`footer ${user ? "" : "login"}`}>
<p className='m-0'>Made with by <a href="https://corgi.wtf" target="_blank" rel="noreferrer">Navy.gif</a> &nbsp;|&nbsp; Front-end magic by <a href="https://d3vision.dev" target="_blank" rel="noreferrer">D3vision</a></p> <Route path='*' element={<Navigate to='/home' />} />
</div>
</div> </Routes>
</ErrorBoundary>
</div>
<div className={`footer ${user ? "" : "login"}`}>
<p className='m-0'>Made with by <a href="https://corgi.wtf" target="_blank" rel="noreferrer">Navy.gif</a> &nbsp;|&nbsp; Front-end magic by <a href="https://d3vision.dev" target="_blank" rel="noreferrer">D3vision</a></p>
</div> </div>
); </div>
}
</div>
export default App;
</div>
);
}
export default App;

View File

@ -1,217 +1,245 @@
import React, { Children, useRef, useState } from "react"; import React, { Children, useRef, useState } from "react";
import ClickDetector from "../util/ClickDetector"; import ClickDetector from "../util/ClickDetector";
import { DropdownBaseProps, DropdownItemProps } from "../@types/Components"; import { DropdownBaseProps, DropdownItemProps } from "../@types/Components";
import '../css/components/InputElements.css'; import '../css/components/InputElements.css';
export const FileSelector = ({ cb }: { cb: (file: File) => void }) => { export const FileSelector = ({ cb }: { cb: (file: File) => void }) =>
{
if (!cb) throw new Error('Missing callback');
const [file, setFile] = useState<File | null>(null); if (!cb)
throw new Error('Missing callback');
const onDragOver: React.MouseEventHandler = (event) => { const [file, setFile] = useState<File | null>(null);
event.stopPropagation();
event.preventDefault(); const onDragOver: React.MouseEventHandler = (event) =>
}; {
event.stopPropagation();
const onDrop: React.DragEventHandler = (event) => { event.preventDefault();
event.preventDefault(); };
const { dataTransfer } = event;
if (!dataTransfer.files.length) return; const onDrop: React.DragEventHandler = (event) =>
{
const [file] = dataTransfer.files; event.preventDefault();
setFile(file); const { dataTransfer } = event;
cb(file); if (!dataTransfer.files.length)
}; return;
return <label onDrop={onDrop} onDragOver={onDragOver} htmlFor="pfp_upload" className="drop-container"> const [file] = dataTransfer.files;
<span className="drop-title">Drag &apos;n&apos; drop a file here</span> setFile(file);
or cb(file);
<span className="drop-title">Click to select a file</span> };
<p className="fileName m-0">{file ? `Selected: ${file.name}` : null}</p>
<input onChange={(event) => { return <label onDrop={onDrop} onDragOver={onDragOver} htmlFor="pfp_upload" className="drop-container">
if (!event.target.files) return; <span className="drop-title">Drag &apos;n&apos; drop a file here</span>
const [f] = event.target.files; or
setFile(f); <span className="drop-title">Click to select a file</span>
cb(f); <p className="fileName m-0">{file ? `Selected: ${file.name}` : null}</p>
}} <input onChange={(event) =>
type="file" id="pfp_upload" accept="image/*" required></input> {
</label>; if (!event.target.files)
}; return;
const [f] = event.target.files;
export type InputElementProperties<T> = { setFile(f);
value?: T, cb(f);
inputRef?: React.RefObject<HTMLInputElement>, }}
onChange?: React.ChangeEventHandler<HTMLInputElement>, type="file" id="pfp_upload" accept="image/*" required></input>
children?: React.ReactNode, //JSX.Element | JSX.Element[] | string, </label>;
placeholder?: string };
}
export type InputElementProperties<T> = {
export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) => { value?: T,
return <label className="label-fix"> inputRef?: React.RefObject<HTMLInputElement>,
<span className="check-box check-box-row mb-2"> onChange?: React.ChangeEventHandler<HTMLInputElement>,
{children && <b>{children}</b>} children?: React.ReactNode, //JSX.Element | JSX.Element[] | string,
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' /> placeholder?: string
</span> }
</label>;
}; export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) =>
{
export const VerticalToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) => { return <label className="label-fix">
return <label className="label-fix"> <span className="check-box check-box-row mb-2">
{children && <b>{children}</b>} {children && <b>{children}</b>}
<span className="check-box"> <input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' /> </span>
</span> </label>;
</label>; };
};
export const VerticalToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) =>
type ManualInputProperties<T> = { {
onBlur?: React.FocusEventHandler, return <label className="label-fix">
onKeyUp?: React.KeyboardEventHandler {children && <b>{children}</b>}
} & InputElementProperties<T>; <span className="check-box">
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
type StringInputProperties = { </span>
maxLength?: number, </label>;
minLength?: number };
} & ManualInputProperties<string>
export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) => { type ManualInputProperties<T> = {
onBlur?: React.FocusEventHandler,
const input = <input onKeyUp?: React.KeyboardEventHandler
onBlur={onBlur} } & InputElementProperties<T>;
defaultValue={value}
placeholder={placeholder} type StringInputProperties = {
ref={inputRef} maxLength?: number,
onChange={onChange} minLength?: number
onKeyUp={onKeyUp} } & ManualInputProperties<string>
maxLength={maxLength} export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) =>
minLength={minLength} {
/>;
if (children) const input = <input
return <label> onBlur={onBlur}
<b>{children}</b> defaultValue={value}
{input} placeholder={placeholder}
</label>; ref={inputRef}
return input; onChange={onChange}
}; onKeyUp={onKeyUp}
maxLength={maxLength}
export type NumberInputProperties = { minLength={minLength}
min?: number, />;
max?: number, if (children)
type?: 'float' | 'int', return <label>
step?: number <b>{children}</b>
} & ManualInputProperties<number> {input}
</label>;
export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) => { return input;
if (typeof step === 'undefined') { };
if (type === 'float') step = 0.1;
else if (type === 'int') step = 1; export type NumberInputProperties = {
else step = 1; min?: number,
} max?: number,
type?: 'float' | 'int',
const input = <input step?: number
placeholder={placeholder} } & ManualInputProperties<number>
ref={inputRef}
defaultValue={value} export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) =>
onChange={onChange} {
type="number" if (typeof step === 'undefined')
min={min} {
max={max} if (type === 'float')
step={step} step = 0.1;
/>; else if (type === 'int')
step = 1;
if (children) return <label> else
<b>{children}</b> step = 1;
{input} }
</label>;
const input = <input
return input; placeholder={placeholder}
}; ref={inputRef}
defaultValue={value}
export const ClickToEdit = ({ value, onUpdate, inputElement }: onChange={onChange}
{ value: string, onUpdate?: (value: string) => void, inputElement?: React.ReactElement }) => { type="number"
const [editing, setEditing] = useState(false); min={min}
const ref = useRef<HTMLInputElement>(null); max={max}
const onClick = () => { step={step}
setEditing(false); />;
if (ref.current && onUpdate)
onUpdate(ref.current.value); if (children)
}; return <label>
<b>{children}</b>
const input = inputElement ? inputElement : <input defaultValue={value} ref={ref} />; {input}
if (editing) return <span className='input-group'> </label>;
{input}
<button className="button primary" onClick={onClick} > return input;
OK };
</button>
</span>; export const ClickToEdit = ({ value, onUpdate, inputElement }:
return <span onClick={() => setEditing(true)} className='mt-0 mb-1 clickable'>{value}</span>; { value: string, onUpdate?: (value: string) => void, inputElement?: React.ReactElement }) =>
}; {
const [editing, setEditing] = useState(false);
const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) => { const ref = useRef<HTMLInputElement>(null);
return <summary className={`clickable card is-vertical-align header p-2 ${className}`}> const onClick = () =>
{children} {
</summary>; setEditing(false);
}; if (ref.current && onUpdate)
onUpdate(ref.current.value);
const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) => { };
let InnerElement = null;
if (type === 'multi-select') const input = inputElement ? inputElement : <input defaultValue={value} ref={ref} />;
InnerElement = <ToggleSwitch value={selected || false} onChange={onClick}> if (editing)
{children as string} return <span className='input-group'>
</ToggleSwitch>; {input}
else InnerElement = <div onClick={(event) => { <button className="button primary" onClick={onClick} >
event.preventDefault(); OK
if (onClick) onClick(event); </button>
}}> </span>;
{children} return <span onClick={() => setEditing(true)} className='mt-0 mb-1 clickable'>{value}</span>;
</div>; };
return <div className='card item clickable'>
{InnerElement} const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) =>
</div>; {
}; return <summary className={`clickable card is-vertical-align header p-2 ${className}`}>
{children}
const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) => { </summary>;
return <div className={`card item-list ${className}`}> };
{children}
</div>; const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) =>
}; {
let InnerElement = null;
type DropdownProps = { if (type === 'multi-select')
name?: string, InnerElement = <ToggleSwitch value={selected || false} onChange={onClick}>
multi?: boolean, {children as string}
selection?: [], </ToggleSwitch>;
children: React.ReactNode[], else
className?: string InnerElement = <div onClick={(event) =>
} {
event.preventDefault();
const DropdownComp = ({ children, className = '' }: DropdownProps) => { if (onClick)
onClick(event);
if (!children) }}>
throw new Error('Missing children'); {children}
</div>;
if (!children.some(element => element && (element as React.ReactElement).type === DropdownHeader)) return <div className='card item clickable'>
throw new Error('Missing a header'); {InnerElement}
</div>;
const detailsRef = useRef<HTMLDetailsElement>(null); };
return <ClickDetector callback={() => { const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) =>
if (detailsRef.current) detailsRef.current.open = false; {
}}> return <div className={`card item-list ${className}`}>
<details ref={detailsRef} className={`dropdown w-100 ${className}`}> {children}
{children} </div>;
</details> };
</ClickDetector>;
type DropdownProps = {
}; name?: string,
multi?: boolean,
const Dropdown = Object.assign(DropdownComp, { selection?: [],
Header: DropdownHeader, children: React.ReactNode[],
ItemList: DropdownItemList, className?: string
Item: DropdownItem }
});
const DropdownComp = ({ children, className = '' }: DropdownProps) =>
export type InputElementType = {
| ((props: InputElementProperties<string>) => React.ReactElement)
| ((props: InputElementProperties<boolean>) => React.ReactElement) if (!children)
| ((props: NumberInputProperties) => React.ReactElement); throw new Error('Missing children');
if (!children.some(element => element && (element as React.ReactElement).type === DropdownHeader))
throw new Error('Missing a header');
const detailsRef = useRef<HTMLDetailsElement>(null);
return <ClickDetector callback={() =>
{
if (detailsRef.current)
detailsRef.current.open = false;
}}>
<details ref={detailsRef} className={`dropdown w-100 ${className}`}>
{children}
</details>
</ClickDetector>;
};
const Dropdown = Object.assign(DropdownComp, {
Header: DropdownHeader,
ItemList: DropdownItemList,
Item: DropdownItem
});
export type InputElementType =
| ((props: InputElementProperties<string>) => React.ReactElement)
| ((props: InputElementProperties<boolean>) => React.ReactElement)
| ((props: NumberInputProperties) => React.ReactElement);
export { Dropdown }; export { Dropdown };

View File

@ -7,22 +7,27 @@ type PageButtonProps = {
pages: number pages: number
} }
export const PageButtons = ({ setPage, page, pages }: PageButtonProps) => { export const PageButtons = ({ setPage, page, pages }: PageButtonProps) =>
{
return <div className='flex is-vertical-align is-center page-controls'> return <div className='flex is-vertical-align is-center page-controls'>
<button className="button dark" disabled={page === 1} onClick={() => { <button className="button dark" disabled={page === 1} onClick={() =>
{
setPage(page - 1 || 1); setPage(page - 1 || 1);
}}>Previous</button> }}>Previous</button>
<p>Page: {page} / {pages}</p> <p>Page: {page} / {pages}</p>
<button className="button dark" disabled={page === pages} onClick={() => { <button className="button dark" disabled={page === pages} onClick={() =>
{
if (page < pages) if (page < pages)
setPage(page + 1); setPage(page + 1);
}}>Next</button> }}>Next</button>
</div>; </div>;
}; };
export const BackButton = () => { export const BackButton = () =>
{
const navigate = useNavigate(); const navigate = useNavigate();
return <button className="button dark" onClick={() => { return <button className="button dark" onClick={() =>
{
navigate(-1); navigate(-1);
}}>Back</button>; }}>Back</button>;
}; };

View File

@ -1,8 +1,10 @@
import React from "react"; import React from "react";
import '../css/components/PageElements.css'; import '../css/components/PageElements.css';
export const Popup = ({ display = false, children }: { display: boolean, children: React.ReactElement }) => { export const Popup = ({ display = false, children }: { display: boolean, children: React.ReactElement }) =>
if (!display) return null; {
if (!display)
return null;
return <div className='popup'> return <div className='popup'>
{children} {children}
</div>; </div>;

View File

@ -1,82 +1,95 @@
import React, {} from 'react'; import React, {} from 'react';
import '../css/components/Sidebar.css'; import '../css/components/Sidebar.css';
import NavLink from '../structures/NavLink'; import NavLink from '../structures/NavLink';
import logoImg from '../img/logo.png'; import logoImg from '../img/logo.png';
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useLoginContext } from "../structures/UserContext"; import { useLoginContext } from "../structures/UserContext";
import { clearSession, post } from "../util/Util"; import { clearSession, post } from "../util/Util";
import { SidebarMenuItem } from '../@types/Other'; import { SidebarMenuItem } from '../@types/Other';
const Sidebar = ({children}: {children?: React.ReactNode}) => { const Sidebar = ({children}: {children?: React.ReactNode}) =>
return <div className='sidebar card'> {
{children} return <div className='sidebar card'>
</div>; {children}
}; </div>;
};
const toggleMenu: React.MouseEventHandler = (event) => {
event.preventDefault(); const toggleMenu: React.MouseEventHandler = (event) =>
(event.target as HTMLElement).parentElement?.classList.toggle("open"); {
}; event.preventDefault();
(event.target as HTMLElement).parentElement?.classList.toggle("open");
const expandMobileMenu: React.MouseEventHandler = (event) => { };
event.preventDefault();
(event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu"); const expandMobileMenu: React.MouseEventHandler = (event) =>
}; {
event.preventDefault();
const closeMobileMenu: React.MouseEventHandler = (event) => { (event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu");
const element = event.target as HTMLElement; };
if(element.classList.contains("sidebar-menu-item-carrot")) return;
(element.getRootNode() as ParentNode).querySelector(".sidebar")?.classList.remove("show-menu"); const closeMobileMenu: React.MouseEventHandler = (event) =>
}; {
const element = event.target as HTMLElement;
// Nav menu if(element.classList.contains("sidebar-menu-item-carrot"))
const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) => { return;
const [user, updateUser] = useLoginContext(); (element.getRootNode() as ParentNode).querySelector(".sidebar")?.classList.remove("show-menu");
const navigate = useNavigate(); };
if (!user) return null; // Nav menu
const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) =>
const logOut = async () => { {
const response = await post('/api/logout'); const [user, updateUser] = useLoginContext();
if (response.status === 200) { const navigate = useNavigate();
clearSession();
updateUser(); if (!user)
navigate('/login'); return null;
}
}; const logOut = async () =>
{
const elements = []; const response = await post('/api/logout');
for (const menuItem of menuItems) { if (response.status === 200)
const { label, items: subItems = [], ...rest } = menuItem; {
clearSession();
let subElements = null; updateUser();
if (subItems.length) subElements = subItems.map(({ label, to, relative = true }) => { navigate('/login');
if(relative) to = `${menuItem.to}${to}`; }
return <NavLink className='sidebar-menu-item sidebar-menu-child-item' onClick={closeMobileMenu} activeClassName='active' to={to} key={label}>{label}</NavLink>; };
});
const elements = [];
elements.push(<div key={label} className='parent-menu'> for (const menuItem of menuItems)
<NavLink className={`sidebar-menu-item ${subElements?.length ? 'has-children' : ''}`} onClick={closeMobileMenu} activeClassName='active open' {...rest} key={label}> {
{label}{subElements && <i className="sidebar-menu-item-carrot" onClick={toggleMenu}></i>} const { label, items: subItems = [], ...rest } = menuItem;
</NavLink>
{subElements && <div className='sidebar-menu-child-wrapper'> let subElements = null;
{subElements} if (subItems.length)
</div>} subElements = subItems.map(({ label, to, relative = true }) =>
</div>); {
} if(relative)
to = `${menuItem.to}${to}`;
return <div className='sidebar-menu'> return <NavLink className='sidebar-menu-item sidebar-menu-child-item' onClick={closeMobileMenu} activeClassName='active' to={to} key={label}>{label}</NavLink>;
<span className='hamburger' onClick={expandMobileMenu}></span> });
<img className="sidebar-logo" src={logoImg}/>
{elements} elements.push(<div key={label} className='parent-menu'>
<NavLink className={`sidebar-menu-item ${subElements?.length ? 'has-children' : ''}`} onClick={closeMobileMenu} activeClassName='active open' {...rest} key={label}>
{children} {label}{subElements && <i className="sidebar-menu-item-carrot" onClick={toggleMenu}></i>}
<div className="parent-menu log-out-menu-item"> </NavLink>
<a className="sidebar-menu-item" onClick={logOut} href="#">Log Out</a> {subElements && <div className='sidebar-menu-child-wrapper'>
</div> {subElements}
</div>; </div>}
</div>);
}; }
export {SidebarMenu, Sidebar}; return <div className='sidebar-menu'>
<span className='hamburger' onClick={expandMobileMenu}></span>
<img className="sidebar-logo" src={logoImg}/>
{elements}
{children}
<div className="parent-menu log-out-menu-item">
<a className="sidebar-menu-item" onClick={logOut} href="#">Log Out</a>
</div>
</div>;
};
export {SidebarMenu, Sidebar};
export default Sidebar; export default Sidebar;

View File

@ -1,45 +1,47 @@
import React from "react"; import React from "react";
type Item = { type Item = {
[key: string]: unknown [key: string]: unknown
} }
type TableEntry = { type TableEntry = {
onClick?: () => void, onClick?: () => void,
item: Item, item: Item,
itemKeys: string[] itemKeys: string[]
} }
type TableProps = { type TableProps = {
headerItems: string[], headerItems: string[],
children?: React.ReactNode, children?: React.ReactNode,
items?: Item[], items?: Item[],
itemKeys?: string[] itemKeys?: string[]
} }
export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) => { export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) =>
{
return <tr onClick={onClick}>
{itemKeys.map(key => <td key={key}>{item[key] as string}</td>)} return <tr onClick={onClick}>
</tr>; {itemKeys.map(key => <td key={key}>{item[key] as string}</td>)}
}; </tr>;
};
export const Table = ({headerItems, children, items, itemKeys}: TableProps) => {
export const Table = ({headerItems, children, items, itemKeys}: TableProps) =>
if (!headerItems) {
throw new Error(`Missing table headers`);
if(!children && !items) if (!headerItems)
throw new Error('Missing items or children'); throw new Error(`Missing table headers`);
let i = 0; if(!children && !items)
return <table className="striped hover" > throw new Error('Missing items or children');
<thead> let i = 0;
<tr> return <table className="striped hover" >
{headerItems.map(item => <th key={item}>{item}</th>)} <thead>
</tr> <tr>
</thead> {headerItems.map(item => <th key={item}>{item}</th>)}
<tbody> </tr>
{children ? children : items?.map(item => <TableListEntry key={i++} item={item} itemKeys={itemKeys as string[]} />)} </thead>
</tbody> <tbody>
</table>; {children ? children : items?.map(item => <TableListEntry key={i++} item={item} itemKeys={itemKeys as string[]} />)}
</tbody>
</table>;
}; };

View File

@ -1,14 +1,15 @@
import React from "react"; import React from "react";
import '../css/pages/Empty.css'; import '../css/pages/Empty.css';
const TitledPage = ({title, children}: {title: string, children: React.ReactNode}) => { const TitledPage = ({title, children}: {title: string, children: React.ReactNode}) =>
{
return <div className="page">
<h2 className="pageTitle">{title}</h2> return <div className="page">
<div className='card'> <h2 className="pageTitle">{title}</h2>
{children} <div className='card'>
</div> {children}
</div>; </div>
}; </div>;
};
export default TitledPage; export default TitledPage;

View File

@ -1,45 +1,51 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import '../css/components/UserControls.css'; import '../css/components/UserControls.css';
import { useLoginContext } from "../structures/UserContext"; import { useLoginContext } from "../structures/UserContext";
import ClickDetector from "../util/ClickDetector"; import ClickDetector from "../util/ClickDetector";
import { clearSession, post } from "../util/Util"; import { clearSession, post } from "../util/Util";
const UserControls = () => { const UserControls = () =>
{
const [user, updateUser] = useLoginContext();
const detailsRef = useRef<HTMLDetailsElement>(null); const [user, updateUser] = useLoginContext();
const navigate = useNavigate(); const detailsRef = useRef<HTMLDetailsElement>(null);
const navigate = useNavigate();
if (!user) return null;
if (!user)
const logOut = async () => { return null;
const response = await post('/api/logout');
if (response.status === 200) { const logOut = async () =>
clearSession(); {
updateUser(); const response = await post('/api/logout');
navigate('/login'); if (response.status === 200)
} {
}; clearSession();
updateUser();
return <ClickDetector callback={() => { navigate('/login');
if (detailsRef.current) detailsRef.current.removeAttribute('open'); }
}}> };
<details ref={detailsRef} className='dropdown user-controls'>
<summary className="is-vertical-align"> return <ClickDetector callback={() =>
Hello {user.displayName || user.name} {
<img className="profile-picture" src={`/api/users/${user.id}/avatar`}></img> if (detailsRef.current)
</summary> detailsRef.current.removeAttribute('open');
}}>
<div className="card"> <details ref={detailsRef} className='dropdown user-controls'>
<p><a href="#">Profile</a></p> <summary className="is-vertical-align">
<hr /> Hello {user.displayName || user.name}
<p><a onClick={logOut} href="#" className="text-error">Logout</a></p> <img className="profile-picture" src={`/api/users/${user.id}/avatar`}></img>
</div> </summary>
</details> <div className="card">
</ClickDetector >; <p><a href="#">Profile</a></p>
<hr />
}; <p><a onClick={logOut} href="#" className="text-error">Logout</a></p>
</div>
</details>
</ClickDetector >;
};
export default UserControls; export default UserControls;

View File

@ -8,11 +8,11 @@ import { BrowserRouter } from 'react-router-dom';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(<React.StrictMode> root.render(<React.StrictMode>
<UserContext> <UserContext>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</UserContext> </UserContext>
</React.StrictMode>); </React.StrictMode>);
// root.render(<UserContext> // root.render(<UserContext>
// <App /> // <App />

View File

@ -1,51 +1,54 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Route, Routes } from "react-router"; import { Route, Routes } from "react-router";
import { FileSelector } from "../components/InputElements"; import { FileSelector } from "../components/InputElements";
import '../css/pages/Admin.css'; import '../css/pages/Admin.css';
import ErrorBoundary from "../util/ErrorBoundary"; import ErrorBoundary from "../util/ErrorBoundary";
import Roles from "./admin/Roles"; import Roles from "./admin/Roles";
import Users from "./admin/Users"; import Users from "./admin/Users";
import Flags from "./admin/Flags"; import Flags from "./admin/Flags";
const Main = () => { const Main = () =>
const [file, setFile] = useState<File | null>(null); {
const [file, setFile] = useState<File | null>(null);
const submit: React.MouseEventHandler = (event) => {
event.preventDefault(); const submit: React.MouseEventHandler = (event) =>
console.log(file); {
}; event.preventDefault();
console.log(file);
return <div className="row"> };
<div className="col-6-lg col-12"> return <div className="row">
<h2>Thematic customisation</h2>
<form> <div className="col-6-lg col-12">
<input type='text' placeholder='Site name' /> <h2>Thematic customisation</h2>
<FileSelector cb={setFile} /> <form>
<button onClick={submit}>Submit</button> <input type='text' placeholder='Site name' />
</form> <FileSelector cb={setFile} />
<button onClick={submit}>Submit</button>
</div> </form>
<div className="col-6-lg col-12"> </div>
<h2>Dingus</h2>
<div className="col-6-lg col-12">
</div> <h2>Dingus</h2>
</div>; </div>
};
</div>;
const Admin = () => { };
return <ErrorBoundary> const Admin = () =>
<Routes> {
<Route path="/" element={<Main />} />
<Route path="/users/*" element={<Users />} /> return <ErrorBoundary>
<Route path='/roles/*' element={<Roles />} /> <Routes>
<Route path='/flags/*' element={<Flags />} /> <Route path="/" element={<Main />} />
</Routes> <Route path="/users/*" element={<Users />} />
</ErrorBoundary>; <Route path='/roles/*' element={<Roles />} />
}; <Route path='/flags/*' element={<Flags />} />
</Routes>
</ErrorBoundary>;
};
export default Admin; export default Admin;

View File

@ -1,25 +1,27 @@
import React from "react"; import React from "react";
import { Route, Routes } from "react-router"; import { Route, Routes } from "react-router";
import '../css/pages/Home.css'; import '../css/pages/Home.css';
import ErrorBoundary from "../util/ErrorBoundary"; import ErrorBoundary from "../util/ErrorBoundary";
import Applications from "./home/Applications"; import Applications from "./home/Applications";
import Profile from "./home/Profile"; import Profile from "./home/Profile";
const Main = () => { const Main = () =>
{
return <div>
What to put here? hmmm return <div>
</div>; What to put here? hmmm
}; </div>;
};
const Home = () => {
const Home = () =>
return <ErrorBoundary> {
<Routes>
<Route path="/" element={<Main />} /> return <ErrorBoundary>
<Route path="/profile" element={<Profile />} /> <Routes>
<Route path="/applications/*" element={<Applications />} /> <Route path="/" element={<Main />} />
</Routes> <Route path="/profile" element={<Profile />} />
</ErrorBoundary>; <Route path="/applications/*" element={<Applications />} />
}; </Routes>
</ErrorBoundary>;
};
export default Home; export default Home;

View File

@ -1,146 +1,159 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import { Route, Routes, useNavigate } from "react-router"; import { Route, Routes, useNavigate } from "react-router";
import '../css/pages/Login.css'; import '../css/pages/Login.css';
import logoImg from '../img/logo.png'; import logoImg from '../img/logo.png';
import { useLoginContext } from "../structures/UserContext"; import { useLoginContext } from "../structures/UserContext";
import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar'; import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar';
import { fetchUser, getSettings, post } from "../util/Util"; import { fetchUser, getSettings, post } from "../util/Util";
import { OAuthProvider } from "../@types/Other"; import { OAuthProvider } from "../@types/Other";
const CredentialFields = () => { const CredentialFields = () =>
{
const [, setUser] = useLoginContext();
const [fail, setFail] = useState(false); const [, setUser] = useLoginContext();
const navigate = useNavigate(); const [fail, setFail] = useState(false);
const ref = useRef<LoadingBarRef>(null); const navigate = useNavigate();
const usernameRef = useRef<HTMLInputElement>(null); const ref = useRef<LoadingBarRef>(null);
const passwordRef = useRef<HTMLInputElement>(null); const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const loginMethods = getSettings()?.OAuthProviders || [];
const loginMethods = getSettings()?.OAuthProviders || [];
const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) => {
ref.current?.continuousStart(); const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) =>
event.preventDefault(); {
// const username = (document.getElementById('username') as HTMLInputElement)?.value; ref.current?.continuousStart();
// const password = (document.getElementById('password') as HTMLInputElement)?.value; event.preventDefault();
const username = usernameRef.current?.value; // const username = (document.getElementById('username') as HTMLInputElement)?.value;
const password = passwordRef.current?.value; // const password = (document.getElementById('password') as HTMLInputElement)?.value;
if (!username?.length || !password?.length) return; const username = usernameRef.current?.value;
const password = passwordRef.current?.value;
const result = await post('/api/login', { username, password }); if (!username?.length || !password?.length)
return;
if (![200, 302].includes(result.status)) {
ref.current?.complete(); const result = await post('/api/login', { username, password });
setFail(true);
return; if (![200, 302].includes(result.status))
} {
ref.current?.complete();
if (!result.data?.twoFactor) { setFail(true);
setUser(await fetchUser()); return;
ref.current?.complete(); }
return navigate('/home');
} if (!result.data?.twoFactor)
ref.current?.complete(); {
return navigate('verify'); setUser(await fetchUser());
ref.current?.complete();
}; return navigate('/home');
}
return <div> ref.current?.complete();
<LoadingBar color='#2685ff' ref={ref} /> return navigate('verify');
<div className="is-center">
<img className="logoImg mb-4" src={logoImg}></img> };
</div>
return <div>
<div className="card mb-3 is-center dir-column"> <LoadingBar color='#2685ff' ref={ref} />
<h2>Log in</h2> <div className="is-center">
{fail ? <p>Invalid credentials</p> : null} <img className="logoImg mb-4" src={logoImg}></img>
</div>
<form className="loginForm">
<input ref={usernameRef} autoComplete='off' placeholder='Username' required id='username' type='text' autoFocus /> <div className="card mb-3 is-center dir-column">
<input ref={passwordRef} autoComplete='off' placeholder='Password' required id='password' type='password' /> <h2>Log in</h2>
<button className="button primary" onClick={loginClick}>Enter</button> {fail ? <p>Invalid credentials</p> : null}
</form>
<form className="loginForm">
<div className="text-center registerText"> <input ref={usernameRef} autoComplete='off' placeholder='Username' required id='username' type='text' autoFocus />
Don&apos;t have an account?<br/><a onClick={() => navigate('/register')} className="clickable">Register</a> instead! <input ref={passwordRef} autoComplete='off' placeholder='Password' required id='password' type='password' />
</div> <button className="button primary" onClick={loginClick}>Enter</button>
</form>
</div>
<div className="text-center registerText">
<div className="card is-center dir-column"> Don&apos;t have an account?<br/><a onClick={() => navigate('/register')} className="clickable">Register</a> instead!
<b>Alternate login methods</b> </div>
<div className="methodsWrapper is-center flex-wrap">
{loginMethods.map((method: OAuthProvider) => <div </div>
className='third-party-login'
key={method.name} <div className="card is-center dir-column">
onClick={() => { window.location.pathname = `/api/login/${method.name}`; }}> <b>Alternate login methods</b>
<img <div className="methodsWrapper is-center flex-wrap">
crossOrigin="anonymous" {loginMethods.map((method: OAuthProvider) => <div
alt={method.name} className='third-party-login'
src={method.icon} key={method.name}
width={48} height={48} onClick={() =>
/> {
<p><b>{method.name}</b></p> window.location.pathname = `/api/login/${method.name}`;
</div>)} }}>
</div> <img
</div> crossOrigin="anonymous"
</div>; alt={method.name}
}; src={method.icon}
width={48} height={48}
const TwoFactor = () => { />
<p><b>{method.name}</b></p>
const [fail, setFail] = useState(false); </div>)}
const [, setUser] = useLoginContext(); </div>
const navigate = useNavigate(); </div>
const mfaCode = useRef<HTMLInputElement>(null); </div>;
const ref = useRef<LoadingBarRef>(null); };
const twoFactorClick: React.MouseEventHandler = async (event) => { const TwoFactor = () =>
event.preventDefault(); {
// const code = document.getElementById('2faCode').value;
const code = mfaCode.current?.value; const [fail, setFail] = useState(false);
if (!code) return; const [, setUser] = useLoginContext();
ref.current?.continuousStart(); const navigate = useNavigate();
const result = await post('/api/login/verify', { code }); const mfaCode = useRef<HTMLInputElement>(null);
if (result.status === 200) { const ref = useRef<LoadingBarRef>(null);
setUser(await fetchUser());
ref.current?.complete(); const twoFactorClick: React.MouseEventHandler = async (event) =>
return navigate('/home'); {
} event.preventDefault();
ref.current?.complete(); // const code = document.getElementById('2faCode').value;
setFail(true); const code = mfaCode.current?.value;
}; if (!code)
return;
return <div> ref.current?.continuousStart();
<LoadingBar color='#2685ff' ref={ref} /> const result = await post('/api/login/verify', { code });
<div className="is-center"> if (result.status === 200)
<img className="logoImg mb-4" src={logoImg}></img> {
</div> setUser(await fetchUser());
ref.current?.complete();
<div className="card mb-3 is-center dir-column"> return navigate('/home');
<h2>Verification Code</h2> }
{fail ? <p className="mt-0">Invalid code</p> : null} ref.current?.complete();
<form className="is-center dir-column"> setFail(true);
<input ref={mfaCode} autoComplete='off' placeholder='Your 2FA code...' required id='2faCode' type='password' /> };
<button className="button primary mt-1 mb-3" onClick={twoFactorClick}>Enter</button>
</form> return <div>
</div> <LoadingBar color='#2685ff' ref={ref} />
</div>; <div className="is-center">
}; <img className="logoImg mb-4" src={logoImg}></img>
</div>
const Login = () => {
<div className="card mb-3 is-center dir-column">
return <div className="row is-center is-full-screen is-marginless"> <h2>Verification Code</h2>
<div className='loginWrapper col-6 col-6-md col-3-lg'> {fail ? <p className="mt-0">Invalid code</p> : null}
<form className="is-center dir-column">
<Routes> <input ref={mfaCode} autoComplete='off' placeholder='Your 2FA code...' required id='2faCode' type='password' />
<Route path='/' element={<CredentialFields />} /> <button className="button primary mt-1 mb-3" onClick={twoFactorClick}>Enter</button>
<Route path='/verify' element={<TwoFactor />} /> </form>
</Routes> </div>
</div>;
</div> };
</div>;
const Login = () =>
}; {
return <div className="row is-center is-full-screen is-marginless">
<div className='loginWrapper col-6 col-6-md col-3-lg'>
<Routes>
<Route path='/' element={<CredentialFields />} />
<Route path='/verify' element={<TwoFactor />} />
</Routes>
</div>
</div>;
};
export default Login; export default Login;

View File

@ -1,66 +1,70 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { post } from "../util/Util"; import { post } from "../util/Util";
import '../css/pages/Register.css'; import '../css/pages/Register.css';
import logoImg from '../img/logo.png'; import logoImg from '../img/logo.png';
import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar'; import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar';
const Register = () => { const Register = () =>
{
const [error, setError] = useState<string>();
const [params] = useSearchParams(); const [error, setError] = useState<string>();
const navigate = useNavigate(); const [params] = useSearchParams();
const ref = useRef<LoadingBarRef>(null); const navigate = useNavigate();
const usernameRef = useRef<HTMLInputElement>(null); const ref = useRef<LoadingBarRef>(null);
const passwordRef = useRef<HTMLInputElement>(null); const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
document.body.classList.add('bg-triangles');
const code = params.get('code'); document.body.classList.add('bg-triangles');
const code = params.get('code');
const submit: React.MouseEventHandler = async (event) => {
ref.current?.continuousStart(); const submit: React.MouseEventHandler = async (event) =>
event.preventDefault(); {
const username = usernameRef.current?.value; ref.current?.continuousStart();
const password = passwordRef.current?.value; event.preventDefault();
if (!username?.length || !password?.length) { const username = usernameRef.current?.value;
ref.current?.complete(); const password = passwordRef.current?.value;
return; if (!username?.length || !password?.length)
} {
const response = await post('/api/register', { username, password, code }); ref.current?.complete();
if (response.status !== 200) { return;
ref.current?.complete(); }
return setError(response.message as string || 'unknown error'); const response = await post('/api/register', { username, password, code });
} if (response.status !== 200)
ref.current?.complete(); {
navigate('/login'); ref.current?.complete();
}; return setError(response.message as string || 'unknown error');
}
return <div className="row is-center is-full-screen is-marginless"> ref.current?.complete();
<LoadingBar color='#2685ff' ref={ref} /> navigate('/login');
<div className='registerWrapper col-6 col-6-md col-3-lg'> };
<div>
<div className="is-center"> return <div className="row is-center is-full-screen is-marginless">
<img className="logoImg mb-4" src={logoImg}></img> <LoadingBar color='#2685ff' ref={ref} />
</div> <div className='registerWrapper col-6 col-6-md col-3-lg'>
<div>
<div className="card mb-3 is-center dir-column"> <div className="is-center">
<h2>Register</h2> <img className="logoImg mb-4" src={logoImg}></img>
</div>
{error && <p>{error}</p>}
<div className="card mb-3 is-center dir-column">
<form className="registerForm"> <h2>Register</h2>
<input ref={usernameRef} autoComplete='off' placeholder='Username' required id='username' type='text' autoFocus />
<input ref={passwordRef} autoComplete='off' placeholder='Password' required id='password' type='password' /> {error && <p>{error}</p>}
<button className="button primary" onClick={submit}>Register</button>
</form> <form className="registerForm">
<input ref={usernameRef} autoComplete='off' placeholder='Username' required id='username' type='text' autoFocus />
<div className="text-center registerText"> <input ref={passwordRef} autoComplete='off' placeholder='Password' required id='password' type='password' />
Have an account?<br/><a onClick={() => navigate('/login')} className="clickable">Log in</a> instead! <button className="button primary" onClick={submit}>Register</button>
</div> </form>
</div>
</div> <div className="text-center registerText">
</div> Have an account?<br/><a onClick={() => navigate('/login')} className="clickable">Log in</a> instead!
</div>; </div>
}; </div>
</div>
</div>
</div>;
};
export default Register; export default Register;

View File

@ -9,12 +9,14 @@ import { get, patch, post } from "../../util/Util";
import { Permissions as Perms, Role as R } from "../../@types/ApiStructures"; import { Permissions as Perms, Role as R } from "../../@types/ApiStructures";
import { ToggleSwitch } from "../../components/InputElements"; import { ToggleSwitch } from "../../components/InputElements";
const Role = ({ role }: {role: R}) => { const Role = ({ role }: {role: R}) =>
{
// const perms = { ...role.permissions }; // const perms = { ...role.permissions };
const [perms, updatePerms] = useState<Perms>(role.permissions); const [perms, updatePerms] = useState<Perms>(role.permissions);
const commitPerms = async () => { const commitPerms = async () =>
{
await patch(`/api/roles/${role.id}`, perms); await patch(`/api/roles/${role.id}`, perms);
}; };
@ -35,9 +37,9 @@ const Role = ({ role }: {role: R}) => {
<p><b>Created: </b>{new Date(role.createdTimestamp).toDateString()}</p> <p><b>Created: </b>{new Date(role.createdTimestamp).toDateString()}</p>
{/* <p><b>Disabled: </b>{role.disabled.toString()}</p> */} {/* <p><b>Disabled: </b>{role.disabled.toString()}</p> */}
<ToggleSwitch value={role.disabled}> <ToggleSwitch value={role.disabled}>
Disabled Disabled
</ToggleSwitch> </ToggleSwitch>
<label htmlFor='username'>Name</label> <label htmlFor='username'>Name</label>
<input autoComplete='off' id='username' defaultValue={role.name} /> <input autoComplete='off' id='username' defaultValue={role.name} />
@ -57,21 +59,25 @@ const Role = ({ role }: {role: R}) => {
</div>; </div>;
}; };
const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, className?: string, addRole: (role: R) => void }) => { const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, className?: string, addRole: (role: R) => void }) =>
{
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const nameRef = useRef<HTMLInputElement>(null); const nameRef = useRef<HTMLInputElement>(null);
const positionRef = useRef<HTMLInputElement>(null); const positionRef = useRef<HTMLInputElement>(null);
const createRole: React.MouseEventHandler<HTMLButtonElement> = async (event) => { const createRole: React.MouseEventHandler<HTMLButtonElement> = async (event) =>
{
event.preventDefault(); event.preventDefault();
if (!nameRef.current || !positionRef.current) return setError('Must supply name and position'); if (!nameRef.current || !positionRef.current)
return setError('Must supply name and position');
const name = nameRef.current.value; const name = nameRef.current.value;
const position = parseInt(positionRef.current.value); const position = parseInt(positionRef.current.value);
const response = await post('/api/roles', { name, position }); const response = await post('/api/roles', { name, position });
if (!response.success) return setError(response.message || 'Unknown error'); if (!response.success)
return setError(response.message || 'Unknown error');
addRole(response.data as R); addRole(response.data as R);
}; };
@ -94,7 +100,8 @@ const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, c
}; };
const Roles = () => { const Roles = () =>
{
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [roles, setRoles] = useState<R[]>([]); const [roles, setRoles] = useState<R[]>([]);
@ -102,41 +109,50 @@ const Roles = () => {
const [pages, setPages] = useState(1); const [pages, setPages] = useState(1);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() =>
(async () => { {
(async () =>
{
const result = await get('/api/roles', { page }); const result = await get('/api/roles', { page });
if (result.success && result.data) { if (result.success && result.data)
{
setError(null); setError(null);
setRoles(result.data.roles as R[]); setRoles(result.data.roles as R[]);
setPages(result.data.pages); setPages(result.data.pages);
} else { }
else
{
setError(result.message || 'Unknown error'); setError(result.message || 'Unknown error');
} }
setLoading(false); setLoading(false);
})(); })();
}, [page]); }, [page]);
const RoleWrapper = () => { const RoleWrapper = () =>
{
const { id } = useParams(); const { id } = useParams();
const role = roles.find(r => r.id === id); const role = roles.find(r => r.id === id);
if(!role) return <p>Unknown role</p>; if(!role)
return <p>Unknown role</p>;
return <Role role={role} />; return <Role role={role} />;
}; };
const navigate = useNavigate(); const navigate = useNavigate();
const RoleListWrapper = () => { const RoleListWrapper = () =>
{
return <div className="role-list-wrapper row"> return <div className="role-list-wrapper row">
<div className={`col-6-lg col-12 ld ${loading && 'loading'}`}> <div className={`col-6-lg col-12 ld ${loading && 'loading'}`}>
<h4>All Roles</h4> <h4>All Roles</h4>
<Table headerItems={['Name', 'ID']}> <Table headerItems={['Name', 'ID']}>
{roles.map(role => <TableListEntry {roles.map(role => <TableListEntry
onClick={() => { onClick={() =>
navigate(role.id); {
}} navigate(role.id);
key={role.id} }}
item={role} key={role.id}
itemKeys={['name', 'id']} item={role}
/>)} itemKeys={['name', 'id']}
/>)}
</Table> </Table>
<PageButtons {...{page, setPage, pages}} /> <PageButtons {...{page, setPage, pages}} />

View File

@ -1,321 +1,359 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { Route, Routes, useNavigate, useParams } from "react-router"; import { Route, Routes, useNavigate, useParams } from "react-router";
import ErrorBoundary from "../../util/ErrorBoundary"; import ErrorBoundary from "../../util/ErrorBoundary";
import { get, post } from '../../util/Util'; import { get, post } from '../../util/Util';
import '../../css/pages/Users.css'; import '../../css/pages/Users.css';
import { Table, TableListEntry } from "../../components/Table"; import { Table, TableListEntry } from "../../components/Table";
import { BackButton, PageButtons } from "../../components/PageControls"; import { BackButton, PageButtons } from "../../components/PageControls";
import { Permissions } from "../../views/PermissionsView"; import { Permissions } from "../../views/PermissionsView";
import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures"; import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures";
import { Dropdown, ToggleSwitch } from "../../components/InputElements"; import { Dropdown, ToggleSwitch } from "../../components/InputElements";
type PartialUser = { type PartialUser = {
apps: App[] apps: App[]
} }
const SelectedUserContext = React.createContext<{ const SelectedUserContext = React.createContext<{
user: PartialUser | null, user: PartialUser | null,
updateUser:(user: PartialUser) => void updateUser:(user: PartialUser) => void
}>({ }>({
user: null, user: null,
updateUser: () => { /** */ } updateUser: () =>
}); { /** */ }
const Context = ({ children }: { children: React.ReactNode }) => { });
const [user, updateUser] = useState<PartialUser | null>(null); const Context = ({ children }: { children: React.ReactNode }) =>
return <SelectedUserContext.Provider value={{ user, updateUser }}> {
{children} const [user, updateUser] = useState<PartialUser | null>(null);
</SelectedUserContext.Provider>; return <SelectedUserContext.Provider value={{ user, updateUser }}>
}; {children}
</SelectedUserContext.Provider>;
const ApplicationList = ({ apps }: { apps: App[] }) => { };
const navigate = useNavigate(); const ApplicationList = ({ apps }: { apps: App[] }) =>
return <Table headerItems={['Name', 'ID']}> {
{apps.map(app => <TableListEntry
onClick={() => { const navigate = useNavigate();
navigate(`applications/${app.id}`); return <Table headerItems={['Name', 'ID']}>
}} {apps.map(app => <TableListEntry
key={app.id} onClick={() =>
item={app} {
itemKeys={['name', 'id']} navigate(`applications/${app.id}`);
/>)} }}
</Table>; key={app.id}
item={app}
}; itemKeys={['name', 'id']}
/>)}
const Application = ({ app }: { app: App }) => { </Table>;
const commitPerms = async () => { };
await post(`/api/applications/${app.id}/permissions`, perms);
}; const Application = ({ app }: { app: App }) =>
const [perms, updatePerms] = useState<Perms>(app.permissions); {
const descProps = { defaultValue: app.description || '', placeholder: 'Describe your application' };
return <div> const commitPerms = async () =>
<div className='row'> {
<div className='col-6-lg col-12'> await post(`/api/applications/${app.id}/permissions`, perms);
<div className='flex is-vertical-align flex-wrap'> };
<BackButton /> const [perms, updatePerms] = useState<Perms>(app.permissions);
<h2 className='userId'>Application {app.name} ({app.id})</h2> const descProps = { defaultValue: app.description || '', placeholder: 'Describe your application' };
</div> return <div>
<div className='row'>
<p><b>Created: </b>{new Date(app.createdTimestamp).toDateString()}</p> <div className='col-6-lg col-12'>
<ToggleSwitch value={app.disabled}> <div className='flex is-vertical-align flex-wrap'>
Disabled: <BackButton />
</ToggleSwitch> <h2 className='userId'>Application {app.name} ({app.id})</h2>
</div>
<b>Description</b>
<textarea {...descProps} > <p><b>Created: </b>{new Date(app.createdTimestamp).toDateString()}</p>
<ToggleSwitch value={app.disabled}>
</textarea> Disabled:
</div> </ToggleSwitch>
<div className="col-6-lg col-12"> <b>Description</b>
<Permissions onUpdate={updatePerms} perms={perms} /> <textarea {...descProps} >
<button onClick={commitPerms} className="button primary">Save Permissions</button>
</div> </textarea>
</div>
</div>
</div>; <div className="col-6-lg col-12">
}; <Permissions onUpdate={updatePerms} perms={perms} />
<button onClick={commitPerms} className="button primary">Save Permissions</button>
const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => { </div>
return <div className='card role' >
{role.name} <span className="clickable" onClick={onClick}>X</span> </div>
</div>; </div>;
}; };
const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => { const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) =>
{
const [unequippedRoles, updateUnequipped] = useState<Role[]>(roles.filter(role => !userRoles.some(r => role.id === r.id))); return <div className='card role' >
const [equippedRoles, updateEquipped] = useState<Role[]>(userRoles); {role.name} <span className="clickable" onClick={onClick}>X</span>
</div>;
const roleSelected = (role: Role) => { };
updateEquipped([...equippedRoles, role]);
updateUnequipped(unequippedRoles.filter(r => r.id !== role.id)); const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) =>
}; {
const roleDeselected = (role: Role) => {
updateEquipped(equippedRoles.filter(r => r.id !== role.id)); const [unequippedRoles, updateUnequipped] = useState<Role[]>(roles.filter(role => !userRoles.some(r => role.id === r.id)));
updateUnequipped([...unequippedRoles, role]); const [equippedRoles, updateEquipped] = useState<Role[]>(userRoles);
};
return <Dropdown> const roleSelected = (role: Role) =>
{
<Dropdown.Header className='role-selector'> updateEquipped([...equippedRoles, role]);
{equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={(event) => { updateUnequipped(unequippedRoles.filter(r => r.id !== role.id));
event.preventDefault(); };
roleDeselected(role); const roleDeselected = (role: Role) =>
}} />)} {
</Dropdown.Header> updateEquipped(equippedRoles.filter(r => r.id !== role.id));
updateUnequipped([...unequippedRoles, role]);
<Dropdown.ItemList> };
return <Dropdown>
{unequippedRoles.map(role => <Dropdown.Item
key={role.id} <Dropdown.Header className='role-selector'>
onClick={() => { {equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={(event) =>
roleSelected(role); {
}} event.preventDefault();
> roleDeselected(role);
{role.name} }} />)}
</Dropdown.Item>)} </Dropdown.Header>
</Dropdown.ItemList> <Dropdown.ItemList>
</Dropdown>; {unequippedRoles.map(role => <Dropdown.Item
}; key={role.id}
onClick={() =>
const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => { {
roleSelected(role);
// const [apps, updateApps] = useState<App[]>([]); }}
const [perms, updatePerms] = useState<Perms>(user.permissions); >
const userContext = useContext(SelectedUserContext); {role.name}
</Dropdown.Item>)}
useEffect(() => {
(async () => { </Dropdown.ItemList>
const appsResponse = await get(`/api/users/${user.id}/applications`);
if (appsResponse.success) { </Dropdown>;
const a = appsResponse.data as App[]; };
// updateApps(a);
userContext.updateUser({ apps: a }); const User = ({ user, roles }: { user: APIUser, roles: Role[] }) =>
{
// const [apps, updateApps] = useState<App[]>([]);
const [perms, updatePerms] = useState<Perms>(user.permissions);
const userContext = useContext(SelectedUserContext);
useEffect(() =>
{
(async () =>
{
const appsResponse = await get(`/api/users/${user.id}/applications`);
if (appsResponse.success)
{
const a = appsResponse.data as App[];
// updateApps(a);
userContext.updateUser({ apps: a });
}
})();
}, []);
const commitPerms = async () =>
{
await post(`/api/users/${user.id}/permissions`, perms);
};
return <div className='user-card'>
<div className="row">
<div className="col-6-lg col-12">
<div className="flex is-vertical-align flex-wrap">
<BackButton />
<h2 className="userId">User {user.displayName} ({user.id})</h2>
</div>
<p><b>Created: </b>{new Date(user.createdTimestamp).toDateString()}</p>
<p><b>2FA:</b> {(user.twoFactor && <button className='button danger'>Disable</button>) || 'Disabled'}</p>
<ToggleSwitch value={user.disabled}>
Login Disabled:
</ToggleSwitch>
<label htmlFor='username'>Username</label>
<input autoComplete='off' id='username' defaultValue={user.name} />
<label htmlFor='displayName'>Display Name</label>
<input autoComplete='off' id='displayName' placeholder='Name to display instead of username' defaultValue={user.displayName || ''} />
<label htmlFor='userNote'>Note</label>
<textarea id='userNote' defaultValue={user.note} />
<label>Roles</label>
<Roles userRoles={user.roles} roles={roles} />
{/* <button className="button primary">
Save User
</button> */}
</div>
<div className="col-6-lg col-12">
<Permissions onUpdate={updatePerms} perms={user.permissions} />
<button onClick={commitPerms} className="button primary">Save Permissions</button>
</div>
</div>
<div className='row'>
<div className='col-6-lg col-12'>
<h3>Applications</h3>
{userContext.user && <ApplicationList apps={userContext.user.apps} />}
</div>
</div>
</div>;
};
const CreateUserField = ({ className }: { className: string }) =>
{
const [link, setLink] = useState<string | null>(null);
const getSignupCode = async () =>
{
const response = await get('/api/register/code');
if (response.status === 200 && response.data)
{
const link = `${window.location.origin}/register?code=${response.data.code}`;
setLink(link);
}
};
const copyToClipboard: React.MouseEventHandler = (event) =>
{
const element = event.target as HTMLElement;
const { parentElement } = element;
navigator.clipboard.writeText(element.innerText);
if (parentElement)
(parentElement.childNodes[parentElement.childNodes.length - 1] as HTMLElement).style.visibility = "visible";
};
const closeToolTip: React.MouseEventHandler = (event) =>
{
(event.target as HTMLElement).style.visibility = "hidden";
};
// TODO: Finish this
return <div className={className}>
<h4>One-Click Sign-Up</h4>
{link ?
<div className="registerCodeWrapper tooltip"><p onClick={copyToClipboard}>{link}</p><span onClick={closeToolTip} className="tooltiptext">Code copied!</span></div> :
<button onClick={getSignupCode} className="button primary is-center">Create sign-up link</button>}
<hr />
<h4>Create User</h4>
<form>
<p>Users will be forced to change the password you set here.</p>
<input placeholder="Username" autoComplete="off" type='text' id='username' />
<input placeholder="Password" autoComplete="off" type='password' id='password' />
</form>
</div>;
};
const Users = () =>
{
const [users, setUsers] = useState<APIUser[]>([]);
const [roles, updateRoles] = useState<Role[]>([]);
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() =>
{
(async () =>
{
const result = await get('/api/users', { page });
if (result.success && result.data)
{
setError(null);
setUsers(result.data.users as APIUser[]);
setPages(result.data.pages);
} }
})(); else
}, []); setError(result.message || 'Unknown error');
setLoading(false);
const commitPerms = async () => { const rolesResponse = await get('/api/roles', { all: true });
await post(`/api/users/${user.id}/permissions`, perms); if (rolesResponse.success && rolesResponse.data)
}; updateRoles(rolesResponse.data.roles as Role[]);
})();
return <div className='user-card'> }, [page]);
<div className="row">
<div className="col-6-lg col-12"> const UserWrapper = () =>
<div className="flex is-vertical-align flex-wrap"> {
<BackButton /> const { id } = useParams();
<h2 className="userId">User {user.displayName} ({user.id})</h2> const user = users.find(u => u.id === id);
</div> if (!user)
return <p>Unknown user</p>;
<p><b>Created: </b>{new Date(user.createdTimestamp).toDateString()}</p> return <User roles={roles} user={user} />;
<p><b>2FA:</b> {(user.twoFactor && <button className='button danger'>Disable</button>) || 'Disabled'}</p> };
<ToggleSwitch value={user.disabled}>
Login Disabled: const navigate = useNavigate();
</ToggleSwitch> const UserListWrapper = () =>
{
<label htmlFor='username'>Username</label> return <div className="user-list-wrapper row">
<input autoComplete='off' id='username' defaultValue={user.name} /> <div className={`col-6-lg col-12 ld ${loading && 'loading'}`}>
<h4>All Users</h4>
<label htmlFor='displayName'>Display Name</label> <Table headerItems={['Username', 'ID']}>
<input autoComplete='off' id='displayName' placeholder='Name to display instead of username' defaultValue={user.displayName || ''} /> {users.map(user => <TableListEntry
onClick={() =>
<label htmlFor='userNote'>Note</label> {
<textarea id='userNote' defaultValue={user.note} /> navigate(user.id);
}}
<label>Roles</label> key={user.id}
<Roles userRoles={user.roles} roles={roles} /> item={user}
itemKeys={['name', 'id']} />)}
{/* <button className="button primary"> </Table>
Save User
</button> */} <PageButtons {...{ page, setPage, pages }} />
</div>
</div>
<CreateUserField className="col-6-lg col-12" />
<div className="col-6-lg col-12">
<Permissions onUpdate={updatePerms} perms={user.permissions} /> </div>;
<button onClick={commitPerms} className="button primary">Save Permissions</button> };
</div>
</div> const ApplicationWrapper = () =>
{
<div className='row'> const { appid } = useParams();
<div className='col-6-lg col-12'> if (!appid)
<h3>Applications</h3> return null;
{userContext.user && <ApplicationList apps={userContext.user.apps} />}
</div> const userContext = useContext(SelectedUserContext);
</div> const [app, setApp] = useState<App | null>(userContext.user?.apps.find(app => app.id === appid) || null);
useEffect(() =>
</div>; {
}; (async () =>
{
const CreateUserField = ({ className }: { className: string }) => { if (userContext.user)
return;
const [link, setLink] = useState<string | null>(null); const result = await get(`/api/applications/${appid}`);
const getSignupCode = async () => { if (result.success)
const response = await get('/api/register/code'); setApp(result.data as App);
if (response.status === 200 && response.data) { })();
const link = `${window.location.origin}/register?code=${response.data.code}`; }, [appid]);
setLink(link);
} if (!app)
}; return <p>Loading...</p>;
return <Application app={app} />;
const copyToClipboard: React.MouseEventHandler = (event) => { };
const element = event.target as HTMLElement;
const { parentElement } = element; return <div className="user-list">
navigator.clipboard.writeText(element.innerText); {error && <p>{error}</p>}
if (parentElement) <ErrorBoundary>
(parentElement.childNodes[parentElement.childNodes.length - 1] as HTMLElement).style.visibility = "visible"; <Context>
}; <Routes>
<Route path='/:id/*' element={<UserWrapper />} />
const closeToolTip: React.MouseEventHandler = (event) => { <Route path='/' element={<UserListWrapper />} />
(event.target as HTMLElement).style.visibility = "hidden"; <Route path='/:id/applications/:appid' element={<ApplicationWrapper />} />
}; </Routes>
</Context>
// TODO: Finish this </ErrorBoundary>
return <div className={className}> </div>;
<h4>One-Click Sign-Up</h4>
{link ? };
<div className="registerCodeWrapper tooltip"><p onClick={copyToClipboard}>{link}</p><span onClick={closeToolTip} className="tooltiptext">Code copied!</span></div> :
<button onClick={getSignupCode} className="button primary is-center">Create sign-up link</button>}
<hr />
<h4>Create User</h4>
<form>
<p>Users will be forced to change the password you set here.</p>
<input placeholder="Username" autoComplete="off" type='text' id='username' />
<input placeholder="Password" autoComplete="off" type='password' id='password' />
</form>
</div>;
};
const Users = () => {
const [users, setUsers] = useState<APIUser[]>([]);
const [roles, updateRoles] = useState<Role[]>([]);
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
const result = await get('/api/users', { page });
if (result.success && result.data) {
setError(null);
setUsers(result.data.users as APIUser[]);
setPages(result.data.pages);
} else setError(result.message || 'Unknown error');
setLoading(false);
const rolesResponse = await get('/api/roles', { all: true });
if (rolesResponse.success && rolesResponse.data)
updateRoles(rolesResponse.data.roles as Role[]);
})();
}, [page]);
const UserWrapper = () => {
const { id } = useParams();
const user = users.find(u => u.id === id);
if (!user) return <p>Unknown user</p>;
return <User roles={roles} user={user} />;
};
const navigate = useNavigate();
const UserListWrapper = () => {
return <div className="user-list-wrapper row">
<div className={`col-6-lg col-12 ld ${loading && 'loading'}`}>
<h4>All Users</h4>
<Table headerItems={['Username', 'ID']}>
{users.map(user => <TableListEntry
onClick={() => {
navigate(user.id);
}}
key={user.id}
item={user}
itemKeys={['name', 'id']} />)}
</Table>
<PageButtons {...{ page, setPage, pages }} />
</div>
<CreateUserField className="col-6-lg col-12" />
</div>;
};
const ApplicationWrapper = () => {
const { appid } = useParams();
if (!appid) return null;
const userContext = useContext(SelectedUserContext);
const [app, setApp] = useState<App | null>(userContext.user?.apps.find(app => app.id === appid) || null);
useEffect(() => {
(async () => {
if (userContext.user) return;
const result = await get(`/api/applications/${appid}`);
if (result.success)
setApp(result.data as App);
})();
}, [appid]);
if (!app) return <p>Loading...</p>;
return <Application app={app} />;
};
return <div className="user-list">
{error && <p>{error}</p>}
<ErrorBoundary>
<Context>
<Routes>
<Route path='/:id/*' element={<UserWrapper />} />
<Route path='/' element={<UserListWrapper />} />
<Route path='/:id/applications/:appid' element={<ApplicationWrapper />} />
</Routes>
</Context>
</ErrorBoundary>
</div>;
};
export default Users; export default Users;

View File

@ -1,108 +1,122 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Route, Routes, useNavigate, useParams } from "react-router"; import { Route, Routes, useNavigate, useParams } from "react-router";
import { Table, TableListEntry } from "../../components/Table"; import { Table, TableListEntry } from "../../components/Table";
import ErrorBoundary from "../../util/ErrorBoundary"; import ErrorBoundary from "../../util/ErrorBoundary";
import { post, get, del } from "../../util/Util"; import { post, get, del } from "../../util/Util";
import { Application as App } from "../../@types/ApiStructures"; import { Application as App } from "../../@types/ApiStructures";
import { Res } from "../../@types/Other"; import { Res } from "../../@types/Other";
const Application = ({ app }: {app: App}) => { const Application = ({ app }: {app: App}) =>
{
const navigate = useNavigate();
const navigate = useNavigate();
const deleteApp = async () => {
// const response = const deleteApp = async () =>
await del(`/api/user/applications/${app.id}`); {
}; // const response =
await del(`/api/user/applications/${app.id}`);
return <div> };
<button className="button secondary" onClick={() => navigate(-1)}>Back</button>
<h2>{app.name}</h2> return <div>
<p>{app.description}</p> <button className="button secondary" onClick={() => navigate(-1)}>Back</button>
<p>{app.token}</p> <h2>{app.name}</h2>
<button onClick={deleteApp}>Delete</button> <p>{app.description}</p>
</div>; <p>{app.token}</p>
<button onClick={deleteApp}>Delete</button>
}; </div>;
const Applications = () => { };
const [applications, setApplications] = useState<App[]>([]); const Applications = () =>
const [loading, setLoading] = useState(true); {
const navigate = useNavigate();
const [applications, setApplications] = useState<App[]>([]);
useEffect(() => { const [loading, setLoading] = useState(true);
(async () => { const navigate = useNavigate();
const response = await get('/api/user/applications') as Res;
if (response.status === 200) setApplications(response.data as App[]); useEffect(() =>
setLoading(false); {
})(); (async () =>
}, []); {
const response = await get('/api/user/applications') as Res;
const descField = useRef<HTMLTextAreaElement>(null); if (response.status === 200)
const nameField = useRef<HTMLInputElement>(null); setApplications(response.data as App[]);
const [error, setError] = useState<string | null>(null); setLoading(false);
})();
const createApp: React.MouseEventHandler = async (event) => { }, []);
event.preventDefault();
const button = event.target as HTMLButtonElement; const descField = useRef<HTMLTextAreaElement>(null);
const nameField = useRef<HTMLInputElement>(null);
if (!nameField.current || !descField.current) return; const [error, setError] = useState<string | null>(null);
const name = nameField.current?.value; const createApp: React.MouseEventHandler = async (event) =>
if (!name) return setError('Missing name'); {
event.preventDefault();
const description = descField.current?.value || null; const button = event.target as HTMLButtonElement;
nameField.current.value = '';
descField.current.value = ''; if (!nameField.current || !descField.current)
button.disabled = true; return;
const response = await post('/api/user/applications', { name, description });
if (response.status !== 200) setError(response.message || 'Unknown error'); const name = nameField.current?.value;
else setApplications((apps) => [...apps, response.data as App]); if (!name)
return setError('Missing name');
button.disabled = false;
}; const description = descField.current?.value || null;
nameField.current.value = '';
const Main = () => { descField.current.value = '';
return <div className="row"> button.disabled = true;
const response = await post('/api/user/applications', { name, description });
<div className={`col ld ${loading && 'loading'}`}> if (response.status !== 200)
<h2>Applications</h2> setError(response.message || 'Unknown error');
<Table headerItems={['Name', 'ID']}> else
{applications.map(application => <TableListEntry setApplications((apps) => [...apps, response.data as App]);
key={application.id}
onClick={() => navigate(application.id)} button.disabled = false;
item={application} };
itemKeys={['name', 'id']}
/>)} const Main = () =>
</Table> {
</div> return <div className="row">
<div className="col"> <div className={`col ld ${loading && 'loading'}`}>
<h2>Create Application</h2> <h2>Applications</h2>
{error && <p>{error}</p>} <Table headerItems={['Name', 'ID']}>
<form> {applications.map(application => <TableListEntry
<input ref={nameField} placeholder="Name" type='text' /> key={application.id}
<textarea ref={descField} rows={5} placeholder="Describe your application" /> onClick={() => navigate(application.id)}
<button onClick={createApp} className="button primary mt-3">Create</button> item={application}
</form> itemKeys={['name', 'id']}
</div> />)}
</div>; </Table>
}; </div>
const ApplicationWrapper = () => { <div className="col">
const { id } = useParams(); <h2>Create Application</h2>
const application = applications.find(app => app.id === id); {error && <p>{error}</p>}
if(!application) return <p>Unknown application</p>; <form>
return <Application app={application} />; <input ref={nameField} placeholder="Name" type='text' />
}; <textarea ref={descField} rows={5} placeholder="Describe your application" />
<button onClick={createApp} className="button primary mt-3">Create</button>
return <ErrorBoundary> </form>
<Routes> </div>
<Route path="/" element={<Main />} /> </div>;
<Route path="/:id" element={<ApplicationWrapper />} /> };
</Routes>
</ErrorBoundary>; const ApplicationWrapper = () =>
}; {
const { id } = useParams();
const application = applications.find(app => app.id === id);
if(!application)
return <p>Unknown application</p>;
return <Application app={application} />;
};
return <ErrorBoundary>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/:id" element={<ApplicationWrapper />} />
</Routes>
</ErrorBoundary>;
};
export default Applications; export default Applications;

View File

@ -1,189 +1,210 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { capitalise, get, post } from "../../util/Util"; import { capitalise, get, post } from "../../util/Util";
import { useLoginContext } from "../../structures/UserContext"; import { useLoginContext } from "../../structures/UserContext";
import { FileSelector } from "../../components/InputElements"; import { FileSelector } from "../../components/InputElements";
import { ExternalProfile as EP, User } from "../../@types/ApiStructures"; import { ExternalProfile as EP, User } from "../../@types/ApiStructures";
const TwoFactorControls = ({ user }: {user: User}) => { const TwoFactorControls = ({ user }: {user: User}) =>
{
const [twoFactorError, set2FAError] = useState<string | null>(null);
const [displayInput, setDisplayInput] = useState(false); const [twoFactorError, set2FAError] = useState<string | null>(null);
const [displayInput, setDisplayInput] = useState(false);
const [qr, setQr] = useState<string | null>(null);
const displayQr = async () => { const [qr, setQr] = useState<string | null>(null);
const response = await get('/api/user/2fa'); const displayQr = async () =>
if (response.status !== 200) return set2FAError(response.message || 'Uknown error'); {
setQr(response.message as string); const response = await get('/api/user/2fa');
}; if (response.status !== 200)
return set2FAError(response.message || 'Uknown error');
const codeRef = useRef<HTMLInputElement>(null); setQr(response.message as string);
const disable2FA: React.MouseEventHandler = async (event) => { };
event.preventDefault();
const code = codeRef.current?.value; const codeRef = useRef<HTMLInputElement>(null);
if (!code) return; const disable2FA: React.MouseEventHandler = async (event) =>
const response = await post('/api/user/2fa/disable', { code }); {
if (response.status !== 200) return set2FAError(response.message || 'Unknown error'); event.preventDefault();
}; const code = codeRef.current?.value;
if (!code)
const submitCode: React.MouseEventHandler = async (event) => { return;
event.preventDefault(); const response = await post('/api/user/2fa/disable', { code });
const code = codeRef.current?.value; if (response.status !== 200)
if (!code) return; return set2FAError(response.message || 'Unknown error');
const response = await post('/api/user/2fa/verify', { code }); };
if (response.status !== 200) return set2FAError(response.message || 'Unknown error');
}; const submitCode: React.MouseEventHandler = async (event) =>
{
let inner = <div> event.preventDefault();
{qr ? const code = codeRef.current?.value;
<div> if (!code)
<img src={qr} /> return;
<form> const response = await post('/api/user/2fa/verify', { code });
<input placeholder='Authenticator code' ref={codeRef} type='password' /> if (response.status !== 200)
<button onClick={submitCode} className="button success">Submit</button> return set2FAError(response.message || 'Unknown error');
</form> };
</div> :
<button onClick={displayQr} className="button primary">Enable 2FA</button>} let inner = <div>
</div>; {qr ?
<div>
<img src={qr} />
if (user.twoFactor) inner = <div> <form>
{displayInput ? <input placeholder='Authenticator code' ref={codeRef} type='password' />
<form> <button onClick={submitCode} className="button success">Submit</button>
<input placeholder='Authenticator code to disable' ref={codeRef} type='password' /> </form>
<button className="button error" onClick={disable2FA}>Submit</button> </div> :
</form> <button onClick={displayQr} className="button primary">Enable 2FA</button>}
: <button onClick={() => setDisplayInput(true)} className="button error">Disable 2FA</button>} </div>;
</div>;
return <div> if (user.twoFactor)
{twoFactorError && <p>{twoFactorError}</p>} inner = <div>
{inner} {displayInput ?
</div>; <form>
<input placeholder='Authenticator code to disable' ref={codeRef} type='password' />
}; <button className="button error" onClick={disable2FA}>Submit</button>
</form>
const ExternalProfile = ({ profile }: {profile: EP}) => { : <button onClick={() => setDisplayInput(true)} className="button error">Disable 2FA</button>}
return <div> </div>;
<b>{capitalise(profile.provider)}</b>
<p className="m-0">Username: {profile.username}</p> return <div>
<p className="m-0">ID: {profile.id}</p> {twoFactorError && <p>{twoFactorError}</p>}
</div>; {inner}
}; </div>;
const ThirdPartyConnections = ({ user }: {user: User}) => { };
const { externalProfiles } = user; const ExternalProfile = ({ profile }: {profile: EP}) =>
{
return <div> return <div>
{externalProfiles.map(profile => <ExternalProfile key={profile.id} profile={profile} />)} <b>{capitalise(profile.provider)}</b>
</div>; <p className="m-0">Username: {profile.username}</p>
<p className="m-0">ID: {profile.id}</p>
}; </div>;
};
const Profile = () => {
const ThirdPartyConnections = ({ user }: {user: User}) =>
const user = useLoginContext()[0] as User; {
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState<string | null>(null); const { externalProfiles } = user;
const usernameRef = useRef<HTMLInputElement>(null); return <div>
const displayNameRef = useRef<HTMLInputElement>(null); {externalProfiles.map(profile => <ExternalProfile key={profile.id} profile={profile} />)}
const currPassRef = useRef<HTMLInputElement>(null); </div>;
const newPassRef = useRef<HTMLInputElement>(null);
const newPassRepeatRef = useRef<HTMLInputElement>(null); };
const submit: React.MouseEventHandler = async (event) => { const Profile = () =>
if (!file) return; {
const button = event.target as HTMLButtonElement;
button.disabled = true; const user = useLoginContext()[0] as User;
const [file, setFile] = useState<File | null>(null);
const body = new FormData(); const [error, setError] = useState<string | null>(null);
body.append('file', file, file.name);
const usernameRef = useRef<HTMLInputElement>(null);
const response = await post('/api/user/avatar', body, { const displayNameRef = useRef<HTMLInputElement>(null);
headers: null const currPassRef = useRef<HTMLInputElement>(null);
}); const newPassRef = useRef<HTMLInputElement>(null);
if (!response.success) setError(response.message || 'Unknown error'); const newPassRepeatRef = useRef<HTMLInputElement>(null);
else setFile(null);
button.disabled = false; const submit: React.MouseEventHandler = async (event) =>
}; {
if (!file)
const updateSettings: React.MouseEventHandler = async (event) => { return;
event.preventDefault(); const button = event.target as HTMLButtonElement;
const button = event.target as HTMLButtonElement; button.disabled = true;
const username = usernameRef.current?.value; const body = new FormData();
const displayName = displayNameRef.current?.value; body.append('file', file, file.name);
const currentPassword = currPassRef.current?.value;
const newPassword = newPassRef.current?.value; const response = await post('/api/user/avatar', body, {
const newPasswordRepeat = newPassRepeatRef.current?.value; headers: null
});
if (!currentPassword) return setError('Missing password'); if (!response.success)
setError(response.message || 'Unknown error');
button.disabled = true; else
const data: {username?: string, displayName?: string, password?: string, newPassword?: string} = { username, displayName, password: currentPassword }; setFile(null);
if (currentPassword && newPassword && newPasswordRepeat) { button.disabled = false;
if (newPassword !== newPasswordRepeat) { };
button.disabled = false;
return setError('Passwords do not match'); const updateSettings: React.MouseEventHandler = async (event) =>
} {
data.newPassword = newPassword; event.preventDefault();
} const button = event.target as HTMLButtonElement;
const response = await post('/api/user/settings', data);
console.log(response); const username = usernameRef.current?.value;
button.disabled = false; const displayName = displayNameRef.current?.value;
const currentPassword = currPassRef.current?.value;
}; const newPassword = newPassRef.current?.value;
const newPasswordRepeat = newPassRepeatRef.current?.value;
return <div className="row">
if (!currentPassword)
<div className="col-6-lg col-12"> return setError('Missing password');
<h3>Profile</h3>
<div className="dir-row pfp-wrapper"> button.disabled = true;
<div className="w-auto f-s0"> const data: {username?: string, displayName?: string, password?: string, newPassword?: string} = { username, displayName, password: currentPassword };
<p><b>Profile Picture</b></p> if (currentPassword && newPassword && newPasswordRepeat)
<img draggable={false} width={256} height={256} src={`/api/users/${user.id}/avatar`} /> {
</div> if (newPassword !== newPasswordRepeat)
<div className="f-g f-b0"> {
<p><b>Change Profile Picture</b></p> button.disabled = false;
<form className="f-g f-b0"> return setError('Passwords do not match');
<FileSelector cb={setFile} /> }
<button type='button' onClick={submit} className="button primary mt-3">Submit</button> data.newPassword = newPassword;
</form> }
</div> const response = await post('/api/user/settings', data);
</div> console.log(response);
<h4 className="mt-5">Third party connections</h4> button.disabled = false;
<ThirdPartyConnections user={user} />
};
</div>
return <div className="row">
<div className="col-6-lg col-12">
<h3>Settings</h3> <div className="col-6-lg col-12">
<h3>Profile</h3>
<p><b>ID:</b> {user.id}</p> <div className="dir-row pfp-wrapper">
<div className="w-auto f-s0">
<p>{error}</p> <p><b>Profile Picture</b></p>
<img draggable={false} width={256} height={256} src={`/api/users/${user.id}/avatar`} />
<form> </div>
<label>Username</label> <div className="f-g f-b0">
<input ref={usernameRef} id='username' defaultValue={user.name} type='text' autoComplete="off" /> <p><b>Change Profile Picture</b></p>
<form className="f-g f-b0">
<label>Display Name</label> <FileSelector cb={setFile} />
<input ref={displayNameRef} id='displayName' defaultValue={user.displayName || ''} type='text' autoComplete="off" /> <button type='button' onClick={submit} className="button primary mt-3">Submit</button>
</form>
<label>Change password</label> </div>
<input ref={currPassRef} id='currentPassword' placeholder="Current password" type='password' autoComplete="off" /> </div>
<input ref={newPassRef} id='newPassword' placeholder="New password" type='password' autoComplete="off" /> <h4 className="mt-5">Third party connections</h4>
<input ref={newPassRepeatRef} id='newPasswordRepeat' placeholder="Repeat new password" type='password' autoComplete="off" /> <ThirdPartyConnections user={user} />
<button onClick={updateSettings} className="button primary">Save</button> </div>
</form>
<div className="col-6-lg col-12">
<p className="mb-0 mt-5"><b>Two Factor:</b> {user.twoFactor ? 'enabled' : 'disabled'}</p> <h3>Settings</h3>
<TwoFactorControls user={user} />
<p><b>ID:</b> {user.id}</p>
</div>
</div>; <p>{error}</p>
};
<form>
<label>Username</label>
<input ref={usernameRef} id='username' defaultValue={user.name} type='text' autoComplete="off" />
<label>Display Name</label>
<input ref={displayNameRef} id='displayName' defaultValue={user.displayName || ''} type='text' autoComplete="off" />
<label>Change password</label>
<input ref={currPassRef} id='currentPassword' placeholder="Current password" type='password' autoComplete="off" />
<input ref={newPassRef} id='newPassword' placeholder="New password" type='password' autoComplete="off" />
<input ref={newPassRepeatRef} id='newPasswordRepeat' placeholder="Repeat new password" type='password' autoComplete="off" />
<button onClick={updateSettings} className="button primary">Save</button>
</form>
<p className="mb-0 mt-5"><b>Two Factor:</b> {user.twoFactor ? 'enabled' : 'disabled'}</p>
<TwoFactorControls user={user} />
</div>
</div>;
};
export default Profile; export default Profile;

View File

@ -1,29 +1,30 @@
import React from 'react'; import React from 'react';
import { NavLink as BaseNavLink } from 'react-router-dom'; import { NavLink as BaseNavLink } from 'react-router-dom';
type NavLinkOptions = { type NavLinkOptions = {
activeClassName?: string, activeClassName?: string,
activeStyle?: object, activeStyle?: object,
children?: React.ReactNode, children?: React.ReactNode,
className?: string, className?: string,
style?: object, style?: object,
to: string, to: string,
onClick?: React.MouseEventHandler onClick?: React.MouseEventHandler
} }
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const NavLink = React.forwardRef(({ activeClassName, activeStyle, ...props }: NavLinkOptions, ref: React.ForwardedRef<HTMLAnchorElement>) => { const NavLink = React.forwardRef(({ activeClassName, activeStyle, ...props }: NavLinkOptions, ref: React.ForwardedRef<HTMLAnchorElement>) =>
return <BaseNavLink {
ref={ref} return <BaseNavLink
{...props} ref={ref}
className={({ isActive }) => [props.className, isActive ? activeClassName : null].filter(Boolean).join(' ')} {...props}
style={({ isActive }) => ({ className={({ isActive }) => [props.className, isActive ? activeClassName : null].filter(Boolean).join(' ')}
...props.style, style={({ isActive }) => ({
...isActive ? activeStyle : null ...props.style,
})}> ...isActive ? activeStyle : null
{props?.children} })}>
</BaseNavLink>; {props?.children}
</BaseNavLink>;
});
});
export default NavLink; export default NavLink;

View File

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { Navigate, useLocation } from "react-router-dom"; import { Navigate, useLocation } from "react-router-dom";
import { getUser } from "../util/Util"; import { getUser } from "../util/Util";
export const PrivateRoute = ({ children }: {children: JSX.Element}) => { export const PrivateRoute = ({ children }: {children: JSX.Element}) =>
const user = getUser(); {
const location = useLocation(); const user = getUser();
if (!user) return <Navigate to='/login' replace state={{ from: location }} />; const location = useLocation();
return children; if (!user)
return <Navigate to='/login' replace state={{ from: location }} />;
return children;
}; };

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { getUser } from "../util/Util"; import { getUser } from "../util/Util";
export const UnauthedRoute = ({ children }: {children: JSX.Element}) => { export const UnauthedRoute = ({ children }: {children: JSX.Element}) =>
const user = getUser(); {
if (user) return <Navigate to='/home' replace />; const user = getUser();
return children; if (user)
return <Navigate to='/home' replace />;
return children;
}; };

View File

@ -1,31 +1,35 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { getUser } from '../util/Util'; import { getUser } from '../util/Util';
import {User} from '../@types/ApiStructures'; import {User} from '../@types/ApiStructures';
type UpdateFunc = ((user?: User | null) => void) type UpdateFunc = ((user?: User | null) => void)
const LoginContext = React.createContext<User | null>(null); const LoginContext = React.createContext<User | null>(null);
const LoginUpdateContext = React.createContext<UpdateFunc>(() => { /* */ }); const LoginUpdateContext = React.createContext<UpdateFunc>(() =>
{ /* */ });
// Hook
export const useLoginContext = (): [User | null, UpdateFunc] => { // Hook
return [useContext(LoginContext), useContext(LoginUpdateContext)]; export const useLoginContext = (): [User | null, UpdateFunc] =>
}; {
return [useContext(LoginContext), useContext(LoginUpdateContext)];
// Component };
export const UserContext = ({ children }: {children: React.ReactNode}) => {
// Component
const [user, setLoginState] = useState(getUser()); export const UserContext = ({ children }: {children: React.ReactNode}) =>
const updateLoginState = () => { {
setLoginState(getUser());
}; const [user, setLoginState] = useState(getUser());
const updateLoginState = () =>
return ( {
<LoginContext.Provider value={user}> setLoginState(getUser());
<LoginUpdateContext.Provider value={updateLoginState}> };
{children}
</LoginUpdateContext.Provider> return (
</LoginContext.Provider> <LoginContext.Provider value={user}>
); <LoginUpdateContext.Provider value={updateLoginState}>
{children}
</LoginUpdateContext.Provider>
</LoginContext.Provider>
);
}; };

View File

@ -1,41 +1,47 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
// Listens for mouse clicks outside of the wrapped element // Listens for mouse clicks outside of the wrapped element
const alerter = (ref: React.RefObject<HTMLElement>, callback: () => void) => { const alerter = (ref: React.RefObject<HTMLElement>, callback: () => void) =>
useEffect(() => { {
useEffect(() =>
const listener = (event: MouseEvent) => { {
if (ref.current && !ref.current.contains(event.target as Node)) {
return callback(); const listener = (event: MouseEvent) =>
} {
}; if (ref.current && !ref.current.contains(event.target as Node))
{
document.addEventListener('mousedown', listener); return callback();
}
return () => { };
document.removeEventListener('mousedown', listener);
}; document.addEventListener('mousedown', listener);
}, [ref]); return () =>
}; {
document.removeEventListener('mousedown', listener);
/** };
* Component wrapper to enable listening for clicks outside of the given component
* }, [ref]);
* @param {{children: React.ReactNode, callback: () => void}} { children, callback } };
* @return {*}
*/ /**
const ClickDetector = ({ children, callback }: {children: React.ReactNode, callback: () => void}) => { * Component wrapper to enable listening for clicks outside of the given component
*
const wrapperRef = useRef(null); * @param {{children: React.ReactNode, callback: () => void}} { children, callback }
alerter(wrapperRef, callback); * @return {*}
*/
return ( const ClickDetector = ({ children, callback }: {children: React.ReactNode, callback: () => void}) =>
<div className='click-detector' ref={wrapperRef}> {
{children}
</div> const wrapperRef = useRef(null);
); alerter(wrapperRef, callback);
}; return (
<div className='click-detector' ref={wrapperRef}>
{children}
</div>
);
};
export default ClickDetector; export default ClickDetector;

View File

@ -1,34 +1,39 @@
import React from "react"; import React from "react";
type BoundaryProps = { type BoundaryProps = {
[key: string]: unknown, [key: string]: unknown,
fallback?: React.ReactNode, fallback?: React.ReactNode,
children: React.ReactNode children: React.ReactNode
} }
type StateProps = { type StateProps = {
error: boolean error: boolean
} }
class ErrorBoundary extends React.Component<BoundaryProps, StateProps> { class ErrorBoundary extends React.Component<BoundaryProps, StateProps>
{
fallback?: React.ReactNode;
fallback?: React.ReactNode;
constructor(props: BoundaryProps) {
super(props); constructor(props: BoundaryProps)
this.state = { error: false }; {
this.fallback = props.fallback; super(props);
} this.state = { error: false };
this.fallback = props.fallback;
override componentDidCatch(error: Error, errorInfo: unknown) { }
this.setState({error: true});
console.error(error, errorInfo); override componentDidCatch(error: Error, errorInfo: unknown)
} {
this.setState({error: true});
override render() { console.error(error, errorInfo);
if (this.state.error) return this.fallback || <h1>Something went wrong :/</h1>; }
return this.props.children;
} override render()
{
} if (this.state.error)
return this.fallback || <h1>Something went wrong :/</h1>;
return this.props.children;
}
}
export default ErrorBoundary; export default ErrorBoundary;

View File

@ -1,11 +1,14 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { Permissions as Perms } from "../@types/ApiStructures"; import { Permissions as Perms } from "../@types/ApiStructures";
export const Permission = ({ name, value, chain, updatePerms }: {name: string, value: number | Perms, chain: string, updatePerms: (chain: string, perm: string) => void}) => { export const Permission = ({ name, value, chain, updatePerms }: {name: string, value: number | Perms, chain: string, updatePerms: (chain: string, perm: string) => void}) =>
{
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const onChange = () => { const onChange = () =>
{
const val = inputRef.current?.value || null; const val = inputRef.current?.value || null;
if (val === null) return; if (val === null)
return;
updatePerms(chain, val); updatePerms(chain, val);
}; };
@ -15,13 +18,17 @@ export const Permission = ({ name, value, chain, updatePerms }: {name: string, v
</li>; </li>;
}; };
export const PermissionGroup = ({ name, value, chain, updatePerms }: {name: string, value: number | Perms, chain: string, updatePerms: (chain: string, perm: string) => void}) => { export const PermissionGroup = ({ name, value, chain, updatePerms }: {name: string, value: number | Perms, chain: string, updatePerms: (chain: string, perm: string) => void}) =>
{
const elements = []; const elements = [];
for (const [perm, val] of Object.entries(value)) { for (const [perm, val] of Object.entries(value))
{
const props = { key: perm, name: perm, value: val, updatePerms, chain: `${chain}:${perm}` }; const props = { key: perm, name: perm, value: val, updatePerms, chain: `${chain}:${perm}` };
if(typeof val ==='object') elements.push(<PermissionGroup {...props} />); if(typeof val ==='object')
else elements.push(<Permission {...props} />); elements.push(<PermissionGroup {...props} />);
else
elements.push(<Permission {...props} />);
} }
return <li> return <li>
<div className="groupName"> <div className="groupName">
@ -33,39 +40,48 @@ export const PermissionGroup = ({ name, value, chain, updatePerms }: {name: stri
</li>; </li>;
}; };
export const Permissions = ({ perms, onUpdate }: { perms: Perms, onUpdate: (perms: Perms) => void }) => { export const Permissions = ({ perms, onUpdate }: { perms: Perms, onUpdate: (perms: Perms) => void }) =>
{
const updatePerms = (chain: string, raw: string) => { const updatePerms = (chain: string, raw: string) =>
{
const value = parseInt(raw); const value = parseInt(raw);
if (isNaN(value)) return; if (isNaN(value))
return;
let selected = perms; let selected = perms;
const keys = chain.split(':'); const keys = chain.split(':');
for (const key of keys) { for (const key of keys)
if (key === keys[keys.length - 1]) selected[key] = value; {
else selected = selected[key] as Perms; if (key === keys[keys.length - 1])
selected[key] = value;
else
selected = selected[key] as Perms;
} }
onUpdate(perms); onUpdate(perms);
}; };
const elements = []; const elements = [];
const keys = Object.keys(perms); const keys = Object.keys(perms);
for (const perm of keys) { for (const perm of keys)
{
const props = { key: perm, name: perm, value: perms[perm], chain: perm, updatePerms }; const props = { key: perm, name: perm, value: perms[perm], chain: perm, updatePerms };
let Elem = null; let Elem = null;
switch (typeof perms[perm]) { switch (typeof perms[perm])
case 'number': {
Elem = Permission; case 'number':
break; Elem = Permission;
case 'object': break;
Elem = PermissionGroup; case 'object':
break; Elem = PermissionGroup;
default: break;
// eslint-disable-next-line react/display-name default:
Elem = () => { // eslint-disable-next-line react/display-name
return <p>Uknown permission structure</p>; Elem = () =>
}; {
break; return <p>Uknown permission structure</p>;
};
break;
} }
elements.push(<Elem {...props} />); elements.push(<Elem {...props} />);
} }

View File

@ -1,38 +1,41 @@
import React, {Fragment} from "react"; import React, {Fragment} from "react";
import { RateLimit as RL, RateLimits as RLs } from "../@types/ApiStructures"; import { RateLimit as RL, RateLimits as RLs } from "../@types/ApiStructures";
const RateLimit = ({route, limit}: {route: string, limit: RL}) => { const RateLimit = ({route, limit}: {route: string, limit: RL}) =>
{
return <Fragment>
<p><b>{route}</b></p> return <Fragment>
<label>Limit</label> <p><b>{route}</b></p>
<input min={0} defaultValue={limit.limit} type='number' /> <label>Limit</label>
<label>Time</label> <input min={0} defaultValue={limit.limit} type='number' />
<input min={0} defaultValue={limit.time} type='number' /> <label>Time</label>
<label>Enabled</label> <input min={0} defaultValue={limit.time} type='number' />
<div className="check-box"> <label>Enabled</label>
<input defaultChecked={!limit.disabled} type='checkbox' /> <div className="check-box">
</div> <input defaultChecked={!limit.disabled} type='checkbox' />
</Fragment>; </div>
}; </Fragment>;
};
export const RateLimits = ({ rateLimits }: {rateLimits: RLs}) => {
export const RateLimits = ({ rateLimits }: {rateLimits: RLs}) =>
const routes = Object.keys(rateLimits); {
const elements = routes.map((route, index) => {
return <div key={index} className="card"> const routes = Object.keys(rateLimits);
<RateLimit {...{route, limit:rateLimits[route], index}} /> const elements = routes.map((route, index) =>
</div>; {
}); return <div key={index} className="card">
<RateLimit {...{route, limit:rateLimits[route], index}} />
return <div className='col-6-lg col-12'> </div>;
<h2>Rate Limits</h2> });
<div className="flex flex-wrap">
{elements} return <div className='col-6-lg col-12'>
</div> <h2>Rate Limits</h2>
<div className="flex flex-wrap">
<code> {elements}
{JSON.stringify(rateLimits, null, 4)} </div>
</code>
</div>; <code>
{JSON.stringify(rateLimits, null, 4)}
</code>
</div>;
}; };