bunch of bullshit

This commit is contained in:
Erik 2022-12-09 18:11:49 +02:00
parent b5090c02ea
commit 40d5d9da4a
Signed by: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
10 changed files with 438 additions and 234 deletions

View File

@ -39,7 +39,12 @@ function App() {
}, []); }, []);
const menuItems = [ const menuItems = [
{ to: '/home', label: 'Home', items: [{ to: '/profile', label: 'Profile', relative: true }] }, {
to: '/home', label: 'Home', items: [
{ to: '/profile', label: 'Profile', relative: true },
{ to: '/applications', label: 'Applications', relative: true }
]
},
{ to: '/users', label: 'Users' }, { to: '/users', label: 'Users' },
{ to: '/admin', label: 'Admin' } { to: '/admin', label: 'Admin' }
]; ];
@ -51,55 +56,58 @@ function App() {
<div className='background'> <div className='background'>
{user ? {user ?
<div> <div>
<header className="card"> <header className="card">
<UserControls /> <UserControls />
</header> </header>
<Sidebar> <Sidebar>
<SidebarMenu menuItems={menuItems} /> <SidebarMenu menuItems={menuItems} />
</Sidebar> </Sidebar>
</div>
: null}
<div className={`main-content ${user ? "" : "login"}`}>
<ErrorBoundary>
<Routes>
<Route path='/home/*' element={<PrivateRoute>
<TitledPage title='Home'>
<Home />
</TitledPage>
</PrivateRoute >} />
<Route path='/users/*' element={<PrivateRoute>
<TitledPage title='Users' >
<Users />
</TitledPage>
</PrivateRoute >} />
<Route path='/admin/*' element={<PrivateRoute>
<Admin />
</PrivateRoute >} />
<Route path='/login/*' element={<UnauthedRoute>
<Login />
</UnauthedRoute>} />
<Route path='/register/*' element={<UnauthedRoute>
<Register />
</UnauthedRoute>} />
<Route path='*' element={<Navigate to='/home' />} />
</Routes>
</ErrorBoundary>
<div className={`footer ${user ? "" : "login"}`}>
<p className='m-0'>Made with by <a href="https://corgi.wtf" target="_blank" rel="noreferrer">Navy.gif</a> &nbsp;|&nbsp; CSS by <a href="https://d3vision.dev" target="_blank" rel="noreferrer">D3vision</a></p>
</div>
</div> </div>
: null}
<div className={`main-content ${user ? "" : "login"}`}>
<ErrorBoundary>
<Routes>
<Route path='/home/*' element={<PrivateRoute>
<TitledPage title='Home'>
<Home />
</TitledPage>
</PrivateRoute >} />
<Route path='/users/*' element={<PrivateRoute>
<TitledPage title='Users' >
<Users />
</TitledPage>
</PrivateRoute >} />
<Route path='/admin/*' element={<PrivateRoute>
<TitledPage title='Admin'>
<Admin />
</TitledPage>
</PrivateRoute >} />
<Route path='/login/*' element={<UnauthedRoute>
<Login />
</UnauthedRoute>} />
<Route path='/register/*' element={<UnauthedRoute>
<Register />
</UnauthedRoute>} />
<Route path='*' element={<Navigate to='/home' />} />
</Routes>
</ErrorBoundary>
<div className={`footer ${user ? "" : "login"}`}>
<p className='m-0'>Made with by <a href="https://corgi.wtf" target="_blank" rel="noreferrer">Navy.gif</a> &nbsp;|&nbsp; CSS by <a href="https://d3vision.dev" target="_blank" rel="noreferrer">D3vision</a></p>
</div>
</div>
</div> </div>

View File

@ -0,0 +1,36 @@
import React, { useState } from "react";
const FileSelector = ({cb}) => {
if (!cb) throw new Error('Missing callback');
const [file, setFile] = useState(null);
const onDragOver = (event) => {
event.stopPropagation();
event.preventDefault();
};
const onDrop = (event) => {
event.preventDefault();
const { dataTransfer } = event;
if (!dataTransfer.files.length) return;
const [file] = dataTransfer.files;
setFile(file);
cb(file);
};
return <label onDrop={onDrop} onDragOver={onDragOver} htmlFor="pfp_upload" className="drop-container">
<span className="drop-title">Drag &apos;n&apos; drop a file here</span>
or
<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]);
}}
type="file" id="pfp_upload" accept="image/*" required></input>
</label>;
};
export default FileSelector;

