bunch of bullshit
This commit is contained in:
parent
b5090c02ea
commit
40d5d9da4a
104
src/App.js
104
src/App.js
@ -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> | 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> | CSS by <a href="https://d3vision.dev" target="_blank" rel="noreferrer">D3vision</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
36
src/components/FileSelector.js
Normal file
36
src/components/FileSelector.js
Normal 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 'n' 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;
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 'n' 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>;
|
||||||
};
|
};
|
||||||
|
@ -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} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
102
src/pages/home/Applications.js
Normal file
102
src/pages/home/Applications.js
Normal 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
187
src/pages/home/Profile.js
Normal 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;
|
@ -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)}`;
|
||||||
|
Loading…
Reference in New Issue
Block a user