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,
disabled: boolean,
permissions: Permissions,
createdTimestamp: number
createdTimestamp: number,
note: string
}
export type UserLike = {
icon: string | null,
roles: string[],
roles: Role[],
temporary: boolean
} & APIEntity

View File

@ -1,7 +1,7 @@
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');
const [file, setFile] = useState<File | null>(null);
@ -35,4 +35,32 @@ const FileSelector = ({cb}: {cb: (file: File) => void}) => {
</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 FileSelector from "../components/FileSelector";
import { FileSelector } from "../components/Selectors";
import '../css/pages/Admin.css';
import ErrorBoundary from "../util/ErrorBoundary";
import Roles from "./admin/Roles";

View File

@ -57,7 +57,7 @@ const CredentialFields = () => {
<form className="loginForm">
<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>
</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 { BackButton, PageButtons } from "../../components/PageControls";
import { Permissions } from "../../components/PermissionsView";
import { RateLimits } from "../../components/RateLimitsView";
import { Permissions } from "../../views/PermissionsView";
import { RateLimits } from "../../views/RateLimitsView";
import { Table, TableListEntry } from "../../components/Table";
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 { ToggleSwitch } from "../../components/Selectors";
const Role = ({ role }: {role: R}) => {
const perms = { ...role.permissions };
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 perms = { ...role.permissions };
const [perms, updatePerms] = useState<Perms>(role.permissions);
const commitPerms = async () => {
await patch(`/api/roles/${role.id}`, perms);
@ -42,7 +33,11 @@ const Role = ({ role }: {role: R}) => {
</div>
<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>
<input autoComplete='off' id='username' defaultValue={role.name} />
@ -50,8 +45,8 @@ const Role = ({ role }: {role: R}) => {
</div>
<div className="col-6-lg col-12">
<Permissions updatePerms={updatePerms} perms={role.permissions} />
<button onClick={commitPerms} className="button primary">Update</button>
<Permissions onUpdate={updatePerms} perms={role.permissions} />
<button onClick={commitPerms} className="button primary">Save Permissions</button>
</div>
</div>
@ -62,6 +57,43 @@ const Role = ({ role }: {role: R}) => {
</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 [error, setError] = useState<string | null>(null);
@ -108,6 +140,9 @@ const Roles = () => {
<PageButtons {...{page, setPage, length: roles.length}} />
</div>
<CreateRoleField addRole={role => setRoles([...roles, role])} className='col-6-lg col-12' numRoles={roles.length} />
</div>;
};

View File

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

View File

@ -1,7 +1,7 @@
import React, { useRef, useState } from "react";
import { capitalise, get, post } from "../../util/Util";
import { useLoginContext } from "../../structures/UserContext";
import FileSelector from "../../components/FileSelector";
import { FileSelector } from "../../components/Selectors";
import { ExternalProfile as EP, User } from "../../@types/ApiStructures";
const TwoFactorControls = ({ user }: {user: User}) => {

View File

@ -13,10 +13,10 @@ class ErrorBoundary extends React.Component<BoundaryProps, StateProps> {
fallback?: React.ReactNode;
constructor({fallback, ...props}: BoundaryProps) {
constructor(props: BoundaryProps) {
super(props);
this.state = { error: false };
this.fallback = fallback;
this.fallback = props.fallback;
}
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);
else options.body = body as string;
console.log(url, options);
const response = await fetch(url, options);
return parseResponse(response);
};

View File

@ -33,7 +33,20 @@ export const PermissionGroup = ({ name, value, chain, updatePerms }: {name: stri
</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 keys = Object.keys(perms);