Compare commits

..

79 Commits

Author SHA1 Message Date
Kayne Ruse 4e94c5338d Removed client-side markdown rendering 2021-12-30 13:21:34 +00:00
Kayne Ruse 55ff5765c6 Swapped out a library 2021-12-27 03:34:00 +00:00
Kayne Ruse d486059430 Updated webpack.config.js 2021-12-26 16:12:23 +00:00
Kayne Ruse 9d8c948dbb Updated dependencies 2021-12-24 07:37:43 +00:00
Kayne Ruse b21fa8db9e Tweaked webpack.config.js 2021-12-20 13:52:43 +00:00
Kayne Ruse c130b74e2d Added an example proxy route to webpack.config.js 2021-12-20 07:33:01 +00:00
Kayne Ruse d37b93d5f7 Updated webpack-dev-server to 4.6.0 2021-12-20 07:07:36 +00:00
Kayne Ruse 829cb2e3da Disabled source map in prod 2021-12-20 06:19:23 +00:00
Kayne Ruse 29f0dbb1ca Fixed HTTPS redirection 2021-12-15 18:39:37 +00:00
Kayne Ruse 13ad7d2435 Updated README.md 2021-12-11 16:57:48 +00:00
Kayne Ruse e30853e0cd Updated configure-script.js 2021-12-11 11:38:17 +00:00
Kayne Ruse 1e16a96f86 Updated README.md 2021-12-11 05:01:10 +00:00
Kayne Ruse cff73107b2 Updated package-lock.json 2021-11-17 06:13:41 +00:00
Kayne Ruse 76417747b3 Bumped node to version 16 LTS 2021-11-17 04:58:08 +00:00
Kayne Ruse b8323723ed Updated package-lock.json 2021-11-15 22:37:51 +00:00
Kayne Ruse 415b2f32f1 Bumped version number 2021-08-24 08:02:17 +01:00
Kayne Ruse a0dbe0aee1 Imported the directory structure from egg trainer 2021-08-22 02:22:28 +10:00
Kayne Ruse f415a7ece2 Fixed analyzer 2021-08-20 21:21:18 +01:00
Kayne Ruse 9c863f309f Fixed webpack config 2021-08-21 05:49:01 +10:00
Kayne Ruse 7547b1717e Added gzip compression for JS files 2021-08-21 05:36:23 +10:00
Kayne Ruse 22e6286e0a Webpack config tweak 2021-08-16 07:44:32 +10:00
Kayne Ruse c766c43223 No longer needs the refreshToken to logout 2021-08-15 00:54:28 +10:00
Kayne Ruse bb1590bae7 Bumped version number 2021-08-10 17:39:09 +10:00
Kayne Ruse 5f7b9dda3a Tweaked README.md 2021-08-07 13:54:39 +10:00
Kayne Ruse 051f3dfb2a Updated README.md 2021-07-30 10:20:14 +10:00
Kayne Ruse 85456e0892 Minor markdown tweak 2021-07-30 03:36:40 +10:00
Kayne Ruse 6130337846 Fixed classNames 2021-07-30 01:34:06 +10:00
Kayne Ruse ac99f3bf38 Fixed CSS classes 2021-07-29 22:13:17 +10:00
Kayne Ruse 20e94db628 I really hate these line endings 2021-07-29 21:15:32 +10:00
Kayne Ruse bcb4a37f5a Tweak 2021-07-29 21:13:53 +10:00
Kayne Ruse 3b0d3c87b1 Tweak 2021-07-28 15:52:50 +01:00
Kayne Ruse 51a116503d Bumped version number 2021-07-28 15:43:09 +01:00
Kayne Ruse eb370663d2 Tweaked some display 2021-07-29 00:37:23 +10:00
Kayne Ruse 462116d980 Fixed whitespace 2021-07-28 23:18:22 +10:00
Kayne Ruse af06ddc06d Working on password recovery 2021-07-28 23:01:41 +10:00
Kayne Ruse f937ee47db Removed universal-cookie, fixed package.json versioning 2021-07-26 05:05:39 +01:00
Kayne Ruse 6d0dd419ca Fixed #22 2021-07-26 13:48:08 +10:00
Kayne Ruse 2919467dff Fixed #23 2021-07-26 13:38:33 +10:00
Kayne Ruse 269caac88c Tweaked README.md 2021-07-25 20:35:34 +01:00
Kayne Ruse 69c297fa74 Stupid line endings 2021-07-25 19:22:17 +10:00
Kayne Ruse 0f538be3e5 Fixed #20 2021-07-25 18:53:14 +10:00
Kayne Ruse f85b6e8793 Fixed passwords 2021-07-25 18:52:46 +10:00
Kayne Ruse 2af9532930 Fixed deploy bug, properly 2021-07-24 14:01:20 +01:00
Kayne Ruse 191da50740 Deploy bug 2021-07-24 22:20:17 +10:00
Kayne Ruse f24c7990f6 Fixed line-endings 2021-07-24 20:23:18 +10:00
Kayne Ruse 7a48780f50 Update package.json 2021-07-23 21:25:24 +10:00
Kayne Ruse 33952a9896 Changed chat line id to index 2021-07-23 21:24:19 +10:00
Kayne Ruse 8076b0cc40 Pointed to new dev-services 2021-07-22 21:48:42 +01:00
Kayne Ruse e216474196 Bumped version number 2021-07-15 08:53:57 +10:00
Kayne Ruse 2532bf1867 Updated packages to fix vulnerabilities 2021-07-15 08:52:54 +10:00
Kayne Ruse 93a3c30e81 Fixed the database hostname
It's supposed to point to localhost when developing locally.
2021-04-08 04:21:31 +10:00
Kayne Ruse ae8c82e83a Fixed embedded source-map error 2021-04-07 05:00:16 +10:00
Kayne Ruse bc6a795750 WHOOPS THAT WAS A MISTAKE 2021-04-07 03:18:48 +10:00
Kayne Ruse 9947ef13c1 Added a workaround for a mysql bug 2021-04-07 02:21:44 +10:00
Kayne Ruse d3f0b1ac7d Fixed WEB_PORT setting 2021-04-03 03:59:08 +11:00
Kayne Ruse b5f9c45a1b Updated README.md 2021-03-28 08:56:02 +11:00
Kayne Ruse 5189415e1a Implemented permabans 2021-03-28 08:33:26 +11:00
Kayne Ruse be793ae2ff Chat report table working 2021-03-28 07:58:52 +11:00
Kayne Ruse 19f5c20056 "Fixed" errors on logout, read more
I really don't like this solution.

The problem was caused by fetch being called twice after the component was
rendered twice. I don't know why it renders twice.
2021-03-28 04:38:03 +11:00
Kayne Ruse 7923f51aae Updated README.md 2021-03-25 18:10:03 +11:00
Kayne Ruse 793e54e334 Updated admin and mod flag system 2021-03-24 08:21:40 +11:00
Kayne Ruse 4fa54668e6 Added reporting feature 2021-03-24 03:22:16 +11:00
Kayne Ruse ff0230b77f Tested the new libs 2021-03-24 01:53:33 +11:00
Kayne Ruse 6b53bee033 Updated libraries 2021-03-24 01:11:01 +11:00
Kayne Ruse 4280319443 Replaced react-loadable with @loadable/component 2021-03-24 01:03:11 +11:00
Kayne Ruse 9788552d0c Updated README.md 2021-03-22 16:49:24 +11:00
Kayne Ruse 3fd76375dd Each microservice has received a tweak to .envdev, read more
This should make it easier to set time zones and enable database logging.

