diff --git a/.envdev b/.envdev index 0d4ed8c..fb8aa9a 100644 --- a/.envdev +++ b/.envdev @@ -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 \ No newline at end of file +SECRET_ACCESS=access \ No newline at end of file diff --git a/README.md b/README.md index a9db435..b2504b2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +//TODO: update this README + # MERN-template A website template using the MERN stack. diff --git a/client/client.jsx b/client/client.jsx index a7e1286..1b52ed2 100644 --- a/client/client.jsx +++ b/client/client.jsx @@ -1,16 +1,15 @@ //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'; +import TokenProvider from './components/utilities/token-provider'; ReactDOM.render( - + - , + , document.querySelector('#root') ); diff --git a/client/components/app.jsx b/client/components/app.jsx index 5e66b85..f626eae 100644 --- a/client/components/app.jsx +++ b/client/components/app.jsx @@ -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 ( -
-

This website won't operate correctly without cookies.

- -
- ); - } - } - //default render return ( @@ -43,7 +24,6 @@ const App = props => { import('./pages/signup')} /> import('./pages/login')} /> import('./pages/account')} /> - import('./pages/chat')} /> import('./pages/admin')} /> diff --git a/client/components/pages/account.jsx b/client/components/pages/account.jsx index 0dac79f..a6ff3eb 100644 --- a/client/components/pages/account.jsx +++ b/client/components/pages/account.jsx @@ -1,53 +1,66 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useContext, useRef } from 'react'; import { Redirect } from 'react-router-dom'; -import { useCookies } from 'react-cookie'; + +import { TokenContext } from '../utilities/token-provider'; import DeleteAccount from '../panels/delete-account'; const Account = props => { - const [cookies, setCookie] = useCookies(); + //context + const authTokens = useContext(TokenContext); - //check for logged in redirect - if (!cookies['loggedin']) { + //misplaced? + if (!authTokens.accessToken) { return ; } //refs - let contactElement, passwordElement, retypeElement; + const passwordRef = useRef(); + const retypeRef = useRef(); + const contactRef = useRef(); - //once before render + //grab the user's info useEffect(() => { - fetch('/api/accounts') + authTokens.tokenFetch(`${process.env.AUTH_URI}/account`, { + method: 'GET', + headers: { + 'Access-Control-Allow-Origin': '*' + } + }) .then(blob => blob.json()) - .then(json => { - contactElement.checked = json.contact; - }) + .then(json => contactRef.current.checked = json.contact) .catch(e => console.error(e)) ; }, []); + //render the thing return (

Account

{ evt.preventDefault(); - await update(contactElement.checked, passwordElement.value, retypeElement.value); - passwordElement.value = retypeElement.value = ''; + const [err, result] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch); + + if (err) { + alert(err); + return; + } + passwordRef.current.value = retypeRef.current.value = ''; }}>
-
- - contactElement = e} /> -
-
- passwordElement = e} /> +
- retypeElement = e} /> + +
+ +
+ +
@@ -59,27 +72,32 @@ const Account = props => { ); }; -const update = async (contact, password, retype) => { +const update = async (password, retype, contact, tokenFetch) => { if (password != retype) { - alert('Passwords do not match'); + return ['Passwords do not match']; } - //generate a new formdata payload - let formData = new FormData(); - - formData.append('contact', contact); - - if (password) { - formData.append('password', password); + if (password && password.length < 8) { + return ['Password is too short']; } - 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) { - alert(await result.text()); + if (!result.ok) { + return [`${await result.status}: ${await result.text()}`]; } else { - alert(await result.text()); + return [null]; } } -export default Account; +export default Account; \ No newline at end of file diff --git a/client/components/pages/admin.jsx b/client/components/pages/admin.jsx index 936708b..f620d94 100644 --- a/client/components/pages/admin.jsx +++ b/client/components/pages/admin.jsx @@ -1,26 +1,27 @@ -import React from 'react'; +import React, { useContext } from 'react'; 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 NewsEditor from '../panels/news-editor'; const Admin = props => { - const [cookies, setCookie] = useCookies(); + //context + const authTokens = useContext(TokenContext); - //check for logged in redirect - if (!cookies['admin']) { + //misplaced? (admin only) + if (!authTokens.accessToken || !authTokens.getPayload().privilege == 'administrator') { return ; } return (

Administration

- - + +
); }; -export default Admin; +export default Admin; \ No newline at end of file diff --git a/client/components/pages/chat.jsx b/client/components/pages/chat.jsx deleted file mode 100644 index 13494c6..0000000 --- a/client/components/pages/chat.jsx +++ /dev/null @@ -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 ; - } - - return ( -
- -
- ); -}; - -export default ChatPage; \ No newline at end of file diff --git a/client/components/pages/login.jsx b/client/components/pages/login.jsx index 6613d22..c8b85a5 100644 --- a/client/components/pages/login.jsx +++ b/client/components/pages/login.jsx @@ -1,46 +1,50 @@ -import React from 'react'; +import React, { useContext, useRef } from 'react'; import { Redirect } from 'react-router-dom'; -import { useCookies } from 'react-cookie'; -//utilities -const validateEmail = require('../../../common/utilities/validate-email.js'); +import { TokenContext } from '../utilities/token-provider'; const LogIn = props => { - const [cookies, setCookie] = useCookies(); + //context + const authTokens = useContext(TokenContext); - //check for logged in redirect - if (cookies['loggedin']) { + //misplaced? + if (authTokens.accessToken) { return ; } //refs - let emailElement, passwordElement; + const emailRef = useRef(); + const passwordRef = useRef(); return (

