Stripped out a whole bunch of pages, read more

The purpose of this branch is to bring this project in line with the JWT
protcol that the microservice is using. For the time being, it's easier
to get a stripped-down and stable build and replace the lost parts, one-
by-one.
This commit is contained in:
2021-03-08 12:34:41 +11:00
parent e3e5af4af0
commit 7c09ac46da
46 changed files with 310 additions and 4150 deletions
+1 -27
View File
@@ -1,7 +1,6 @@
//react
import React, { useState } from 'react';
import React from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import { useCookies } from 'react-cookie';
//library components
import LazyRoute from './lazy-route';
@@ -15,24 +14,6 @@ import Header from './panels/header.jsx';
import Footer from './panels/footer.jsx';
const App = props => {
//handle cookies prompt
const [cookies, setCookie] = useCookies();
if (!cookies['accept-cookies']) {
const accept = confirm('This website uses cookies to operate correctly. By clicking "ok", you agree to accept said cookies.');
if (accept) {
setCookie('accept-cookies', true);
} else {
return (
<div>
<p>This website won't operate correctly without cookies.</p>
<button onClick={() => window.location.reload()}>Reload Page</button>
</div>
);
}
}
//default render
return (
<BrowserRouter>
@@ -40,13 +21,6 @@ const App = props => {
<Switch>
<LazyRoute exact path='/' component={() => import('./pages/homepage')} />
<LazyRoute path='/signup' component={() => import('./pages/signup')} />
<LazyRoute path='/login' component={() => import('./pages/login')} />
<LazyRoute path='/account' component={() => import('./pages/account')} />
<LazyRoute path='/chat' component={() => import('./pages/chat')} />
<LazyRoute path='/admin' component={() => import('./pages/admin')} />
<LazyRoute path='/privacypolicy' component={async () => () => <Markdown content={require('../markdown/privacy-policy.md').default} />} />
<LazyRoute path='/credits' component={async () => () => <Markdown content={require('../markdown/credits.md').default} />} />
-85
View File
@@ -1,85 +0,0 @@
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import { useCookies } from 'react-cookie';
import DeleteAccount from '../panels/delete-account';
const Account = props => {
const [cookies, setCookie] = useCookies();
//check for logged in redirect
if (!cookies['loggedin']) {
return <Redirect to='/' />;
}
//refs
let contactElement, passwordElement, retypeElement;
//once before render
useEffect(() => {
fetch('/api/accounts')
.then(blob => blob.json())
.then(json => {
contactElement.checked = json.contact;
})
.catch(e => console.error(e))
;
}, []);
return (
<div className='page'>
<h1 className='centered'>Account</h1>
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
await update(contactElement.checked, passwordElement.value, retypeElement.value);
passwordElement.value = retypeElement.value = '';
}}>
<div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={e => contactElement = e} />
</div>
<div>
<label htmlFor='password'>Change Password:</label>
<input type='password' name='password' ref={e => passwordElement = e} />
</div>
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={e => retypeElement = e} />
</div>
</div>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount className='constricted' />
</div>
);
};
const update = async (contact, password, retype) => {
if (password != retype) {
alert('Passwords do not match');
}
//generate a new formdata payload
let formData = new FormData();
formData.append('contact', contact);
if (password) {
formData.append('password', password);
}
const result = await fetch('/api/accounts', { method: 'PATCH', body: formData });
if (result.ok) {
alert(await result.text());
} else {
alert(await result.text());
}
}
export default Account;
-26
View File
@@ -1,26 +0,0 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useCookies } from 'react-cookie';
//import BannedEmails from '../panels/banned-emails';
import NewsPublisher from '../panels/news-publisher';
import NewsEditor from '../panels/news-editor';
const Admin = props => {
const [cookies, setCookie] = useCookies();
//check for logged in redirect
if (!cookies['admin']) {
return <Redirect to='/' />;
}
return (
<div className='page'>
<h1 className='centered'>Administration</h1>
<NewsPublisher uri={process.env.NEWS_URI} newsKey={process.env.NEWS_KEY} />
<NewsEditor uri={process.env.NEWS_URI} newsKey={process.env.NEWS_KEY} />
</div>
);
};
export default Admin;
-23
View File
@@ -1,23 +0,0 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useCookies } from 'react-cookie';
import Chat from '../panels/chat';
//Temporary chat page
const ChatPage = props => {
const [cookies, setCookie] = useCookies();
//check for logged in redirect
if (!cookies['loggedin']) {
return <Redirect to='/' />;
}
return (
<div className='page'>
<Chat uri={process.env.CHAT_URI} />
</div>
);
};
export default ChatPage;
-71
View File
@@ -1,71 +0,0 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useCookies } from 'react-cookie';
//utilities
const validateEmail = require('../../../common/utilities/validate-email.js');
const LogIn = props => {
const [cookies, setCookie] = useCookies();
//check for logged in redirect
if (cookies['loggedin']) {
return <Redirect to='/' />;
}
//refs
let emailElement, passwordElement;
return (
<div className='page'>
<h1 className='centered'>Login</h1>
<form className='constricted' onSubmit={
evt => {
evt.preventDefault();
handleSubmit(emailElement.value, passwordElement.value)
.then(([res, ok]) => {
alert(res);
if (ok) {
window.location.reload(true); //BUFGIX: force reload of the header element
}
})
.catch(e => console.error(e))
;
}
}>
<div>
<label htmlFor="email">Email:</label>
<input type="email" name="email" ref={e => emailElement = e} />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={e => passwordElement = e} />
</div>
<button type='submit'>Login</button>
</form>
</div>
);
};
//DOCS: returns two values: response and OK
const handleSubmit = async (email, password) => {
email = email.trim();
//generate a new formdata payload
let formData = new FormData();
formData.append('email', email);
formData.append('password', password);
const result = await fetch('/api/accounts/login', { method: 'POST', body: formData });
if (result.ok) {
return [await result.text(), true];
} else {
return [await result.text(), false];
}
};
export default LogIn;
+1 -1
View File
@@ -3,7 +3,7 @@ import React from 'react';
const NotFound = props => {
return (
<div className='page'>
<h1 className='middle centered'>Not Found</h1>
<h1 className='middle centered'>Page Not Found</h1>
</div>
);
};
-114
View File
@@ -1,114 +0,0 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import { useCookies } from 'react-cookie';
//utilities
const validateEmail = require('../../../common/utilities/validate-email.js');
const validateUsername = require('../../../common/utilities/validate-username.js');
const SignUp = props => {
const [cookies, setCookie] = useCookies();
//check for logged in redirect
if (cookies['loggedin']) {
return <Redirect to='/' />;
}
//refs
let emailElement, usernameElement, passwordElement, retypeElement, contactElement;
return (
<div className='page'>
<h1 className='centered'>Signup</h1>
<form className='constricted' onSubmit={
evt => {
evt.preventDefault();
handleSubmit(emailElement.value, usernameElement.value, passwordElement.value, retypeElement.value, contactElement.checked)
.then(res => res ? alert(res) : null)
.then(() => emailElement.value = usernameElement.value = passwordElement.value = retypeElement.value = '') //clear input
.then(() => contactElement.checked = false)
.then(() => props.history.push('/'))
.catch(e => console.error(e))
;
}
}>
<div>
<label htmlFor='email'>Email:</label>
<input type='email' name='email' ref={e => emailElement = e} />
</div>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={e => usernameElement = e} />
</div>
<div>
<label htmlFor='password'>Password:</label>
<input type='password' name='password' ref={e => passwordElement = e} />
</div>
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={e => retypeElement = e} />
</div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={e => contactElement = e} />
</div>
<button type='submit'>Signup</button>
</form>
</div>
);
};
const handleSubmit = async (email, username, password, retype, contact) => {
email = email.trim();
username = username.trim();
const err = handleValidation(email, username, password, retype);
if (err) {
return err;
}
//generate a new formdata payload
let formData = new FormData();
formData.append('email', email);
formData.append('username', username);
formData.append('password', password);
formData.append('contact', contact)
const result = await fetch('/api/accounts/signup', { method: 'POST', body: formData });
if (result.ok) {
return result.text();
} else {
return result.text();
}
};
//returns an error message, or null on success
const handleValidation = (email, username, password, retype) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (!validateUsername(username)) {
return 'invalid username';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
if (password !== retype) {
return 'passwords do not match';
}
return null;
};
export default SignUp;
-108
View File
@@ -1,108 +0,0 @@
import React, { useState, useEffect } from 'react';
const BannedEmails = props => {
const [data, setData] = useState(null);
let usernameElement, emailElement, expiryElement, reasonElement;
let unbanElement;
fetch('/api/admin/banned', { method: 'GET' })
.then(banned => banned.json())
.then(banned => !data ? setData(banned) : null)
.catch(e => console.error(e))
;
return (
<div>
<h2>Banned Accounts</h2>
<table>
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Privilege</th>
<th>Expiry</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
{(data || []).map((entry, index) =>
<tr key={index}>
<td>{entry.username}</td>
<td>{entry.email}</td>
<td>{entry.privilege}</td>
<td>{entry.expiry ? (new Date(entry.expiry)).toISOString() : null}</td>
<td>{entry.reason}</td>
</tr>
)}
</tbody>
</table>
<h2>Ban</h2>
<form onSubmit={async e => { e.preventDefault(); await handleBan(usernameElement.value, emailElement.value, expiryElement.value, reasonElement.value); }}>
<div>
<label htmlFor='username'>Username: </label>
<input type='text' name='username' ref={e => usernameElement = e} />
</div>
<div>
<label htmlFor='email'>Email: </label>
<input type='email' name='email' ref={e => emailElement = e} />
</div>
<div>
<label htmlFor='expiry'>Expiry: </label>
<input type='date' name='expiry' ref={e => expiryElement = e} />
</div>
<div>
<label htmlFor='reason'>Reason: </label>
<textarea rows='4' cols='50' name='reason' ref={e => reasonElement = e} />
</div>
<button type='submit'>Drop The Banhammer</button>
</form>
<h2>Unban</h2>
<form onSubmit={async e => { e.preventDefault(); await handleUnban(unbanElement.value); }}>
<div>
<label htmlFor='entry'>Unban User: </label>
<input type='text' name='entry' ref={e => unbanElement = e} />
</div>
<button type='submit'>Release From Horny Jail</button>
</form>
</div>
);
};
const handleBan = async (username, email, expiry, reason) => {
username = username.trim();
email = email.trim();
reason = reason.trim();
//generate a new formdata payload
let formData = new FormData();
formData.append('username', username);
formData.append('email', email);
formData.append('expiry', expiry);
formData.append('reason', reason);
const result = await fetch('/api/admin/ban', { method: 'POST', body: formData });
alert(await result.text());
};
const handleUnban = async (entry) => {
entry = entry.trim();
let formData = new FormData();
formData.append('entry', entry);
const result = await fetch('/api/admin/unban', { method: 'POST', body: formData });
alert(await result.text());
};
export default BannedEmails;
-32
View File
@@ -1,32 +0,0 @@
import React from 'react';
import { useCookies } from 'react-cookie';
const Chat = props => {
requestPseudonym();
return (
<div className='chat'>
<p>Chat URI: {props.uri}</p>
<p>Chat Paragraph TODO</p>
</div>
);
};
const requestPseudonym = () => {
const [cookies, setCookie] = useCookies();
//if your username hasn't been reserved
if (!cookies['pseudonym']) {
fetch('/api/chat/reserve', { method: 'POST' })
.then(msg => msg.json())
.then(json => {
if (!json.ok) { //I don't like doing this
console.error(json.error);
}
})
.catch(e => console.error(e))
;
}
};
export default Chat;
@@ -1,51 +0,0 @@
import React, { useState } from 'react';
//DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed
const DeleteAccount = props => {
const [open, setOpen] = useState(false);
if (!open) {
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
}
let passwordElement;
return (
<form className={props.className} onSubmit={async evt => {
evt.preventDefault();
const password = passwordElement.value;
passwordElement.value = '';
await handleSubmit(password);
}}>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={e => passwordElement = e} />
</div>
<button type='submit'>Delete Account</button>
<button type='cancel' onClick={() => { passwordElement.value = ''; setOpen(false); }}>Cancel</button>
</form>
);
};
const handleSubmit = async (password) => {
//generate a new formdata payload
let formData = new FormData();
formData.append('password', password);
const result = await fetch('/api/accounts/deletion', { method: 'DELETE', body: formData });
if (!result.ok) {
alert(await result.text());
} else {
//force logout
fetch('/api/accounts/logout', { method: 'POST' })
.then(alert(await result.text()))
.then(() => window.location.reload(true)) //BUFGIX: force reload of the header element
.catch(e => console.error(e))
;
}
};
export default DeleteAccount;
+8 -16
View File
@@ -1,12 +1,12 @@
import React from 'react';
import { useCookies } from 'react-cookie';
import { Link } from 'react-router-dom';
const Visitor = () => {
return (
<div>
<a href='/signup'>Sign Up</a>
<Link to='/signup'>Sign Up</Link>
<em> - </em>
<a href='/login'>Log In</a>
<Link to='/login'>Log In</Link>
</div>
);
};
@@ -14,34 +14,26 @@ const Visitor = () => {
const Member = () => {
return (
<div>
<a href='/account'>Account</a>
<Link to='/account'>Account</Link>
<em> - </em>
<a href='/' onClick={logout}>Log out</a>
<Link to='/' onClick={logout}>Log out</Link>
</div>
);
};
const logout = async () => {
//TODO: update API
await fetch('/api/accounts/logout', { method: 'POST' })
.catch(e => console.error(e))
;
};
const Header = () => {
const [cookies, setCookie] = useCookies(['loggedin']);
let Options;
//check for logged in/out status
if (cookies['loggedin']) {
Options = Member;
} else {
Options = Visitor;
}
let Options = Visitor;
return (
<header>
<h1><a href='/'>MERN Template</a></h1>
<h1><Link to='/'>MERN Template</Link></h1>
<Options />
</header>
);
-116
View File
@@ -1,116 +0,0 @@
import React, { useState } from 'react';
import Select from 'react-dropdown-select';
//DOCS: props.uri is the address of a live news-server
//DOCS: props.newsKey is the key of the live news-server
const NewsEditor = props => {
let titleElement, authorElement, bodyElement;
const [articles, setArticles] = useState(null);
const [index, setIndex] = useState(null);
if (!articles) {
fetch(`${props.uri}/titles?limit=999`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
})
.then(a => {
if (!a.ok) {
throw `Network error ${a.status}: ${a.statusText} ${a.url}`;
}
return a.json();
})
.then(a => setArticles(a))
.catch(e => console.error(e))
;
}
return (
<div>
<h2 className='centered'>News Editor</h2>
<div>
<label htmlFor='article'>Article: </label>
<Select
options={(articles || []).map(article => { return { label: article.title, value: article.index }; })}
onChange={values => setIndex(fetchSelection(values[0].value, titleElement, authorElement, bodyElement, props.uri))}
/>
</div>
<form onSubmit={async e => {
e.preventDefault();
await handleSubmit(index, titleElement.value, authorElement.value, bodyElement.value, props.uri, props.newsKey);
titleElement.value = authorElement.value = bodyElement.value = '';
}}>
<div>
<label htmlFor='title'>Title: </label>
<input type='text' name='title' ref={ e => titleElement = e } />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={ e => authorElement = e } />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={ e => bodyElement = e } />
</div>
<button type='submit'>Update</button>
</form>
</div>
);
};
const fetchSelection = (index, titleElement, authorElement, bodyElement, uri) => {
fetch(`${uri}/archive/${index}`, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
})
.then(blob => blob.json())
.then(article => {
titleElement.value = article.title;
authorElement.value = article.author;
bodyElement.value = article.body;
})
.catch(e => console.error(e))
;
return index; //this is admittedly odd
};
const handleSubmit = async (index, title, author, body, uri, newsKey) => {
title = title.trim();
author = author.trim();
body = body.trim();
uri = uri.trim();
newsKey = newsKey.trim();
//fetch POST json data
const raw = await fetch(
`${uri}/${index}`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ title: title, author: author, body: body, key: newsKey })
}
);
if (raw.ok) {
const result = await raw.json();
if (result.ok) {
alert(`Updated article index ${index}`);
} else {
alert(result.error);
}
} else {
alert(raw.statusText);
}
};
export default NewsEditor;
+1 -6
View File
@@ -13,12 +13,7 @@ const NewsFeed = props => {
'Access-Control-Allow-Origin': '*'
},
})
.then(a => {
if (!a.ok) {
throw `Network error ${a.status}: ${a.statusText} ${a.url}`;
}
return a.json();
})
.then(blob => blob.json())
.then(a => setArticles(a))
.catch(e => console.error(e))
;
@@ -1,70 +0,0 @@
import React from 'react';
//DOCS: props.uri is the address of a live news-server
//DOCS: props.newsKey is the key of the live news-server
const NewsPublisher = props => {
let titleElement, authorElement, bodyElement;
return (
<div>
<h2 className='centered'>News Publisher</h2>
<form onSubmit={async e => {
e.preventDefault();
await handleSubmit(titleElement.value, authorElement.value, bodyElement.value, props.uri, props.newsKey);
titleElement.value = authorElement.value = bodyElement.value = '';
}}>
<div>
<label htmlFor='title'>Title: </label>
<input type='text' name='title' ref={ e => titleElement = e } />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={ e => authorElement = e } />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={ e => bodyElement = e } />
</div>
<button type='submit'>Publish</button>
</form>
</div>
);
};
const handleSubmit = async (title, author, body, uri, newsKey) => {
title = title.trim();
author = author.trim();
body = body.trim();
uri = uri.trim();
newsKey = newsKey.trim();
//fetch POST json data
const raw = await fetch(
uri,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ title: title, author: author, body: body, key: newsKey })
}
);
if (raw.ok) {
const result = await raw.json();
if (result.ok) {
alert(`Published article index ${result.index}`);
} else {
alert(result.error);
}
} else {
alert(raw.statusText);
}
};
export default NewsPublisher;