Related to krgamestudios/MERN-template#16
2021-03-22 16:39:44 +11:00
Kayne Ruse 9c8ece5c06 Implemented DB_LOGGING 2021-03-20 05:13:12 +11:00
Kayne Ruse 9201e9374b Updated README.md 2021-03-19 20:39:52 +11:00
Kayne Ruse 8c8b78462f Updated README.md 2021-03-19 18:49:01 +11:00
Kayne Ruse 07e578b4c4 Updated README.md 2021-03-19 18:47:17 +11:00
Kayne Ruse 732eeb933e Tweak 2021-03-18 05:26:21 +11:00
Kayne Ruse 104e15d714 Stupid tabs 2021-03-18 05:21:26 +11:00
Kayne Ruse 2c9871d82a Tweak 2021-03-18 05:17:12 +11:00
Kayne Ruse f5f44ae9f7 Updated configure-script.js with chat info 2021-03-18 05:15:32 +11:00
Kayne Ruse 1c3d24575e Added backlog to chat 2021-03-18 02:40:22 +11:00
Kayne Ruse 13e3ce6db8 Chat is working with a local chat-server 2021-03-17 16:52:14 +11:00
Kayne Ruse 8561219542 Wrote CSS for a chatbox 2021-03-17 03:44:38 +11:00
Kayne Ruse e288a43519 Tweaked README.md 2021-03-17 00:56:33 +11:00
51 changed files with 9155 additions and 10181 deletions
+8 -2
View File
@@ -1,9 +1,15 @@
WEB_PORT=3000
DB_HOSTNAME=database
DB_HOSTNAME=localhost
DB_DATABASE=template
DB_USERNAME=template
DB_PASSWORD=pikachu
# Select a "TZ database name" that suits your needs: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
DB_TIMEZONE=Australia/Sydney
SECRET_ACCESS=access
# Give this any value to enable database logging (such as "true")
DB_LOGGING=
# Make sure this value matches the system that you connect to
SECRET_ACCESS=access
+119 -118
View File
@@ -1,118 +1,119 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Output files
public/*.html
public/*.js
public/*.css
public/*.map
public/*.gz
letsencrypt/
mysql/
Dockerfile
docker-compose.yml
startup.sql
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Output files
public/*.html
public/*.js
public/*.css
public/*.map
public/*.gz
public/*.txt
letsencrypt/
mysql/
Dockerfile
docker-compose.yml
startup.sql
+38 -32
View File
@@ -1,22 +1,26 @@
# MERN-template
A website template using the MERN stack. The primary technology involved is:
A website template using the MERN stack. It is geared towards Persistent Browser Based Games (think neopets), but is flexible enough for a number of different uses.
The primary technology involved is:
* React
* Nodejs
* MariaDB (with Sequelize)
* Docker
* Docker (with docker-compose)
This template is designed to support the development of persistent browser based games (PBBGs), but it, and it's component microservices, can be used elsewhere.
This template is released under the zlib license (see LICENSE).
See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation.
# Microservices
There are external components to this template referred to as "microservices". These can be omitted entirely by simply removing the React components that access them. These are also available via [docker hub](https://hub.docker.com/u/krgamestudios).
* Auth Server: https://github.com/krgamestudios/auth-server
* News Server: https://github.com/krgamestudios/news-server
* Auth Server: https://github.com/krgamestudios/auth-server
* Chat Server: https://github.com/krgamestudios/chat-server
# Setup Deployment
@@ -25,6 +29,7 @@ A clean install is this easy:
```
git clone https://github.com/krgamestudios/MERN-template.git
cd MERN-template
node configure-script.js
docker-compose up --build
```
@@ -42,15 +47,27 @@ To set up this template in development mode:
# Features List
- Fully Featured Account System
- Mainly one language across the codebase (JavaScript)
- Full documentation
- Setup tutorial
- Fully Featured Account System (as a microservice)
- Email validation
- Logging in and out
- Account deletion
- Password management
- JSON web token authentication
- News Blog
- Optional microservice
- Secure publishing and editing of articles via admin panel
- Optional post validation hook
- Fully Featured News Blog (as a microservice)
- Publish, edit or delete articles as needed
- Secured via admin panel
- Fully Featured Chat System (as a microservice)
- Available when logged in
- Chat logs saved to the database
- Room-based chat (type `/room name` to access a specific room)
- Moderation tools
- Permanently banning users
- Chat-muting users for a time period
- Users reporting offensive chat-content
- Easy To Use Configuration Script
- Sets up everything via docker
- A default admin account (if desired)
@@ -58,31 +75,20 @@ To set up this template in development mode:
# Coming Soon
- Full documentation
- Setup tutorial
- Modding tutorial
- Fully Featured Chat System
- Optional microservice
- Chat logs
- Custom emoji
- Global and room-based chat
- Private messaging?
- Broadcasting to all channels
- Badges next to usernames?
- Moderation tools for banning, suspending, or chat-banning users
- Modding tutorials
- Fully Featured News Blog (as a microservice)
- Individual pages for news articles
# Coming Eventually
- Better compression for client files
- Backend for energy systems
- Backend for leaderboards
- Backend for items, shops, trading and currency
# Gmail Email Settings
If you decide to use gmail as your email provider (as I do), then use the following `.env` settings:
MAIL_SMTP=smtp.gmail.com
MAIL_USERNAME=you@gmail.com
MAIL_PASSWORD=yourpassword
you'll also need to enable "less secure apps" for the specified email address. Remember - don't ever commit the `.env` file! You might even want to create a dedicated email address just for your project.
- Fully Featured News Blog (as a microservice)
- Restore deleted articles
- Undo edits
- Fully Featured Chat System (as a microservice)
- Custom emoji
- Private messaging
- Broadcasting to all channels
- Badges next to usernames
- Backend for leaderboards (modding tutorial?)
- Backend for energy systems (modding tutorial?)
- Backend for items, shops, trading and currency (modding tutorial?)
+2 -2
View File
@@ -4,8 +4,8 @@ import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import TokenProvider from './components/utilities/token-provider';
import App from './pages/app';
import TokenProvider from './pages/utilities/token-provider';
ReactDOM.render(
<TokenProvider>
-40
View File
@@ -1,40 +0,0 @@
//react
import React from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
//library components
import LazyRoute from './lazy-route';
import Markdown from './panels/markdown';
//styling
//import a styling template here
//common components
import Header from './panels/header.jsx';
import Footer from './panels/footer.jsx';
const App = props => {
//default render
return (
<BrowserRouter>
<Header />
<Switch>
<LazyRoute exact path='/' component={() => import('./pages/homepage')} />
<LazyRoute path='/signup' component={() => import('./pages/signup')} />
<LazyRoute path='/login' component={() => import('./pages/login')} />
<LazyRoute path='/account' component={() => import('./pages/account')} />
<LazyRoute path='/admin' component={() => import('./pages/admin')} />
<LazyRoute path='/privacypolicy' component={async () => () => <Markdown content={require('../markdown/privacy-policy.md').default} />} />
<LazyRoute path='/credits' component={async () => () => <Markdown content={require('../markdown/credits.md').default} />} />
<LazyRoute path='*' component={() => import('./pages/not-found')} />
</Switch>
<Footer />
</BrowserRouter>
);
};
export default App;
-39
View File
@@ -1,39 +0,0 @@
import React from 'react';
import { Route } from 'react-router-dom';
import Loadable from 'react-loadable';
const Loading = props => {
if (props.error) {
return <p>{props.error}</p>;
}
if (props.timedOut) {
return (
<div className='page'>
<p>Page Timed Out</p>
</div>
);
}
if (props.pastDelay) {
return (
<div className='page'>
<p>Page Loading...</p>
</div>
);
}
return null;
};
const LazyRoute = lazyProps => {
const component = Loadable({
loader: lazyProps.component,
loading: Loading,
timeout: 10000
});
return <Route {...lazyProps} component={component} />
};
export default LazyRoute;
-29
View File
@@ -1,29 +0,0 @@
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
import NewsPublisher from '../panels/news-publisher';
import NewsEditor from '../panels/news-editor';
import PrivilegeEditor from '../panels/privilege-editor';
const Admin = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || authTokens.getPayload().privilege != 'administrator') {
return <Redirect to='/' />;
}
return (
<div className='page'>
<h1 className='centered'>Administration</h1>
<NewsPublisher />
<NewsEditor />
<PrivilegeEditor />
</div>
);
};
export default Admin;
-14
View File
@@ -1,14 +0,0 @@
import React from 'react';
import NewsFeed from '../panels/news-feed';
const HomePage = props => {
return (
<div className='page'>
<p>This is the MERN template homepage.</p>
<NewsFeed />
</div>
);
};
export default HomePage;
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
const NotFound = props => {
return (
<div className='page'>
<h1 className='middle centered'>Page Not Found</h1>
</div>
);
};
export default NotFound;
-12
View File
@@ -1,12 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Footer = () => {
return (
<footer>
<p className='centered'>MERN template designed by <a href='https://krgamestudios.com'>Kayne Ruse, KR Game Studios</a> - <Link to='/privacypolicy'>Privacy Policy</Link> - <Link to='/credits'>Credits</Link></p>
</footer>
);
};
export default Footer;
-68
View File
@@ -1,68 +0,0 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
const Visitor = () => {
return (
<div>
<Link to='/signup'>Sign Up</Link>
<span> - </span>
<Link to='/login'>Log In</Link>
</div>
);
};
const Member = () => {
const authTokens = useContext(TokenContext);
return (
<div>
<Link to='/account'>Account</Link>
<span> - </span>
{ authTokens.getPayload().privilege == 'administrator' ?
<span>
<Link to='/admin'>Admin</Link>
<span> - </span>
</span>:
<span />
}
{ /* Logout button logs you out of the server too */ }
<Link to='/' onClick={async () => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/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>
);
};
const Header = () => {
const authTokens = useContext(TokenContext);
return (
<header>
<h1><Link to='/'>MERN Template</Link></h1>
{ authTokens.accessToken ? <Member /> : <Visitor /> }
</header>
);
};
export default Header;
-34
View File
@@ -1,34 +0,0 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown/with-html';
const Markdown = props => {
//content?
let [contentHook, setContentHook] = useState(null);
//check arguments
if (!props.content) {
if (!props.uri) {
throw 'Markdown requires either content or uri prop';
}
//once
useEffect(() => {
fetch(props.uri)
.then(blob => blob.text())
.then(blob => setContentHook(blob))
.catch(e => console.error(e))
;
}, []);
} else
//assume raw info
if (!contentHook) {
setContentHook(props.content);
}
return (
<ReactMarkdown escapeHtml={false} props={{...props}}>{contentHook}</ReactMarkdown>
);
};
export default Markdown;
-47
View File
@@ -1,47 +0,0 @@
import React, { useState, useEffect } from 'react';
import dateFormat from 'dateformat';
//DOCS: props.uri is the address of a live news-server
const NewsFeed = props => {
const [articles, setArticles] = useState([]);
useEffect(async () => {
const result = await fetch(`${process.env.NEWS_URI}/news`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
setArticles(await result.json());
}
}, []);
return (
<div>
<h1 className='centered'>News Feed</h1>
{articles.map((article, index) => {
return (
<div key={index}>
<hr />
<h2>{article.title}</h2>
<p>Written by <strong>{article.author}</strong>, {
article.edits > 0 ?
<span>Last Updated {dateFormat(article.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`})</span> :
<span>Published {dateFormat(article.createdAt, 'fullDate')}</span>
}</p>
<p style={{whiteSpace: 'pre-wrap'}}>{article.body}</p>
</div>
);
})}
</div>
);
};
export default NewsFeed;
@@ -1,70 +0,0 @@
import React, { useState, useRef, useContext } from 'react';
import Select from 'react-dropdown-select';
import { TokenContext } from '../utilities/token-provider';
const PrivilegeEditor = props => {
//context
const authTokens = useContext(TokenContext);
//state
const [privilege, setPrivilege] = useState('normal');
//ref
const usernameRef = useRef();
return (
<div>
<h2 className='centered'>Privilege Editor</h2>
<form onSubmit={async evt => {
evt.preventDefault();
const [err, result] = await handleSubmit(usernameRef.current.value, privilege, authTokens.tokenFetch);
if (err) {
alert(err);
}
if (result) {
alert('Privilege set');
usernameRef.current.value = '';
}
}}>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<Select
options={[{ label: 'administrator', value: 1 }, { label: 'moderator', value: 2 }, { label: 'alpha', value: 3 }, { label: 'beta', value: 4 }, { label: 'gamma', value: 5 }, { label: 'normal', value: 6 }]}
onChange={values => setPrivilege(values[0].label)}
/>
<button type='submit'>Change</button>
</form>
</div>
);
};
const handleSubmit = async (username, privilege, tokenFetch) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/privilege`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
username,
privilege
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
return [err, false];
}
return [null, true];
};
export default PrivilegeEditor;
-6
View File
@@ -1,6 +0,0 @@
# Credits
MERN Template developed by Kayne Ruse, KR Game Studios
[https://github.com/krgamestudios/MERN-template](https://github.com/krgamestudios/MERN-template)
-2
View File
@@ -1,2 +0,0 @@
# Privacy Policy
@@ -1,9 +1,11 @@
import React, { useEffect, useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import DeleteAccount from '../panels/delete-account';
import DeleteAccount from './panels/delete-account';
const Account = props => {
//context
@@ -35,42 +37,42 @@ const Account = props => {
//render the thing
return (
<div className='page'>
<h1 className='centered'>Account</h1>
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<div className='panel'>
<h1 className='text centered'>Account</h1>
<div className='panel'>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
if (err) {
alert(err);
return;
}
if (err) {
alert(err);
return;
}
alert('Details updated');
passwordRef.current.value = retypeRef.current.value = '';
}}>
<div>
<div>
<label htmlFor='password'>Change Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
alert('Details updated');
passwordRef.current.value = retypeRef.current.value = '';
}}>
<input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
<span>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</span>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
<button type='submit'>Update Information</button>
</form>
<DeleteAccount />
</div>
<Link to='/' className='text centered'>Return Home</Link>\
</div>
</div>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount className='constricted' />
</div>
</div>
</>
);
};
@@ -1,11 +1,13 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const LogIn = props => {
const Login = props => {
//context
const authTokens = useContext(TokenContext);
@@ -19,39 +21,40 @@ const LogIn = props => {
const passwordRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Login</h1>
<form className='constricted' onSubmit={
async evt => {
//on submit
evt.preventDefault();
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<div className='panel'>
<h1 className='text centered'>Login</h1>
<form className='constrained' onSubmit={
async evt => {
//on submit
evt.preventDefault();
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);
//save auth tokens and redirect
if (newTokens) {
authTokens.setAccessToken(newTokens.accessToken);
authTokens.setRefreshToken(newTokens.refreshToken);
props.history.push('/');
}
}
}>
<div>
<label htmlFor="email">Email:</label>
<input type="email" name="email" ref={emailRef} />
props.history.push('/');
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='password' name='password' placeholder='********' ref={passwordRef} />
<button type='submit'>Login</button>
</form>
<Link to='/recover' className='text centered'>Forgot Password?</Link>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<button type='submit'>Login</button>
</form>
</div>
</div>
</>
);
};
@@ -104,4 +107,4 @@ const handleValidation = (email, password) => {
};
export default LogIn;
export default Login;
@@ -1,6 +1,6 @@
import React, { useState, useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider';
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
const DeleteAccount = props => {
@@ -9,25 +9,27 @@ const DeleteAccount = props => {
const passwordRef = useRef();
if (!open) {
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
return (
<button onClick={() => setOpen(true)}>Delete Account</button>
);
}
return (
<form className={props.className} onSubmit={async evt => {
evt.preventDefault();
const [err] = await handleSubmit(passwordRef.current.value, authTokens);
if (err) {
alert(err);
}
}}>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<div className='panel centered middle'>
<h2 className='text centered'>Delete Your Account?</h2>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err] = await handleSubmit(passwordRef.current.value, authTokens);
if (err) {
alert(err);
}
}}>
<input type="password" name="password" placeholder='Password' ref={passwordRef} />
<button type='submit'>Delete Account</button>
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
</form>
<button type='submit' style={{backgroundColor: 'red'}}>Delete Account</button>
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
</form>
</div>
);
};
+37
View File
@@ -0,0 +1,37 @@
import React, { useContext, useRef } from 'react';
import { Link } from 'react-router-dom';
import { TokenContext } from '../../utilities/token-provider';
//TODO: make this an ACTUAL BUTTON
const Logout = () => {
const authTokens = useContext(TokenContext);
return (
<>
{ /* Logout logs you out of the server too */ }
<Link to='/' onClick={async () => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/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('');
}
}}>Logout</Link>
</>
);
};
export default Logout;
+95
View File
@@ -0,0 +1,95 @@
import React, { useContext, useRef } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
//utilities
const validateEmail = require('../../../common/utilities/validate-email');
const Recover = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const emailRef = useRef();
const recoverRef = useRef();
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Forgot Password</h1>
<form className='constrained' onSubmit={
async evt => { //on submit
recoverRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value);
if (result) {
alert(result);
recoverRef.current.disabled = false;
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<button type='submit' ref={recoverRef}>Recover Password</button>
</form>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
const handleSubmit = async (email) => {
email = email.trim();
const err = handleValidation(email);
if (err) {
return [err];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/recover`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
email
})
});
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
const handleValidation = (email) => {
if (!validateEmail(email)) {
return 'invalid email';
}
return null;
};
export default Recover;
+86
View File
@@ -0,0 +1,86 @@
import React, { useContext, useRef } from 'react';
import { Link, Redirect } from 'react-router-dom';
import queryString from 'query-string';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const Reset = props => {
//context
const authTokens = useContext(TokenContext);
//query
const query = queryString.parse(props.location.search);
//misplaced?
if (authTokens.accessToken || !query.email || !query.token) {
return <Redirect to='/' />;
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
//render the thing
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Reset Password</h1>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err, redirect] = await update(passwordRef.current.value, retypeRef.current.value, query);
if (err) {
alert(err);
return;
}
alert('Details updated'); //TODO: replace with a message from the auth server
//redirect
if (redirect) {
props.history.push('/');
}
}}>
<input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<button type='submit'>Update Information</button>
</form>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
const update = async (password, retype, query) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await fetch(`${process.env.AUTH_URI}/auth/reset?email=${query.email}&token=${query.token}`, {
method: 'PATCH',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null, true];
}
}
export default Reset;
@@ -1,5 +1,7 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
@@ -7,7 +9,7 @@ import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const validateUsername = require('../../../common/utilities/validate-username');
const SignUp = props => {
const Signup = props => {
//context
const authTokens = useContext(TokenContext);
@@ -22,52 +24,48 @@ const SignUp = props => {
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
const signupRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Signup</h1>
<form className='constricted' onSubmit={
async evt => { //on submit
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
if (result) {
alert(result);
}
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Signup</h1>
<form className='constrained' onSubmit={
async evt => { //on submit
signupRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
if (result) {
alert(result);
signupRef.current.disabled = false;
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<div>
<label htmlFor='email'>Email:</label>
<input type='email' name='email' ref={emailRef} />
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='text' name='username' placeholder='Username' ref={usernameRef} />
<input type='password' name='password' placeholder='********' ref={passwordRef} />
<input type='password' name='retype' placeholder='********' ref={retypeRef} />
<span>
<label htmlFor='contact'>Allow Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} defaultChecked='true' />
</span>
<button type='submit' ref={signupRef}>Signup</button>
</form>
<Link to='/recover' className='text centered'>Forgot Password?</Link>
<Link to='/' className='text centered'>Return Home</Link>
</div>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<div>
<label htmlFor='password'>Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</div>
<button type='submit'>Signup</button>
</form>
</div>
</div>
</>
);
};
@@ -126,4 +124,4 @@ const handleValidation = (email, username, password, retype) => {
return null;
};
export default SignUp;
export default Signup;
+43
View File
@@ -0,0 +1,43 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import NewsPublisher from './panels/news-publisher';
import NewsEditor from './panels/news-editor';
import GrantAdmin from './panels/grant-admin';
import GrantMod from './panels/grant-mod';
const Admin = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !authTokens.getPayload().admin) {
return <Redirect to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Administration Tools</h1>
<NewsPublisher />
<br />
<NewsEditor />
<br />
<GrantAdmin />
<br />
<GrantMod />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Admin;
+35
View File
@@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import ChatReports from './panels/chat-reports';
import BanUser from './panels/ban-user';
const Mod = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !(authTokens.getPayload().admin || authTokens.getPayload().mod)) {
return <Redirect to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Moderation Tools</h1>
<BanUser />
<ChatReports />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Mod;
@@ -0,0 +1,62 @@
import React, { useRef, useContext } from 'react';
import { TokenContext } from '../../utilities/token-provider';
const BanUser = props => {
//context
const authTokens = useContext(TokenContext);
//ref
const usernameRef = useRef();
return (
<div className='panel'>
<h2 className='text centered'>Permanently Ban User</h2>
<form className='constrained'>
<input type='text' name='username' placeholder='Username' ref={usernameRef} />
<button type='button' onClick={async evt => {
evt.preventDefault();
const yes = confirm('Permanently ban this user from the website?');
if (!yes) {
return;
}
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch);
if (err) {
alert(err);
}
if (result) {
usernameRef.current.value = '';
}
}}>Submit</button>
</form>
</div>
);
};
const handleButtonPress = async (username, tokenFetch) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/banuser`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
username
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
return [err, false];
}
return [null, true];
};
export default BanUser;
@@ -0,0 +1,71 @@
import React, { useState, useEffect, useContext } from 'react';
import { TokenContext } from '../../utilities/token-provider';
import dateFormat from 'dateformat';
const ChatReports = props => {
const [reports, setReports] = useState([]);
const authTokens = useContext(TokenContext);
useEffect(async () => {
const result = await authTokens.tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
}
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
setReports(await result.json());
}
}, []);
return (
<div className='panel' style={{minWidth: '100%'}}>
<h2 className='text centered'>Chat Reports</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Username</th>
<th className='mobile hide'>Room Name</th>
<th>Content</th>
<th>Reported By</th>
<th className='mobile hide'>Delete</th>
</tr>
</thead>
<tbody>
{reports.map((report, index) => (
<tr key={index}>
<td className='text centered'>{dateFormat(report.chatlog.createdAt, 'yyyy-mm-dd, H:MM:ss')}</td>
<td className='text centered'>{report.chatlog.username}</td>
<td className='text mobile hide centered'>{report.chatlog.room}</td>
<td className='text centered'>{report.chatlog.text}</td>
<td className='text centered'>{report.reporter.join(', ')}</td>
<td className='text mobile hide centered'><button onClick={() => deleteReportsFor(report.chatlogIndex, authTokens.tokenFetch, setReports)}>Delete</button></td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const deleteReportsFor = (chatlogIndex, tokenFetch, setReports) => {
tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ chatlogIndex })
});
setReports(reports => reports.filter(report => report.chatlogIndex != chatlogIndex));
};
export default ChatReports;
@@ -0,0 +1,71 @@
import React, { useRef, useContext } from 'react';
import { TokenContext } from '../../utilities/token-provider';
const GrantAdmin = props => {
//context
const authTokens = useContext(TokenContext);
//ref
const usernameRef = useRef();
return (
<div className='panel'>
<h2 className='text centered'>Grant Admin Privileges</h2>
<form className='constrained'>
<input type='text' name='username' placeholder='Username' ref={usernameRef} />
<button type='button' onClick={async evt => {
evt.preventDefault();
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'POST');
if (err) {
alert(err);
}
if (result) {
alert('admin set');
usernameRef.current.value = '';
}
}}>Submit</button>
<button type='button' onClick={async evt => {
evt.preventDefault();
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'DELETE');
if (err) {
alert(err);
}
if (result) {
alert('admin removed');
usernameRef.current.value = '';
}
}}>Remove</button>
</form>
</div>
);
};
const handleButtonPress = async (username, tokenFetch, method) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/admin`, {
method: method,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
username
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
return [err, false];
}
return [null, true];
};
export default GrantAdmin;
@@ -0,0 +1,71 @@
import React, { useRef, useContext } from 'react';
import { TokenContext } from '../../utilities/token-provider';
const GrantMod = props => {
//context
const authTokens = useContext(TokenContext);
//ref
const usernameRef = useRef();
return (
<div className='panel'>
<h2 className='text centered'>Grant Moderation Privileges</h2>
<form className='constrained'>
<input type='text' name='username' placeholder='Username' ref={usernameRef} />
<button type='button' onClick={async evt => {
evt.preventDefault();
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'POST');
if (err) {
alert(err);
}
if (result) {
alert('mod set');
usernameRef.current.value = '';
}
}}>Submit</button>
<button type='button' onClick={async evt => {
evt.preventDefault();
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'DELETE');
if (err) {
alert(err);
}
if (result) {
alert('mod removed');
usernameRef.current.value = '';
}
}}>Remove</button>
</form>
</div>
);
};
const handleButtonPress = async (username, tokenFetch, method) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/mod`, {
method: method,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
username
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
return [err, false];
}
return [null, true];
};
export default GrantMod;
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import Select from 'react-dropdown-select';
import Select from 'react-select';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const NewsEditor = props => {
//context
@@ -36,38 +36,34 @@ const NewsEditor = props => {
}, []);
return (
<div>
<h2 className='centered'>News Editor</h2>
<div>
<label htmlFor='article'>Article: </label>
<Select
options={articles.map(article => { return { label: article.title, value: article.index }; })}
onChange={async values => {
//fetch this article
const index = values[0].value;
const result = await fetch(`${process.env.NEWS_URI}/news/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 className='panel'>
<h2 className='text centered'>News Editor</h2>
<Select
options={articles.map(article => { return { label: article.title, index: article.index }; })}
onChange={async ({index}) => {
//fetch this article
const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`, {
headers: {
'Access-Control-Allow-Origin': '*'
}
}}
/>
</div>
});
<form onSubmit={async evt => {
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);
}
}}
placeholder='Select Article'
/>
<form className='constrained' onSubmit={async evt => {
//onSubmit
evt.preventDefault();
const [err] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, index, authTokens.tokenFetch);
@@ -78,20 +74,9 @@ const NewsEditor = props => {
alert(`Edited as article index ${index}`);
}
}}>
<div>
<label htmlFor='title'>Title: </label>
<input type='text' name='title' ref={titleRef} />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={authorRef} />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
</div>
<input type='text' name='title' placeholder='Title' ref={titleRef} />
<input type='text' name='author' placeholder='Author' ref={authorRef} />
<textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
<button type='submit'>Update</button>
<button type='button' onClick={async evt => {
@@ -1,6 +1,6 @@
import React, { useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const NewsPublisher = props => {
//context
@@ -12,33 +12,22 @@ const NewsPublisher = props => {
const bodyRef = useRef();
return (
<div>
<h2 className='centered'>News Publisher</h2>
<form onSubmit={async evt => {
<div className='panel'>
<h2 className='text centered'>News Publisher</h2>
<form className='constrained' onSubmit={async evt => {
//on submit
evt.preventDefault();
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 = '';
titleRef.current.value = authorRef.current.value = bodyRef.current.value = ''; //TODO: null bug here?
alert(`Published as article index ${index}`);
}
}}>
<div>
<label htmlFor='title'>Title: </label>
<input type='text' name='title' ref={titleRef} />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={authorRef} />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
</div>
<input type='text' name='title' placeholder='Title' ref={titleRef} />
<input type='text' name='author' placeholder='Author' ref={authorRef} />
<textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
<button type='submit'>Publish</button>
</form>
+47
View File
@@ -0,0 +1,47 @@
//react
import React, { useContext } from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import { TokenContext } from './utilities/token-provider';
//library components
import LazyRoute from './utilities/lazy-route';
//styling
import '../styles/styles.css';
//common components
import Footer from './panels/footer';
import PopupChat from './panels/popup-chat';
const App = props => {
const authTokens = useContext(TokenContext);
//default render
return (
<BrowserRouter>
<Switch>
<LazyRoute exact path='/' component={() => import('./homepage')} />
<LazyRoute path='/signup' component={() => import('./accounts/signup')} />
<LazyRoute path='/login' component={() => import('./accounts/login')} />
<LazyRoute path='/account' component={() => import('./accounts/account')} />
<LazyRoute path='/dashboard' component={() => import('./dashboard')} />
<LazyRoute path='/recover' component={() => import('./accounts/recover')} />
<LazyRoute path='/reset' component={() => import('./accounts/reset')} />
<LazyRoute path='/admin' component={() => import('./administration/admin')} />
<LazyRoute path='/mod' component={() => import('./administration/mod')} />
<LazyRoute path='/privacypolicy' component={() => import('./static/privacy-policy')} />
<LazyRoute path='/credits' component={() => import('./static/credits')} />
<LazyRoute path='*' component={() => import('./not-found')} />
</Switch>
{ authTokens.accessToken ? <PopupChat /> : <></> }
<Footer />
</BrowserRouter>
);
};
export default App;
+34
View File
@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from './utilities/apply-to-body';
import { TokenContext } from './utilities/token-provider';
import Logout from './accounts/panels/logout';
const Dashboard = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Redirect to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<Link to='/account'>Account</Link>
{ authTokens.getPayload().admin ? <Link to='/admin' className='text centered'>Admin</Link> : <></> }
{ authTokens.getPayload().mod ? <Link to='/mod' className='text centered'>Mod</Link> : <></> }
<Logout />
</div>
</div>
</>
);
};
export default Dashboard;
+45
View File
@@ -0,0 +1,45 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from './utilities/apply-to-body';
import { TokenContext } from './utilities/token-provider';
import NewsFeed from './panels/news-feed';
const HomePage = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/dashboard' />;
}
return (
<>
<ApplyToBody className='homepage' />
<div className='page'>
<div className='panel above'>
<header>
<h1 className='text centered'>MERN Template</h1>
<h2 className='text centered'>This is the MERN-template</h2>
</header>
<div className='panel centered middle'>
<Link to='/signup'><button>Sign Up</button></Link>
<Link to='/login'><button>Login</button></Link>
</div>
</div>
<div className='panel below'>
<div className='central'>
<NewsFeed />
</div>
</div>
</div>
</>
);
};
export default HomePage;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Link } from 'react-router-dom';
import ApplyToBody from './utilities/apply-to-body';
const NotFound = props => {
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Page Not Found</h1>
<br />
<Link className='text centered' to='/'>Return Home</Link>
</div>
</div>
</>
);
};
export default NotFound;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Break = () => {
return (
<>
<span className='mobile hide'> - </span>
<br className='mobile show' />
</>
);
}
const Footer = () => {
return (
<footer>
<p className='text centered'>© <a href='https://krgamestudios.com'>KR Game Studios</a> 2020-2021<Break /><Link to='/privacypolicy'>Privacy Policy</Link><Break /><Link to='/credits'>Credits</Link></p>
</footer>
);
};
export default Footer;
+50
View File
@@ -0,0 +1,50 @@
import React, { useState, useEffect, useRef } from 'react';
import dateFormat from 'dateformat';
const NewsFeed = props => {
const [articles, setArticles] = useState([]);
const aborter = useRef(new AbortController()); //BUGFIX: double-renders = double fetches + react update after unmount
useEffect(() => {
//this... um...
fetch(`${process.env.NEWS_URI}/news`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
signal: aborter.current.signal //oh dear
})
.then(blob => blob.json())
.then(json => setArticles(json))
.catch(e => null) //swallow errors
;
return () => aborter.current.abort(); //This is an ugly, ugly solution, but it's the only one that works
}, []);
return (
<div className='panel'>
<h1 className='text centered'>News Feed</h1>
{articles.map((article, index) => {
console.log(article)
return (
<div key={index} className='panel'>
<hr />
<h2>{article.title}</h2>
<br />
<p><em>Written by <strong>{article.author}</strong>, {
article.edits > 0 ?
<span>Last Updated {dateFormat(article.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`})</span> :
<span>Published {dateFormat(article.createdAt, 'fullDate')}</span>
}</em></p>
<br />
<div dangerouslySetInnerHTML={{ __html: article.rendered }} />
</div>
);
})}
</div>
);
};
export default NewsFeed;
+114
View File
@@ -0,0 +1,114 @@
import React, { useState, useEffect, useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { io } from 'socket.io-client';
import '../../styles/popup-chat.css';
//TODO: I very much need to move this out of global state
const socket = io(`${process.env.CHAT_URI}/chat`);
const PopupChat = props => {
const [open, setOpen] = useState(false);
const [chatlog, setChatlog] = useState([{ emphasis: true, text: 'If chat doesn\'t load, reload the page' }]);
const inputRef = useRef();
const sendRef = useRef();
const endRef = useRef();
const authTokens = useContext(TokenContext);
const pushChatlog = line => setChatlog(prevChatlog => [...prevChatlog, line]);
useEffect(() => {
socket.on('message', message => pushChatlog(message));
socket.on('backlog', messages => setChatlog(prev => [...prev, ...messages]));
socket.on('disconnect', reason => pushChatlog({ emphasis: true, text: 'Lost connection' }));
}, []);
useEffect(() => {
if (open) {
endRef.current.scrollIntoView();
}
}, [chatlog, open]);
if (!open) {
return (
<div className='chat'>
<button type='button' className='open' onClick={() => authTokens.tokenCallback(accessToken => handleOpen(setOpen, accessToken))}>Open Chat</button>
</div>
);
}
return (
<div className='chat'>
<div className='log'>
<ul className='scrollable'>
{chatlog.map((line, index) => processLine(line, index, authTokens.accessToken))}
<li ref={endRef} />
</ul>
</div>
<input type='text' className='input' placeholder='message' onKeyPress={evt => evt.key == 'Enter' ? sendRef.current.click() : ''} ref={inputRef} />
<button type='button' className='send' onClick={() => authTokens.tokenCallback(accessToken => handleSend(inputRef, pushChatlog, authTokens.getPayload().username, accessToken))} ref={sendRef}>Send</button>
<button type='button' className='close' onClick={() => handleClose(setOpen)}>Close Chat</button>
</div>
);
};
//handlers
const handleOpen = (setOpen, accessToken) => {
setOpen(true);
socket.emit('open chat', {
accessToken
});
};
const handleClose = setOpen => {
setOpen(false);
};
const handleSend = (inputRef, pushChatlog, username, accessToken) => {
if (inputRef.current.value == '') {
return;
}
socket.emit('message', {
accessToken,
text: inputRef.current.value
});
if (!inputRef.current.value.startsWith('/')) {
pushChatlog({ username: username, text: inputRef.current.value });
}
inputRef.current.value = '';
};
//render each line
const processLine = (line, index, accessToken) => {
let content = <div className='content'>{line.username ? <span className='username'>{line.username}: </span> : ''}{line.text ? <span className='text'>{line.text}</span> : ''}</div>;
//decorators
if (line.emphasis) {
content = <em>{content}</em>;
}
if (line.strong) {
content = <strong>{content}</strong>;
}
return <li key={index} className='line'>{content}<a className='report' onClick={() => processReport(line, accessToken)}>!!!</a></li>;
};
const processReport = (line, accessToken) => {
const yes = confirm('Report this message?');
if (yes) {
socket.emit('report', {
accessToken,
index: line.index
});
}
};
export default PopupChat;
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
const Static = props => {
return (
<>
<header>
<h1 className='text centered'>Credits</h1>
</header>
<h2>MERN-template</h2>
<p>The <a href='https://github.com/krgamestudios/MERN-template'>MERN-template</a> developed by Kayne Ruse, KR Game Studios</p>
</>
);
};
export default Static;
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
const Static = props => {
return (
<header>
<h1 className="text centered">Privacy Policy</h1>
</header>
);
};
export default Static;
+19
View File
@@ -0,0 +1,19 @@
import React, { useEffect } from 'react';
//applies the classname of 'body'
const ApplyToBody = (props) => {
useEffect(() => {
document.body.classList.add(props.className);
return () => {
document.body.classList.remove(props.className);
};
}, []);
return (
<></>
);
};
export default ApplyToBody;
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import { Route } from 'react-router-dom';
import loadable from '@loadable/component';
const LazyRoute = props => {
const { component, ...lazyProps } = props;
const lazyComponent = loadable(component);
return <Route {...lazyProps} component={lazyComponent} />
};
export default LazyRoute;
@@ -3,7 +3,10 @@ import decode from 'jwt-decode';
export const TokenContext = createContext();
//DOCS: tokenFetch() and tokenCallback() are actually closures here
const TokenProvider = props => {
//state to be used
const [accessToken, setAccessToken] = useState('');
const [refreshToken, setRefreshToken] = useState('');
@@ -11,8 +14,9 @@ const TokenProvider = props => {
useEffect(() => {
setAccessToken(localStorage.getItem("accessToken") || '');
setRefreshToken(localStorage.getItem("refreshToken") || '');
}, [])
}, []);
//update the stored copies
useEffect(() => {
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
@@ -26,6 +30,62 @@ const TokenProvider = props => {
//if expired (10 minutes, normally)
const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
if (expired) {
//BUGFIX: if logging out, just skip over the refresh token
if (url === `${process.env.AUTH_URI}/auth/logout`) {
return fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Authorization': `Bearer ${bearer}`
},
body: JSON.stringify({
token: refreshToken
})
});
}
//ping the auth server for a new token
const response = await fetch(`${process.env.AUTH_URI}/auth/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;
}
//finally, delegate to fetch
return fetch(url, {
...(options || {}),
headers: {
...(options || { headers: {} }).headers,
'Authorization': `Bearer ${bearer}`
}
});
};
//access the refreshed token via callback
const tokenCallback = async (cb) => {
//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}/auth/token`, {
@@ -49,36 +109,16 @@ const TokenProvider = props => {
setAccessToken(newAuth.accessToken);
setRefreshToken(newAuth.refreshToken);
bearer = newAuth.accessToken;
//BUGFIX: logging out correctly requires the new refresh token
if (url == `${process.env.AUTH_URI}/auth/logout`) {
return fetch(`${process.env.AUTH_URI}/auth/logout`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Authorization': `Bearer ${bearer}`
},
body: JSON.stringify({
token: newAuth.refreshToken
})
});
}
//finally
return cb(newAuth.accessToken);
} else {
return cb(accessToken);
}
//finally, delegate to fetch
return fetch(url, {
...(options || {}),
headers: {
...(options || { headers: {} }).headers,
'Authorization': `Bearer ${bearer}`
}
});
};
return (
<TokenContext.Provider value={{ accessToken, refreshToken, setAccessToken, setRefreshToken, tokenFetch, getPayload: () => decode(accessToken) }}>
<TokenContext.Provider value={{ accessToken, refreshToken, setAccessToken, setRefreshToken, tokenFetch, tokenCallback, getPayload: () => decode(accessToken) }}>
{props.children}
</TokenContext.Provider>
)
+105
View File
@@ -0,0 +1,105 @@
/* clear from the rest of the CSS files */
.chat button, .chat input {
border-radius: unset !important;
font-size: unset !important;
margin: unset !important;
}
.chat {
position: fixed;
bottom: 3.6em; /* Allow space for the footer */
right: 28px;
width: 280px;
border: solid;
border-color: black;
border-width: 2px;
background-color: #CCC;
display: inline-block;
}
.chat button.open {
color: white;
background-color: grey;
}
.chat button.send {
color: white;
background-color: green;
border-style: solid;
border-width: 2px;
border-color: darkslategray;
}
.chat button.close {
color: black;
background-color: red;
border-color: maroon;
border-width: 2px;
}
.chat button {
width: 100%;
height: 2em;
opacity: 0.8;
border: unset;
}
.chat button:hover {
opacity: 1;
}
.chat .input {
width: 100%;
height: 2em;
}
.chat .log {
min-height: 280px;
}
.chat .line {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.chat .report {
color: red;
display: none;
}
.chat .line:hover {
background-color: #BBB;
}
.chat .line:hover .report {
display: flex;
}
.chat .username {
font-weight: bold;
}
.chat .scrollable {
margin: 0;
padding: 10px;
min-height: 280px;
max-height: 180px;
overflow-x: wrap;
overflow-y: scroll;
}
.chat ul {
list-style: none;
}
@media screen and (max-width: 768px) {
.chat {
position: unset;
bottom: unset;
right: unset;
width: calc(100% + 20px);
margin-left: -10px;
}
}
+323
View File
@@ -0,0 +1,323 @@
/* global defaults */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body, #root {
font: 12pt Helvetica, Arial;
min-width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
background-color: #fefefe;
color: #010101;
}
h1 {
font-size: 40pt;
font-weight: bold;
}
h2 {
font-size: 24pt;
font-weight: bold;
}
h3 {
font-size: 16pt;
font-weight: bold;
}
ul {
list-style-type: disc;
list-style-position: inside;
padding-bottom: .5em;
}
p {
margin-bottom: 0 !important;
padding-bottom: 1em;
}
pre {
padding: 5px;
margin-bottom: 1em;
background-color: lightgray;
overflow-x: scroll;
}
a {
color: blue;
text-decoration: none;
}
blockquote {
padding-top: 0.5em;
padding-left: 1em;
margin-bottom: 1em;
border-left: 3px solid #ccc;
}
.text.left {
text-align: left;
}
.text.centered {
text-align: center;
}
.text.right {
text-align: right;
}
.centered {
justify-content: center;
}
.middle {
align-items: center;
}
/* header */
header {
flex: 0 1 auto;
margin-top: 1em;
margin-bottom: 1em;
justify-self: flex-start;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* footer */
footer {
padding-top: 0.5em;
flex: 0 1 auto;
justify-self: flex-end;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
/* central display */
.central {
padding: 0 10px;
margin: 0 20%;
min-height: calc(100vh - 3.6em);
}
@media screen and (max-width: 768px) {
.central {
margin: 0;
}
}
/* components */
.page {
flex: 1 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.page.centered {
justify-content: center;
}
.page.middle {
align-items: center;
}
.panel {
flex: 0 1 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-bottom: 1em;
}
.panel.centered {
justify-content: center;
}
.panel.middle {
align-items: middle;
}
button:disabled {
opacity: 0.5;
}
input, button {
text-indent: 0.3em;
border-radius: 0.2em;
font-size: 1.8em;
margin: 0.2em;
padding: 0.2em 0;
}
textarea {
margin-left: 0.5em;
margin-right: -0.4em;
max-height: none !important;
resize: vertical;
overflow: auto;
}
/* "constrained" means reusable input area */
.constrained {
flex: 0 1 auto;
align-self: center;
max-width: 480px;
display: flex;
flex-direction: column;
margin-bottom: 1em;
}
.constrained > * {
flex: 1 0 auto;
max-height: 2em;
display: flex;
flex-direction: row;
}
.constrained button, button.constrained {
display: inline-block;
}
.constrained label {
font-size: 1.8em;
text-indent: 0.4em;
}
@media screen and (max-width: 480px) {
.constrained {
max-width: 100vw;
}
}
/* flexbox tables */
.table {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.table .row {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
@media screen and (max-width: 480px) {
.table .row {
flex-direction: column;
}
}
@media screen and (max-width: 768px) {
.table .row.tabletCollapse {
flex-direction: column;
}
}
.table .row .col {
flex: 1 1 1%;
display: flex;
flex-direction: column;
min-width: 0;
}
.table .row .col.double {
flex: 2 1 2%;
}
.table .row .col.half {
flex: 0.5 1 0.5%;
}
@media screen and (max-width: 480px) {
.table .row .col.double {
flex: 2 1 2.5%;
}
}
.table.noCollapse .row, .table .row.noCollapse {
flex-direction: row;
}
/* mobile control */
.mobile.show {
display: none;
}
@media screen and (max-width: 480px) {
.mobile.centered {
text-align: initial;
}
.mobile.show {
display: inline-block;
}
.mobile.hide {
display: none;
}
.mobile.centered {
text-align: center;
}
/* hybrid of table and mobile control */
.mobile.hide.col {
display: none;
}
.mobile.col.half {
flex: 0.5;
}
}
/* tablet control */
.tablet.show {
display: none;
}
@media screen and (max-width: 768px) {
.tablet.centered {
text-align: initial;
}
.tablet.show {
display: inline-block;
}
.tablet.hide {
display: none;
}
.tablet.centered {
text-align: center;
}
/* hybrid of table and tablet control */
.tablet.hide.col {
display: none;
}
.tablet.col.half {
flex: 0.5;
}
}
+54 -13
View File
@@ -40,6 +40,7 @@ impelented are:
* auth-server
* news-server
* chat-server
See https://github.com/krgamestudios/MERN-template/wiki for help.
`
@@ -56,13 +57,15 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
const newsName = await question('News Name', 'news');
const newsWebAddress = await question('News Web Address', `${newsName}.${projectWebAddress}`);
const newsDBUser = await question('News DB Username', newsName);
const newsDBPass = await question('News DB Password', 'charizard');
const newsDBPass = await question('News DB Password', 'venusaur');
//auth configuration
const authName = await question('Auth Name', 'auth');
const authWebAddress = await question('Auth Web Address', `${authName}.${projectWebAddress}`);
const authPostValidationHook = await question('Auth Post Validation Hook', '');
const authResetAddress = await question('Auth Reset Addr', `${projectWebAddress}/reset`);
const authDBUser = await question('Auth DB Username', authName);
const authDBPass = await question('Auth DB Password', 'venusaur');
const authDBPass = await question('Auth DB Password', 'charizard');
const emailSMTP = await question('Email SMTP', 'smtp.example.com');
const emailUser = await question('Email Address', 'foobar@example.com');
@@ -70,6 +73,10 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
const emailPhysical = await question('Physical Mailing Address', '');
//chat goes here
const chatName = await question('Chat Name', 'chat');
const chatWebAddress = await question('Chat Web Address', `${chatName}.${projectWebAddress}`);
const chatDBUser = await question('Chat DB Username', chatName);
const chatDBPass = await question('Chat DB Password', 'blastoise');
//database configuration
const dbRootPassword = await question('Database Root Password', 'password');
@@ -101,7 +108,7 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
const projectPort = 3000;
const newsPort = 3100;
const authPort = 3200;
//const chatPort = 3300;
const chatPort = 3300;
const ymlfile = `
version: "3.6"
@@ -118,7 +125,7 @@ services:
- traefik.http.routers.${projectName}router.service=${projectName}service@docker
- traefik.http.services.${projectName}service.loadbalancer.server.port=${projectPort}
environment:
- WEB_PORT=3000
- WEB_PORT=${projectPort}
- DB_HOSTNAME=database
- DB_DATABASE=${projectName}
- DB_USERNAME=${projectDBUser}
@@ -126,6 +133,7 @@ services:
- DB_TIMEZONE=${dbTimeZone}
- NEWS_URI=https://${newsWebAddress}
- AUTH_URI=https://${authWebAddress}
- CHAT_URI=https://${chatWebAddress}
- SECRET_ACCESS=${accessToken}
networks:
- app-network
@@ -143,9 +151,9 @@ services:
- traefik.http.routers.${newsName}router.entrypoints=websecure
- traefik.http.routers.${newsName}router.tls.certresolver=myresolver
- 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=${newsPort}
environment:
- WEB_PORT=3100
- WEB_PORT=${newsPort}
- DB_HOSTNAME=database
- DB_DATABASE=${newsName}
- DB_USERNAME=${newsDBUser}
@@ -173,6 +181,8 @@ services:
environment:
- WEB_PROTOCOL=https
- WEB_ADDRESS=${authWebAddress}
- HOOK_POST_VALIDATION=${authPostValidationHook}
- WEB_RESET_ADDRESS=${authResetAddress}
- WEB_PORT=${authPort}
- DB_HOSTNAME=database
- DB_DATABASE=${authName}
@@ -192,9 +202,31 @@ services:
depends_on:
- database
- traefik
#chat:
# image: krgamestudios/chat-server
${chatName}:
image: krgamestudios/chat-server:latest
ports:
- ${chatPort}
labels:
- traefik.enable=true
- traefik.http.routers.${chatName}router.rule=Host(\`${chatWebAddress}\`)
- traefik.http.routers.${chatName}router.entrypoints=websecure
- traefik.http.routers.${chatName}router.tls.certresolver=myresolver
- traefik.http.routers.${chatName}router.service=${chatName}service@docker
- traefik.http.services.${chatName}service.loadbalancer.server.port=${chatPort}
environment:
- WEB_PORT=${chatPort}
- DB_HOSTNAME=database
- DB_DATABASE=${chatName}
- DB_USERNAME=${chatDBUser}
- DB_PASSWORD=${chatDBPass}
- DB_TIMEZONE=${dbTimeZone}
- SECRET_ACCESS=${accessToken}
networks:
- app-network
depends_on:
- database
- traefik
database:
image: mariadb
@@ -215,11 +247,14 @@ services:
- --api.insecure=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entrypoints.web.http.redirections.entrypoint.permanent=true
- --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.tlschallenge=true
- --certificatesresolvers.myresolver.acme.email=${supportEmail}
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- traefik.docker.network=app-network
ports:
- 80:80
- 443:443
@@ -235,12 +270,14 @@ networks:
`;
const dockerfile = `
FROM node:15
FROM node:16
WORKDIR "/app"
COPY package*.json ./
RUN npm install
COPY . /app
RUN mkdir /app/public
RUN chown node:node /app/public
RUN npm install --production
EXPOSE ${projectPort}
USER node
ENTRYPOINT ["bash", "-c"]
CMD ["sleep 10 && npm start"]
`;
@@ -258,6 +295,10 @@ CREATE DATABASE IF NOT EXISTS ${authName};
CREATE USER IF NOT EXISTS '${authDBUser}'@'%' IDENTIFIED BY '${authDBPass}';
GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'%';
CREATE DATABASE IF NOT EXISTS ${chatName};
CREATE USER IF NOT EXISTS '${chatDBUser}'@'%' IDENTIFIED BY '${chatDBPass}';
GRANT ALL PRIVILEGES ON ${chatName}.* TO '${chatDBUser}'@'%';
FLUSH PRIVILEGES;
`;
+7216 -9356
View File
File diff suppressed because it is too large Load Diff
+31 -24
View File
@@ -1,6 +1,6 @@
{
"name": "mern-template",
"version": "1.0.0",
"version": "1.2.0",
"description": "A website template using the MERN stack.",
"main": "server/server.js",
"scripts": {
@@ -8,10 +8,13 @@
"build": "npm run build:server && npm run build:client",
"build:server": "exit 0",
"build:client": "webpack --env=production --config webpack.config.js",
"dev": "concurrently npm:watch:server npm:watch:client",
"watch:server": "nodemon . --ext js,jsx,json --ignore 'node_modules/*'",
"watch:client": "webpack serve --env=development --config webpack.config.js",
"analyzer": "webpack --env=production --analyzer --config webpack.config.js"
"dev": "concurrently npm:dev:server npm:dev:client",
"dev:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'",
"dev:client": "webpack serve --env=development --config webpack.config.js",
"local": "concurrently npm:local:server npm:local:client",
"local:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'",
"local:client": "webpack serve --env=local --config webpack.config.js",
"analyze": "webpack --env=production --env=analyze --config webpack.config.js"
},
"repository": {
"type": "git",
@@ -24,34 +27,38 @@
},
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": {
"@babel/core": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8",
"@babel/preset-react": "^7.14.5",
"@loadable/component": "^5.15.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"concurrently": "^5.3.0",
"compression-webpack-plugin": "^8.0.1",
"concurrently": "^6.2.0",
"css-loader": "^6.2.0",
"dateformat": "^4.5.1",
"dotenv": "^8.2.0",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"html-webpack-plugin": "^5.0.0-alpha.14",
"html-webpack-plugin": "^5.3.2",
"jwt-decode": "^3.1.2",
"mariadb": "^2.5.2",
"mariadb": "^2.5.4",
"query-string": "^7.0.1",
"raw-loader": "^4.0.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-dropdown-select": "^4.7.4",
"react-loadable": "^5.5.0",
"react-markdown": "^5.0.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"sequelize": "^6.4.0",
"universal-cookie": "^4.0.4",
"webpack": "^5.15.0",
"webpack-cli": "^4.3.1"
"react-select": "^5.2.1",
"rehype-raw": "^5.1.0",
"sequelize": "^6.6.5",
"socket.io-client": "^4.1.3",
"style-loader": "^3.2.1",
"webpack": "^5.46.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2"
},
"devDependencies": {
"nodemon": "^2.0.7",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-dev-server": "^3.11.2"
"nodemon": "^2.0.12",
"webpack-dev-server": "^4.6.0"
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME
host: process.env.DB_HOSTNAME,
dialect: 'mariadb',
timezone: process.env.DB_TIMEZONE,
logging: false
logging: process.env.DB_LOGGING ? console.log : false
});
sequelize.sync();
+16 -2
View File
@@ -8,10 +8,24 @@ const path = require('path');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const bodyParser = require('body-parser');
//config
app.use(bodyParser.json());
app.use(express.json());
//handle compressed files (middleware)
app.get('*.js', (req, res, next) => {
req.url = req.url + '.gz';
res.set('Content-Encoding', 'gzip');
res.set('Content-Type', 'text/javascript');
next();
});
app.get('*.css', (req, res, next) => {
req.url = req.url + '.gz';
res.set('Content-Encoding', 'gzip');
res.set('Content-Type', 'text/css');
next();
});
//database connection
const database = require('./database');
+43 -42
View File
@@ -2,23 +2,24 @@
const { DefinePlugin } = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
//libraries
const path = require('path');
//the exported config function
module.exports = ({ production, analyzer }) => {
module.exports = ({ production, development, local, analyze }) => {
return {
mode: production ? "production" : "development",
entry: path.resolve(__dirname, 'client', 'client.jsx'),
output: {
path: path.resolve(__dirname, 'public'),
publicPath: '/',
filename: '[name].[chunkhash].js',
sourceMapFilename: '[name].[chunkhash].js.map'
},
devtool: 'eval-source-map',
devtool: production ? false : 'eval-source-map',
resolve: {
extensions: ['.js', '.jsx']
},
@@ -32,11 +33,15 @@ module.exports = ({ production, analyzer }) => {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['react-loadable/babel', '@babel/plugin-syntax-dynamic-import']
plugins: ['@babel/plugin-syntax-dynamic-import']
}
}
]
},
{
test: /\.(css)$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(md)$/,
use: [
@@ -51,9 +56,9 @@ module.exports = ({ production, analyzer }) => {
new DefinePlugin({
'process.env': {
'PRODUCTION': production,
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.eggtrainer.com"',
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.eggtrainer.com"',
// 'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.eggtrainer.com"',
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : development ? '"https://dev-news.krgamestudios.com"' : '"http://localhost:3100"',
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : development ? '"https://dev-auth.krgamestudios.com"' : '"http://localhost:3200"',
'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : development ? '"https://dev-chat.krgamestudios.com"' : '"http://localhost:3300"',
}
}),
new CleanWebpackPlugin({
@@ -67,46 +72,42 @@ module.exports = ({ production, analyzer }) => {
removeAttributeQuotes: production
}
}),
new CompressionPlugin({
filename: "[path][base].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$/,
minRatio: 0.8
}),
new BundleAnalyzerPlugin({
analyzerMode: analyzer ? 'server' : 'disabled'
analyzerMode: analyze ? 'server' : 'disabled'
})
],
devServer: {
contentBase: path.resolve(__dirname, 'public'),
compress: true,
port: 3001,
proxy: {
'/api/': 'http://localhost:3000/'
},
overlay: {
errors: true
},
stats: {
colors: true,
hash: false,
version: false,
timings: false,
assets: false,
chunks: false,
modules: false,
reasons: false,
children: false,
source: false,
errors: true,
errorDetails: false,
warnings: true,
publicPath: false
},
host: '0.0.0.0',
disableHostCheck: true,
clientLogLevel: 'silent',
historyApiFallback: true,
hot: true,
injectHot: true
},
watchOptions: {
ignored: /(node_modules)/
host: 'localhost',
port: 3001,
client: {
overlay: {
errors: true,
warnings: true,
},
},
watchFiles: {
options: {
ignored: ['node_modules/**']
}
},
proxy: {
'/api': {
target: 'http://localhost:3000'
}
},
static: '/public',
historyApiFallback: true
}
}
};