Create role fields and button and some other WIP
This commit is contained in:
parent
d342c71fd1
commit
d4858d809d
@ -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
|
||||
|
||||
|
@ -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>;
|
||||
};
|
@ -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";
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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}) => {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
Loading…
Reference in New Issue
Block a user