Rewrite in TypeScript
This commit is contained in:
parent
19fc71cc82
commit
d342c71fd1
@ -6,6 +6,7 @@
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
@ -19,7 +20,8 @@
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-vars": "warn",
|
||||
// "@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-var-requires":"off",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
"react/prop-types": "off",
|
||||
"accessor-pairs": "error",
|
||||
@ -116,7 +118,6 @@
|
||||
"line-comment-position": "off",
|
||||
"linebreak-style": "off",
|
||||
"lines-around-comment": "error",
|
||||
"lines-between-class-members": "error",
|
||||
"max-classes-per-file": "error",
|
||||
"max-depth": "error",
|
||||
"max-len": "off",
|
||||
@ -160,7 +161,7 @@
|
||||
"no-extend-native": "error",
|
||||
"no-extra-bind": "error",
|
||||
"no-extra-label": "error",
|
||||
"no-extra-parens": "error",
|
||||
// "no-extra-parens": "error",
|
||||
"no-floating-decimal": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implicit-globals": "error",
|
||||
@ -228,7 +229,7 @@
|
||||
"no-void": "error",
|
||||
"no-warning-comments": "warn",
|
||||
"no-whitespace-before-property": "error",
|
||||
"nonblock-statement-body-position": "error",
|
||||
// "nonblock-statement-body-position": ["warn", "below"],
|
||||
"object-curly-newline": "error",
|
||||
"object-curly-spacing": "off",
|
||||
// "object-property-newline": "error",
|
||||
|
@ -3,18 +3,23 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^6.4.3",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-top-loading-bar": "^2.3.1",
|
||||
"typescript": "^5.0.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^29.5.1",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
|
54
src/@types/ApiStructures.ts
Normal file
54
src/@types/ApiStructures.ts
Normal file
@ -0,0 +1,54 @@
|
||||
export type Permissions = {
|
||||
[key: string]: number | Permissions
|
||||
}
|
||||
|
||||
export type RateLimit = {
|
||||
limit: number,
|
||||
time: number,
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export type RateLimits = {
|
||||
[key: string]: RateLimit
|
||||
}
|
||||
|
||||
export type ExternalProfile = {
|
||||
provider: string,
|
||||
username: string,
|
||||
id: string
|
||||
}
|
||||
|
||||
type APIEntity = {
|
||||
[key:string]: unknown
|
||||
id: string,
|
||||
name: string,
|
||||
disabled: boolean,
|
||||
permissions: Permissions,
|
||||
createdTimestamp: number
|
||||
}
|
||||
|
||||
export type UserLike = {
|
||||
icon: string | null,
|
||||
roles: string[],
|
||||
temporary: boolean
|
||||
} & APIEntity
|
||||
|
||||
export type User = {
|
||||
applications: string[],
|
||||
externalProfiles: ExternalProfile[],
|
||||
password: string | null,
|
||||
otpSecret: string | null,
|
||||
twoFactor: boolean,
|
||||
displayName: string | null
|
||||
} & UserLike
|
||||
|
||||
export type Application = {
|
||||
token: string,
|
||||
ownerId: string,
|
||||
description: string | null
|
||||
} & UserLike
|
||||
|
||||
export type Role = {
|
||||
position: number,
|
||||
rateLimits: RateLimits
|
||||
} & APIEntity
|
34
src/@types/Other.ts
Normal file
34
src/@types/Other.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export type RequestOptions = {
|
||||
headers?: { [key: string]: string },
|
||||
|
||||
}
|
||||
|
||||
export type Res = {
|
||||
success: boolean,
|
||||
status: number,
|
||||
data?: { [key: string]: any },
|
||||
message?: string
|
||||
}
|
||||
|
||||
// export type DataResponse = {
|
||||
// data: {[key: string]: any}
|
||||
// } & Response
|
||||
|
||||
// export type TextResponse = {
|
||||
// message: string
|
||||
// } & Response
|
||||
|
||||
export type OAuthProvider = {
|
||||
name: string,
|
||||
icon: string
|
||||
}
|
||||
|
||||
export type ClientSettings = {
|
||||
OAuthProviders: OAuthProvider[]
|
||||
}
|
||||
|
||||
export type SidebarMenuItem = {
|
||||
label: string,
|
||||
to: string,
|
||||
items: {label: string, to: string, relative: boolean}[]
|
||||
}
|
1
src/@types/index.d.ts
vendored
Normal file
1
src/@types/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module '*.png';
|
@ -14,6 +14,8 @@ import { UnauthedRoute } from './structures/UnauthedRoute';
|
||||
import Admin from './pages/Admin';
|
||||
import TitledPage from './components/TitledPage';
|
||||
import Register from './pages/Register';
|
||||
import { ClientSettings } from './@types/Other';
|
||||
import { User } from './@types/ApiStructures';
|
||||
|
||||
function App() {
|
||||
|
||||
@ -25,11 +27,11 @@ function App() {
|
||||
(async () => {
|
||||
|
||||
const settings = await get('/api/settings');
|
||||
setSettings(settings.data);
|
||||
setSettings(settings.data as ClientSettings);
|
||||
|
||||
const result = await get('/api/user');
|
||||
if (result.status === 200) {
|
||||
setSession(result.data);
|
||||
setSession(result.data as User);
|
||||
updateUser();
|
||||
}
|
||||
setLoading(false);
|
@ -1,16 +1,16 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const FileSelector = ({cb}) => {
|
||||
const FileSelector = ({cb}: {cb: (file: File) => void}) => {
|
||||
|
||||
if (!cb) throw new Error('Missing callback');
|
||||
const [file, setFile] = useState(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const onDragOver = (event) => {
|
||||
const onDragOver: React.MouseEventHandler = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onDrop = (event) => {
|
||||
const onDrop: React.DragEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
const { dataTransfer } = event;
|
||||
if (!dataTransfer.files.length) return;
|
||||
@ -26,8 +26,10 @@ const FileSelector = ({cb}) => {
|
||||
<span className="drop-title">Click to select a file</span>
|
||||
<p className="fileName m-0">{file ? `Selected: ${file.name}` : null}</p>
|
||||
<input onChange={(event) => {
|
||||
setFile(event.target.files[0]);
|
||||
cb(event.target.files[0]);
|
||||
if (!event.target.files) return;
|
||||
const [f] = event.target.files;
|
||||
setFile(f);
|
||||
cb(f);
|
||||
}}
|
||||
type="file" id="pfp_upload" accept="image/*" required></input>
|
||||
</label>;
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export const PageButtons = ({setPage, page, length}) => {
|
||||
export const PageButtons = ({ setPage, page, length }: { setPage: (page: number) => void, page: number, length: number}) => {
|
||||
return <div className='flex is-vertical-align is-center page-controls'>
|
||||
<button className="button dark" onClick={() => setPage(page - 1 || 1)}>Previous</button>
|
||||
<p>Page: {page}</p>
|
@ -1,19 +1,21 @@
|
||||
import React, { useRef } from "react";
|
||||
import { Permissions as Perms } from "../@types/ApiStructures";
|
||||
|
||||
export const Permission = ({ name, value, chain, updatePerms }) => {
|
||||
const inputRef = useRef();
|
||||
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 onChange = () => {
|
||||
const val = inputRef.current.value;
|
||||
const val = inputRef.current?.value || null;
|
||||
if (val === null) return;
|
||||
updatePerms(chain, val);
|
||||
};
|
||||
|
||||
return <li className="flex is-vertical-align">
|
||||
<label>{name}</label>
|
||||
<input ref={inputRef} onChange={onChange} max={10} min={0} type='number' defaultValue={value}></input>
|
||||
<input ref={inputRef} onChange={onChange} max={10} min={0} type='number' defaultValue={value as number}></input>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export const PermissionGroup = ({ name, value, chain, updatePerms }) => {
|
||||
export const PermissionGroup = ({ name, value, chain, updatePerms }: {name: string, value: number | Perms, chain: string, updatePerms: (chain: string, perm: string) => void}) => {
|
||||
const elements = [];
|
||||
|
||||
for (const [perm, val] of Object.entries(value)) {
|
||||
@ -31,7 +33,7 @@ export const PermissionGroup = ({ name, value, chain, updatePerms }) => {
|
||||
</li>;
|
||||
};
|
||||
|
||||
export const Permissions = ({ perms, updatePerms }) => {
|
||||
export const Permissions = ({ perms, updatePerms }: {perms: Perms, updatePerms: (chain: string, perm: string) => void}) => {
|
||||
|
||||
const elements = [];
|
||||
const keys = Object.keys(perms);
|
@ -1,6 +1,7 @@
|
||||
import React, {Fragment} from "react";
|
||||
import { RateLimit as RL, RateLimits as RLs } from "../@types/ApiStructures";
|
||||
|
||||
const RateLimit = ({route, limit}) => {
|
||||
const RateLimit = ({route, limit}: {route: string, limit: RL}) => {
|
||||
|
||||
return <Fragment>
|
||||
<p><b>{route}</b></p>
|
||||
@ -10,12 +11,12 @@ const RateLimit = ({route, limit}) => {
|
||||
<input min={0} defaultValue={limit.time} type='number' />
|
||||
<label>Enabled</label>
|
||||
<div className="check-box">
|
||||
<input defaultValue={!limit.disabled} type='checkbox' />
|
||||
<input defaultChecked={!limit.disabled} type='checkbox' />
|
||||
</div>
|
||||
</Fragment>;
|
||||
};
|
||||
|
||||
export const RateLimits = ({ rateLimits }) => {
|
||||
export const RateLimits = ({ rateLimits }: {rateLimits: RLs}) => {
|
||||
|
||||
const routes = Object.keys(rateLimits);
|
||||
const elements = routes.map((route, index) => {
|
@ -5,36 +5,36 @@ import logoImg from '../img/logo.png';
|
||||
import { useNavigate } from "react-router";
|
||||
import { useLoginContext } from "../structures/UserContext";
|
||||
import { clearSession, post } from "../util/Util";
|
||||
import { SidebarMenuItem } from '../@types/Other';
|
||||
|
||||
const Sidebar = ({children}) => {
|
||||
|
||||
const Sidebar = ({children}: {children?: React.ReactNode}) => {
|
||||
return <div className='sidebar card'>
|
||||
{children}
|
||||
</div>;
|
||||
|
||||
};
|
||||
|
||||
const toggleMenu = (event) => {
|
||||
const toggleMenu: React.MouseEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
event.target.parentElement.classList.toggle("open");
|
||||
(event.target as HTMLElement).parentElement?.classList.toggle("open");
|
||||
};
|
||||
|
||||
const expandMobileMenu = (event) => {
|
||||
const expandMobileMenu: React.MouseEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
event.target.parentElement.parentElement.classList.toggle("show-menu");
|
||||
(event.target as HTMLElement).parentElement?.parentElement?.classList.toggle("show-menu");
|
||||
};
|
||||
|
||||
const closeMobileMenu = (event) => {
|
||||
if(event.target.classList.contains("sidebar-menu-item-carrot")) return;
|
||||
event.target.getRootNode().querySelector(".sidebar").classList.remove("show-menu");
|
||||
const closeMobileMenu: React.MouseEventHandler = (event) => {
|
||||
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");
|
||||
};
|
||||
|
||||
// Nav menu
|
||||
const SidebarMenu = ({menuItems = [], children}) => {
|
||||
const SidebarMenu = ({menuItems = [], children}: {menuItems: SidebarMenuItem[], children?: React.ReactNode}) => {
|
||||
const [user, updateUser] = useLoginContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!user) return;
|
||||
if (!user) return null;
|
||||
|
||||
const logOut = async () => {
|
||||
const response = await post('/api/logout');
|
@ -1,25 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export const TableListEntry = ({onClick, item, itemKeys}) => {
|
||||
|
||||
return <tr onClick={onClick}>
|
||||
{itemKeys.map(key => <td key={key}>{item[key]}</td>)}
|
||||
</tr>;
|
||||
};
|
||||
|
||||
export const Table = ({headerItems, children, items, itemKeys}) => {
|
||||
|
||||
if (!headerItems) throw new Error(`Missing table headers`);
|
||||
let i = 0;
|
||||
return <table className="striped hover" >
|
||||
<thead>
|
||||
<tr>
|
||||
{headerItems.map(item => <th key={item}>{item}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{children ? children : items.map(item => <TableListEntry key={i++} item={item} itemKeys={itemKeys} />)}
|
||||
</tbody>
|
||||
</table>;
|
||||
|
||||
};
|
45
src/components/Table.tsx
Normal file
45
src/components/Table.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
|
||||
type Item = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type TableEntry = {
|
||||
onClick?: () => void,
|
||||
item: Item,
|
||||
itemKeys: string[]
|
||||
}
|
||||
|
||||
type TableProps = {
|
||||
headerItems: string[],
|
||||
children?: React.ReactNode,
|
||||
items?: Item[],
|
||||
itemKeys?: string[]
|
||||
}
|
||||
|
||||
export const TableListEntry = ({onClick, item, itemKeys}: TableEntry) => {
|
||||
|
||||
return <tr onClick={onClick}>
|
||||
{itemKeys.map(key => <td key={key}>{item[key] as string}</td>)}
|
||||
</tr>;
|
||||
};
|
||||
|
||||
export const Table = ({headerItems, children, items, itemKeys}: TableProps) => {
|
||||
|
||||
if (!headerItems)
|
||||
throw new Error(`Missing table headers`);
|
||||
if(!children && !items)
|
||||
throw new Error('Missing items or children');
|
||||
let i = 0;
|
||||
return <table className="striped hover" >
|
||||
<thead>
|
||||
<tr>
|
||||
{headerItems.map(item => <th key={item}>{item}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{children ? children : items?.map(item => <TableListEntry key={i++} item={item} itemKeys={itemKeys as string[]} />)}
|
||||
</tbody>
|
||||
</table>;
|
||||
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import '../css/pages/Empty.css';
|
||||
|
||||
const TitledPage = ({title, children}) => {
|
||||
const TitledPage = ({title, children}: {title: string, children: React.ReactNode}) => {
|
||||
|
||||
return <div className="page">
|
||||
<h2 className="pageTitle">{title}</h2>
|
@ -8,10 +8,10 @@ import { clearSession, post } from "../util/Util";
|
||||
const UserControls = () => {
|
||||
|
||||
const [user, updateUser] = useLoginContext();
|
||||
const detailsRef = useRef();
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!user) return;
|
||||
if (!user) return null;
|
||||
|
||||
const logOut = async () => {
|
||||
const response = await post('/api/logout');
|
||||
@ -27,7 +27,7 @@ const UserControls = () => {
|
||||
}}>
|
||||
<details ref={detailsRef} className='dropdown user-controls'>
|
||||
<summary className="is-vertical-align">
|
||||
Hello {user.displayName || user.username}
|
||||
Hello {user.displayName || user.name}
|
||||
<img className="profile-picture" src={`/api/users/${user.id}/avatar`}></img>
|
||||
</summary>
|
||||
|
@ -6,7 +6,7 @@ import App from './App';
|
||||
import { UserContext } from './structures/UserContext';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(<React.StrictMode>
|
||||
<UserContext>
|
||||
<BrowserRouter>
|
@ -8,9 +8,9 @@ import Users from "./admin/Users";
|
||||
|
||||
|
||||
const Main = () => {
|
||||
const [file, setFile] = useState(null);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
|
||||
const submit = (event) => {
|
||||
const submit: React.MouseEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
console.log(file);
|
||||
};
|
@ -3,39 +3,44 @@ import { Route, Routes, useNavigate } from "react-router";
|
||||
import '../css/pages/Login.css';
|
||||
import logoImg from '../img/logo.png';
|
||||
import { useLoginContext } from "../structures/UserContext";
|
||||
import LoadingBar from 'react-top-loading-bar';
|
||||
import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar';
|
||||
import { fetchUser, getSettings, post } from "../util/Util";
|
||||
import { OAuthProvider } from "../@types/Other";
|
||||
|
||||
const CredentialFields = () => {
|
||||
|
||||
const [, setUser] = useLoginContext();
|
||||
const [fail, setFail] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<LoadingBarRef>(null);
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const loginMethods = getSettings()?.OAuthProviders || [];
|
||||
|
||||
const loginClick = async (event) => {
|
||||
ref.current.continuousStart();
|
||||
const loginClick: React.MouseEventHandler = async (event: React.MouseEvent) => {
|
||||
ref.current?.continuousStart();
|
||||
event.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
if (!username.length || !password.length) return;
|
||||
// const username = (document.getElementById('username') as HTMLInputElement)?.value;
|
||||
// const password = (document.getElementById('password') as HTMLInputElement)?.value;
|
||||
const username = usernameRef.current?.value;
|
||||
const password = passwordRef.current?.value;
|
||||
if (!username?.length || !password?.length) return;
|
||||
|
||||
const result = await post('/api/login', { username, password });
|
||||
|
||||
if (![200, 302].includes(result.status)) {
|
||||
ref.current.complete();
|
||||
ref.current?.complete();
|
||||
setFail(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.data?.twoFactor) {
|
||||
setUser(await fetchUser());
|
||||
ref.current.complete();
|
||||
ref.current?.complete();
|
||||
return navigate('/home');
|
||||
}
|
||||
ref.current.complete();
|
||||
ref.current?.complete();
|
||||
return navigate('verify');
|
||||
|
||||
};
|
||||
@ -51,7 +56,7 @@ const CredentialFields = () => {
|
||||
{fail ? <p>Invalid credentials</p> : null}
|
||||
|
||||
<form className="loginForm">
|
||||
<input autoComplete='off' placeholder='Username' required id='username' type='text' autoFocus />
|
||||
<input ref={usernameRef} autoComplete='off' placeholder='Username' required id='username' type='text' autoFocus />
|
||||
<input autoComplete='off' placeholder='Password' required id='password' type='password' />
|
||||
<button className="button primary" onClick={loginClick}>Enter</button>
|
||||
</form>
|
||||
@ -65,7 +70,7 @@ const CredentialFields = () => {
|
||||
<div className="card is-center dir-column">
|
||||
<b>Alternate login methods</b>
|
||||
<div className="methodsWrapper is-center flex-wrap">
|
||||
{loginMethods.map(method => <div
|
||||
{loginMethods.map((method: OAuthProvider) => <div
|
||||
className='third-party-login'
|
||||
key={method.name}
|
||||
onClick={() => { window.location.pathname = `/api/login/${method.name}`; }}>
|
||||
@ -87,20 +92,22 @@ const TwoFactor = () => {
|
||||
const [fail, setFail] = useState(false);
|
||||
const [, setUser] = useLoginContext();
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef(null);
|
||||
const mfaCode = useRef<HTMLInputElement>(null);
|
||||
const ref = useRef<LoadingBarRef>(null);
|
||||
|
||||
const twoFactorClick = async (event) => {
|
||||
const twoFactorClick: React.MouseEventHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
const code = document.getElementById('2faCode').value;
|
||||
// const code = document.getElementById('2faCode').value;
|
||||
const code = mfaCode.current?.value;
|
||||
if (!code) return;
|
||||
ref.current.continuousStart();
|
||||
ref.current?.continuousStart();
|
||||
const result = await post('/api/login/verify', { code });
|
||||
if (result.status === 200) {
|
||||
setUser(await fetchUser());
|
||||
ref.current.complete();
|
||||
ref.current?.complete();
|
||||
return navigate('/home');
|
||||
}
|
||||
ref.current.complete();
|
||||
ref.current?.complete();
|
||||
setFail(true);
|
||||
};
|
||||
|
||||
@ -114,7 +121,7 @@ const TwoFactor = () => {
|
||||
<h2>Verification Code</h2>
|
||||
{fail ? <p className="mt-0">Invalid code</p> : null}
|
||||
<form className="is-center dir-column">
|
||||
<input autoComplete='off' placeholder='Your 2FA code...' required id='2faCode' type='password' />
|
||||
<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>
|
||||
</div>
|
||||
@ -127,7 +134,7 @@ const Login = () => {
|
||||
<div className='loginWrapper col-6 col-6-md col-3-lg'>
|
||||
|
||||
<Routes>
|
||||
<Route exact path='/' element={<CredentialFields />} />
|
||||
<Route path='/' element={<CredentialFields />} />
|
||||
<Route path='/verify' element={<TwoFactor />} />
|
||||
</Routes>
|
||||
|
@ -3,33 +3,37 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { post } from "../util/Util";
|
||||
import '../css/pages/Register.css';
|
||||
import logoImg from '../img/logo.png';
|
||||
import LoadingBar from 'react-top-loading-bar';
|
||||
import LoadingBar, {LoadingBarRef} from 'react-top-loading-bar';
|
||||
|
||||
const Register = () => {
|
||||
|
||||
const [error, setError] = useState();
|
||||
const [error, setError] = useState<string>();
|
||||
const [params] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<LoadingBarRef>(null);
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
document.body.classList.add('bg-triangles');
|
||||
const code = params.get('code');
|
||||
|
||||
const submit = async (event) => {
|
||||
ref.current.continuousStart();
|
||||
const submit: React.MouseEventHandler = async (event) => {
|
||||
ref.current?.continuousStart();
|
||||
event.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
if (!username.length || !password.length) {
|
||||
ref.current.complete();
|
||||
// const username = document.getElementById('username').value;
|
||||
// const password = document.getElementById('password').value;
|
||||
const username = usernameRef.current?.value;
|
||||
const password = passwordRef.current?.value;
|
||||
if (!username?.length || !password?.length) {
|
||||
ref.current?.complete();
|
||||
return;
|
||||
}
|
||||
const response = await post('/api/register', { username, password, code });
|
||||
if (response.status !== 200) {
|
||||
ref.current.complete();
|
||||
return setError(response.message);
|
||||
ref.current?.complete();
|
||||
return setError(response.message as string || 'unknown error');
|
||||
}
|
||||
ref.current.complete();
|
||||
ref.current?.complete();
|
||||
navigate('/login');
|
||||
|
||||
};
|
@ -6,19 +6,20 @@ import { RateLimits } from "../../components/RateLimitsView";
|
||||
import { Table, TableListEntry } from "../../components/Table";
|
||||
import ErrorBoundary from "../../util/ErrorBoundary";
|
||||
import { get, patch } from "../../util/Util";
|
||||
import { Permissions as Perms, Role as R } from "../../@types/ApiStructures";
|
||||
|
||||
const Role = ({ role }) => {
|
||||
const Role = ({ role }: {role: R}) => {
|
||||
|
||||
const perms = { ...role.permissions };
|
||||
const updatePerms = (chain, value) => {
|
||||
value = parseInt(value);
|
||||
const updatePerms = (chain: string, raw: string) => {
|
||||
const value = parseInt(raw);
|
||||
if (isNaN(value)) return;
|
||||
|
||||
let selected = perms;
|
||||
const keys = chain.split(':');
|
||||
for (const key of keys) {
|
||||
if (key === keys[keys.length - 1]) selected[key] = value;
|
||||
else selected = selected[key];
|
||||
else selected = selected[key] as Perms;
|
||||
}
|
||||
};
|
||||
|
||||
@ -63,8 +64,8 @@ const Role = ({ role }) => {
|
||||
|
||||
const Roles = () => {
|
||||
|
||||
const [error, setError] = useState(null);
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [roles, setRoles] = useState<R[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@ -73,9 +74,9 @@ const Roles = () => {
|
||||
const result = await get('/api/roles', { page });
|
||||
if (result.success) {
|
||||
setError(null);
|
||||
setRoles(result.data);
|
||||
setRoles(result.data as R[]);
|
||||
} else {
|
||||
setError(result.message);
|
||||
setError(result.message || 'Unknown error');
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
@ -6,8 +6,9 @@ import '../../css/pages/Users.css';
|
||||
import { Table, TableListEntry } from "../../components/Table";
|
||||
import { BackButton, PageButtons } from "../../components/PageControls";
|
||||
import { Permissions } from "../../components/PermissionsView";
|
||||
import { Application as App, Permissions as Perms, User as APIUser } from "../../@types/ApiStructures";
|
||||
|
||||
const ApplicationList = ({ apps }) => {
|
||||
const ApplicationList = ({ apps }: {apps: App[]}) => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
return <Table headerItems={['Name', 'ID']}>
|
||||
@ -23,9 +24,9 @@ const ApplicationList = ({ apps }) => {
|
||||
|
||||
};
|
||||
|
||||
const Application = ({ app }) => {
|
||||
const Application = ({ app }: {app: App}) => {
|
||||
|
||||
const descProps = {defaultValue: app.description, placeholder: 'Describe your application'};
|
||||
const descProps = {defaultValue: app.description || '', placeholder: 'Describe your application'};
|
||||
return <div>
|
||||
<h4>{app.name} ({app.id})</h4>
|
||||
|
||||
@ -37,27 +38,27 @@ const Application = ({ app }) => {
|
||||
};
|
||||
|
||||
// TODO: Groups, description, notes
|
||||
const User = ({ user }) => {
|
||||
const User = ({ user }: {user: APIUser}) => {
|
||||
|
||||
const [apps, updateApps] = useState([]);
|
||||
const [apps, updateApps] = useState<App[]>([]);
|
||||
const perms = {...user.permissions};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await get(`/api/users/${user.id}/applications`);
|
||||
if(response.status === 200) updateApps(response.data);
|
||||
if(response.status === 200) updateApps(response.data as App[]);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const updatePerms = (chain, value) => {
|
||||
value = parseInt(value);
|
||||
const updatePerms = (chain: string, raw: string) => {
|
||||
const value = parseInt(raw);
|
||||
if (isNaN(value)) return;
|
||||
|
||||
let selected = perms;
|
||||
const keys = chain.split(':');
|
||||
for (const key of keys) {
|
||||
if (key === keys[keys.length - 1]) selected[key] = value;
|
||||
else selected = selected[key];
|
||||
else selected = selected[key] as Perms;
|
||||
}
|
||||
};
|
||||
|
||||
@ -92,7 +93,7 @@ const User = ({ user }) => {
|
||||
<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} />
|
||||
<input autoComplete='off' id='displayName' placeholder='Name to display instead of username' defaultValue={user.displayName || ''} />
|
||||
|
||||
<h3 className="mt-5">Applications</h3>
|
||||
<Routes>
|
||||
@ -117,23 +118,25 @@ const User = ({ user }) => {
|
||||
|
||||
const CreateUserField = () => {
|
||||
|
||||
const [link, setLink] = useState(null);
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const getSignupCode = async () => {
|
||||
const response = await get('/api/register/code');
|
||||
if (response.status === 200) {
|
||||
if (response.status === 200 && response.data) {
|
||||
const link = `${window.location.origin}/register?code=${response.data.code}`;
|
||||
setLink(link);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (event) => {
|
||||
const {parentElement} = event.target;
|
||||
navigator.clipboard.writeText(event.target.innerText);
|
||||
parentElement.childNodes[parentElement.childNodes.length-1].style.visibility = "visible";
|
||||
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 = (event) => {
|
||||
event.target.style.visibility = "hidden";
|
||||
const closeToolTip: React.MouseEventHandler = (event) => {
|
||||
(event.target as HTMLElement).style.visibility = "hidden";
|
||||
};
|
||||
|
||||
// TODO: Finish this
|
||||
@ -155,9 +158,9 @@ const CreateUserField = () => {
|
||||
|
||||
const Users = () => {
|
||||
|
||||
const [users, setUsers] = useState([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [error, setError] = useState(null);
|
||||
const [users, setUsers] = useState<APIUser[]>([]);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@ -165,8 +168,8 @@ const Users = () => {
|
||||
const result = await get('/api/users', { page });
|
||||
if (result.success) {
|
||||
setError(null);
|
||||
setUsers(result.data);
|
||||
} else setError(result.message);
|
||||
setUsers(result.data as APIUser[]);
|
||||
} else setError(result.message || 'Unknown error');
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [page]);
|
@ -3,13 +3,16 @@ import { Route, Routes, useNavigate, useParams } from "react-router";
|
||||
import { Table, TableListEntry } from "../../components/Table";
|
||||
import ErrorBoundary from "../../util/ErrorBoundary";
|
||||
import { post, get, del } from "../../util/Util";
|
||||
import { Application as App } from "../../@types/ApiStructures";
|
||||
import { Res } from "../../@types/Other";
|
||||
|
||||
const Application = ({ app }) => {
|
||||
const Application = ({ app }: {app: App}) => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const deleteApp = async () => {
|
||||
const response = await del(`/api/user/applications/${app.id}`);
|
||||
// const response =
|
||||
await del(`/api/user/applications/${app.id}`);
|
||||
};
|
||||
|
||||
return <div>
|
||||
@ -24,35 +27,38 @@ const Application = ({ app }) => {
|
||||
|
||||
const Applications = () => {
|
||||
|
||||
const [applications, setApplications] = useState([]);
|
||||
const [applications, setApplications] = useState<App[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await get('/api/user/applications');
|
||||
if (response.status === 200) setApplications(response.data);
|
||||
const response = await get('/api/user/applications') as Res;
|
||||
if (response.status === 200) setApplications(response.data as App[]);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const descField = useRef();
|
||||
const nameField = useRef();
|
||||
const [error, setError] = useState(null);
|
||||
const descField = useRef<HTMLTextAreaElement>(null);
|
||||
const nameField = useRef<HTMLInputElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const createApp = async (event) => {
|
||||
const createApp: React.MouseEventHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
const button = event.target;
|
||||
const button = event.target as HTMLButtonElement;
|
||||
|
||||
const name = nameField.current.value;
|
||||
if(!name) return setError('Missing name');
|
||||
const description = descField.current.value;
|
||||
if (!nameField.current || !descField.current) return;
|
||||
|
||||
const name = nameField.current?.value;
|
||||
if (!name) return setError('Missing name');
|
||||
|
||||
const description = descField.current?.value || null;
|
||||
nameField.current.value = '';
|
||||
descField.current.value = '';
|
||||
button.disabled = true;
|
||||
const response = await post('/api/user/applications', { name, description });
|
||||
if (response.status !== 200) setError(response.message);
|
||||
else setApplications((apps) => [...apps, response.data]);
|
||||
if (response.status !== 200) setError(response.message || 'Unknown error');
|
||||
else setApplications((apps) => [...apps, response.data as App]);
|
||||
|
||||
button.disabled = false;
|
||||
};
|
||||
@ -93,7 +99,7 @@ const Applications = () => {
|
||||
|
||||
return <ErrorBoundary>
|
||||
<Routes>
|
||||
<Route exact path="/" element={<Main />} />
|
||||
<Route path="/" element={<Main />} />
|
||||
<Route path="/:id" element={<ApplicationWrapper />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>;
|
@ -2,34 +2,35 @@ import React, { useRef, useState } from "react";
|
||||
import { capitalise, get, post } from "../../util/Util";
|
||||
import { useLoginContext } from "../../structures/UserContext";
|
||||
import FileSelector from "../../components/FileSelector";
|
||||
import { ExternalProfile as EP, User } from "../../@types/ApiStructures";
|
||||
|
||||
const TwoFactorControls = ({ user }) => {
|
||||
const TwoFactorControls = ({ user }: {user: User}) => {
|
||||
|
||||
const [twoFactorError, set2FAError] = useState(null);
|
||||
const [twoFactorError, set2FAError] = useState<string | null>(null);
|
||||
const [displayInput, setDisplayInput] = useState(false);
|
||||
|
||||
const [qr, setQr] = useState(null);
|
||||
const [qr, setQr] = useState<string | null>(null);
|
||||
const displayQr = async () => {
|
||||
const response = await get('/api/user/2fa');
|
||||
if (response.status !== 200) return set2FAError(response.message);
|
||||
setQr(response.message);
|
||||
if (response.status !== 200) return set2FAError(response.message || 'Uknown error');
|
||||
setQr(response.message as string);
|
||||
};
|
||||
|
||||
const codeRef = useRef();
|
||||
const disable2FA = async (event) => {
|
||||
const codeRef = useRef<HTMLInputElement>(null);
|
||||
const disable2FA: React.MouseEventHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
const code = codeRef.current.value;
|
||||
const code = codeRef.current?.value;
|
||||
if (!code) return;
|
||||
const response = await post('/api/user/2fa/disable', { code });
|
||||
if (response.status !== 200) return set2FAError(response.message);
|
||||
if (response.status !== 200) return set2FAError(response.message || 'Unknown error');
|
||||
};
|
||||
|
||||
const submitCode = async (event) => {
|
||||
const submitCode: React.MouseEventHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
const code = codeRef.current.value;
|
||||
const code = codeRef.current?.value;
|
||||
if (!code) return;
|
||||
const response = await post('/api/user/2fa/verify', { code });
|
||||
if (response.status !== 200) return set2FAError(response.message);
|
||||
if (response.status !== 200) return set2FAError(response.message || 'Unknown error');
|
||||
};
|
||||
|
||||
let inner = <div>
|
||||
@ -61,7 +62,7 @@ const TwoFactorControls = ({ user }) => {
|
||||
|
||||
};
|
||||
|
||||
const ExternalProfile = ({ profile }) => {
|
||||
const ExternalProfile = ({ profile }: {profile: EP}) => {
|
||||
return <div>
|
||||
<b>{capitalise(profile.provider)}</b>
|
||||
<p className="m-0">Username: {profile.username}</p>
|
||||
@ -69,7 +70,7 @@ const ExternalProfile = ({ profile }) => {
|
||||
</div>;
|
||||
};
|
||||
|
||||
const ThirdPartyConnections = ({ user }) => {
|
||||
const ThirdPartyConnections = ({ user }: {user: User}) => {
|
||||
|
||||
const { externalProfiles } = user;
|
||||
|
||||
@ -81,44 +82,45 @@ const ThirdPartyConnections = ({ user }) => {
|
||||
|
||||
const Profile = () => {
|
||||
|
||||
const [user] = useLoginContext();
|
||||
const [file, setFile] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const user = useLoginContext()[0] as User;
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const usernameRef = useRef();
|
||||
const displayNameRef = useRef();
|
||||
const currPassRef = useRef();
|
||||
const newPassRef = useRef();
|
||||
const newPassRepeatRef = useRef();
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const displayNameRef = useRef<HTMLInputElement>(null);
|
||||
const currPassRef = useRef<HTMLInputElement>(null);
|
||||
const newPassRef = useRef<HTMLInputElement>(null);
|
||||
const newPassRepeatRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const submit = async ({ target: button }) => {
|
||||
const submit: React.MouseEventHandler = async (event) => {
|
||||
if (!file) return;
|
||||
const button = event.target as HTMLButtonElement;
|
||||
button.disabled = true;
|
||||
|
||||
const body = new FormData();
|
||||
body.set('file', file, file.name);
|
||||
// body.set('name', file.name);
|
||||
|
||||
const response = await post('/api/user/avatar', body, { headers: null });
|
||||
if (!response.success) setError(response.message);
|
||||
const response = await post('/api/user/avatar', body, { headers: {} });
|
||||
if (!response.success) setError(response.message || 'Unknown error');
|
||||
else setFile(null);
|
||||
button.disabled = false;
|
||||
};
|
||||
|
||||
const updateSettings = async (event) => {
|
||||
const updateSettings: React.MouseEventHandler = async (event) => {
|
||||
event.preventDefault();
|
||||
const button = event.target;
|
||||
const button = event.target as HTMLButtonElement;
|
||||
|
||||
const username = usernameRef.current.value;
|
||||
const displayName = displayNameRef.current.value;
|
||||
const currentPassword = currPassRef.current.value;
|
||||
const newPassword = newPassRef.current.value;
|
||||
const newPasswordRepeat = newPassRepeatRef.current.value;
|
||||
const username = usernameRef.current?.value;
|
||||
const displayName = displayNameRef.current?.value;
|
||||
const currentPassword = currPassRef.current?.value;
|
||||
const newPassword = newPassRef.current?.value;
|
||||
const newPasswordRepeat = newPassRepeatRef.current?.value;
|
||||
|
||||
if (!currentPassword) return setError('Missing password');
|
||||
|
||||
button.disabled = true;
|
||||
const data = { username, displayName, password: currentPassword };
|
||||
const data: {username?: string, displayName?: string, password?: string, newPassword?: string} = { username, displayName, password: currentPassword };
|
||||
if (currentPassword && newPassword && newPasswordRepeat) {
|
||||
if (newPassword !== newPasswordRepeat) {
|
||||
button.disabled = false;
|
||||
@ -166,7 +168,7 @@ const Profile = () => {
|
||||
<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" />
|
||||
<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" />
|
@ -1,10 +1,11 @@
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
// import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
module.exports = (app) => {
|
||||
app.use(
|
||||
'/api',
|
||||
createProxyMiddleware({
|
||||
target: 'http://localhost:5000',
|
||||
target: 'http://localhost:4000',
|
||||
changeOrigin: true
|
||||
})
|
||||
);
|
||||
|
@ -1,8 +1,18 @@
|
||||
import React from 'react';
|
||||
import { NavLink as BaseNavLink } from 'react-router-dom';
|
||||
|
||||
type NavLinkOptions = {
|
||||
activeClassName?: string,
|
||||
activeStyle?: object,
|
||||
children?: React.ReactNode,
|
||||
className?: string,
|
||||
style?: object,
|
||||
to: string,
|
||||
onClick?: React.MouseEventHandler
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const NavLink = React.forwardRef(({ activeClassName, activeStyle, ...props }, ref) => {
|
||||
const NavLink = React.forwardRef(({ activeClassName, activeStyle, ...props }: NavLinkOptions, ref: React.ForwardedRef<HTMLAnchorElement>) => {
|
||||
return <BaseNavLink
|
||||
ref={ref}
|
||||
{...props}
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { getUser } from "../util/Util";
|
||||
|
||||
export const PrivateRoute = ({ children }) => {
|
||||
export const PrivateRoute = ({ children }: {children: JSX.Element}) => {
|
||||
const user = getUser();
|
||||
const location = useLocation();
|
||||
if (!user) return <Navigate to='/login' replace state={{ from: location }} />;
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { getUser } from "../util/Util";
|
||||
|
||||
export const UnauthedRoute = ({ children }) => {
|
||||
export const UnauthedRoute = ({ children }: {children: JSX.Element}) => {
|
||||
const user = getUser();
|
||||
if (user) return <Navigate to='/home' replace />;
|
||||
return children;
|
@ -1,16 +1,19 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { getUser } from '../util/Util';
|
||||
import {User} from '../@types/ApiStructures';
|
||||
|
||||
const LoginContext = React.createContext();
|
||||
const LoginUpdateContext = React.createContext();
|
||||
type UpdateFunc = ((user?: User | null) => void)
|
||||
|
||||
const LoginContext = React.createContext<User | null>(null);
|
||||
const LoginUpdateContext = React.createContext<UpdateFunc>(() => { /* */ });
|
||||
|
||||
// Hook
|
||||
export const useLoginContext = () => {
|
||||
export const useLoginContext = (): [User | null, UpdateFunc] => {
|
||||
return [useContext(LoginContext), useContext(LoginUpdateContext)];
|
||||
};
|
||||
|
||||
// Component
|
||||
export const UserContext = ({ children }) => {
|
||||
export const UserContext = ({ children }: {children: React.ReactNode}) => {
|
||||
|
||||
const [user, setLoginState] = useState(getUser());
|
||||
const updateLoginState = () => {
|
@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
const alerter = (ref, callback) => {
|
||||
// Listens for mouse clicks outside of the wrapped element
|
||||
const alerter = (ref: React.RefObject<HTMLElement>, callback: () => void) => {
|
||||
useEffect(() => {
|
||||
|
||||
const listener = (event) => {
|
||||
if (ref.current && !ref.current.contains(event.target)) {
|
||||
const listener = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
return callback();
|
||||
}
|
||||
};
|
||||
@ -18,8 +19,13 @@ const alerter = (ref, callback) => {
|
||||
}, [ref]);
|
||||
};
|
||||
|
||||
// Component wrapper to enable listening for clicks outside of the given component
|
||||
const ClickDetector = ({ children, callback }) => {
|
||||
/**
|
||||
* Component wrapper to enable listening for clicks outside of the given component
|
||||
*
|
||||
* @param {{children: React.ReactNode, callback: () => void}} { children, callback }
|
||||
* @return {*}
|
||||
*/
|
||||
const ClickDetector = ({ children, callback }: {children: React.ReactNode, callback: () => void}) => {
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
alerter(wrapperRef, callback);
|
@ -1,22 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
this.setState({error: true});
|
||||
console.error(error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) return <h1>Something went wrong :/</h1>;
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
34
src/util/ErrorBoundary.tsx
Normal file
34
src/util/ErrorBoundary.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
|
||||
type BoundaryProps = {
|
||||
[key: string]: unknown,
|
||||
fallback?: React.ReactNode,
|
||||
children: React.ReactNode
|
||||
}
|
||||
type StateProps = {
|
||||
error: boolean
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<BoundaryProps, StateProps> {
|
||||
|
||||
fallback?: React.ReactNode;
|
||||
|
||||
constructor({fallback, ...props}: BoundaryProps) {
|
||||
super(props);
|
||||
this.state = { error: false };
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, errorInfo: unknown) {
|
||||
this.setState({error: true});
|
||||
console.error(error, errorInfo);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.error) return this.fallback || <h1>Something went wrong :/</h1>;
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
@ -1,3 +1,11 @@
|
||||
import { User } from "../@types/ApiStructures";
|
||||
import { RequestOptions, ClientSettings, Res } from "../@types/Other";
|
||||
|
||||
type HttpOpts = {
|
||||
body?: string,
|
||||
method: string
|
||||
}
|
||||
|
||||
export const getUser = () => {
|
||||
const data = sessionStorage.getItem('user');
|
||||
if (data) return JSON.parse(data);
|
||||
@ -8,7 +16,7 @@ export const clearSession = () => {
|
||||
sessionStorage.removeItem('user');
|
||||
};
|
||||
|
||||
export const setSession = (user) => {
|
||||
export const setSession = (user: User) => {
|
||||
sessionStorage.setItem('user', JSON.stringify(user));
|
||||
};
|
||||
|
||||
@ -18,29 +26,29 @@ export const fetchUser = async (force = false) => {
|
||||
|
||||
const result = await get('/api/user');
|
||||
if (result.status === 200) {
|
||||
setSession(result.data);
|
||||
setSession(result.data as User);
|
||||
return result.data;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const setSettings = (settings) => {
|
||||
export const setSettings = (settings: ClientSettings) => {
|
||||
if(settings) sessionStorage.setItem('settings', JSON.stringify(settings));
|
||||
};
|
||||
|
||||
export const getSettings = () => {
|
||||
export const getSettings = (): ClientSettings | null => {
|
||||
const data = sessionStorage.getItem('settings');
|
||||
if (data) return JSON.parse(data);
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseResponse = async (response) => {
|
||||
const parseResponse = async (response: Response): Promise<Res> => {
|
||||
const { headers: rawHeaders, status } = response;
|
||||
// Fetch returns heders as an interable for some reason
|
||||
const headers = [...rawHeaders].reduce((acc, [key, val]) => {
|
||||
acc[key.toLowerCase()] = val;
|
||||
return acc;
|
||||
}, {});
|
||||
}, {} as {[key: string]: string});
|
||||
const success = status >= 200 && status < 300;
|
||||
const base = { success, status };
|
||||
|
||||
@ -58,11 +66,10 @@ const parseResponse = async (response) => {
|
||||
return { ...base, message: await response.text() };
|
||||
};
|
||||
|
||||
export const post = async (url, body, opts = {}) => {
|
||||
export const post = async (url: string, body?: object | string, opts: RequestOptions = {}) => {
|
||||
|
||||
const options = {
|
||||
method: 'post',
|
||||
body
|
||||
const options: HttpOpts & RequestOptions = {
|
||||
method: 'post'
|
||||
};
|
||||
|
||||
if (opts.headers !== null) {
|
||||
@ -73,15 +80,15 @@ export const post = async (url, body, opts = {}) => {
|
||||
}
|
||||
|
||||
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
|
||||
else options.body = body as string;
|
||||
|
||||
const response = await fetch(url, options);
|
||||
return parseResponse(response);
|
||||
};
|
||||
|
||||
export const patch = async (url, body, opts = {}) => {
|
||||
const options = {
|
||||
method: 'patch',
|
||||
body
|
||||
export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) => {
|
||||
const options: HttpOpts & RequestOptions = {
|
||||
method: 'patch'
|
||||
};
|
||||
|
||||
if (opts.headers !== null) {
|
||||
@ -92,13 +99,14 @@ export const patch = async (url, body, opts = {}) => {
|
||||
}
|
||||
|
||||
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
|
||||
else options.body = body as string;
|
||||
|
||||
const response = await fetch(url, options);
|
||||
return parseResponse(response);
|
||||
|
||||
};
|
||||
|
||||
export const get = async (url, params) => {
|
||||
export const get = async (url: string, params?: {[key: string]: string | number}) => {
|
||||
if (params) url += '?' + Object.entries(params)
|
||||
.map(([key, val]) => `${key}=${val}`)
|
||||
.join('&');
|
||||
@ -106,10 +114,9 @@ export const get = async (url, params) => {
|
||||
return parseResponse(response);
|
||||
};
|
||||
|
||||
export const del = async (url, body, opts = {}) => {
|
||||
const options = {
|
||||
method: 'delete',
|
||||
body
|
||||
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) => {
|
||||
const options: HttpOpts & RequestOptions = {
|
||||
method: 'delete'
|
||||
};
|
||||
|
||||
if (opts.headers !== null) {
|
||||
@ -120,12 +127,13 @@ export const del = async (url, body, opts = {}) => {
|
||||
}
|
||||
|
||||
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
|
||||
else options.body = body as string;
|
||||
|
||||
const response = await fetch(url, options);
|
||||
return parseResponse(response);
|
||||
};
|
||||
|
||||
export const capitalise = (str) => {
|
||||
export const capitalise = (str: string) => {
|
||||
const first = str[0].toUpperCase();
|
||||
return `${first}${str.substring(1)}`;
|
||||
};
|
121
tsconfig.json
Normal file
121
tsconfig.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
"jsx": "react-jsx", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ESNext", /* Specify what module code is generated. */
|
||||
// "module": "AMD",
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"src/@types"
|
||||
], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": false, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "outFile": "./build/out-esm.js", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./build", /* Specify an output folder for all emitted files. */
|
||||
"removeComments": true, /* Disable emitting comments. */
|
||||
"noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
"stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
"isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
"strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
"strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
"noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
"noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user