View File

@ -9,6 +9,7 @@ export const TableListEntry = ({onClick, item, itemKeys}) => {
export const Table = ({headerItems, children, items, itemKeys}) => { export const Table = ({headerItems, children, items, itemKeys}) => {
if (!headerItems) throw new Error(`Missing table headers`);
let i = 0; let i = 0;
return <table className="striped hover" > return <table className="striped hover" >
<thead> <thead>

View File

@ -18,6 +18,10 @@
--font-family-mono: monaco, "Consolas", "Lucida Console", monospace; --font-family-mono: monaco, "Consolas", "Lucida Console", monospace;
} }
textarea {
resize: none;
}
.w-100{ .w-100{
width: 100% !important; width: 100% !important;
} }

View File

@ -1,12 +1,33 @@
import React from "react"; import React, { useRef, useState } from "react";
import FileSelector from "../components/FileSelector";
import '../css/pages/Admin.css'; import '../css/pages/Admin.css';
const Admin = () => { const Admin = () => {
return <div className="page"> const [file, setFile] = useState(null);
<h2 className="pageTitle">Admin</h2>
<div className='card'> const submit = (event) => {
event.preventDefault();
console.log(file);
};
return <div className="row">
<div className="col-6-lg col-12">
<h2>Thematic customisation</h2>
<form>
<input type='text' placeholder='Site name' />
<FileSelector cb={setFile} />
<button onClick={submit}>Submit</button>
</form>
</div> </div>
<div className="col-6-lg col-12">
<h2>Dingus</h2>
</div>
</div>; </div>;
}; };

View File

@ -1,190 +1,14 @@
import React, { useRef, useState } from "react"; import React from "react";
import { Route, Routes } from "react-router"; import { Route, Routes } from "react-router";
import '../css/pages/Home.css'; import '../css/pages/Home.css';
import { useLoginContext } from "../structures/UserContext";
import ErrorBoundary from "../util/ErrorBoundary"; import ErrorBoundary from "../util/ErrorBoundary";
import { capitalise, get, post } from "../util/Util"; import Applications from "./home/Applications";
import Profile from "./home/Profile";
const TwoFactorControls = ({ user }) => {
const [twoFactorError, set2FAError] = useState(null);
const [displayInput, setDisplayInput] = useState(false);
const [qr, setQr] = useState(null);
const displayQr = async () => {
const response = await get('/api/user/2fa');
if (response.status !== 200) return set2FAError(response.message);
setQr(response.message);
};
const codeRef = useRef();
const disable2FA = async (event) => {
event.preventDefault();
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);
};
const submitCode = async (event) => {
event.preventDefault();
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);
};
let inner = <div>
{qr ?
<div>
<img src={qr} />
<form>
<input placeholder='Authenticator code' ref={codeRef} type='password' />
<button onClick={submitCode} className="button success">Submit</button>
</form>
</div>:
<button onClick={displayQr} className="button primary">Enable 2FA</button>}
</div>;
if (user.twoFactor) inner = <div>
{displayInput ?
<form>
<input placeholder='Authenticator code to disable' ref={codeRef} type='password' />
<button className="button error" onClick={disable2FA}>Submit</button>
</form>
: <button onClick={() => setDisplayInput(true)} className="button error">Disable 2FA</button>}
</div>;
return <div>
{twoFactorError && <p>{twoFactorError}</p>}
{inner}
</div>;
};
const ExternalProfile = ({ profile }) => {
return <div>
<b>{capitalise(profile.provider)}</b>
<p className="m-0">Username: {profile.username}</p>
<p className="m-0">ID: {profile.id}</p>
</div>;
};
const ThirdPartyConnections = ({user}) => {
const {externalProfiles} = user;
return <div>
{externalProfiles.map(profile => <ExternalProfile key={profile.id} profile={profile} />)}
</div>;
};
const Profile = () => {
const [user] = useLoginContext();
const [file, setFile] = useState(null);
const fileSelector = useRef();
const [fileName, setFileName] = useState(null);
const [error, setError] = useState(null);
// For some reason this has to be done for onDrop to work
const onDragOver = (event) => {
event.stopPropagation();
event.preventDefault();
};
const onDrop = (event) => {
event.preventDefault();
const { dataTransfer } = event;
if (!dataTransfer.files.length) return;
const [file] = dataTransfer.files;
console.log(file.name);
setFileName(`Selected: ${file.name}`);
setFile(file);
};
const fileGarbage = (event) => {
setFile(event.target.files[0]);
setFileName(`Selected: ${event.target.files[0].name}`);
};
const submit = async ({target: button}) => {
button.disabled = true;
if (!file) return;
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);
else fileSelector.current.value = null;
button.disabled = false;
};
return <div className="row">
<div className="col-6-lg col-12">
<h3>Profile</h3>
<div className="dir-row pfp-wrapper">
<div className="w-auto f-s0">
<p><b>Profile Picture</b></p>
<img width={256} height={256} src={`/api/users/${user.id}/avatar`} />
</div>
<div className="f-g f-b0">
<p><b>Change Profile Picture</b></p>
<form className="f-g f-b0">
<label onDrop={onDrop} onDragOver={onDragOver} htmlFor="pfp_upload" className="drop-container">
<span className="drop-title">Drag &apos;n&apos; drop a file here</span>
or
<span className="drop-title">Click to select a file</span>
<p className="fileName m-0">{fileName}</p>
<input ref={fileSelector} onChange={(event) => fileGarbage(event)}
type="file" id="pfp_upload" accept="image/*" required></input>
</label>
<button onClick={submit} className="button primary mt-3">Submit</button>
</form>
</div>
</div>
<h4 className="mt-5">Third party connections</h4>
<ThirdPartyConnections user={user} />
</div>
<div className="col-6-lg col-12">
<h3>Settings</h3>
<p><b>ID:</b> {user.id}</p>
<form>
<label>Username</label>
<input id='username' defaultValue={user.username} type='text' autoComplete="off" />
<label>Display Name</label>
<input id='displayName' defaultValue={user.displayName} type='text' autoComplete="off" />
<label>Change password</label>
<input id='currentPassword' placeholder="Current password" type='password' autoComplete="off" />
<input id='newPassword' placeholder="New password" type='password' autoComplete="off" />
<input id='newPasswordRepeat' placeholder="Repeat new password" type='password' autoComplete="off" />
<button className="button primary">Save</button>
</form>
<p className="mb-0 mt-5"><b>Two Factor:</b> {user.twoFactor ? 'enabled' : 'disabled'}</p>
<TwoFactorControls user={user} />
</div>
</div>;
};
const Main = () => { const Main = () => {
return <div> return <div>
What to put here? hmmm
</div>; </div>;
}; };
@ -194,6 +18,7 @@ const Home = () => {
<Routes> <Routes>
<Route path="/" element={<Main />} /> <Route path="/" element={<Main />} />
<Route path="/profile" element={<Profile />} /> <Route path="/profile" element={<Profile />} />
<Route path="/applications/*" element={<Applications />} />
</Routes> </Routes>
</ErrorBoundary>; </ErrorBoundary>;
}; };