Login

{ + async evt => { + //on submit 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)) - ; + const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value); + if (err) { + alert(err); + } + + //save auth tokens and redirect + if (newTokens) { + authTokens.setAccessToken(newTokens.accessToken); + authTokens.setRefreshToken(newTokens.refreshToken); + + props.history.push('/'); + } } }>
- emailElement = e} /> +
- passwordElement = e} /> +
@@ -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) => { - email = email.trim(); + email = email.trim(); //TODO: validate email on login - //generate a new formdata payload - let formData = new FormData(); + //send to the auth server + 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); - 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]; + //handle errors + if (!result.ok) { + const err = `${result.status}: ${await result.text()}`; + console.error(err); + return [err, false]; } + + //return the new auth tokens + const newTokens = await result.json(); + return [null, newTokens]; }; -export default LogIn; +export default LogIn; \ No newline at end of file diff --git a/client/components/pages/not-found.jsx b/client/components/pages/not-found.jsx index 0f694bb..bd080b1 100644 --- a/client/components/pages/not-found.jsx +++ b/client/components/pages/not-found.jsx @@ -3,7 +3,7 @@ import React from 'react'; const NotFound = props => { return (
-

Not Found

+

Page Not Found

); }; diff --git a/client/components/pages/signup.jsx b/client/components/pages/signup.jsx index 43c4726..905e389 100644 --- a/client/components/pages/signup.jsx +++ b/client/components/pages/signup.jsx @@ -1,60 +1,68 @@ -import React from 'react'; +import React, { useContext, useRef } from 'react'; import { Redirect } from 'react-router-dom'; -import { useCookies } from 'react-cookie'; + +import { TokenContext } from '../utilities/token-provider'; //utilities const validateEmail = require('../../../common/utilities/validate-email.js'); const validateUsername = require('../../../common/utilities/validate-username.js'); const SignUp = props => { - const [cookies, setCookie] = useCookies(); + //context + const authTokens = useContext(TokenContext); - //check for logged in redirect - if (cookies['loggedin']) { + //misplaced? + if (authTokens.accessToken) { return ; } //refs - let emailElement, usernameElement, passwordElement, retypeElement, contactElement; + const emailRef = useRef(); + const usernameRef = useRef(); + const passwordRef = useRef(); + const retypeRef = useRef(); + const contactRef = useRef(); return (

Signup

{ + async evt => { //on submit 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)) - ; + const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked); + if (result) { + alert(result); + } + + //redirect + if (redirect) { + props.history.push('/'); + } } }>
- emailElement = e} /> +
- usernameElement = e} /> +
- passwordElement = e} /> +
- retypeElement = e} /> +
- contactElement = e} /> +
@@ -73,21 +81,28 @@ const handleSubmit = async (email, username, password, retype, contact) => { return err; } - //generate a new formdata payload - let formData = new FormData(); + //send to the auth server + 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); - 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(); + if (!result.ok) { + const err = `${result.status}: ${await result.text()}`; + console.error(err); + return [err, false]; } + + return [await result.text(), true]; }; //returns an error message, or null on success @@ -111,4 +126,4 @@ const handleValidation = (email, username, password, retype) => { return null; }; -export default SignUp; +export default SignUp; \ No newline at end of file diff --git a/client/components/panels/banned-emails.jsx b/client/components/panels/banned-emails.jsx deleted file mode 100644 index 0aab2c3..0000000 --- a/client/components/panels/banned-emails.jsx +++ /dev/null @@ -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 ( -
-

Banned Accounts

- - - - - - - - - - - - {(data || []).map((entry, index) => - - - - - - - - )} - -
UsernameEmailPrivilegeExpiryReason
{entry.username}{entry.email}{entry.privilege}{entry.expiry ? (new Date(entry.expiry)).toISOString() : null}{entry.reason}
- -

Ban

- { e.preventDefault(); await handleBan(usernameElement.value, emailElement.value, expiryElement.value, reasonElement.value); }}> -
- - usernameElement = e} /> -
- -
- - emailElement = e} /> -
- -
- - expiryElement = e} /> -
- -
- -