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

@ -28,7 +28,8 @@
"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": { "eslintConfig": {
"extends": [ "extends": [

View File

@ -17,103 +17,109 @@ 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 [user, updateUser] = useLoginContext();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() =>
(async () => {
const settings = await get('/api/settings');
setSettings(settings.data as ClientSettings);
const result = await get('/api/user');
if (result.status === 200) {
setSession(result.data as User);
updateUser();
}
setLoading(false);
if (result.data?.twoFactor) return navigate('/login/verify');
})();
}, []);
const menuItems = [
{ {
to: '/home', label: 'Home', items: [ (async () =>
{ to: '/profile', label: 'Profile', relative: true }, {
{ to: '/applications', label: 'Applications', relative: true }
]
},
{
to: '/admin', label: 'Admin', items: [
{ to: '/users', label: 'Users', relative: true },
{ to: '/roles', label: 'Roles', relative: true },
{ to: '/flags', label: 'Flags', relative: true }
]
}
];
if (loading) return null; const settings = await get('/api/settings');
setSettings(settings.data as ClientSettings);
return ( const result = await get('/api/user');
<div className='app'> if (result.status === 200)
{
setSession(result.data as User);
updateUser();
}
setLoading(false);
if (result.data?.twoFactor)
return navigate('/login/verify');
})();
}, []);
<div className='background'> const menuItems = [
{
to: '/home', label: 'Home', items: [
{ to: '/profile', label: 'Profile', relative: true },
{ to: '/applications', label: 'Applications', relative: true }
]
},
{
to: '/admin', label: 'Admin', items: [
{ to: '/users', label: 'Users', relative: true },
{ to: '/roles', label: 'Roles', relative: true },
{ to: '/flags', label: 'Flags', relative: true }
]
}
];
{user ? if (loading)
<div> return null;
<header className="card">
<UserControls />
</header>
<Sidebar>
<SidebarMenu menuItems={menuItems} />
</Sidebar>
</div>
: null}
<div className={`main-content ${user ? "" : "login"}`}> return (
<div className='app'>
<ErrorBoundary> <div className='background'>
<Routes> {user ?
<div>
<header className="card">
<UserControls />
</header>
<Sidebar>
<SidebarMenu menuItems={menuItems} />
</Sidebar>
</div>
: null}
<Route path='/home/*' element={<PrivateRoute> <div className={`main-content ${user ? "" : "login"}`}>
<TitledPage title='Home'>
<Home />
</TitledPage>
</PrivateRoute >} />
<Route path='/admin/*' element={<PrivateRoute> <ErrorBoundary>
<TitledPage title='Admin'>
<Admin />
</TitledPage>
</PrivateRoute >} />
<Route path='/login/*' element={<UnauthedRoute> <Routes>
<Login />
</UnauthedRoute>} />
<Route path='/register/*' element={<UnauthedRoute> <Route path='/home/*' element={<PrivateRoute>
<Register /> <TitledPage title='Home'>
</UnauthedRoute>} /> <Home />
</TitledPage>
</PrivateRoute >} />
<Route path='*' element={<Navigate to='/home' />} /> <Route path='/admin/*' element={<PrivateRoute>
<TitledPage title='Admin'>
<Admin />
</TitledPage>
</PrivateRoute >} />
<Route path='/login/*' element={<UnauthedRoute>
<Login />
</UnauthedRoute>} />
<Route path='/register/*' element={<UnauthedRoute>
<Register />
</UnauthedRoute>} />
<Route path='*' element={<Navigate to='/home' />} />
</Routes>
</ErrorBoundary>
<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>
</Routes>
</ErrorBoundary>
<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>
</div>
);
} }
export default App; export default App;

View File