View File

@ -68,7 +68,7 @@ const ApplicationList = ({ apps }) => {
return <Table headerItems={['Name', 'ID']}> return <Table headerItems={['Name', 'ID']}>
{apps.map(app => <TableListEntry {apps.map(app => <TableListEntry
onClick={() => { onClick={() => {
navigate(`${window.location.pathname}/applications/${app.id}`); navigate(`applications/${app.id}`);
}} }}
key={app.id} key={app.id}
item={app} item={app}
@ -219,7 +219,7 @@ const Users = () => {
const UserWrapper = () => { const UserWrapper = () => {
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 null; if (!user) return <p>Unknown user</p>;
return <User user={user} />; return <User user={user} />;
}; };

View File

@ -0,0 +1,102 @@
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 } from "../../util/Util";
const Application = ({ app }) => {
const navigate = useNavigate();
const deleteApp = async () => {
const response = await del(`/api/user/applications/${app.id}`);
};
return <div>
<button className="button secondary" onClick={() => navigate(-1)}>Back</button>
<h2>{app.name}</h2>
<p>{app.description}</p>
<p>{app.token}</p>
<button onClick={deleteApp}>Delete</button>
</div>;
};
const Applications = () => {
const [applications, setApplications] = useState([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
(async () => {
const response = await get('/api/user/applications');
if (response.status === 200) setApplications(response.data);
setLoading(false);
})();
}, []);
const descField = useRef();
const nameField = useRef();
const [error, setError] = useState(null);
const createApp = async (event) => {
event.preventDefault();
const button = event.target;
const name = nameField.current.value;
if(!name) return setError('Missing name');
const description = descField.current.value;
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]);
button.disabled = false;
};
const Main = () => {
return <div className="row">
<div className={`col ld ${loading && 'loading'}`}>
<h2>Applications</h2>
<Table headerItems={['Name', 'ID']}>
{applications.map(application => <TableListEntry
key={application.id}
onClick={() => navigate(application.id)}
item={application}
itemKeys={['name', 'id']}
/>)}
</Table>
</div>
<div className="col">
<h2>Create Application</h2>
{error && <p>{error}</p>}
<form>
<input ref={nameField} placeholder="Name" type='text' />
<textarea ref={descField} rows={5} placeholder="Describe your application" />
<button onClick={createApp} className="button primary mt-3">Create</button>
</form>
</div>
</div>;
};
const ApplicationWrapper = () => {
const { id } = useParams();
const application = applications.find(app => app.id === id);
if(!application) return <p>Unknown application</p>;
return <Application app={application} />;
};
return <ErrorBoundary>
<Routes>
<Route exact path="/" element={<Main />} />
<Route path="/:id" element={<ApplicationWrapper />} />
</Routes>
</ErrorBoundary>;
};
export default Applications;

187
src/pages/home/Profile.js Normal file
View File

@ -0,0 +1,187 @@
import React, { useRef, useState } from "react";
import { capitalise, get, post } from "../../util/Util";
import { useLoginContext } from "../../structures/UserContext";
import FileSelector from "../../components/FileSelector";
const TwoFactorControls = ({ user }) => {
const [twoFactorError, set2FAError] = useState(null);
const [displayInput, setDisplayInput] = useState(false);
const [qr, setQr] = useState(null);
const displayQr = async () => {
const response = await get('/api/user/2fa');
if (response.status !== 200) return set2FAError(response.message);
setQr(response.message);
};
const codeRef = useRef();
const disable2FA = async (event) => {
event.preventDefault();
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);
};
const submitCode = async (event) => {
event.preventDefault();
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);
};
let inner = <div>
{qr ?
<div>
<img src={qr} />
<form>
<input placeholder='Authenticator code' ref={codeRef} type='password' />
<button onClick={submitCode} className="button success">Submit</button>
</form>
</div> :
<button onClick={displayQr} className="button primary">Enable 2FA</button>}
</div>;
if (user.twoFactor) inner = <div>
{displayInput ?
<form>
<input placeholder='Authenticator code to disable' ref={codeRef} type='password' />
<button className="button error" onClick={disable2FA}>Submit</button>
</form>
: <button onClick={() => setDisplayInput(true)} className="button error">Disable 2FA</button>}
</div>;
return <div>
{twoFactorError && <p>{twoFactorError}</p>}
{inner}
</div>;
};
const ExternalProfile = ({ profile }) => {
return <div>
<b>{capitalise(profile.provider)}</b>
<p className="m-0">Username: {profile.username}</p>
<p className="m-0">ID: {profile.id}</p>
</div>;
};
const ThirdPartyConnections = ({ user }) => {
const { externalProfiles } = user;
return <div>
{externalProfiles.map(profile => <ExternalProfile key={profile.id} profile={profile} />)}
</div>;
};
const Profile = () => {
const [user] = useLoginContext();
const [file, setFile] = useState(null);
const [error, setError] = useState(null);
const fileSelector = useRef();
const usernameRef = useRef();
const displayNameRef = useRef();
const currPassRef = useRef();
const newPassRef = useRef();
const newPassRepeatRef = useRef();
const submit = async ({ target: button }) => {
button.disabled = true;
if (!file) return;
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);
else fileSelector.current.value = null;
button.disabled = false;
};
const updateSettings = async (event) => {
event.preventDefault();
const button = event.target;
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 };
if (currentPassword && newPassword && newPasswordRepeat) {
if (newPassword !== newPasswordRepeat) {
button.disabled = false;
return setError('Passwords do not match');
}
data.newPassword = newPassword;
}
const response = await post('/api/user/settings', data);
console.log(response);
button.disabled = false;
};
return <div className="row">
<div className="col-6-lg col-12">
<h3>Profile</h3>
<div className="dir-row pfp-wrapper">
<div className="w-auto f-s0">
<p><b>Profile Picture</b></p>
<img draggable={false} width={256} height={256} src={`/api/users/${user.id}/avatar`} />
</div>
<div className="f-g f-b0">
<p><b>Change Profile Picture</b></p>
<form className="f-g f-b0">
<FileSelector cb={setFile} />
<button onClick={submit} className="button primary mt-3">Submit</button>
</form>
</div>
</div>
<h4 className="mt-5">Third party connections</h4>
<ThirdPartyConnections user={user} />
</div>
<div className="col-6-lg col-12">
<h3>Settings</h3>
<p><b>ID:</b> {user.id}</p>
<p>{error}</p>
<form>
<label>Username</label>
<input ref={usernameRef} id='username' defaultValue={user.username} type='text' autoComplete="off" />
<label>Display Name</label>
<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" />
<input ref={newPassRef} id='newPassword' placeholder="New password" type='password' autoComplete="off" />
<input ref={newPassRepeatRef} id='newPasswordRepeat' placeholder="Repeat new password" type='password' autoComplete="off" />
<button onClick={updateSettings} className="button primary">Save</button>
</form>
<p className="mb-0 mt-5"><b>Two Factor:</b> {user.twoFactor ? 'enabled' : 'disabled'}</p>
<TwoFactorControls user={user} />
</div>
</div>;
};
export default Profile;

View File

@ -46,6 +46,7 @@ const parseResponse = async (response) => {
if (status === 401) { if (status === 401) {
clearSession(); clearSession();
if(!location.pathname.includes('/login')) location.pathname = '/login';
} }
if (headers['content-type']?.includes('application/json')) { if (headers['content-type']?.includes('application/json')) {
@ -83,6 +84,25 @@ export const get = async (url, params) => {
return parseResponse(response); return parseResponse(response);
}; };
export const del = async (url, body, opts = {}) => {
const options = {
method: 'delete',
body
};
if (opts.headers !== null) {
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
const response = await fetch(url, options);
return parseResponse(response);
};
export const capitalise = (str) => { export const capitalise = (str) => {
const first = str[0].toUpperCase(); const first = str[0].toUpperCase();
return `${first}${str.substring(1)}`; return `${first}${str.substring(1)}`;