diff --git a/src/@types/ApiStructures.ts b/src/@types/ApiStructures.ts index 5a7a8e1..346d3b3 100644 --- a/src/@types/ApiStructures.ts +++ b/src/@types/ApiStructures.ts @@ -56,7 +56,8 @@ export type Application = { export type Role = { position: number, - rateLimits: RateLimits + rateLimits: RateLimits, + tag: boolean, } & APIEntity export type FlagType = string | number | boolean | number[] | null diff --git a/src/@types/Other.ts b/src/@types/Other.ts index c82a7c9..2325ccc 100644 --- a/src/@types/Other.ts +++ b/src/@types/Other.ts @@ -6,10 +6,10 @@ export type RequestOptions = { headers?: { [key: string]: string }, } -export type Res = { +export type Res = { success: boolean, status: number, - data?: { [key: string]: any }, + data?: T, //{ [key: string]: any }, message?: string } diff --git a/src/App.tsx b/src/App.tsx index 3f54a49..164fe84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,7 +47,7 @@ const App = () => { (async () => { - const result = await get('/api/user'); + const result = await get<{twoFactor: boolean}>('/api/user'); if (result.status === 200) { setSession(result.data as User); diff --git a/src/components/PageElements.tsx b/src/components/PageElements.tsx index 2eb4697..cb5c413 100644 --- a/src/components/PageElements.tsx +++ b/src/components/PageElements.tsx @@ -83,7 +83,7 @@ export const DataCard = ({ id, className, children = [] }: DataCardProps) => }; -type PaginatedCardUrlProps = { +type PaginatedCardUrlProps = { path: string, query?: {[key: string | number]: unknown} dataKey?: string, @@ -91,14 +91,14 @@ type PaginatedCardUrlProps = { tableDivClass?: string, children?: React.ReactNode | React.ReactNode[], // eslint-disable-next-line @typescript-eslint/no-explicit-any - formatter: (entry: any, onClick?: (...args: any[]) => void, idx?: number) => JSX.Element + formatter: (entry: T, callback?: (...args: any[]) => void, idx?: number) => JSX.Element // FormatterCallback // eslint-disable-next-line @typescript-eslint/no-explicit-any callback?: (...args: any[]) => void, pageSize?: number, refresh?: boolean } -export const PaginatedCard = ({ +export const PaginatedCard = ({ // id = randomString(), className, tableDivClass, @@ -108,14 +108,14 @@ export const PaginatedCard = ({ formatter, callback, pageSize = 10, - query = {}, + query, refresh -}: PaginatedCardUrlProps) => +}: PaginatedCardUrlProps) => { if (!(children instanceof Array)) children = [ children ]; - const [ data, setData ] = useState(null); + const [ data, setData ] = useState(null); const [ page, setPage ] = useState(1); const [ pages, setPages ] = useState(1); const [ loadState, setLoadState ] = useState(RefresherState.loading); @@ -131,7 +131,7 @@ export const PaginatedCard = ({ setRefresh(val => !val); }; const deps: (string | number | object | boolean)[] = [ page, path, refreshLocal ]; - if(query && Object.keys(query).length) + if(query) deps.push(query); if (typeof refresh !== 'undefined') deps.push(refresh); @@ -141,19 +141,28 @@ export const PaginatedCard = ({ (async () => { setLoadState(RefresherState.loading); - const keys = Object.keys(query); - for (const key of keys) - if (typeof query[key] === 'undefined') - delete query[key]; - const response = await get(path, { page, pageSize, ...query }); + if(query) + { + const keys = Object.keys(query); + for (const key of keys) + if (typeof query[key] === 'undefined') + delete query[key]; + } + const response = await get<{ [key: string]: number | T[], pages: number }>(path, { page, pageSize, ...query }); + // if (!response.data?.bruh) + // throw new Error('bruh'); if (!response.success || !response.data) { setLoadState(RefresherState.error); return errorToast(response.message); } // return setError(response.message ?? null); - setData(response.data[dataKey]); + setData(response.data[dataKey] as T[]); setPages(response.data.pages ?? 1); + if(response.data.pages && page > response.data.pages) + setPage(response.data.pages == 0 ? 1 : response.data.pages); + else if(response.data.pages == 0) + setPage(1); setLoadState(RefresherState.success); })(); }, deps); @@ -184,7 +193,9 @@ type BasePaginatedCardProps = { onRefresh?: () => void, // Callback that is fired whenever the refresher is clicked // onlineList?: string } -export const BasePaginatedCard = ({ children, data, className, id, tableDivClass, formatter, callback, page, pages, setPage, loadState, onRefresh }: BasePaginatedCardProps) => +export const BasePaginatedCard = ({ children, data, + className, id, tableDivClass, formatter, callback, + page, pages, setPage, loadState, onRefresh }: BasePaginatedCardProps) => { if (!(children instanceof Array)) children = [ children ]; @@ -235,7 +246,7 @@ export const TableCard = ({ id, children, className = '', tableDivClass = '', da {/*

{'Loading...'}

*/} ; - return + return {cardHeader}
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index e82bda3..b92d463 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -31,7 +31,7 @@ const CredentialFields = () => return; button.disabled = true; - const result = await post('/api/login', { username, password }); + const result = await post<{twoFactor: boolean}>('/api/login', { username, password }); button.disabled = false; if (![ 200, 302 ].includes(result.status)) { diff --git a/src/pages/admin/Flags.tsx b/src/pages/admin/Flags.tsx index b8bfbfd..a7db9aa 100644 --- a/src/pages/admin/Flags.tsx +++ b/src/pages/admin/Flags.tsx @@ -321,7 +321,7 @@ const Main = () => if (listView) query.all = true; - const response = await get('/api/flags', query); + const response = await get<{pages: number, flags: APIFlag[], tags: string[]}>('/api/flags', query); if (!response.success || !response.data) { setFlags([]); @@ -329,7 +329,7 @@ const Main = () => return errorToast(response.message); } - setFlags(response.data.flags as APIFlag[]); + setFlags(response.data.flags); setPages(response.data.pages); if (availableFilters === null) setAvailableFilters(response.data.tags); diff --git a/src/pages/admin/Roles.tsx b/src/pages/admin/Roles.tsx index f81e1ac..ae3ff3a 100644 --- a/src/pages/admin/Roles.tsx +++ b/src/pages/admin/Roles.tsx @@ -1,11 +1,11 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { Route, Routes, useNavigate, useParams } from 'react-router'; import ErrorBoundary from '../../util/ErrorBoundary'; -import { Permissions as PermissionsStruct, Role as RoleStruct, User } from '../../@types/ApiStructures'; +import { Role as RoleStruct, User } from '../../@types/ApiStructures'; import { BasePaginatedCard, DataCard, PaginatedCard, RefresherState } from '../../components/PageElements'; import { dateString, del, errorToast, get, patch, post, successToast } from '../../util/Util'; import { BackButton } from '../../components/PageControls'; -import { ClickToEdit } from '../../components/InputElements'; +import { ClickToEdit, ToggleSwitch } from '../../components/InputElements'; import { Permissions } from '../../views/PermissionsView'; import { useSubtitle } from '../../components/TitledPage'; @@ -58,13 +58,13 @@ const Role = ({ refreshList }: RoleProps) =>

Loading...

; - const updatePerms = (perms: PermissionsStruct) => + const updateRoleStruct = (data: Partial) => { setRole(role => { if (!role) return role; - return { ...role, permissions: perms }; + return { ...role, ...data }; }); }; @@ -79,6 +79,14 @@ const Role = ({ refreshList }: RoleProps) => successToast('Permissions updated'); }; + const updateRole = async ({ tag }: Partial) => + { + const response = await patch(`/api/roles/${roleId}`, { tag }); + if (!response.success || !response.data) + return errorToast(response.message); + updateRoleStruct(response.data); + }; + const deleteRole = async (e: React.MouseEvent) => { const button = e.target as HTMLButtonElement; @@ -136,7 +144,8 @@ const Role = ({ refreshList }: RoleProps) => - + +
{dateString(role.createdTimestamp)}
Tag updateRole({ tag: target.checked })} />
@@ -160,7 +169,7 @@ const Role = ({ refreshList }: RoleProps) =>
Permissions
- + updateRoleStruct({ permissions })} />

@@ -289,14 +298,14 @@ const Main = () => (async () => { setLoadState(RefresherState.loading); - const response = await get('/api/roles', { page, pageSize: 15, ...filters }); + const response = await get<{pages: number, roles: RoleStruct[]}>('/api/roles', { page, pageSize: 15, ...filters }); if (!response.success || !response.data) { setLoadState(RefresherState.error); return errorToast(response.message); } setPages(response.data.pages); - setRoles(new Map(response.data.roles.map((role: RoleStruct) => [ role.id, role ]))); + setRoles(new Map(response.data.roles.map((role) => [ role.id, role ]))); setLoadState(RefresherState.success); })(); }, [ page, filters, refresh ]); diff --git a/src/pages/admin/Users.tsx b/src/pages/admin/Users.tsx index 2ffa311..b3f28c3 100644 --- a/src/pages/admin/Users.tsx +++ b/src/pages/admin/Users.tsx @@ -204,7 +204,7 @@ const UserData = ({ user, updateUser }: {user: UserStruct | null, updateUser: (u { const button = e.target as HTMLButtonElement; button.disabled = true; - const response = await get(`/api/users/${user.id}/passwdreset`); + const response = await get<{token: string}>(`/api/users/${user.id}/passwdreset`); button.disabled = false; if (!response.success || !response.data) return errorToast(response.message); @@ -418,7 +418,7 @@ const UserList = ({ page, pages, setPage, loadState, onRefresh }: UserListProps) const getSignupCode = async () => { - const response = await get('/api/register/code'); + const response = await get<{code: string}>('/api/register/code'); if (!response.success || !response.data) return errorToast(response.message); const link = `${window.location.origin}/register?code=${response.data.code}`; @@ -533,14 +533,14 @@ const Main = () => (async () => { setLoadState(RefresherState.loading); - const response = await get('/api/users', { page, pageSize: 15, ...searchFilter }); + const response = await get<{pages: number, users: UserStruct[]}>('/api/users', { page, pageSize: 15, ...searchFilter }); if (!response.success || !response.data) { setLoadState(RefresherState.error); return errorToast(response.message); } setPages(response.data.pages); - setUsers(new Map(response.data.users.map((user: UserStruct) => [ user.id, user ]))); + setUsers(new Map(response.data.users.map((user) => [ user.id, user ]))); setLoadState(RefresherState.success); })(); }, [ page, searchFilter, refresh ]); @@ -549,10 +549,10 @@ const Main = () => { (async () => { - const response = await get('/api/roles', { all: true }); + const response = await get<{roles: RoleStruct[]}>('/api/roles', { all: true }); if (!response.success || !response.data) return errorToast(response.message); - setRoles(new Map(response.data.roles.map((role: RoleStruct) => [ role.id, role ]))); + setRoles(new Map(response.data.roles.map((role) => [ role.id, role ]))); })(); }, [ refresh ]); diff --git a/src/pages/home/Applications.tsx b/src/pages/home/Applications.tsx index a8e5ea3..9b5a652 100644 --- a/src/pages/home/Applications.tsx +++ b/src/pages/home/Applications.tsx @@ -1,20 +1,36 @@ import React, { useEffect, useRef, useState } from 'react'; import { Route, Routes, useNavigate, useParams } from 'react-router'; -import { Table, TableListEntry } from '../../components/Table'; import ErrorBoundary from '../../util/ErrorBoundary'; -import { post, get, del, successToast, patch, errorToast } from '../../util/Util'; -import { Application as App } from '../../@types/ApiStructures'; -import { Res } from '../../@types/Other'; -import { DataCard } from '../../components/PageElements'; +import { post, del, successToast, patch, errorToast, get } from '../../util/Util'; +import { Application as App, Permissions as Perms } from '../../@types/ApiStructures'; +import { DataCard, PaginatedCard } from '../../components/PageElements'; import { BackButton } from '../../components/PageControls'; import { ClickToEdit } from '../../components/InputElements'; import { Permissions } from '../../views/PermissionsView'; import { useSubtitle } from '../../components/TitledPage'; -const Application = ({ app }: {app: App}) => +const Application = () => { - const [ perms, updatePerms ] = useState(app.permissions); + const { id } = useParams(); + const [ app, setApp ] = useState(null); + + useEffect(() => + { + (async () => + { + const response = await get(`/api/users/applications/${id}`); + if (!response.success || !response.data) + return errorToast(response.message); + setApp(response.data); + })(); + }, [ id ]); + + if (!app) + return
+ +

Loading...

+
; const deleteApp = async () => { @@ -33,7 +49,7 @@ const Application = ({ app }: {app: App}) => const button = e.target as HTMLButtonElement; button.disabled = true; const response = await patch(`/api/user/applications/${app.id}`, { - permissions: perms + permissions: app.permissions }); button.disabled = false; if (!response.success) @@ -41,18 +57,28 @@ const Application = ({ app }: {app: App}) => return successToast('Successfully updated permissions'); }; + const updatePerms = (permissions: Perms) => + { + setApp(app => + { + if (!app) + return app; + return { ...app, permissions }; + }); + }; + const resetPerms = async (e: React.MouseEvent) => { const button = e.target as HTMLButtonElement; button.disabled = true; - const response = await patch(`/api/user/applications/${app.id}`, { + const response = await patch(`/api/user/applications/${app.id}`, { permissions: null }); button.disabled = false; - if (!response.success) + if (!response.success || !response.data) return errorToast(response.message); successToast('Successfully reset permissions'); - updatePerms(response.data?.permissions); + updatePerms(response.data.permissions); }; return
@@ -95,7 +121,7 @@ const Application = ({ app }: {app: App}) =>
- +

@@ -109,23 +135,22 @@ const Application = ({ app }: {app: App}) => }; +const applicationFormatter = (app: App, callback?: (id: string) => void) => +{ + return callback && callback(app.id)}> + {app.name} + {app.id} + ; +}; + const Applications = () => { const subtitle = useSubtitle(); - const [ applications, setApplications ] = useState([]); - const [ loading, setLoading ] = useState(true); const navigate = useNavigate(); useEffect(() => { subtitle.set('Applications'); - (async () => - { - const response = await get('/api/user/applications') as Res; - if (response.status === 200) - setApplications(response.data as App[]); - setLoading(false); - })(); }, []); const descField = useRef(null); @@ -151,8 +176,6 @@ const Applications = () => const response = await post('/api/user/applications', { name, description }); if (response.status !== 200) errorToast(response.message); - else - setApplications((apps) => [ ...apps, response.data as App ]); button.disabled = false; }; @@ -161,7 +184,26 @@ const Applications = () => { return
-
+ + { + navigate(id); + }} + query={{}} + dataKey='applications' + className='col' + tableDivClass='scrollable-table' + > + Applications + + Name + ID + + + + {/*

Applications

{applications.map(application => itemKeys={[ 'name', 'id' ]} />)}
-
+
*/}

Create Application

@@ -184,19 +226,19 @@ const Applications = () =>
; }; - const ApplicationWrapper = () => - { - const { id } = useParams(); - const application = applications.find(app => app.id === id); - if(!application) - return

Unknown application

; - return ; - }; + // const ApplicationWrapper = () => + // { + // const { id } = useParams(); + // const application = applications.find(app => app.id === id); + // if(!application) + // return

Unknown application

; + // return ; + // }; return } /> - } /> + } /> ; }; diff --git a/src/pages/home/Profile.tsx b/src/pages/home/Profile.tsx index d388ddc..beca5f1 100644 --- a/src/pages/home/Profile.tsx +++ b/src/pages/home/Profile.tsx @@ -155,7 +155,7 @@ const Profile = () => } button.disabled = true; - const response = await post('/api/user/settings', data); + const response = await post<{ reAuth: boolean }>('/api/user/settings', data); button.disabled = false; if (response.data?.reAuth) { diff --git a/src/util/Util.ts b/src/util/Util.ts index 6b77d55..b2c7e88 100644 --- a/src/util/Util.ts +++ b/src/util/Util.ts @@ -184,7 +184,7 @@ export const setSetting = async (name: string, value: unknown) => await post('/api/settings', settings.user); }; -const parseResponse = async (response: Response): Promise => +const parseResponse = async (response: Response): Promise> => { const { headers: rawHeaders, status } = response; // Fetch returns heders as an interable for some reason @@ -214,7 +214,7 @@ const parseResponse = async (response: Response): Promise => return { ...base, message: (await response.text() || response.statusText) }; }; -export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) => +export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) => { const options: HttpOpts & RequestOptions = { method: 'POST' @@ -235,10 +235,10 @@ export const post = async (url: string, body?: object | string, opts: ReqOptions // console.log(url, options); const response = await fetch(url, options); - return parseResponse(response); + return parseResponse(response); }; -export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) => +export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) => { const options: HttpOpts & RequestOptions = { method: 'PATCH' @@ -259,18 +259,18 @@ export const patch = async (url: string, body: object | string, opts: RequestOpt // console.log(url, options); const response = await fetch(url, options); - return parseResponse(response); + return parseResponse(response); }; -export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) => +export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) => { if (params) url += '?' + Object.entries(params) - .map(([ key, val ]) => `${key}=${val instanceof Array ? val.join(',') : val}`) + .map(([ key, val ]) => val !== null && val !== '' && typeof val !== 'undefined' ? `${key}=${val instanceof Array ? val.join(',') : val}` : key) .join('&'); const response = await fetch(url); - return parseResponse(response); + return parseResponse(response); }; export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) =>