Linter pass
This commit is contained in:
parent
2da5c82360
commit
fd4d72eb21
103
package.json
103
package.json
@ -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"
|
||||||
}
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
244
src/App.tsx
244
src/App.tsx
@ -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> | 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> | 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;
|
||||||
|
@ -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 'n' 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 'n' 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 };
|
@ -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>;
|
||||||
};
|
};
|
@ -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>;
|
||||||
|
@ -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;
|
@ -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>;
|
||||||
|
|
||||||
};
|
};
|
@ -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;
|
@ -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;
|
@ -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 />
|
||||||
|
@ -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;
|
@ -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;
|
@ -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'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'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;
|
@ -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;
|
@ -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}} />
|
||||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||||
};
|
};
|
@ -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;
|
||||||
};
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
|
||||||
};
|
};
|
@ -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;
|
@ -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;
|
@ -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} />);
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
Loading…
Reference in New Issue
Block a user