Create role fields and button and some other WIP

This commit is contained in:
Erik 2023-04-22 20:18:04 +03:00
parent d342c71fd1
commit d4858d809d
Signed by: Navy.gif
GPG Key ID: 2532FBBB61C65A68
11 changed files with 172 additions and 83 deletions

View File

@ -24,12 +24,13 @@ type APIEntity = {
name: string, name: string,
disabled: boolean, disabled: boolean,
permissions: Permissions, permissions: Permissions,
createdTimestamp: number createdTimestamp: number,
note: string
} }
export type UserLike = { export type UserLike = {
icon: string | null, icon: string | null,
roles: string[], roles: Role[],
temporary: boolean temporary: boolean
} & APIEntity } & APIEntity

View File

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
const FileSelector = ({cb}: {cb: (file: File) => void}) => { export const FileSelector = ({ cb }: { cb: (file: File) => void }) => {
if (!cb) throw new Error('Missing callback'); if (!cb) throw new Error('Missing callback');
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@ -35,4 +35,32 @@ const FileSelector = ({cb}: {cb: (file: File) => void}) => {
</label>; </label>;
}; };
export default FileSelector; type DropdownProps = {
name?: string,
multi?: boolean,
selection?: [],
children: React.ReactNode
}
export const DropdownMenu = (props: DropdownProps) => {
return <div>
<div>
</div>
<div>
{props.children}
</div>
</div>;
};
export const ToggleSwitch = ({ value, onChange, ref, children }:
{ value: boolean, ref?: React.RefObject<HTMLInputElement>, onChange?: React.ChangeEventHandler, children: string }) => {
return <p>
<span className="check-box">
<b>{children}</b>
<input ref={ref} className="check-box" defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
</p>;
};

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from "react"; import React, { useState } from "react";
import { Route, Routes } from "react-router"; import { Route, Routes } from "react-router";
import FileSelector from "../components/FileSelector"; import { FileSelector } from "../components/Selectors";
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";

View File

@ -57,7 +57,7 @@ const CredentialFields = () => {
<form className="loginForm"> <form className="loginForm">
<input ref={usernameRef} 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' /> <input ref={passwordRef} autoComplete='off' placeholder='Password' required id='password' type='password' />
<button className="button primary" onClick={loginClick}>Enter</button> <button className="button primary" onClick={loginClick}>Enter</button>
</form> </form>

View File

@ -1,27 +1,18 @@
import React, { useEffect, 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 { BackButton, PageButtons } from "../../components/PageControls"; import { BackButton, PageButtons } from "../../components/PageControls";
import { Permissions } from "../../components/PermissionsView"; import { Permissions } from "../../views/PermissionsView";
import { RateLimits } from "../../components/RateLimitsView"; import { RateLimits } from "../../views/RateLimitsView";
import { Table, TableListEntry } from "../../components/Table"; import { Table, TableListEntry } from "../../components/Table";
import ErrorBoundary from "../../util/ErrorBoundary"; import ErrorBoundary from "../../util/ErrorBoundary";
import { get, patch } from "../../util/Util"; 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/Selectors";
const Role = ({ role }: {role: R}) => { const Role = ({ role }: {role: R}) => {
const perms = { ...role.permissions }; // const perms = { ...role.permissions };
const updatePerms = (chain: string, raw: string) => { const [perms, updatePerms] = useState<Perms>(role.permissions);
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] as Perms;
}
};
const commitPerms = async () => { const commitPerms = async () => {
await patch(`/api/roles/${role.id}`, perms); await patch(`/api/roles/${role.id}`, perms);
@ -42,7 +33,11 @@ const Role = ({ role }: {role: R}) => {
</div> </div>
<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}>
Disabled
</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} />
@ -50,8 +45,8 @@ const Role = ({ role }: {role: R}) => {
</div> </div>
<div className="col-6-lg col-12"> <div className="col-6-lg col-12">
<Permissions updatePerms={updatePerms} perms={role.permissions} /> <Permissions onUpdate={updatePerms} perms={role.permissions} />
<button onClick={commitPerms} className="button primary">Update</button> <button onClick={commitPerms} className="button primary">Save Permissions</button>
</div> </div>
</div> </div>
@ -62,6 +57,43 @@ const Role = ({ role }: {role: R}) => {
</div>; </div>;
}; };
const CreateRoleField = ({ numRoles, className, addRole }: { numRoles: number, className?: string, addRole: (role: R) => void }) => {
const [error, setError] = useState<string | null>(null);
const nameRef = useRef<HTMLInputElement>(null);
const positionRef = useRef<HTMLInputElement>(null);
const createRole: React.MouseEventHandler<HTMLButtonElement> = async (event) => {
event.preventDefault();
if (!nameRef.current || !positionRef.current) return setError('Must supply name and position');
const name = nameRef.current.value;
const position = parseInt(positionRef.current.value);
const response = await post('/api/roles', { name, position });
if (!response.success) return setError(response.message || 'Unknown error');
addRole(response.data as R);
};
return <div className={className}>
<h4>Create Role</h4>
{error && <p>{error}</p>}
<form>
<label>Name</label>
<input ref={nameRef} required={true} placeholder='Special access' autoComplete='off' type='text' />
<label>Position</label>
<input ref={positionRef} defaultValue={numRoles} type='number' min={0} max={numRoles} />
<button className='button primary' onClick={createRole}>Create</button>
</form>
</div>;
};
const Roles = () => { const Roles = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -108,6 +140,9 @@ const Roles = () => {
<PageButtons {...{page, setPage, length: roles.length}} /> <PageButtons {...{page, setPage, length: roles.length}} />
</div> </div>
<CreateRoleField addRole={role => setRoles([...roles, role])} className='col-6-lg col-12' numRoles={roles.length} />
</div>; </div>;
}; };

View File

@ -5,10 +5,11 @@ 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 "../../components/PermissionsView"; import { Permissions } from "../../views/PermissionsView";
import { Application as App, Permissions as Perms, User as APIUser } from "../../@types/ApiStructures"; import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures";
import { DropdownMenu, ToggleSwitch } from "../../components/Selectors";
const ApplicationList = ({ apps }: {apps: App[]}) => { const ApplicationList = ({ apps }: { apps: App[] }) => {
const navigate = useNavigate(); const navigate = useNavigate();
return <Table headerItems={['Name', 'ID']}> return <Table headerItems={['Name', 'ID']}>
@ -24,9 +25,9 @@ const ApplicationList = ({ apps }: {apps: App[]}) => {
}; };
const Application = ({ app }: {app: 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> return <div>
<h4>{app.name} ({app.id})</h4> <h4>{app.name} ({app.id})</h4>
@ -37,31 +38,30 @@ const Application = ({ app }: {app: App}) => {
</div>; </div>;
}; };
// TODO: Groups, description, notes const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
const User = ({ user }: {user: APIUser}) => { console.log(roles);
return <DropdownMenu>
{roles.map(role => {
return <div key={role.id}>
<label>{role.name}</label>
<input defaultChecked={Boolean(userRoles.find((r: Role) => r.id === role.id))} type='checkbox' />
</div>;
})}
</DropdownMenu>;
};
const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
const [apps, updateApps] = useState<App[]>([]); const [apps, updateApps] = useState<App[]>([]);
const perms = {...user.permissions}; const [perms, updatePerms] = useState<Perms>(user.permissions);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const response = await get(`/api/users/${user.id}/applications`); const appsResponse = await get(`/api/users/${user.id}/applications`);
if(response.status === 200) updateApps(response.data as App[]); if (appsResponse.success) updateApps(appsResponse.data as App[]);
})(); })();
}, []); }, []);
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] as Perms;
}
};
const commitPerms = async () => { const commitPerms = async () => {
await post(`/api/users/${user.id}/permissions`, perms); await post(`/api/users/${user.id}/permissions`, perms);
}; };
@ -80,14 +80,12 @@ const User = ({ user }: {user: APIUser}) => {
<BackButton /> <BackButton />
<h2 className="userId">User {user.id}</h2> <h2 className="userId">User {user.id}</h2>
</div> </div>
<div className="actionButtons mt-4">
{user.disabled ? <button className="button success">Enable</button> : <button className="button error">Disable</button>}
<button className="button error">Delete</button>
</div>
<p><b>Created: </b>{new Date(user.createdTimestamp).toDateString()}</p> <p><b>Created: </b>{new Date(user.createdTimestamp).toDateString()}</p>
<p><b>2FA: </b>{user.twoFactor.toString()}</p> <p><b>2FA:</b> {(user.twoFactor && <button className='button danger'>Disable</button>) || 'Disabled'}</p>
<p><b>Disabled: </b>{user.disabled.toString()}</p> <ToggleSwitch value={user.disabled}>
Login Disabled:
</ToggleSwitch>
<label htmlFor='username'>Username</label> <label htmlFor='username'>Username</label>
<input autoComplete='off' id='username' defaultValue={user.name} /> <input autoComplete='off' id='username' defaultValue={user.name} />
@ -95,28 +93,40 @@ const User = ({ user }: {user: APIUser}) => {
<label htmlFor='displayName'>Display Name</label> <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> <label htmlFor='userNote'>Note</label>
<Routes> <textarea id='userNote' defaultValue={user.note} />
<Route path='/' element={<ApplicationList apps={apps} />} />
<Route path='/applications/:appid' element={<ApplicationWrapper />} /> <label>Roles</label>
</Routes> <Roles userRoles={user.roles} roles={roles} />
<button className="button primary">
Save User
</button>
</div> </div>
<div className="col-6-lg col-12"> <div className="col-6-lg col-12">
<Permissions updatePerms={updatePerms} perms={user.permissions} /> <Permissions onUpdate={updatePerms} perms={user.permissions} />
<button onClick={commitPerms} className="button primary">Update</button> <button onClick={commitPerms} className="button primary">Save Permissions</button>
</div> </div>
</div> </div>
<button className="button primary"> <div className='row'>
Save <div className='col-6-lg col-12'>
</button> <h3>Applications</h3>
<BackButton />
<Routes>
<Route path='/' element={<ApplicationList apps={apps} />} />
<Route path='/applications/:appid' element={<ApplicationWrapper />} />
</Routes>
</div>
</div>
</div>; </div>;
}; };
const CreateUserField = () => { const CreateUserField = ({ className }: { className: string }) => {
const [link, setLink] = useState<string | null>(null); const [link, setLink] = useState<string | null>(null);
const getSignupCode = async () => { const getSignupCode = async () => {
@ -131,8 +141,8 @@ const CreateUserField = () => {
const element = event.target as HTMLElement; const element = event.target as HTMLElement;
const { parentElement } = element; const { parentElement } = element;
navigator.clipboard.writeText(element.innerText); navigator.clipboard.writeText(element.innerText);
if(parentElement) if (parentElement)
(parentElement.childNodes[parentElement.childNodes.length-1] as HTMLElement).style.visibility = "visible"; (parentElement.childNodes[parentElement.childNodes.length - 1] as HTMLElement).style.visibility = "visible";
}; };
const closeToolTip: React.MouseEventHandler = (event) => { const closeToolTip: React.MouseEventHandler = (event) => {
@ -140,12 +150,12 @@ const CreateUserField = () => {
}; };
// TODO: Finish this // TODO: Finish this
return <div> return <div className={className}>
<h4>One-Click Sign-Up</h4> <h4>One-Click Sign-Up</h4>
{link ? {link ?
<div className="registerCodeWrapper tooltip"><p onClick={copyToClipboard}>{link}</p><span onClick={closeToolTip} className="tooltiptext">Code copied!</span></div> : <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>} <button onClick={getSignupCode} className="button primary is-center">Create sign-up link</button>}
<hr/> <hr />
<h4>Create User</h4> <h4>Create User</h4>
<form> <form>
<p>Users will be forced to change the password you set here.</p> <p>Users will be forced to change the password you set here.</p>
@ -159,6 +169,7 @@ const CreateUserField = () => {
const Users = () => { const Users = () => {
const [users, setUsers] = useState<APIUser[]>([]); const [users, setUsers] = useState<APIUser[]>([]);
const [roles, updateRoles] = useState<Role[]>([]);
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -171,6 +182,8 @@ const Users = () => {
setUsers(result.data as APIUser[]); setUsers(result.data as APIUser[]);
} else setError(result.message || 'Unknown error'); } else setError(result.message || 'Unknown error');
setLoading(false); setLoading(false);
const rolesResponse = await get('/api/roles');
if (rolesResponse.success) updateRoles(rolesResponse.data as Role[]);
})(); })();
}, [page]); }, [page]);
@ -178,7 +191,7 @@ const Users = () => {
const { id } = useParams(); const { id } = useParams();
const user = users.find(u => u.id === id); const user = users.find(u => u.id === id);
if (!user) return <p>Unknown user</p>; if (!user) return <p>Unknown user</p>;
return <User user={user} />; return <User roles={roles} user={user} />;
}; };
const navigate = useNavigate(); const navigate = useNavigate();
@ -196,12 +209,10 @@ const Users = () => {
itemKeys={['name', 'id']} />)} itemKeys={['name', 'id']} />)}
</Table> </Table>
<PageButtons {...{page, setPage, length: users.length}} /> <PageButtons {...{ page, setPage, length: users.length }} />
</div> </div>
<div className="col-6-lg col-12"> <CreateUserField className="col-6-lg col-12" />
<CreateUserField />
</div>
</div>; </div>;
}; };

