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:
@@ -1,20 +1,9 @@
|
||||
WEB_PROTOCOL=http
|
||||
WEB_ADDRESS=localhost
|
||||
WEB_PORT=3000
|
||||
|
||||
MAIL_SMTP=smtp.example.com
|
||||
MAIL_USERNAME=foobar@example.com
|
||||
MAIL_PASSWORD=foobar
|
||||
MAIL_PHYSICAL=42 Placeholder Ave, Placeholder, 0000, USA
|
||||
|
||||
DB_HOSTNAME=127.0.0.1
|
||||
DB_HOSTNAME=database
|
||||
DB_DATABASE=template
|
||||
DB_USERNAME=template
|
||||
DB_PASSWORD=pikachu
|
||||
DB_TIMEZONE=Australia/Sydney
|
||||
|
||||
CHAT_URI=http://example.com:3200/chat
|
||||
CHAT_KEY=chattychattybangbang
|
||||
|
||||
SESSION_SECRET=secret
|
||||
SESSION_ADMIN=adminsecret
|
||||
SECRET_ACCESS=access
|
||||
+1
-5
@@ -1,16 +1,12 @@
|
||||
//polyfills
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { CookiesProvider } from 'react-cookie';
|
||||
|
||||
import App from './components/app';
|
||||
|
||||
ReactDOM.render(
|
||||
<CookiesProvider>
|
||||
<App />
|
||||
</CookiesProvider>,
|
||||
<App />,
|
||||
document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -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} />} />
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
MERN Template developed by Kayne Ruse, KR Game Studios
|
||||
|
||||
[https://github.com/krgamestudios/MERN-template](https://github.com/krgamestudios/MERN-template)
|
||||
[https://github.com/krgamestudios/MERN-template](https://github.com/krgamestudios/MERN-template)
|
||||
|
||||
TODO: generate the credits using config script
|
||||
@@ -1,2 +1,3 @@
|
||||
# Privacy Policy
|
||||
|
||||
TODO: generate the privacy policy using config script
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
//TODO: update this file
|
||||
|
||||
//setup
|
||||
const readline = require('readline');
|
||||
const fs = require('fs');
|
||||
@@ -107,12 +109,12 @@ services:
|
||||
- "traefik.http.routers.${newsName}router.rule=Host(\`${newsWebAddress}\`)"
|
||||
- "traefik.http.routers.${newsName}router.entrypoints=websecure"
|
||||
- "traefik.http.routers.${newsName}router.tls.certresolver=myresolver"
|
||||
- "traefik.http.routers.${newsName}router.service=newsservice@docker"
|
||||
- "traefik.http.routers.${newsName}router.service=${newsName}service@docker"
|
||||
- "traefik.http.services.${newsName}service.loadbalancer.server.port=3100"
|
||||
environment:
|
||||
- WEB_PORT=3100
|
||||
- DB_HOSTNAME=database
|
||||
- DB_DATABASE=news
|
||||
- DB_DATABASE=${newsName}
|
||||
- DB_USERNAME=${newsDBUser}
|
||||
- DB_PASSWORD=${newsDBPass}
|
||||
- DB_TIMEZONE=${databaseTimeZone}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//TODO: move this to the wiki?
|
||||
|
||||
# Setup Tutorial
|
||||
|
||||
Last Updated February 15th 2020
|
||||
Last Updated February 15th 2021
|
||||
|
||||
Hello! This is the tutorial for setting up the MERN-template. If you haven't already, I recommend you download the MERN-template from here:
|
||||
|
||||
|
||||
Generated
+262
-2599
File diff suppressed because it is too large
Load Diff
+10
-25
@@ -4,8 +4,6 @@
|
||||
"description": "A website template using the MERN stack.",
|
||||
"main": "server/server.js",
|
||||
"scripts": {
|
||||
"configure": "node configure-script.js",
|
||||
"clean": "rm docker-compose.yml; rm Dockerfile; rm startup.sql",
|
||||
"start": "npm run build && node server/server.js",
|
||||
"build": "npm run build:server && npm run build:client",
|
||||
"build:server": "exit 0",
|
||||
@@ -26,44 +24,31 @@
|
||||
},
|
||||
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"connect-session-sequelize": "^7.1.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"core-js": "^3.8.3",
|
||||
"dateformat": "^4.5.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-formidable": "^1.2.0",
|
||||
"express-session": "^1.17.1",
|
||||
"form-data": "^4.0.0",
|
||||
"mariadb": "^2.5.2",
|
||||
"node-cron": "^2.0.3",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nodemailer": "^6.4.17",
|
||||
"react-cookie": "^4.0.3",
|
||||
"react-dropdown-select": "^4.7.3",
|
||||
"react-markdown": "^5.0.3",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"sequelize": "^6.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"babel-loader": "^8.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"concurrently": "^5.3.0",
|
||||
"dateformat": "^4.5.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"html-webpack-plugin": "^5.0.0-alpha.14",
|
||||
"nodemon": "^2.0.7",
|
||||
"mariadb": "^2.5.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"sequelize": "^6.4.0",
|
||||
"webpack": "^5.15.0",
|
||||
"webpack-cli": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.7",
|
||||
"webpack-bundle-analyzer": "^4.3.0",
|
||||
"webpack-cli": "^4.3.1",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
//libraries
|
||||
const utils = require('util');
|
||||
const bcrypt = require('bcryptjs');
|
||||
var cron = require('node-cron');
|
||||
|
||||
const Sequelize = require('sequelize');
|
||||
const Op = Sequelize.Op;
|
||||
const { accounts } = require('../database/models');
|
||||
|
||||
//api/accounts/deletion
|
||||
const route = async (req, res) => {
|
||||
//make sure the account is logged in
|
||||
if (req.cookies['loggedin'] !== process.env.WEB_ADDRESS) {
|
||||
return res.status(401).send('invalid session status');
|
||||
}
|
||||
|
||||
//compare the user's password
|
||||
const compare = utils.promisify(bcrypt.compare);
|
||||
const match = await compare(req.fields.password, req.session.account.hash);
|
||||
|
||||
if (!match) {
|
||||
return res.status(401).send('incorrect password');
|
||||
}
|
||||
|
||||
//set the deletion time (2 days from now)
|
||||
const interval = new Date(new Date().setDate(new Date().getDate() + 2)); //wow
|
||||
await accounts.update({
|
||||
deletion: interval
|
||||
},
|
||||
{
|
||||
where: {
|
||||
id: req.session.account.id
|
||||
}
|
||||
});
|
||||
|
||||
//finally
|
||||
return res.status(200).send('account will be deleted in two days - log in to cancel');
|
||||
};
|
||||
|
||||
//actually delete the accounts
|
||||
cron.schedule('0 * * * *', () => {
|
||||
accounts.destroy({
|
||||
where: {
|
||||
deletion: {
|
||||
[Op.lt]: Sequelize.fn('NOW')
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,17 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
//basic account management
|
||||
router.get('/', require('./query'));
|
||||
router.patch('/', require('./update'));
|
||||
|
||||
//signup -> login -> logout
|
||||
router.post('/signup', require('./signup'));
|
||||
router.get('/validation', require('./validation'));
|
||||
router.post('/login', require('./login'));
|
||||
router.post('/logout', require('./logout'));
|
||||
|
||||
//account deletion
|
||||
router.delete('/deletion', require('./deletion'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,86 +0,0 @@
|
||||
//libraries
|
||||
const utils = require('util');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const Sequelize = require('sequelize');
|
||||
const Op = Sequelize.Op;
|
||||
const { bannedEmails, accounts } = require('../database/models');
|
||||
|
||||
//utilities
|
||||
const validateEmail = require('../../common/utilities/validate-email.js');
|
||||
|
||||
//api/accounts/login
|
||||
const route = async (req, res) => {
|
||||
//validate the given details
|
||||
const validateErr = await validateDetails(req.fields);
|
||||
if (validateErr) {
|
||||
return res.status(401).send(validateErr);
|
||||
}
|
||||
|
||||
//get the existing account
|
||||
const account = await accounts.findOne({
|
||||
where: {
|
||||
email: req.fields.email
|
||||
}
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(401).send('incorrect email or password');
|
||||
}
|
||||
|
||||
//compare passwords
|
||||
const compare = utils.promisify(bcrypt.compare);
|
||||
const match = await compare(req.fields.password, account.hash);
|
||||
|
||||
if (!match) {
|
||||
return res.status(401).send('incorrect email or password');
|
||||
}
|
||||
|
||||
//save the session and cookie data
|
||||
req.session.account = JSON.parse(JSON.stringify(account.dataValues));
|
||||
res.cookie('loggedin', process.env.WEB_ADDRESS);
|
||||
|
||||
if (account.privilege == 'administrator') {
|
||||
res.cookie('admin', process.env.SESSION_ADMIN);
|
||||
}
|
||||
|
||||
//cancel deletion if any
|
||||
await accounts.update({ deletion: null }, {
|
||||
where: {
|
||||
id: account.id
|
||||
}
|
||||
});
|
||||
|
||||
//finally
|
||||
res.status(200).send('login succeeded');
|
||||
};
|
||||
|
||||
const validateDetails = async (fields) => {
|
||||
//basic formatting (with an exception for the default admin account)
|
||||
if (!validateEmail(fields.email) && fields.email != `admin@${process.env.WEB_ADDRESS}`) {
|
||||
return 'invalid email';
|
||||
}
|
||||
|
||||
//check for existing (banned)
|
||||
const banned = await bannedEmails.findAll({
|
||||
where: {
|
||||
[Op.and]: {
|
||||
email: fields.email,
|
||||
expiry: {
|
||||
[Op.or]: {
|
||||
[Op.gt]: Sequelize.fn('NOW'),
|
||||
[Op.eq]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (banned.length > 0) {
|
||||
return 'banned email';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,11 +0,0 @@
|
||||
const route = (req, res) => {
|
||||
//clear cookies and stored data
|
||||
req.session.account = null;
|
||||
res.clearCookie('loggedin');
|
||||
res.clearCookie('admin');
|
||||
res.clearCookie('pseudonym');
|
||||
|
||||
return res.status(200).end();
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,21 +0,0 @@
|
||||
const { accounts } = require('../database/models');
|
||||
|
||||
const route = async (req, res) => {
|
||||
if (!req.session.account || !req.session.account.id) {
|
||||
res.status(401).send('Unknown account');
|
||||
}
|
||||
|
||||
//update the reference
|
||||
req.session.account = (await accounts.findOne({
|
||||
where: {
|
||||
id: req.session.account.id
|
||||
}
|
||||
})).dataValues;
|
||||
|
||||
//respond with the private-facing data
|
||||
res.status(200).json({
|
||||
contact: req.session.account.contact
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,159 +0,0 @@
|
||||
//libraries
|
||||
const bcrypt = require('bcryptjs');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const Sequelize = require('sequelize');
|
||||
const Op = Sequelize.Op;
|
||||
const { bannedEmails, accounts, pendingSignups } = require('../database/models');
|
||||
|
||||
//utilities
|
||||
const validateEmail = require('../../common/utilities/validate-email.js');
|
||||
const validateUsername = require('../../common/utilities/validate-username.js');
|
||||
|
||||
//api/accounts/signup
|
||||
const route = async (req, res) => {
|
||||
//validate the given details
|
||||
const validateErr = await validateDetails(req.fields);
|
||||
if (validateErr) {
|
||||
return res.status(401).send(validateErr);
|
||||
}
|
||||
|
||||
//generate the password hash
|
||||
const salt = await bcrypt.genSalt(11);
|
||||
const hash = await bcrypt.hash(req.fields.password, salt);
|
||||
|
||||
//generate the validation field
|
||||
const token = Math.floor(Math.random() * 2000000000);
|
||||
|
||||
//register signup
|
||||
const signupErr = await registerPendingSignup(req.fields, hash, token);
|
||||
if (signupErr) {
|
||||
return res.status(500).send(signupErr);
|
||||
}
|
||||
|
||||
//send the validation email
|
||||
const emailErr = await sendValidationEmail(req.fields.email, req.fields.username, token);
|
||||
if (emailErr) {
|
||||
return res.status(500).send(emailErr);
|
||||
}
|
||||
|
||||
//finally
|
||||
res.status(200).send("Validation email sent!");
|
||||
return null;
|
||||
}
|
||||
|
||||
const validateDetails = async (fields) => {
|
||||
//basic formatting
|
||||
if (!validateEmail(fields.email)) {
|
||||
return 'invalid email';
|
||||
}
|
||||
|
||||
if (!validateUsername(fields.username)) {
|
||||
return 'invalid username';
|
||||
}
|
||||
|
||||
//check for existing (banned)
|
||||
const banned = await bannedEmails.findAll({
|
||||
where: {
|
||||
[Op.and]: {
|
||||
email: fields.email,
|
||||
expiry: {
|
||||
[Op.or]: {
|
||||
[Op.gt]: Sequelize.fn('NOW'),
|
||||
[Op.eq]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (banned.length > 0) {
|
||||
return 'banned email';
|
||||
}
|
||||
|
||||
//check for existing email
|
||||
const email = await accounts.findOne({
|
||||
where: {
|
||||
email: fields.email
|
||||
}
|
||||
});
|
||||
|
||||
if (email) {
|
||||
return 'email already exists';
|
||||
}
|
||||
|
||||
//check for existing username
|
||||
const username = await accounts.findOne({
|
||||
where: {
|
||||
username: fields.username
|
||||
}
|
||||
});
|
||||
|
||||
if (username) {
|
||||
return 'username already exists';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const registerPendingSignup = async (fields, hash, token) => {
|
||||
const record = await pendingSignups.upsert({
|
||||
email: fields.email,
|
||||
username: fields.username,
|
||||
hash: hash,
|
||||
contact: fields.contact,
|
||||
token: token
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const sendValidationEmail = async (email, username, token) => {
|
||||
const addr = `${process.env.WEB_PROTOCOL}://${process.env.WEB_ADDRESS}/api/accounts/validation?username=${username}&token=${token}`;
|
||||
const msg = `Hello ${username}!
|
||||
|
||||
Please visit the following link to validate your account: ${addr}
|
||||
|
||||
You can contact us directly at our physical mailing address here: ${process.env.MAIL_PHYSICAL}
|
||||
`;
|
||||
|
||||
let transporter, info;
|
||||
|
||||
//what exactly is a transport?
|
||||
try {
|
||||
transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_SMTP,
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME,
|
||||
pass: process.env.MAIL_PASSWORD
|
||||
},
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
return `failed to create transport: ${e}`;
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
try {
|
||||
info = await transporter.sendMail({
|
||||
from: `signup@${process.env.WEB_ADDRESS}`, //WARNING: google overwrites this
|
||||
to: email,
|
||||
subject: 'Email Validation',
|
||||
text: msg
|
||||
});
|
||||
}
|
||||
catch(e) {
|
||||
return `failed to send mail ${e}`;
|
||||
}
|
||||
|
||||
if (info.accepted[0] != email) {
|
||||
return 'validation email failed to send';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { accounts } = require('../database/models');
|
||||
|
||||
const route = async (req, res) => {
|
||||
if (!req.session.account.id) {
|
||||
return res.status(500).send('missing account data');
|
||||
}
|
||||
|
||||
//generate the password hash
|
||||
const salt = await bcrypt.genSalt(11);
|
||||
const hash = await bcrypt.hash(req.fields.password, salt);
|
||||
|
||||
//update the account
|
||||
await accounts.update({
|
||||
contact: req.fields.contact,
|
||||
hash: hash
|
||||
}, {
|
||||
where: {
|
||||
id: req.session.account.id
|
||||
}
|
||||
});
|
||||
|
||||
//update the reference
|
||||
req.session.account = (await accounts.findOne({
|
||||
where: {
|
||||
id: req.session.account.id
|
||||
}
|
||||
})).dataValues;
|
||||
|
||||
//respond with an OK
|
||||
res.status(200).send('Information updated');
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,40 +0,0 @@
|
||||
const { pendingSignups, accounts } = require('../database/models');
|
||||
|
||||
//api/accounts/validation
|
||||
const route = async (req, res) => {
|
||||
//get the existing pending signup
|
||||
const info = await pendingSignups.findOne({
|
||||
where: {
|
||||
username: req.query.username
|
||||
}
|
||||
});
|
||||
|
||||
//check the given info
|
||||
if (!info) {
|
||||
return res.status(401).send('validation failed');
|
||||
}
|
||||
|
||||
if (info.token != req.query.token) {
|
||||
return res.status(401).send('tokens do not match');
|
||||
}
|
||||
|
||||
//delete the pending signup
|
||||
pendingSignups.destroy({
|
||||
where: {
|
||||
username: req.query.username
|
||||
}
|
||||
});
|
||||
|
||||
//move data to the accounts table
|
||||
accounts.create({
|
||||
email: info.email,
|
||||
username: info.username,
|
||||
hash: info.hash,
|
||||
contact: info.contact
|
||||
});
|
||||
|
||||
//finally
|
||||
res.status(200).send('Validation succeeded!');
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,40 +0,0 @@
|
||||
const { Op } = require('sequelize');
|
||||
const { bannedEmails, accounts } = require('../database/models');
|
||||
|
||||
const route = async (req, res) => {
|
||||
//fetch the account based on the email or username
|
||||
const account = await accounts.findOne({
|
||||
attrubutes: ['username', 'email'],
|
||||
where: {
|
||||
[Op.or]: {
|
||||
username: {
|
||||
[Op.eq]: req.fields.username,
|
||||
},
|
||||
email: {
|
||||
[Op.eq]: req.fields.email
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//just in case
|
||||
if (account && account.privilege == 'administrator') {
|
||||
return res.status(401).send('Couldn\'t ban an admin');
|
||||
}
|
||||
|
||||
//need either an email or an account
|
||||
if (!account && !req.fields.email) {
|
||||
return res.status(401).send('Couldn\'t determine the ban info');
|
||||
}
|
||||
|
||||
//apply the ban
|
||||
await bannedEmails.upsert({
|
||||
email: (account || req.fields).email,
|
||||
reason: req.fields.reason ? req.fields.reason : null,
|
||||
expiry: req.fields.expiry ? new Date(Date.parse(req.fields.expiry)) : null
|
||||
});
|
||||
|
||||
return res.status(200).send(`Email ${(account || req.fields).email} banned (username ${account ? account.username : 'not found'})`);
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,34 +0,0 @@
|
||||
const { Op } = require('sequelize');
|
||||
const { bannedEmails, accounts } = require('../database/models');
|
||||
|
||||
const route = async (req, res) => {
|
||||
//merge the banned accounts with the account data, if any
|
||||
const data = await bannedEmails.findAll()
|
||||
.then(bans => bans.map(async ban => {
|
||||
//find a matching account
|
||||
const account = await accounts.findOne({
|
||||
attrubutes: ['username', 'privilege'],
|
||||
where: {
|
||||
email: {
|
||||
[Op.eq]: ban.email
|
||||
}
|
||||
}
|
||||
}) || {};
|
||||
|
||||
//merge the data and return (becomes a promise)
|
||||
return {
|
||||
username: account.username,
|
||||
email: ban.email,
|
||||
privilege: account.privilege,
|
||||
expiry: ban.expiry,
|
||||
reason: ban.reason
|
||||
};
|
||||
}))
|
||||
.then(promises => Promise.all(promises)) //resolve promises
|
||||
.catch(e => console.error(e))
|
||||
;
|
||||
|
||||
return res.status(200).json(data);
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,29 +0,0 @@
|
||||
//DOCS: this whole file is just a big bugfix
|
||||
//DOCS: ensure that there is at least one administration account
|
||||
const bcrypt = require('bcryptjs');
|
||||
const sequelize = require('../database');
|
||||
const { accounts } = require('../database/models');
|
||||
|
||||
const defaultAdminAccount = async () => {
|
||||
await sequelize.sync(); //this whole file is just one big BUGFIX
|
||||
|
||||
const admin = await accounts.findOne({
|
||||
where: {
|
||||
privilege: 'administrator'
|
||||
}
|
||||
});
|
||||
|
||||
if (admin == null) {
|
||||
await accounts.create({
|
||||
privilege: 'administrator',
|
||||
email: `admin@${process.env.WEB_ADDRESS}`,
|
||||
username: `admin`,
|
||||
hash: await bcrypt.hash('password', await bcrypt.genSalt(11))
|
||||
});
|
||||
|
||||
//TODO: (1) Replace this default admin account password with UUID
|
||||
console.log(`Created default admin account (email: admin@${process.env.WEB_ADDRESS}; password: password)`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = defaultAdminAccount;
|
||||
@@ -1,19 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
//middleware
|
||||
router.use((req, res, next) => {
|
||||
//make sure the account is an admin
|
||||
if (req.cookies['admin'] !== process.env.SESSION_ADMIN) { //TODO: Eew not good.
|
||||
return res.status(401).send('invalid admin status');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
//basic account ban management
|
||||
router.get('/banned', require('./banned'));
|
||||
router.post('/ban', require('./ban'));
|
||||
router.post('/unban', require('./unban'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,46 +0,0 @@
|
||||
const Sequelize = require('sequelize');
|
||||
const Op = Sequelize.Op;
|
||||
const { bannedEmails, accounts } = require('../database/models');
|
||||
var cron = require('node-cron');
|
||||
|
||||
const route = async (req, res) => {
|
||||
console.log(req.fields.entry)
|
||||
//get the account, if one is found
|
||||
const account = await accounts.findOne({
|
||||
where: {
|
||||
[Op.or]: {
|
||||
email: {
|
||||
[Op.eq]: req.fields.entry
|
||||
},
|
||||
username: {
|
||||
[Op.eq]: req.fields.entry
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
//accept either email or username
|
||||
const affectedRows = await bannedEmails.destroy({
|
||||
where: {
|
||||
email: {
|
||||
[Op.eq]: account?.email || req.fields.entry || ''
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(200).send(`${affectedRows} emails unbanned`);
|
||||
};
|
||||
|
||||
//delete any expired bans
|
||||
cron.schedule('0 * * * *', () => {
|
||||
bannedEmails.destroy({
|
||||
where: {
|
||||
expiry: {
|
||||
[Op.lt]: Sequelize.fn('NOW'),
|
||||
[Op.not]: null
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = route;
|
||||
@@ -1,7 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
//reserve the name on the chat server (then get out of the way)
|
||||
router.post('/reserve', require('./reserve'));
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,32 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
|
||||
const route = async (req, res) => {
|
||||
if (!req.session.account) {
|
||||
return status(403).send('No account detected');
|
||||
}
|
||||
|
||||
//build the fake form data object
|
||||
let form = new FormData();
|
||||
form.append('username', req.session?.account?.username);
|
||||
form.append('key', process.env.CHAT_KEY);
|
||||
|
||||
try {
|
||||
//reserve the UUID with the chat server (hop 1)
|
||||
const result = await fetch(`http${process.env.PRODUCTION ? 's' : ''}://${process.env.CHAT_URI}/reserve`, { method: 'POST', body: form });
|
||||
|
||||
if (result.status == 200) {
|
||||
const json = await result.json();
|
||||
res.cookie('pseudonym', json.pseudonym);
|
||||
res.status(200).send({ ok: true });
|
||||
} else {
|
||||
throw await result.text();
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(`Chat server error: ${e}`);
|
||||
res.cookie('pseudonym', '.null');
|
||||
res.status(200).send({ ok: false, error: `Chat server error ${e}` });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = route;
|
||||
@@ -4,7 +4,7 @@ const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME
|
||||
host: process.env.DB_HOSTNAME,
|
||||
dialect: 'mariadb',
|
||||
timezone: process.env.DB_TIMEZONE,
|
||||
// logging: false
|
||||
logging: false
|
||||
});
|
||||
|
||||
sequelize.sync();
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
const Sequelize = require('sequelize');
|
||||
const sequelize = require('..');
|
||||
|
||||
module.exports = sequelize.define('accounts', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER(11),
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
privilege: {
|
||||
type: Sequelize.ENUM,
|
||||
values: ['administrator', 'moderator', 'alpha', 'beta', 'gamma', 'normal'],
|
||||
defaultValue: 'normal'
|
||||
},
|
||||
|
||||
email: {
|
||||
type: 'varchar(320)',
|
||||
unique: true
|
||||
},
|
||||
|
||||
username: {
|
||||
type: 'varchar(320)',
|
||||
unique: true
|
||||
},
|
||||
|
||||
hash: 'varchar(100)', //for passwords
|
||||
|
||||
contact: {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
deletion: {
|
||||
type: 'DATETIME',
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
}
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
const Sequelize = require('sequelize');
|
||||
const sequelize = require('..');
|
||||
|
||||
module.exports = sequelize.define('bannedEmails', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER(11),
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
unique: true
|
||||
},
|
||||
|
||||
email: {
|
||||
type: 'varchar(320)',
|
||||
unique: true
|
||||
},
|
||||
|
||||
reason: Sequelize.TEXT,
|
||||
|
||||
expiry: {
|
||||
type: 'DATETIME',
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
module.exports = {
|
||||
bannedEmails: require('./banned-emails'),
|
||||
accounts: require('./accounts'),
|
||||
pendingSignups: require('./pending-signups')
|
||||
//TODO: models
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
const Sequelize = require('sequelize');
|
||||
const sequelize = require('..');
|
||||
|
||||
module.exports = sequelize.define('pendingSignups', {
|
||||
email: {
|
||||
type: 'varchar(320)',
|
||||
unique: true
|
||||
},
|
||||
|
||||
username: {
|
||||
type: 'varchar(320)',
|
||||
unique: true
|
||||
},
|
||||
|
||||
hash: 'varchar(100)', //for passwords
|
||||
|
||||
contact: {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
|
||||
token: Sequelize.INTEGER(11)
|
||||
});
|
||||
+6
-31
@@ -1,46 +1,21 @@
|
||||
//environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
//libraries
|
||||
const path = require('path');
|
||||
|
||||
//create the server
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const server = require('http').Server(app);
|
||||
const bodyParser = require('body-parser');
|
||||
|
||||
//libraries used here
|
||||
const path = require('path');
|
||||
const formidable = require('express-formidable');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const SequelizeStore = require("connect-session-sequelize")(session.Store);
|
||||
//config
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//database connection
|
||||
const database = require('./database');
|
||||
|
||||
//setup the app middleware
|
||||
app.use(formidable());
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET,
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
store: new SequelizeStore({
|
||||
db: database
|
||||
})
|
||||
}));
|
||||
|
||||
//invoke all models
|
||||
const models = require('./database/models');
|
||||
|
||||
//account management
|
||||
app.use('/api/accounts', require('./accounts'));
|
||||
|
||||
//chat management
|
||||
app.use('/api/chat', require('./chat'));
|
||||
|
||||
//administration
|
||||
app.use('/api/admin', require('./admin'));
|
||||
require('./admin/bookkeeper')(); //BUGFIX
|
||||
|
||||
//send static files
|
||||
app.use('/', express.static(path.resolve(__dirname, '..', 'public')));
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#This file should be used for altering the database in production - make sure it works!
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#TODO: move this into configure-script.js
|
||||
|
||||
#This file only needs to be run once, during initial development setup
|
||||
#This file isnt needed for actual deployment
|
||||
|
||||
|
||||
+3
-4
@@ -51,10 +51,9 @@ module.exports = ({ production, analyzer }) => {
|
||||
new DefinePlugin({
|
||||
'process.env': {
|
||||
'PRODUCTION': production,
|
||||
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"http://dev-news.eggtrainer.com:3100/news"',
|
||||
/* TODO: (1) NEWS_KEY needs to be set in the server, and auth'd via admin accounts, NOT embedded in the client */
|
||||
'NEWS_KEY': production ? `"${process.env.NEWS_KEY}"` : '"key"',
|
||||
'CHAT_URI': production ? `"${process.env.NEWS_URI}"` : '"http://dev-chat.eggtrainer.com:3200/chat"',
|
||||
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.eggtrainer.com/news"',
|
||||
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.eggtrainer.com/auth"',
|
||||
// 'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.eggtrainer.com/chat"',
|
||||
}
|
||||
}),
|
||||
new CleanWebpackPlugin({
|
||||
|
||||
Reference in New Issue
Block a user