@@ -1,20 +1,9 @@
|
|||||||
WEB_PROTOCOL=http
|
|
||||||
WEB_ADDRESS=localhost
|
|
||||||
WEB_PORT=3000
|
WEB_PORT=3000
|
||||||
|
|
||||||
MAIL_SMTP=smtp.example.com
|
DB_HOSTNAME=database
|
||||||
MAIL_USERNAME=foobar@example.com
|
|
||||||
MAIL_PASSWORD=foobar
|
|
||||||
MAIL_PHYSICAL=42 Placeholder Ave, Placeholder, 0000, USA
|
|
||||||
|
|
||||||
DB_HOSTNAME=127.0.0.1
|
|
||||||
DB_DATABASE=template
|
DB_DATABASE=template
|
||||||
DB_USERNAME=template
|
DB_USERNAME=template
|
||||||
DB_PASSWORD=pikachu
|
DB_PASSWORD=pikachu
|
||||||
DB_TIMEZONE=Australia/Sydney
|
DB_TIMEZONE=Australia/Sydney
|
||||||
|
|
||||||
CHAT_URI=http://example.com:3200/chat
|
SECRET_ACCESS=access
|
||||||
CHAT_KEY=chattychattybangbang
|
|
||||||
|
|
||||||
SESSION_SECRET=secret
|
|
||||||
SESSION_ADMIN=adminsecret
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//TODO: update this README
|
||||||
|
|
||||||
# MERN-template
|
# MERN-template
|
||||||
|
|
||||||
A website template using the MERN stack.
|
A website template using the MERN stack.
|
||||||
|
|||||||
+3
-4
@@ -1,16 +1,15 @@
|
|||||||
//polyfills
|
//polyfills
|
||||||
import 'core-js/stable';
|
|
||||||
import 'regenerator-runtime/runtime';
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { CookiesProvider } from 'react-cookie';
|
|
||||||
|
|
||||||
import App from './components/app';
|
import App from './components/app';
|
||||||
|
import TokenProvider from './components/utilities/token-provider';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<CookiesProvider>
|
<TokenProvider>
|
||||||
<App />
|
<App />
|
||||||
</CookiesProvider>,
|
</TokenProvider>,
|
||||||
document.querySelector('#root')
|
document.querySelector('#root')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
//react
|
//react
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Switch } from 'react-router-dom';
|
import { BrowserRouter, Switch } from 'react-router-dom';
|
||||||
import { useCookies } from 'react-cookie';
|
|
||||||
|
|
||||||
//library components
|
//library components
|
||||||
import LazyRoute from './lazy-route';
|
import LazyRoute from './lazy-route';
|
||||||
@@ -15,24 +14,6 @@ import Header from './panels/header.jsx';
|
|||||||
import Footer from './panels/footer.jsx';
|
import Footer from './panels/footer.jsx';
|
||||||
|
|
||||||
const App = props => {
|
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
|
//default render
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -43,7 +24,6 @@ const App = props => {
|
|||||||
<LazyRoute path='/signup' component={() => import('./pages/signup')} />
|
<LazyRoute path='/signup' component={() => import('./pages/signup')} />
|
||||||
<LazyRoute path='/login' component={() => import('./pages/login')} />
|
<LazyRoute path='/login' component={() => import('./pages/login')} />
|
||||||
<LazyRoute path='/account' component={() => import('./pages/account')} />
|
<LazyRoute path='/account' component={() => import('./pages/account')} />
|
||||||
<LazyRoute path='/chat' component={() => import('./pages/chat')} />
|
|
||||||
|
|
||||||
<LazyRoute path='/admin' component={() => import('./pages/admin')} />
|
<LazyRoute path='/admin' component={() => import('./pages/admin')} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,66 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useContext, useRef } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useCookies } from 'react-cookie';
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
import DeleteAccount from '../panels/delete-account';
|
import DeleteAccount from '../panels/delete-account';
|
||||||
|
|
||||||
const Account = props => {
|
const Account = props => {
|
||||||
const [cookies, setCookie] = useCookies();
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
//check for logged in redirect
|
//misplaced?
|
||||||
if (!cookies['loggedin']) {
|
if (!authTokens.accessToken) {
|
||||||
return <Redirect to='/' />;
|
return <Redirect to='/' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
//refs
|
//refs
|
||||||
let contactElement, passwordElement, retypeElement;
|
const passwordRef = useRef();
|
||||||
|
const retypeRef = useRef();
|
||||||
|
const contactRef = useRef();
|
||||||
|
|
||||||
//once before render
|
//grab the user's info
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/accounts')
|
authTokens.tokenFetch(`${process.env.AUTH_URI}/account`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
})
|
||||||
.then(blob => blob.json())
|
.then(blob => blob.json())
|
||||||
.then(json => {
|
.then(json => contactRef.current.checked = json.contact)
|
||||||
contactElement.checked = json.contact;
|
|
||||||
})
|
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
;
|
;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
//render the thing
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Account</h1>
|
<h1 className='centered'>Account</h1>
|
||||||
<form className='constricted' onSubmit={async evt => {
|
<form className='constricted' onSubmit={async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
await update(contactElement.checked, passwordElement.value, retypeElement.value);
|
const [err, result] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
|
||||||
passwordElement.value = retypeElement.value = '';
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passwordRef.current.value = retypeRef.current.value = '';
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
|
||||||
<label htmlFor='contact'>Allow Promotional Emails:</label>
|
|
||||||
<input type='checkbox' name='contact' ref={e => contactElement = e} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='password'>Change Password:</label>
|
<label htmlFor='password'>Change Password:</label>
|
||||||
<input type='password' name='password' ref={e => passwordElement = e} />
|
<input type='password' name='password' ref={passwordRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='retype'>Retype Password:</label>
|
<label htmlFor='retype'>Retype Password:</label>
|
||||||
<input type='password' name='retype' ref={e => retypeElement = e} />
|
<input type='password' name='retype' ref={retypeRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor='contact'>Allow Promotional Emails:</label>
|
||||||
|
<input type='checkbox' name='contact' ref={contactRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -59,26 +72,31 @@ const Account = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = async (contact, password, retype) => {
|
const update = async (password, retype, contact, tokenFetch) => {
|
||||||
if (password != retype) {
|
if (password != retype) {
|
||||||
alert('Passwords do not match');
|
return ['Passwords do not match'];
|
||||||
}
|
}
|
||||||
|
|
||||||
//generate a new formdata payload
|
if (password && password.length < 8) {
|
||||||
let formData = new FormData();
|
return ['Password is too short'];
|
||||||
|
|
||||||
formData.append('contact', contact);
|
|
||||||
|
|
||||||
if (password) {
|
|
||||||
formData.append('password', password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await fetch('/api/accounts', { method: 'PATCH', body: formData });
|
const result = await tokenFetch(`${process.env.AUTH_URI}/update`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: password ? password : null,
|
||||||
|
contact
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (result.ok) {
|
if (!result.ok) {
|
||||||
alert(await result.text());
|
return [`${await result.status}: ${await result.text()}`];
|
||||||
} else {
|
} else {
|
||||||
alert(await result.text());
|
return [null];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useCookies } from 'react-cookie';
|
|
||||||
|
|
||||||
//import BannedEmails from '../panels/banned-emails';
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
import NewsPublisher from '../panels/news-publisher';
|
import NewsPublisher from '../panels/news-publisher';
|
||||||
import NewsEditor from '../panels/news-editor';
|
import NewsEditor from '../panels/news-editor';
|
||||||
|
|
||||||
const Admin = props => {
|
const Admin = props => {
|
||||||
const [cookies, setCookie] = useCookies();
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
//check for logged in redirect
|
//misplaced? (admin only)
|
||||||
if (!cookies['admin']) {
|
if (!authTokens.accessToken || !authTokens.getPayload().privilege == 'administrator') {
|
||||||
return <Redirect to='/' />;
|
return <Redirect to='/' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Administration</h1>
|
<h1 className='centered'>Administration</h1>
|
||||||
<NewsPublisher uri={process.env.NEWS_URI} newsKey={process.env.NEWS_KEY} />
|
<NewsPublisher />
|
||||||
<NewsEditor uri={process.env.NEWS_URI} newsKey={process.env.NEWS_KEY} />
|
<NewsEditor />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,46 +1,50 @@
|
|||||||
import React from 'react';
|
import React, { useContext, useRef } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useCookies } from 'react-cookie';
|
|
||||||
|
|
||||||
//utilities
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
const validateEmail = require('../../../common/utilities/validate-email.js');
|
|
||||||
|
|
||||||
const LogIn = props => {
|
const LogIn = props => {
|
||||||
const [cookies, setCookie] = useCookies();
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
//check for logged in redirect
|
//misplaced?
|
||||||
if (cookies['loggedin']) {
|
if (authTokens.accessToken) {
|
||||||
return <Redirect to='/' />;
|
return <Redirect to='/' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
//refs
|
//refs
|
||||||
let emailElement, passwordElement;
|
const emailRef = useRef();
|
||||||
|
const passwordRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Login</h1>
|
<h1 className='centered'>Login</h1>
|
||||||
<form className='constricted' onSubmit={
|
<form className='constricted' onSubmit={
|
||||||
evt => {
|
async evt => {
|
||||||
|
//on submit
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
handleSubmit(emailElement.value, passwordElement.value)
|
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
|
||||||
.then(([res, ok]) => {
|
if (err) {
|
||||||
alert(res);
|
alert(err);
|
||||||
if (ok) {
|
}
|
||||||
window.location.reload(true); //BUFGIX: force reload of the header element
|
|
||||||
}
|
//save auth tokens and redirect
|
||||||
})
|
if (newTokens) {
|
||||||
.catch(e => console.error(e))
|
authTokens.setAccessToken(newTokens.accessToken);
|
||||||
;
|
authTokens.setRefreshToken(newTokens.refreshToken);
|
||||||
|
|
||||||
|
props.history.push('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email">Email:</label>
|
<label htmlFor="email">Email:</label>
|
||||||
<input type="email" name="email" ref={e => emailElement = e} />
|
<input type="email" name="email" ref={emailRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password">Password:</label>
|
<label htmlFor="password">Password:</label>
|
||||||
<input type="password" name="password" ref={e => passwordElement = e} />
|
<input type="password" name="password" ref={passwordRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Login</button>
|
<button type='submit'>Login</button>
|
||||||
@@ -49,23 +53,33 @@ const LogIn = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
//DOCS: returns two values: response and OK
|
//DOCS: returns two values: err and authTokens
|
||||||
const handleSubmit = async (email, password) => {
|
const handleSubmit = async (email, password) => {
|
||||||
email = email.trim();
|
email = email.trim(); //TODO: validate email on login
|
||||||
|
|
||||||
//generate a new formdata payload
|
//send to the auth server
|
||||||
let formData = new FormData();
|
const result = await fetch(`${process.env.AUTH_URI}/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
formData.append('email', email);
|
//handle errors
|
||||||
formData.append('password', password);
|
if (!result.ok) {
|
||||||
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
const result = await fetch('/api/accounts/login', { method: 'POST', body: formData });
|
console.error(err);
|
||||||
|
return [err, false];
|
||||||
if (result.ok) {
|
|
||||||
return [await result.text(), true];
|
|
||||||
} else {
|
|
||||||
return [await result.text(), false];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//return the new auth tokens
|
||||||
|
const newTokens = await result.json();
|
||||||
|
return [null, newTokens];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LogIn;
|
export default LogIn;
|
||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
const NotFound = props => {
|
const NotFound = props => {
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='middle centered'>Not Found</h1>
|
<h1 className='middle centered'>Page Not Found</h1>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +1,68 @@
|
|||||||
import React from 'react';
|
import React, { useContext, useRef } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useCookies } from 'react-cookie';
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
//utilities
|
//utilities
|
||||||
const validateEmail = require('../../../common/utilities/validate-email.js');
|
const validateEmail = require('../../../common/utilities/validate-email.js');
|
||||||
const validateUsername = require('../../../common/utilities/validate-username.js');
|
const validateUsername = require('../../../common/utilities/validate-username.js');
|
||||||
|
|
||||||
const SignUp = props => {
|
const SignUp = props => {
|
||||||
const [cookies, setCookie] = useCookies();
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
//check for logged in redirect
|
//misplaced?
|
||||||
if (cookies['loggedin']) {
|
if (authTokens.accessToken) {
|
||||||
return <Redirect to='/' />;
|
return <Redirect to='/' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
//refs
|
//refs
|
||||||
let emailElement, usernameElement, passwordElement, retypeElement, contactElement;
|
const emailRef = useRef();
|
||||||
|
const usernameRef = useRef();
|
||||||
|
const passwordRef = useRef();
|
||||||
|
const retypeRef = useRef();
|
||||||
|
const contactRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Signup</h1>
|
<h1 className='centered'>Signup</h1>
|
||||||
<form className='constricted' onSubmit={
|
<form className='constricted' onSubmit={
|
||||||
evt => {
|
async evt => { //on submit
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
handleSubmit(emailElement.value, usernameElement.value, passwordElement.value, retypeElement.value, contactElement.checked)
|
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
|
||||||
.then(res => res ? alert(res) : null)
|
if (result) {
|
||||||
.then(() => emailElement.value = usernameElement.value = passwordElement.value = retypeElement.value = '') //clear input
|
alert(result);
|
||||||
.then(() => contactElement.checked = false)
|
}
|
||||||
.then(() => props.history.push('/'))
|
|
||||||
.catch(e => console.error(e))
|
//redirect
|
||||||
;
|
if (redirect) {
|
||||||
|
props.history.push('/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='email'>Email:</label>
|
<label htmlFor='email'>Email:</label>
|
||||||
<input type='email' name='email' ref={e => emailElement = e} />
|
<input type='email' name='email' ref={emailRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='username'>Username:</label>
|
<label htmlFor='username'>Username:</label>
|
||||||
<input type='text' name='username' ref={e => usernameElement = e} />
|
<input type='text' name='username' ref={usernameRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='password'>Password:</label>
|
<label htmlFor='password'>Password:</label>
|
||||||
<input type='password' name='password' ref={e => passwordElement = e} />
|
<input type='password' name='password' ref={passwordRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='retype'>Retype Password:</label>
|
<label htmlFor='retype'>Retype Password:</label>
|
||||||
<input type='password' name='retype' ref={e => retypeElement = e} />
|
<input type='password' name='retype' ref={retypeRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='contact'>Allow Promotional Emails:</label>
|
<label htmlFor='contact'>Allow Promotional Emails:</label>
|
||||||
<input type='checkbox' name='contact' ref={e => contactElement = e} />
|
<input type='checkbox' name='contact' ref={contactRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Signup</button>
|
<button type='submit'>Signup</button>
|
||||||
@@ -73,21 +81,28 @@ const handleSubmit = async (email, username, password, retype, contact) => {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
//generate a new formdata payload
|
//send to the auth server
|
||||||
let formData = new FormData();
|
const result = await fetch(`${process.env.AUTH_URI}/signup`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
contact
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
formData.append('email', email);
|
if (!result.ok) {
|
||||||
formData.append('username', username);
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
formData.append('password', password);
|
console.error(err);
|
||||||
formData.append('contact', contact)
|
return [err, false];
|
||||||
|
|
||||||
const result = await fetch('/api/accounts/signup', { method: 'POST', body: formData });
|
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
return result.text();
|
|
||||||
} else {
|
|
||||||
return result.text();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [await result.text(), true];
|
||||||
};
|
};
|
||||||
|
|
||||||
//returns an error message, or null on success
|
//returns an error message, or null on success
|
||||||
|
|||||||
@@ -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 +1,73 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useContext, useRef } from 'react';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
//DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed
|
//DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed
|
||||||
const DeleteAccount = props => {
|
const DeleteAccount = props => {
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const passwordRef = useRef();
|
||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
|
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
let passwordElement;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={props.className} onSubmit={async evt => {
|
<form className={props.className} onSubmit={async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const password = passwordElement.value;
|
const [err] = await handleSubmit(passwordRef.current.value, authTokens);
|
||||||
passwordElement.value = '';
|
if (err) {
|
||||||
await handleSubmit(password);
|
alert(err);
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password">Password:</label>
|
<label htmlFor="password">Password:</label>
|
||||||
<input type="password" name="password" ref={e => passwordElement = e} />
|
<input type="password" name="password" ref={passwordRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Delete Account</button>
|
<button type='submit'>Delete Account</button>
|
||||||
<button type='cancel' onClick={() => { passwordElement.value = ''; setOpen(false); }}>Cancel</button>
|
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (password) => {
|
const handleSubmit = async (password, authTokens) => {
|
||||||
//generate a new formdata payload
|
//schedule a deletion
|
||||||
let formData = new FormData();
|
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/deletion`, {
|
||||||
|
method: 'DELETE',
|
||||||
formData.append('password', password);
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
const result = await fetch('/api/accounts/deletion', { method: 'DELETE', body: formData });
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
alert(await result.text());
|
return [`${await result.status}: ${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))
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//force a logout
|
||||||
|
const result2 = await authTokens.tokenFetch(`${process.env.AUTH_URI}/logout`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: authTokens.refreshToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result2.ok) {
|
||||||
|
return [`${await result2.status}: ${await result2.text()}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
authTokens.setAccessToken('');
|
||||||
|
authTokens.setRefreshToken('');
|
||||||
|
|
||||||
|
return [null];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteAccount;
|
export default DeleteAccount;
|
||||||
@@ -1,48 +1,57 @@
|
|||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useCookies } from 'react-cookie';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
const Visitor = () => {
|
const Visitor = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a href='/signup'>Sign Up</a>
|
<Link to='/signup'>Sign Up</Link>
|
||||||
<em> - </em>
|
<em> - </em>
|
||||||
<a href='/login'>Log In</a>
|
<Link to='/login'>Log In</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Member = () => {
|
const Member = () => {
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a href='/account'>Account</a>
|
<Link to='/account'>Account</Link>
|
||||||
<em> - </em>
|
<em> - </em>
|
||||||
<a href='/' onClick={logout}>Log out</a>
|
{ /* Logout button logs you out of the server too */ }
|
||||||
|
<Link to='/' onClick={async () => {
|
||||||
|
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/logout`, { //NOTE: this gets overwritten as a bugfix
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: authTokens.refreshToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
//any problems?
|
||||||
|
if (!result.ok) {
|
||||||
|
console.error(await result.text());
|
||||||
|
} else {
|
||||||
|
authTokens.setAccessToken('');
|
||||||
|
authTokens.setRefreshToken('');
|
||||||
|
}
|
||||||
|
}}>Log out</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
await fetch('/api/accounts/logout', { method: 'POST' })
|
|
||||||
.catch(e => console.error(e))
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [cookies, setCookie] = useCookies(['loggedin']);
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
let Options;
|
|
||||||
|
|
||||||
//check for logged in/out status
|
|
||||||
if (cookies['loggedin']) {
|
|
||||||
Options = Member;
|
|
||||||
} else {
|
|
||||||
Options = Visitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<h1><a href='/'>MERN Template</a></h1>
|
<h1><Link to='/'>MERN Template</Link></h1>
|
||||||
<Options />
|
{ authTokens.accessToken ? <Member /> : <Visitor /> }
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,39 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||||
import Select from 'react-dropdown-select';
|
import Select from 'react-dropdown-select';
|
||||||
|
|
||||||
//DOCS: props.uri is the address of a live news-server
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
//DOCS: props.newsKey is the key of the live news-server
|
|
||||||
const NewsEditor = props => {
|
const NewsEditor = props => {
|
||||||
let titleElement, authorElement, bodyElement;
|
//context
|
||||||
const [articles, setArticles] = useState(null);
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//refs
|
||||||
|
const titleRef = useRef();
|
||||||
|
const authorRef = useRef();
|
||||||
|
const bodyRef = useRef();
|
||||||
|
|
||||||
|
//state
|
||||||
|
const [articles, setArticles] = useState([]);
|
||||||
const [index, setIndex] = useState(null);
|
const [index, setIndex] = useState(null);
|
||||||
|
|
||||||
if (!articles) {
|
//run once
|
||||||
fetch(`${props.uri}/titles?limit=999`, {
|
useEffect(async () => {
|
||||||
|
const result = await fetch(`${process.env.NEWS_URI}/metadata?limit=999`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.then(a => {
|
|
||||||
if (!a.ok) {
|
if (!result.ok) {
|
||||||
throw `Network error ${a.status}: ${a.statusText} ${a.url}`;
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
}
|
console.log(err);
|
||||||
return a.json();
|
alert(err);
|
||||||
})
|
} else {
|
||||||
.then(a => setArticles(a))
|
setArticles(await result.json());
|
||||||
.catch(e => console.error(e))
|
}
|
||||||
;
|
}, []);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -33,28 +41,56 @@ const NewsEditor = props => {
|
|||||||
<div>
|
<div>
|
||||||
<label htmlFor='article'>Article: </label>
|
<label htmlFor='article'>Article: </label>
|
||||||
<Select
|
<Select
|
||||||
options={(articles || []).map(article => { return { label: article.title, value: article.index }; })}
|
options={(articles).map(article => { return { label: article.title, value: article.index }; })}
|
||||||
onChange={values => setIndex(fetchSelection(values[0].value, titleElement, authorElement, bodyElement, props.uri))}
|
onChange={async values => {
|
||||||
|
//fetch this article
|
||||||
|
const index = values[0].value;
|
||||||
|
|
||||||
|
const result = await fetch(`${process.env.NEWS_URI}/archive/${index}`, {
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
|
console.log(err);
|
||||||
|
alert(err);
|
||||||
|
} else {
|
||||||
|
const article = await result.json();
|
||||||
|
titleRef.current.value = article.title;
|
||||||
|
authorRef.current.value = article.author;
|
||||||
|
bodyRef.current.value = article.body;
|
||||||
|
setIndex(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={async e => {
|
|
||||||
e.preventDefault();
|
<form onSubmit={async evt => {
|
||||||
await handleSubmit(index, titleElement.value, authorElement.value, bodyElement.value, props.uri, props.newsKey);
|
//onSubmit
|
||||||
titleElement.value = authorElement.value = bodyElement.value = '';
|
evt.preventDefault();
|
||||||
|
const [err] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, index, authTokens.tokenFetch);
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
} else {
|
||||||
|
titleRef.current.value = authorRef.current.value = bodyRef.current.value = '';
|
||||||
|
alert(`Edited as article index ${index}`);
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='title'>Title: </label>
|
<label htmlFor='title'>Title: </label>
|
||||||
<input type='text' name='title' ref={ e => titleElement = e } />
|
<input type='text' name='title' ref={titleRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='author'>Author: </label>
|
<label htmlFor='author'>Author: </label>
|
||||||
<input type='text' name='author' ref={ e => authorElement = e } />
|
<input type='text' name='author' ref={authorRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='body'>Body: </label>
|
<label htmlFor='body'>Body: </label>
|
||||||
<textarea name='body' rows='10' cols='150' ref={ e => bodyElement = e } />
|
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Update</button>
|
<button type='submit'>Update</button>
|
||||||
@@ -63,54 +99,30 @@ const NewsEditor = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSelection = (index, titleElement, authorElement, bodyElement, uri) => {
|
const handleSubmit = async (title, author, body, index, tokenFetch) => {
|
||||||
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();
|
title = title.trim();
|
||||||
author = author.trim();
|
author = author.trim();
|
||||||
body = body.trim();
|
body = body.trim();
|
||||||
uri = uri.trim();
|
|
||||||
newsKey = newsKey.trim();
|
|
||||||
|
|
||||||
//fetch POST json data
|
//fetch POST json data
|
||||||
const raw = await fetch(
|
const result = await tokenFetch(`${process.env.NEWS_URI}/${index}`, {
|
||||||
`${uri}/${index}`,
|
method: 'PATCH',
|
||||||
{
|
headers: {
|
||||||
method: 'PATCH',
|
'Content-Type': 'application/json',
|
||||||
headers: {
|
'Access-Control-Allow-Origin': '*'
|
||||||
'Content-Type': 'application/json',
|
},
|
||||||
'Access-Control-Allow-Origin': '*'
|
body: JSON.stringify({
|
||||||
},
|
title,
|
||||||
body: JSON.stringify({ title: title, author: author, body: body, key: newsKey })
|
author,
|
||||||
}
|
body
|
||||||
);
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (raw.ok) {
|
if (!result.ok) {
|
||||||
const result = await raw.json();
|
return [`${result.status}: ${await result.text()}`];
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
alert(`Updated article index ${index}`);
|
|
||||||
} else {
|
|
||||||
alert(result.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert(raw.statusText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [null];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewsEditor;
|
export default NewsEditor;
|
||||||
@@ -13,12 +13,7 @@ const NewsFeed = props => {
|
|||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(a => {
|
.then(blob => blob.json())
|
||||||
if (!a.ok) {
|
|
||||||
throw `Network error ${a.status}: ${a.statusText} ${a.url}`;
|
|
||||||
}
|
|
||||||
return a.json();
|
|
||||||
})
|
|
||||||
.then(a => setArticles(a))
|
.then(a => setArticles(a))
|
||||||
.catch(e => console.error(e))
|
.catch(e => console.error(e))
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -1,31 +1,43 @@
|
|||||||
import React from 'react';
|
import React, { useContext, useRef } from 'react';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
//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 => {
|
const NewsPublisher = props => {
|
||||||
let titleElement, authorElement, bodyElement;
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//refs
|
||||||
|
const titleRef = useRef();
|
||||||
|
const authorRef = useRef();
|
||||||
|
const bodyRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className='centered'>News Publisher</h2>
|
<h2 className='centered'>News Publisher</h2>
|
||||||
<form onSubmit={async e => {
|
<form onSubmit={async evt => {
|
||||||
e.preventDefault();
|
//on submit
|
||||||
await handleSubmit(titleElement.value, authorElement.value, bodyElement.value, props.uri, props.newsKey);
|
evt.preventDefault();
|
||||||
titleElement.value = authorElement.value = bodyElement.value = '';
|
const [err, index] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, authTokens.tokenFetch);
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
} else {
|
||||||
|
titleRef.current.value = authorRef.current.value = bodyRef.current.value = '';
|
||||||
|
alert(`Published as article index ${index}`);
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='title'>Title: </label>
|
<label htmlFor='title'>Title: </label>
|
||||||
<input type='text' name='title' ref={ e => titleElement = e } />
|
<input type='text' name='title' ref={titleRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='author'>Author: </label>
|
<label htmlFor='author'>Author: </label>
|
||||||
<input type='text' name='author' ref={ e => authorElement = e } />
|
<input type='text' name='author' ref={authorRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor='body'>Body: </label>
|
<label htmlFor='body'>Body: </label>
|
||||||
<textarea name='body' rows='10' cols='150' ref={ e => bodyElement = e } />
|
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Publish</button>
|
<button type='submit'>Publish</button>
|
||||||
@@ -34,37 +46,35 @@ const NewsPublisher = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (title, author, body, uri, newsKey) => {
|
const handleSubmit = async (title, author, body, tokenFetch) => {
|
||||||
title = title.trim();
|
title = title.trim();
|
||||||
author = author.trim();
|
author = author.trim();
|
||||||
body = body.trim();
|
body = body.trim();
|
||||||
uri = uri.trim();
|
|
||||||
newsKey = newsKey.trim();
|
|
||||||
|
|
||||||
//fetch POST json data
|
//fetch POST json data
|
||||||
const raw = await fetch(
|
const result = await tokenFetch(
|
||||||
uri,
|
`${process.env.NEWS_URI}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ title: title, author: author, body: body, key: newsKey })
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
author,
|
||||||
|
body
|
||||||
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (raw.ok) {
|
if (!result.ok) {
|
||||||
const result = await raw.json();
|
return [`${result.status}: ${await result.text()}`];
|
||||||
|
|
||||||
if (result.ok) {
|
|
||||||
alert(`Published article index ${result.index}`);
|
|
||||||
} else {
|
|
||||||
alert(result.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert(raw.statusText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const json = await result.json();
|
||||||
|
|
||||||
|
return [null, json.index];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewsPublisher;
|
export default NewsPublisher;
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useState, useEffect, createContext } from 'react';
|
||||||
|
import decode from 'jwt-decode';
|
||||||
|
|
||||||
|
export const TokenContext = createContext();
|
||||||
|
|
||||||
|
const TokenProvider = props => {
|
||||||
|
const [accessToken, setAccessToken] = useState('');
|
||||||
|
const [refreshToken, setRefreshToken] = useState('');
|
||||||
|
|
||||||
|
//make the access and refresh tokens persist between reloads
|
||||||
|
useEffect(() => {
|
||||||
|
setAccessToken(localStorage.getItem("accessToken") || '');
|
||||||
|
setRefreshToken(localStorage.getItem("refreshToken") || '');
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("accessToken", accessToken);
|
||||||
|
localStorage.setItem("refreshToken", refreshToken);
|
||||||
|
}, [accessToken, refreshToken]);
|
||||||
|
|
||||||
|
//wrap the default fetch function
|
||||||
|
const tokenFetch = async (url, options) => {
|
||||||
|
//use this?
|
||||||
|
let bearer = accessToken;
|
||||||
|
|
||||||
|
//if expired (10 minutes, normally)
|
||||||
|
const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
|
||||||
|
|
||||||
|
if (expired) {
|
||||||
|
//ping the auth server for a new token
|
||||||
|
const response = await fetch(`${process.env.AUTH_URI}/token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: refreshToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
//any errors, throw them
|
||||||
|
if (!response.ok) {
|
||||||
|
throw `${response.status}: ${await response.text()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//save the new auth stuff (setting bearer as well)
|
||||||
|
const newAuth = await response.json();
|
||||||
|
|
||||||
|
setAccessToken(newAuth.accessToken);
|
||||||
|
setRefreshToken(newAuth.refreshToken);
|
||||||
|
bearer = newAuth.accessToken;
|
||||||
|
|
||||||
|
//BUGFIX: logging out correctly requires the new refresh token
|
||||||
|
if (url == `${process.env.AUTH_URI}/logout`) {
|
||||||
|
return fetch(`${process.env.AUTH_URI}/logout`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Authorization': `Bearer ${bearer}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: newAuth.refreshToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//finally, delegate to fetch
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
'Authorization': `Bearer ${bearer}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TokenContext.Provider value={{ accessToken, refreshToken, setAccessToken, setRefreshToken, tokenFetch, getPayload: () => decode(accessToken) }}>
|
||||||
|
{props.children}
|
||||||
|
</TokenContext.Provider>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TokenProvider;
|
||||||
@@ -3,3 +3,5 @@
|
|||||||
MERN Template developed by Kayne Ruse, KR Game Studios
|
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
|
# Privacy Policy
|
||||||
|
|
||||||
|
TODO: generate the privacy policy using config script
|
||||||
+4
-2
@@ -1,3 +1,5 @@
|
|||||||
|
//TODO: update this file
|
||||||
|
|
||||||
//setup
|
//setup
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -107,12 +109,12 @@ services:
|
|||||||
- "traefik.http.routers.${newsName}router.rule=Host(\`${newsWebAddress}\`)"
|
- "traefik.http.routers.${newsName}router.rule=Host(\`${newsWebAddress}\`)"
|
||||||
- "traefik.http.routers.${newsName}router.entrypoints=websecure"
|
- "traefik.http.routers.${newsName}router.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${newsName}router.tls.certresolver=myresolver"
|
- "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"
|
- "traefik.http.services.${newsName}service.loadbalancer.server.port=3100"
|
||||||
environment:
|
environment:
|
||||||
- WEB_PORT=3100
|
- WEB_PORT=3100
|
||||||
- DB_HOSTNAME=database
|
- DB_HOSTNAME=database
|
||||||
- DB_DATABASE=news
|
- DB_DATABASE=${newsName}
|
||||||
- DB_USERNAME=${newsDBUser}
|
- DB_USERNAME=${newsDBUser}
|
||||||
- DB_PASSWORD=${newsDBPass}
|
- DB_PASSWORD=${newsDBPass}
|
||||||
- DB_TIMEZONE=${databaseTimeZone}
|
- DB_TIMEZONE=${databaseTimeZone}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
//TODO: move this to the wiki?
|
||||||
|
|
||||||
# Setup Tutorial
|
# 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:
|
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
+293
-2052
File diff suppressed because it is too large
Load Diff
+13
-25
@@ -4,8 +4,6 @@
|
|||||||
"description": "A website template using the MERN stack.",
|
"description": "A website template using the MERN stack.",
|
||||||
"main": "server/server.js",
|
"main": "server/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"configure": "node configure-script.js",
|
|
||||||
"clean": "rm docker-compose.yml; rm Dockerfile; rm startup.sql",
|
|
||||||
"start": "npm run build && node server/server.js",
|
"start": "npm run build && node server/server.js",
|
||||||
"build": "npm run build:server && npm run build:client",
|
"build": "npm run build:server && npm run build:client",
|
||||||
"build:server": "exit 0",
|
"build:server": "exit 0",
|
||||||
@@ -26,44 +24,34 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
||||||
"dependencies": {
|
"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/core": "^7.12.10",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.12.10",
|
||||||
"babel-loader": "^8.2.2",
|
"babel-loader": "^8.2.2",
|
||||||
"clean-webpack-plugin": "^3.0.0",
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
"concurrently": "^5.3.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",
|
"html-webpack-plugin": "^5.0.0-alpha.14",
|
||||||
"nodemon": "^2.0.7",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"mariadb": "^2.5.2",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-dropdown-select": "^4.7.4",
|
||||||
"react-loadable": "^5.5.0",
|
"react-loadable": "^5.5.0",
|
||||||
|
"react-markdown": "^5.0.3",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
|
"sequelize": "^6.4.0",
|
||||||
|
"universal-cookie": "^4.0.4",
|
||||||
"webpack": "^5.15.0",
|
"webpack": "^5.15.0",
|
||||||
|
"webpack-cli": "^4.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^2.0.7",
|
||||||
"webpack-bundle-analyzer": "^4.3.0",
|
"webpack-bundle-analyzer": "^4.3.0",
|
||||||
"webpack-cli": "^4.3.1",
|
|
||||||
"webpack-dev-server": "^3.11.2"
|
"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,
|
host: process.env.DB_HOSTNAME,
|
||||||
dialect: 'mariadb',
|
dialect: 'mariadb',
|
||||||
timezone: process.env.DB_TIMEZONE,
|
timezone: process.env.DB_TIMEZONE,
|
||||||
// logging: false
|
logging: false
|
||||||
});
|
});
|
||||||
|
|
||||||
sequelize.sync();
|
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 = {
|
module.exports = {
|
||||||
bannedEmails: require('./banned-emails'),
|
//TODO: models
|
||||||
accounts: require('./accounts'),
|
|
||||||
pendingSignups: require('./pending-signups')
|
|
||||||
}
|
}
|
||||||
@@ -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
|
//environment variables
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
|
//libraries
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
//create the server
|
//create the server
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = require('http').Server(app);
|
const server = require('http').Server(app);
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
//libraries used here
|
//config
|
||||||
const path = require('path');
|
app.use(bodyParser.json());
|
||||||
const formidable = require('express-formidable');
|
|
||||||
const cookieParser = require('cookie-parser');
|
|
||||||
const session = require('express-session');
|
|
||||||
const SequelizeStore = require("connect-session-sequelize")(session.Store);
|
|
||||||
|
|
||||||
//database connection
|
//database connection
|
||||||
const database = require('./database');
|
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
|
//send static files
|
||||||
app.use('/', express.static(path.resolve(__dirname, '..', 'public')));
|
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 only needs to be run once, during initial development setup
|
||||||
#This file isnt needed for actual deployment
|
#This file isnt needed for actual deployment
|
||||||
|
|
||||||
|
|||||||
+3
-4
@@ -51,10 +51,9 @@ module.exports = ({ production, analyzer }) => {
|
|||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
'process.env': {
|
'process.env': {
|
||||||
'PRODUCTION': production,
|
'PRODUCTION': production,
|
||||||
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"http://dev-news.eggtrainer.com:3100/news"',
|
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.eggtrainer.com/news"',
|
||||||
/* TODO: (1) NEWS_KEY needs to be set in the server, and auth'd via admin accounts, NOT embedded in the client */
|
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.eggtrainer.com/auth"',
|
||||||
'NEWS_KEY': production ? `"${process.env.NEWS_KEY}"` : '"key"',
|
// 'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.eggtrainer.com/chat"',
|
||||||
'CHAT_URI': production ? `"${process.env.NEWS_URI}"` : '"http://dev-chat.eggtrainer.com:3200/chat"',
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new CleanWebpackPlugin({
|
new CleanWebpackPlugin({
|
||||||
|
|||||||
Reference in New Issue
Block a user