View File

@ -1,7 +1,7 @@
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/FileSelector"; import { FileSelector } from "../../components/Selectors";
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}) => {

View File

@ -13,10 +13,10 @@ class ErrorBoundary extends React.Component<BoundaryProps, StateProps> {
fallback?: React.ReactNode; fallback?: React.ReactNode;
constructor({fallback, ...props}: BoundaryProps) { constructor(props: BoundaryProps) {
super(props); super(props);
this.state = { error: false }; this.state = { error: false };
this.fallback = fallback; this.fallback = props.fallback;
} }
override componentDidCatch(error: Error, errorInfo: unknown) { override componentDidCatch(error: Error, errorInfo: unknown) {

View File

@ -82,6 +82,7 @@ export const post = async (url: string, body?: object | string, opts: RequestOpt
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body); if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
else options.body = body as string; else options.body = body as string;
console.log(url, options);
const response = await fetch(url, options); const response = await fetch(url, options);
return parseResponse(response); return parseResponse(response);
}; };

View File

@ -33,7 +33,20 @@ export const PermissionGroup = ({ name, value, chain, updatePerms }: {name: stri
</li>; </li>;
}; };
export const Permissions = ({ perms, updatePerms }: {perms: Perms, updatePerms: (chain: string, perm: string) => void}) => { export const Permissions = ({ perms, onUpdate }: { perms: Perms, onUpdate: (perms: Perms) => void }) => {
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] as Perms;
}
onUpdate(perms);
};
const elements = []; const elements = [];
const keys = Object.keys(perms); const keys = Object.keys(perms);