@ -3,20 +3,25 @@ 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'); if (!cb)
throw new Error('Missing callback');
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const onDragOver: React.MouseEventHandler = (event) => { const onDragOver: React.MouseEventHandler = (event) =>
{
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}; };
const onDrop: React.DragEventHandler = (event) => { const onDrop: React.DragEventHandler = (event) =>
{
event.preventDefault(); event.preventDefault();
const { dataTransfer } = event; const { dataTransfer } = event;
if (!dataTransfer.files.length) return; if (!dataTransfer.files.length)
return;
const [file] = dataTransfer.files; const [file] = dataTransfer.files;
setFile(file); setFile(file);
@ -28,13 +33,15 @@ export const FileSelector = ({ cb }: { cb: (file: File) => void }) => {
or or
<span className="drop-title">Click to select a file</span> <span className="drop-title">Click to select a file</span>
<p className="fileName m-0">{file ? `Selected: ${file.name}` : null}</p> <p className="fileName m-0">{file ? `Selected: ${file.name}` : null}</p>
<input onChange={(event) => { <input onChange={(event) =>
if (!event.target.files) return; {
if (!event.target.files)
return;
const [f] = event.target.files; const [f] = event.target.files;
setFile(f); setFile(f);
cb(f); cb(f);
}} }}
type="file" id="pfp_upload" accept="image/*" required></input> type="file" id="pfp_upload" accept="image/*" required></input>
</label>; </label>;
}; };
@ -46,7 +53,8 @@ export type InputElementProperties<T> = {
placeholder?: string placeholder?: string
} }
export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) => { export const ToggleSwitch = ({ 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"> <span className="check-box check-box-row mb-2">
{children && <b>{children}</b>} {children && <b>{children}</b>}
@ -55,7 +63,8 @@ export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputEleme
</label>; </label>;
}; };
export const VerticalToggleSwitch = ({ 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">
{children && <b>{children}</b>} {children && <b>{children}</b>}
<span className="check-box"> <span className="check-box">
@ -73,7 +82,8 @@ type StringInputProperties = {
maxLength?: number, maxLength?: number,
minLength?: number minLength?: number
} & ManualInputProperties<string> } & ManualInputProperties<string>
export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) => { export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) =>
{
const input = <input const input = <input
onBlur={onBlur} onBlur={onBlur}
@ -100,11 +110,16 @@ export type NumberInputProperties = {
step?: number step?: number
} & ManualInputProperties<number> } & ManualInputProperties<number>
export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) => { export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) =>
if (typeof step === 'undefined') { {
if (type === 'float') step = 0.1; if (typeof step === 'undefined')
else if (type === 'int') step = 1; {
else step = 1; if (type === 'float')
step = 0.1;
else if (type === 'int')
step = 1;
else
step = 1;
} }
const input = <input const input = <input
@ -118,58 +133,68 @@ export const NumberInput = ({ children, placeholder, inputRef, onChange, value,
step={step} step={step}
/>; />;
if (children) return <label> if (children)
<b>{children}</b> return <label>
{input} <b>{children}</b>
</label>; {input}
</label>;
return input; return input;
}; };
export const ClickToEdit = ({ value, onUpdate, inputElement }: export const ClickToEdit = ({ value, onUpdate, inputElement }:
{ value: string, onUpdate?: (value: string) => void, inputElement?: React.ReactElement }) => { { value: string, onUpdate?: (value: string) => void, inputElement?: React.ReactElement }) =>
{
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
const onClick = () => { const onClick = () =>
{
setEditing(false); setEditing(false);
if (ref.current && onUpdate) if (ref.current && onUpdate)
onUpdate(ref.current.value); onUpdate(ref.current.value);
}; };
const input = inputElement ? inputElement : <input defaultValue={value} ref={ref} />; const input = inputElement ? inputElement : <input defaultValue={value} ref={ref} />;
if (editing) return <span className='input-group'> if (editing)
{input} return <span className='input-group'>
<button className="button primary" onClick={onClick} > {input}
<button className="button primary" onClick={onClick} >
OK OK
</button> </button>
</span>; </span>;
return <span onClick={() => setEditing(true)} className='mt-0 mb-1 clickable'>{value}</span>; return <span onClick={() => setEditing(true)} className='mt-0 mb-1 clickable'>{value}</span>;
}; };
const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) => { const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) =>
{
return <summary className={`clickable card is-vertical-align header p-2 ${className}`}> return <summary className={`clickable card is-vertical-align header p-2 ${className}`}>
{children} {children}
</summary>; </summary>;
}; };
const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) => { const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) =>
{
let InnerElement = null; let InnerElement = null;
if (type === 'multi-select') if (type === 'multi-select')
InnerElement = <ToggleSwitch value={selected || false} onChange={onClick}> InnerElement = <ToggleSwitch value={selected || false} onChange={onClick}>
{children as string} {children as string}
</ToggleSwitch>; </ToggleSwitch>;
else InnerElement = <div onClick={(event) => { else
event.preventDefault(); InnerElement = <div onClick={(event) =>
if (onClick) onClick(event); {
}}> event.preventDefault();
{children} if (onClick)
</div>; onClick(event);
}}>
{children}
</div>;
return <div className='card item clickable'> return <div className='card item clickable'>
{InnerElement} {InnerElement}
</div>; </div>;
}; };
const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) => { const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) =>
{
return <div className={`card item-list ${className}`}> return <div className={`card item-list ${className}`}>
{children} {children}
</div>; </div>;
@ -183,7 +208,8 @@ type DropdownProps = {
className?: string className?: string
} }
const DropdownComp = ({ children, className = '' }: DropdownProps) => { const DropdownComp = ({ children, className = '' }: DropdownProps) =>
{
if (!children) if (!children)
throw new Error('Missing children'); throw new Error('Missing children');
@ -193,8 +219,10 @@ const DropdownComp = ({ children, className = '' }: DropdownProps) => {
const detailsRef = useRef<HTMLDetailsElement>(null); const detailsRef = useRef<HTMLDetailsElement>(null);
return <ClickDetector callback={() => { return <ClickDetector callback={() =>
if (detailsRef.current) detailsRef.current.open = false; {
if (detailsRef.current)
detailsRef.current.open = false;
}}> }}>
<details ref={detailsRef} className={`dropdown w-100 ${className}`}> <details ref={detailsRef} className={`dropdown w-100 ${className}`}>
{children} {children}

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

@ -7,38 +7,47 @@ 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'> return <div className='sidebar card'>
{children} {children}
</div>; </div>;
}; };
const toggleMenu: React.MouseEventHandler = (event) => { const toggleMenu: React.MouseEventHandler = (event) =>
{
event.preventDefault(); event.preventDefault();
(event.target as HTMLElement).parentElement?.classList.toggle("open"); (event.target as HTMLElement).parentElement?.classList.toggle("open");
}; };
const expandMobileMenu: React.MouseEventHandler = (event) => { const expandMobileMenu: React.MouseEventHandler = (event) =>
{
event.preventDefault(); event.preventDefault();
(event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu"); (event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu");
}; };
const closeMobileMenu: React.MouseEventHandler = (event) => { const closeMobileMenu: React.MouseEventHandler = (event) =>
{
const element = event.target as HTMLElement; const element = event.target as HTMLElement;
if(element.classList.contains("sidebar-menu-item-carrot")) return; if(element.classList.contains("sidebar-menu-item-carrot"))
return;
(element.getRootNode() as ParentNode).querySelector(".sidebar")?.classList.remove("show-menu"); (element.getRootNode() as ParentNode).querySelector(".sidebar")?.classList.remove("show-menu");
}; };
// Nav menu // Nav menu
const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) => { const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) =>
{
const [user, updateUser] = useLoginContext(); const [user, updateUser] = useLoginContext();
const navigate = useNavigate(); const navigate = useNavigate();
if (!user) return null; if (!user)
return null;
const logOut = async () => { const logOut = async () =>
{
const response = await post('/api/logout'); const response = await post('/api/logout');
if (response.status === 200) { if (response.status === 200)
{
clearSession(); clearSession();
updateUser(); updateUser();
navigate('/login'); navigate('/login');
@ -46,14 +55,18 @@ const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[],
}; };
const elements = []; const elements = [];
for (const menuItem of menuItems) { for (const menuItem of menuItems)
{
const { label, items: subItems = [], ...rest } = menuItem; const { label, items: subItems = [], ...rest } = menuItem;
let subElements = null; let subElements = null;
if (subItems.length) subElements = subItems.map(({ label, to, relative = true }) => { if (subItems.length)
if(relative) to = `${menuItem.to}${to}`; subElements = subItems.map(({ label, to, relative = true }) =>
return <NavLink className='sidebar-menu-item sidebar-menu-child-item' onClick={closeMobileMenu} activeClassName='active' to={to} key={label}>{label}</NavLink>; {
}); 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>;
});
elements.push(<div key={label} className='parent-menu'> 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}> <NavLink className={`sidebar-menu-item ${subElements?.length ? 'has-children' : ''}`} onClick={closeMobileMenu} activeClassName='active open' {...rest} key={label}>

View File

@ -17,14 +17,16 @@ type TableProps = {
itemKeys?: string[] itemKeys?: string[]
} }
export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) => { export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) =>
{
return <tr onClick={onClick}> return <tr onClick={onClick}>
{itemKeys.map(key => <td key={key}>{item[key] as string}</td>)} {itemKeys.map(key => <td key={key}>{item[key] as string}</td>)}
</tr>; </tr>;
}; };
export const Table = ({headerItems, children, items, itemKeys}: TableProps) => { export const Table = ({headerItems, children, items, itemKeys}: TableProps) =>
{
if (!headerItems) if (!headerItems)
throw new Error(`Missing table headers`); throw new Error(`Missing table headers`);

View File

@ -1,7 +1,8 @@
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"> return <div className="page">
<h2 className="pageTitle">{title}</h2> <h2 className="pageTitle">{title}</h2>

View File

@ -5,25 +5,31 @@ 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 [user, updateUser] = useLoginContext();
const detailsRef = useRef<HTMLDetailsElement>(null); const detailsRef = useRef<HTMLDetailsElement>(null);
const navigate = useNavigate(); const navigate = useNavigate();
if (!user) return null; if (!user)
return null;
const logOut = async () => { const logOut = async () =>
{
const response = await post('/api/logout'); const response = await post('/api/logout');
if (response.status === 200) { if (response.status === 200)
{
clearSession(); clearSession();
updateUser(); updateUser();
navigate('/login'); navigate('/login');
} }
}; };
return <ClickDetector callback={() => { return <ClickDetector callback={() =>
if (detailsRef.current) detailsRef.current.removeAttribute('open'); {
if (detailsRef.current)
detailsRef.current.removeAttribute('open');
}}> }}>
<details ref={detailsRef} className='dropdown user-controls'> <details ref={detailsRef} className='dropdown user-controls'>
<summary className="is-vertical-align"> <summary className="is-vertical-align">

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

@ -8,10 +8,12 @@ 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) => { const submit: React.MouseEventHandler = (event) =>
{
event.preventDefault(); event.preventDefault();
console.log(file); console.log(file);
}; };
@ -36,7 +38,8 @@ const Main = () => {
</div>; </div>;
}; };
const Admin = () => { const Admin = () =>
{
return <ErrorBoundary> return <ErrorBoundary>
<Routes> <Routes>

View File

@ -5,14 +5,16 @@ 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> return <div>
What to put here? hmmm What to put here? hmmm
</div>; </div>;
}; };
const Home = () => { const Home = () =>
{
return <ErrorBoundary> return <ErrorBoundary>
<Routes> <Routes>

View File

@ -7,7 +7,8 @@ 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 [, setUser] = useLoginContext();
const [fail, setFail] = useState(false); const [fail, setFail] = useState(false);
@ -18,24 +19,28 @@ const CredentialFields = () => {
const loginMethods = getSettings()?.OAuthProviders || []; const loginMethods = getSettings()?.OAuthProviders || [];
const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) => { const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) =>
{
ref.current?.continuousStart(); ref.current?.continuousStart();
event.preventDefault(); event.preventDefault();
// const username = (document.getElementById('username') as HTMLInputElement)?.value; // const username = (document.getElementById('username') as HTMLInputElement)?.value;
// const password = (document.getElementById('password') as HTMLInputElement)?.value; // const password = (document.getElementById('password') as HTMLInputElement)?.value;
const username = usernameRef.current?.value; const username = usernameRef.current?.value;
const password = passwordRef.current?.value; const password = passwordRef.current?.value;
if (!username?.length || !password?.length) return; if (!username?.length || !password?.length)
return;
const result = await post('/api/login', { username, password }); const result = await post('/api/login', { username, password });
if (![200, 302].includes(result.status)) { if (![200, 302].includes(result.status))
{
ref.current?.complete(); ref.current?.complete();
setFail(true); setFail(true);
return; return;
} }
if (!result.data?.twoFactor) { if (!result.data?.twoFactor)
{
setUser(await fetchUser()); setUser(await fetchUser());
ref.current?.complete(); ref.current?.complete();
return navigate('/home'); return navigate('/home');
@ -73,7 +78,10 @@ const CredentialFields = () => {
{loginMethods.map((method: OAuthProvider) => <div {loginMethods.map((method: OAuthProvider) => <div
className='third-party-login' className='third-party-login'
key={method.name} key={method.name}
onClick={() => { window.location.pathname = `/api/login/${method.name}`; }}> onClick={() =>
{
window.location.pathname = `/api/login/${method.name}`;
}}>
<img <img
crossOrigin="anonymous" crossOrigin="anonymous"
alt={method.name} alt={method.name}
@ -81,13 +89,14 @@ const CredentialFields = () => {
width={48} height={48} width={48} height={48}
/> />
<p><b>{method.name}</b></p> <p><b>{method.name}</b></p>
</div>)} </div>)}
</div> </div>
</div> </div>
</div>; </div>;
}; };
const TwoFactor = () => { const TwoFactor = () =>
{
const [fail, setFail] = useState(false); const [fail, setFail] = useState(false);
const [, setUser] = useLoginContext(); const [, setUser] = useLoginContext();
@ -95,14 +104,17 @@ const TwoFactor = () => {
const mfaCode = useRef<HTMLInputElement>(null); const mfaCode = useRef<HTMLInputElement>(null);
const ref = useRef<LoadingBarRef>(null); const ref = useRef<LoadingBarRef>(null);
const twoFactorClick: React.MouseEventHandler = async (event) => { const twoFactorClick: React.MouseEventHandler = async (event) =>
{
event.preventDefault(); event.preventDefault();
// const code = document.getElementById('2faCode').value; // const code = document.getElementById('2faCode').value;
const code = mfaCode.current?.value; const code = mfaCode.current?.value;
if (!code) return; if (!code)
return;
ref.current?.continuousStart(); ref.current?.continuousStart();
const result = await post('/api/login/verify', { code }); const result = await post('/api/login/verify', { code });
if (result.status === 200) { if (result.status === 200)
{
setUser(await fetchUser()); setUser(await fetchUser());
ref.current?.complete(); ref.current?.complete();
return navigate('/home'); return navigate('/home');
@ -128,7 +140,8 @@ const TwoFactor = () => {
</div>; </div>;
}; };
const Login = () => { const Login = () =>
{
return <div className="row is-center is-full-screen is-marginless"> return <div className="row is-center is-full-screen is-marginless">
<div className='loginWrapper col-6 col-6-md col-3-lg'> <div className='loginWrapper col-6 col-6-md col-3-lg'>

View File

@ -5,7 +5,8 @@ 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 [error, setError] = useState<string>();
const [params] = useSearchParams(); const [params] = useSearchParams();
@ -17,17 +18,20 @@ const Register = () => {
document.body.classList.add('bg-triangles'); document.body.classList.add('bg-triangles');
const code = params.get('code'); const code = params.get('code');
const submit: React.MouseEventHandler = async (event) => { const submit: React.MouseEventHandler = async (event) =>
{
ref.current?.continuousStart(); ref.current?.continuousStart();
event.preventDefault(); event.preventDefault();
const username = usernameRef.current?.value; const username = usernameRef.current?.value;
const password = passwordRef.current?.value; const password = passwordRef.current?.value;
if (!username?.length || !password?.length) { if (!username?.length || !password?.length)
{
ref.current?.complete(); ref.current?.complete();
return; return;
} }
const response = await post('/api/register', { username, password, code }); const response = await post('/api/register', { username, password, code });
if (response.status !== 200) { if (response.status !== 200)
{
ref.current?.complete(); ref.current?.complete();
return setError(response.message as string || 'unknown error'); return setError(response.message as string || 'unknown error');
} }

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

@ -15,23 +15,27 @@ type PartialUser = {
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 Context = ({ children }: { children: React.ReactNode }) =>
{
const [user, updateUser] = useState<PartialUser | null>(null); const [user, updateUser] = useState<PartialUser | null>(null);
return <SelectedUserContext.Provider value={{ user, updateUser }}> return <SelectedUserContext.Provider value={{ user, updateUser }}>
{children} {children}
</SelectedUserContext.Provider>; </SelectedUserContext.Provider>;
}; };
const ApplicationList = ({ apps }: { apps: App[] }) => { const ApplicationList = ({ apps }: { apps: App[] }) =>
{
const navigate = useNavigate(); const navigate = useNavigate();
return <Table headerItems={['Name', 'ID']}> return <Table headerItems={['Name', 'ID']}>
{apps.map(app => <TableListEntry {apps.map(app => <TableListEntry
onClick={() => { onClick={() =>
{
navigate(`applications/${app.id}`); navigate(`applications/${app.id}`);
}} }}
key={app.id} key={app.id}
@ -42,9 +46,11 @@ const ApplicationList = ({ apps }: { apps: App[] }) => {
}; };
const Application = ({ app }: { app: App }) => { const Application = ({ app }: { app: App }) =>
{
const commitPerms = async () => { const commitPerms = async () =>
{
await post(`/api/applications/${app.id}/permissions`, perms); await post(`/api/applications/${app.id}/permissions`, perms);
}; };
const [perms, updatePerms] = useState<Perms>(app.permissions); const [perms, updatePerms] = useState<Perms>(app.permissions);
@ -77,29 +83,34 @@ const Application = ({ app }: { app: App }) => {
</div>; </div>;
}; };
const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => { const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) =>
{
return <div className='card role' > return <div className='card role' >
{role.name} <span className="clickable" onClick={onClick}>X</span> {role.name} <span className="clickable" onClick={onClick}>X</span>
</div>; </div>;
}; };
const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => { const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) =>
{
const [unequippedRoles, updateUnequipped] = useState<Role[]>(roles.filter(role => !userRoles.some(r => role.id === r.id))); const [unequippedRoles, updateUnequipped] = useState<Role[]>(roles.filter(role => !userRoles.some(r => role.id === r.id)));
const [equippedRoles, updateEquipped] = useState<Role[]>(userRoles); const [equippedRoles, updateEquipped] = useState<Role[]>(userRoles);
const roleSelected = (role: Role) => { const roleSelected = (role: Role) =>
{
updateEquipped([...equippedRoles, role]); updateEquipped([...equippedRoles, role]);
updateUnequipped(unequippedRoles.filter(r => r.id !== role.id)); updateUnequipped(unequippedRoles.filter(r => r.id !== role.id));
}; };
const roleDeselected = (role: Role) => { const roleDeselected = (role: Role) =>
{
updateEquipped(equippedRoles.filter(r => r.id !== role.id)); updateEquipped(equippedRoles.filter(r => r.id !== role.id));
updateUnequipped([...unequippedRoles, role]); updateUnequipped([...unequippedRoles, role]);
}; };
return <Dropdown> return <Dropdown>
<Dropdown.Header className='role-selector'> <Dropdown.Header className='role-selector'>
{equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={(event) => { {equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={(event) =>
{
event.preventDefault(); event.preventDefault();
roleDeselected(role); roleDeselected(role);
}} />)} }} />)}
@ -109,7 +120,8 @@ const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
{unequippedRoles.map(role => <Dropdown.Item {unequippedRoles.map(role => <Dropdown.Item
key={role.id} key={role.id}
onClick={() => { onClick={() =>
{
roleSelected(role); roleSelected(role);
}} }}
> >
@ -121,16 +133,20 @@ const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
</Dropdown>; </Dropdown>;
}; };
const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => { const User = ({ user, roles }: { user: APIUser, roles: Role[] }) =>
{
// const [apps, updateApps] = useState<App[]>([]); // const [apps, updateApps] = useState<App[]>([]);
const [perms, updatePerms] = useState<Perms>(user.permissions); const [perms, updatePerms] = useState<Perms>(user.permissions);
const userContext = useContext(SelectedUserContext); const userContext = useContext(SelectedUserContext);
useEffect(() => { useEffect(() =>
(async () => { {
(async () =>
{
const appsResponse = await get(`/api/users/${user.id}/applications`); const appsResponse = await get(`/api/users/${user.id}/applications`);
if (appsResponse.success) { if (appsResponse.success)
{
const a = appsResponse.data as App[]; const a = appsResponse.data as App[];
// updateApps(a); // updateApps(a);
userContext.updateUser({ apps: a }); userContext.updateUser({ apps: a });
@ -138,7 +154,8 @@ const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
})(); })();
}, []); }, []);
const commitPerms = async () => { const commitPerms = async () =>
{
await post(`/api/users/${user.id}/permissions`, perms); await post(`/api/users/${user.id}/permissions`, perms);
}; };
@ -190,18 +207,22 @@ const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
</div>; </div>;
}; };
const CreateUserField = ({ className }: { className: string }) => { const CreateUserField = ({ className }: { className: string }) =>
{
const [link, setLink] = useState<string | null>(null); const [link, setLink] = useState<string | null>(null);
const getSignupCode = async () => { const getSignupCode = async () =>
{
const response = await get('/api/register/code'); const response = await get('/api/register/code');
if (response.status === 200 && response.data) { if (response.status === 200 && response.data)
{
const link = `${window.location.origin}/register?code=${response.data.code}`; const link = `${window.location.origin}/register?code=${response.data.code}`;
setLink(link); setLink(link);
} }
}; };
const copyToClipboard: React.MouseEventHandler = (event) => { const copyToClipboard: React.MouseEventHandler = (event) =>
{
const element = event.target as HTMLElement; const element = event.target as HTMLElement;
const { parentElement } = element; const { parentElement } = element;
navigator.clipboard.writeText(element.innerText); navigator.clipboard.writeText(element.innerText);
@ -209,7 +230,8 @@ const CreateUserField = ({ className }: { className: string }) => {
(parentElement.childNodes[parentElement.childNodes.length - 1] as HTMLElement).style.visibility = "visible"; (parentElement.childNodes[parentElement.childNodes.length - 1] as HTMLElement).style.visibility = "visible";
}; };
const closeToolTip: React.MouseEventHandler = (event) => { const closeToolTip: React.MouseEventHandler = (event) =>
{
(event.target as HTMLElement).style.visibility = "hidden"; (event.target as HTMLElement).style.visibility = "hidden";
}; };
@ -230,7 +252,8 @@ const CreateUserField = ({ className }: { className: string }) => {
}; };
const Users = () => { const Users = () =>
{
const [users, setUsers] = useState<APIUser[]>([]); const [users, setUsers] = useState<APIUser[]>([]);
const [roles, updateRoles] = useState<Role[]>([]); const [roles, updateRoles] = useState<Role[]>([]);
@ -239,14 +262,19 @@ const Users = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() =>
(async () => { {
(async () =>
{
const result = await get('/api/users', { page }); const result = await get('/api/users', { page });
if (result.success && result.data) { if (result.success && result.data)
{
setError(null); setError(null);
setUsers(result.data.users as APIUser[]); setUsers(result.data.users as APIUser[]);
setPages(result.data.pages); setPages(result.data.pages);
} else setError(result.message || 'Unknown error'); }
else
setError(result.message || 'Unknown error');
setLoading(false); setLoading(false);
const rolesResponse = await get('/api/roles', { all: true }); const rolesResponse = await get('/api/roles', { all: true });
if (rolesResponse.success && rolesResponse.data) if (rolesResponse.success && rolesResponse.data)
@ -254,21 +282,25 @@ const Users = () => {
})(); })();
}, [page]); }, [page]);
const UserWrapper = () => { const UserWrapper = () =>
{
const { id } = useParams(); const { id } = useParams();
const user = users.find(u => u.id === id); const user = users.find(u => u.id === id);
if (!user) return <p>Unknown user</p>; if (!user)
return <p>Unknown user</p>;
return <User roles={roles} user={user} />; return <User roles={roles} user={user} />;
}; };
const navigate = useNavigate(); const navigate = useNavigate();
const UserListWrapper = () => { const UserListWrapper = () =>
{
return <div className="user-list-wrapper row"> return <div className="user-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 Users</h4> <h4>All Users</h4>
<Table headerItems={['Username', 'ID']}> <Table headerItems={['Username', 'ID']}>
{users.map(user => <TableListEntry {users.map(user => <TableListEntry
onClick={() => { onClick={() =>
{
navigate(user.id); navigate(user.id);
}} }}
key={user.id} key={user.id}
@ -284,22 +316,28 @@ const Users = () => {
</div>; </div>;
}; };
const ApplicationWrapper = () => { const ApplicationWrapper = () =>
{
const { appid } = useParams(); const { appid } = useParams();
if (!appid) return null; if (!appid)
return null;
const userContext = useContext(SelectedUserContext); const userContext = useContext(SelectedUserContext);
const [app, setApp] = useState<App | null>(userContext.user?.apps.find(app => app.id === appid) || null); const [app, setApp] = useState<App | null>(userContext.user?.apps.find(app => app.id === appid) || null);
useEffect(() => { useEffect(() =>
(async () => { {
if (userContext.user) return; (async () =>
{
if (userContext.user)
return;
const result = await get(`/api/applications/${appid}`); const result = await get(`/api/applications/${appid}`);
if (result.success) if (result.success)
setApp(result.data as App); setApp(result.data as App);
})(); })();
}, [appid]); }, [appid]);
if (!app) return <p>Loading...</p>; if (!app)
return <p>Loading...</p>;
return <Application app={app} />; return <Application app={app} />;
}; };

View File

@ -6,13 +6,15 @@ 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 deleteApp = async () =>
{
// const response = // const response =
await del(`/api/user/applications/${app.id}`); await del(`/api/user/applications/${app.id}`);
}; };
return <div> return <div>
@ -25,16 +27,20 @@ const Application = ({ app }: {app: App}) => {
}; };
const Applications = () => { const Applications = () =>
{
const [applications, setApplications] = useState<App[]>([]); const [applications, setApplications] = useState<App[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() =>
(async () => { {
(async () =>
{
const response = await get('/api/user/applications') as Res; const response = await get('/api/user/applications') as Res;
if (response.status === 200) setApplications(response.data as App[]); if (response.status === 200)
setApplications(response.data as App[]);
setLoading(false); setLoading(false);
})(); })();
}, []); }, []);
@ -43,27 +49,33 @@ const Applications = () => {
const nameField = useRef<HTMLInputElement>(null); const nameField = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const createApp: React.MouseEventHandler = async (event) => { const createApp: React.MouseEventHandler = async (event) =>
{
event.preventDefault(); event.preventDefault();
const button = event.target as HTMLButtonElement; const button = event.target as HTMLButtonElement;
if (!nameField.current || !descField.current) return; if (!nameField.current || !descField.current)
return;
const name = nameField.current?.value; const name = nameField.current?.value;
if (!name) return setError('Missing name'); if (!name)
return setError('Missing name');
const description = descField.current?.value || null; const description = descField.current?.value || null;
nameField.current.value = ''; nameField.current.value = '';
descField.current.value = ''; descField.current.value = '';
button.disabled = true; button.disabled = true;
const response = await post('/api/user/applications', { name, description }); const response = await post('/api/user/applications', { name, description });
if (response.status !== 200) setError(response.message || 'Unknown error'); if (response.status !== 200)
else setApplications((apps) => [...apps, response.data as App]); setError(response.message || 'Unknown error');
else
setApplications((apps) => [...apps, response.data as App]);
button.disabled = false; button.disabled = false;
}; };
const Main = () => { const Main = () =>
{
return <div className="row"> return <div className="row">
<div className={`col ld ${loading && 'loading'}`}> <div className={`col ld ${loading && 'loading'}`}>
@ -90,10 +102,12 @@ const Applications = () => {
</div>; </div>;
}; };
const ApplicationWrapper = () => { const ApplicationWrapper = () =>
{
const { id } = useParams(); const { id } = useParams();
const application = applications.find(app => app.id === id); const application = applications.find(app => app.id === id);
if(!application) return <p>Unknown application</p>; if(!application)
return <p>Unknown application</p>;
return <Application app={application} />; return <Application app={application} />;
}; };

View File

@ -4,33 +4,42 @@ 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 [twoFactorError, set2FAError] = useState<string | null>(null);
const [displayInput, setDisplayInput] = useState(false); const [displayInput, setDisplayInput] = useState(false);
const [qr, setQr] = useState<string | null>(null); const [qr, setQr] = useState<string | null>(null);
const displayQr = async () => { const displayQr = async () =>
{
const response = await get('/api/user/2fa'); const response = await get('/api/user/2fa');
if (response.status !== 200) return set2FAError(response.message || 'Uknown error'); if (response.status !== 200)
return set2FAError(response.message || 'Uknown error');
setQr(response.message as string); setQr(response.message as string);
}; };
const codeRef = useRef<HTMLInputElement>(null); const codeRef = useRef<HTMLInputElement>(null);
const disable2FA: React.MouseEventHandler = async (event) => { const disable2FA: React.MouseEventHandler = async (event) =>
{
event.preventDefault(); event.preventDefault();
const code = codeRef.current?.value; const code = codeRef.current?.value;
if (!code) return; if (!code)
return;
const response = await post('/api/user/2fa/disable', { code }); const response = await post('/api/user/2fa/disable', { code });
if (response.status !== 200) return set2FAError(response.message || 'Unknown error'); if (response.status !== 200)
return set2FAError(response.message || 'Unknown error');
}; };
const submitCode: React.MouseEventHandler = async (event) => { const submitCode: React.MouseEventHandler = async (event) =>
{
event.preventDefault(); event.preventDefault();
const code = codeRef.current?.value; const code = codeRef.current?.value;
if (!code) return; if (!code)
return;
const response = await post('/api/user/2fa/verify', { code }); const response = await post('/api/user/2fa/verify', { code });
if (response.status !== 200) return set2FAError(response.message || 'Unknown error'); if (response.status !== 200)
return set2FAError(response.message || 'Unknown error');
}; };
let inner = <div> let inner = <div>
@ -46,14 +55,15 @@ const TwoFactorControls = ({ user }: {user: User}) => {
</div>; </div>;
if (user.twoFactor) inner = <div> if (user.twoFactor)
{displayInput ? inner = <div>
<form> {displayInput ?
<input placeholder='Authenticator code to disable' ref={codeRef} type='password' /> <form>
<button className="button error" onClick={disable2FA}>Submit</button> <input placeholder='Authenticator code to disable' ref={codeRef} type='password' />
</form> <button className="button error" onClick={disable2FA}>Submit</button>
: <button onClick={() => setDisplayInput(true)} className="button error">Disable 2FA</button>} </form>
</div>; : <button onClick={() => setDisplayInput(true)} className="button error">Disable 2FA</button>}
</div>;
return <div> return <div>
{twoFactorError && <p>{twoFactorError}</p>} {twoFactorError && <p>{twoFactorError}</p>}
@ -62,7 +72,8 @@ const TwoFactorControls = ({ user }: {user: User}) => {
}; };
const ExternalProfile = ({ profile }: {profile: EP}) => { const ExternalProfile = ({ profile }: {profile: EP}) =>
{
return <div> return <div>
<b>{capitalise(profile.provider)}</b> <b>{capitalise(profile.provider)}</b>
<p className="m-0">Username: {profile.username}</p> <p className="m-0">Username: {profile.username}</p>
@ -70,7 +81,8 @@ const ExternalProfile = ({ profile }: {profile: EP}) => {
</div>; </div>;
}; };
const ThirdPartyConnections = ({ user }: {user: User}) => { const ThirdPartyConnections = ({ user }: {user: User}) =>
{
const { externalProfiles } = user; const { externalProfiles } = user;
@ -80,7 +92,8 @@ const ThirdPartyConnections = ({ user }: {user: User}) => {
}; };
const Profile = () => { const Profile = () =>
{
const user = useLoginContext()[0] as User; const user = useLoginContext()[0] as User;
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@ -92,8 +105,10 @@ const Profile = () => {
const newPassRef = useRef<HTMLInputElement>(null); const newPassRef = useRef<HTMLInputElement>(null);
const newPassRepeatRef = useRef<HTMLInputElement>(null); const newPassRepeatRef = useRef<HTMLInputElement>(null);
const submit: React.MouseEventHandler = async (event) => { const submit: React.MouseEventHandler = async (event) =>
if (!file) return; {
if (!file)
return;
const button = event.target as HTMLButtonElement; const button = event.target as HTMLButtonElement;
button.disabled = true; button.disabled = true;
@ -103,12 +118,15 @@ const Profile = () => {
const response = await post('/api/user/avatar', body, { const response = await post('/api/user/avatar', body, {
headers: null headers: null
}); });
if (!response.success) setError(response.message || 'Unknown error'); if (!response.success)
else setFile(null); setError(response.message || 'Unknown error');
else
setFile(null);
button.disabled = false; button.disabled = false;
}; };
const updateSettings: React.MouseEventHandler = async (event) => { const updateSettings: React.MouseEventHandler = async (event) =>
{
event.preventDefault(); event.preventDefault();
const button = event.target as HTMLButtonElement; const button = event.target as HTMLButtonElement;
@ -118,12 +136,15 @@ const Profile = () => {
const newPassword = newPassRef.current?.value; const newPassword = newPassRef.current?.value;
const newPasswordRepeat = newPassRepeatRef.current?.value; const newPasswordRepeat = newPassRepeatRef.current?.value;
if (!currentPassword) return setError('Missing password'); if (!currentPassword)
return setError('Missing password');
button.disabled = true; button.disabled = true;
const data: {username?: string, displayName?: string, password?: string, newPassword?: string} = { username, displayName, password: currentPassword }; const data: {username?: string, displayName?: string, password?: string, newPassword?: string} = { username, displayName, password: currentPassword };
if (currentPassword && newPassword && newPasswordRepeat) { if (currentPassword && newPassword && newPasswordRepeat)
if (newPassword !== newPasswordRepeat) { {
if (newPassword !== newPasswordRepeat)
{
button.disabled = false; button.disabled = false;
return setError('Passwords do not match'); return setError('Passwords do not match');
} }

View File

@ -12,7 +12,8 @@ type NavLinkOptions = {
} }
// 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 return <BaseNavLink
ref={ref} ref={ref}
{...props} {...props}
@ -22,7 +23,7 @@ const NavLink = React.forwardRef(({ activeClassName, activeStyle, ...props }: Na
...isActive ? activeStyle : null ...isActive ? activeStyle : null
})}> })}>
{props?.children} {props?.children}
</BaseNavLink>; </BaseNavLink>;
}); });

View File

@ -2,9 +2,11 @@ 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 user = getUser();
const location = useLocation(); const location = useLocation();
if (!user) return <Navigate to='/login' replace state={{ from: location }} />; if (!user)
return <Navigate to='/login' replace state={{ from: location }} />;
return children; return children;
}; };

View File

@ -2,8 +2,10 @@ 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(); const user = getUser();
if (user) return <Navigate to='/home' replace />; if (user)
return <Navigate to='/home' replace />;
return children; return children;
}; };

View File

@ -5,18 +5,22 @@ 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 // Hook
export const useLoginContext = (): [User | null, UpdateFunc] => { export const useLoginContext = (): [User | null, UpdateFunc] =>
{
return [useContext(LoginContext), useContext(LoginUpdateContext)]; return [useContext(LoginContext), useContext(LoginUpdateContext)];
}; };
// Component // Component
export const UserContext = ({ children }: {children: React.ReactNode}) => { export const UserContext = ({ children }: {children: React.ReactNode}) =>
{
const [user, setLoginState] = useState(getUser()); const [user, setLoginState] = useState(getUser());
const updateLoginState = () => { const updateLoginState = () =>
{
setLoginState(getUser()); setLoginState(getUser());
}; };

View File

@ -1,18 +1,23 @@
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) => { const listener = (event: MouseEvent) =>
if (ref.current && !ref.current.contains(event.target as Node)) { {
if (ref.current && !ref.current.contains(event.target as Node))
{
return callback(); return callback();
} }
}; };
document.addEventListener('mousedown', listener); document.addEventListener('mousedown', listener);
return () => { return () =>
{
document.removeEventListener('mousedown', listener); document.removeEventListener('mousedown', listener);
}; };
@ -25,7 +30,8 @@ const alerter = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
* @param {{children: React.ReactNode, callback: () => void}} { children, callback } * @param {{children: React.ReactNode, callback: () => void}} { children, callback }
* @return {*} * @return {*}
*/ */
const ClickDetector = ({ children, callback }: {children: React.ReactNode, callback: () => void}) => { const ClickDetector = ({ children, callback }: {children: React.ReactNode, callback: () => void}) =>
{
const wrapperRef = useRef(null); const wrapperRef = useRef(null);
alerter(wrapperRef, callback); alerter(wrapperRef, callback);

View File

@ -9,23 +9,28 @@ 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) { constructor(props: BoundaryProps)
{
super(props); super(props);
this.state = { error: false }; this.state = { error: false };
this.fallback = props.fallback; this.fallback = props.fallback;
} }
override componentDidCatch(error: Error, errorInfo: unknown) { override componentDidCatch(error: Error, errorInfo: unknown)
{
this.setState({error: true}); this.setState({error: true});
console.error(error, errorInfo); console.error(error, errorInfo);
} }
override render() { override render()
if (this.state.error) return this.fallback || <h1>Something went wrong :/</h1>; {
if (this.state.error)
return this.fallback || <h1>Something went wrong :/</h1>;
return this.props.children; return this.props.children;
} }

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,25 +1,28 @@
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> return <Fragment>
<p><b>{route}</b></p> <p><b>{route}</b></p>
<label>Limit</label> <label>Limit</label>
<input min={0} defaultValue={limit.limit} type='number' /> <input min={0} defaultValue={limit.limit} type='number' />
<label>Time</label> <label>Time</label>
<input min={0} defaultValue={limit.time} type='number' /> <input min={0} defaultValue={limit.time} type='number' />
<label>Enabled</label> <label>Enabled</label>
<div className="check-box"> <div className="check-box">
<input defaultChecked={!limit.disabled} type='checkbox' /> <input defaultChecked={!limit.disabled} type='checkbox' />
</div> </div>
</Fragment>; </Fragment>;
}; };
export const RateLimits = ({ rateLimits }: {rateLimits: RLs}) => { export const RateLimits = ({ rateLimits }: {rateLimits: RLs}) =>
{
const routes = Object.keys(rateLimits); const routes = Object.keys(rateLimits);
const elements = routes.map((route, index) => { const elements = routes.map((route, index) =>
{
return <div key={index} className="card"> return <div key={index} className="card">
<RateLimit {...{route, limit:rateLimits[route], index}} /> <RateLimit {...{route, limit:rateLimits[route], index}} />
</div>; </div>;