Compare commits

...

111 Commits

Author SHA1 Message Date
Kayne Ruse 335c7008aa Bumped version number 2022-12-31 19:19:38 +00:00
Kayne Ruse e89b0645ca Fixed URLSearchParams 2022-12-04 12:08:12 +00:00
Kayne Ruse 36e79a513f Fixed reset page 2022-12-04 11:48:23 +00:00
Kayne Ruse 6ef0affcf6 Fixed useEffect() usage 2022-12-01 12:48:04 +00:00
Kayne Ruse cb0c1284bf You're now kicked out to the root page when the refresh token expires 2022-12-01 12:05:43 +00:00
Kayne Ruse 5cf4b66894 Fixed redirect issue 2022-11-29 04:53:29 +00:00
Kayne Ruse 45cf281c91 Updated dependencies 2022-11-13 02:07:15 +00:00
Kayne Ruse 2794b4c724 Updated dependencies 2022-11-13 01:56:14 +00:00
Kayne Ruse b1f49a4166 Removed a dependency 2022-08-01 11:09:35 +01:00
Kayne Ruse 4cbf67dcbb Updated dependencies 2022-08-01 10:34:35 +01:00
Kayne Ruse fd29385cf8 Update README.md 2022-07-28 03:47:23 +10:00
Kayne Ruse 8e81dccef6 Added cookies 2022-07-26 10:18:49 +01:00
Kayne Ruse adeb8c4267 Updated dependencies 2022-07-23 11:45:33 +01:00
Kayne Ruse eb6c3a40d7 Added hook array to auth config 2022-06-15 23:50:58 +01:00
Kayne Ruse 4d4a0b5401 Updated dependencies 2022-06-10 17:07:56 +01:00
Kayne Ruse 490860159e Cleaned up usage of fetch 2022-06-10 16:53:42 +01:00
Kayne Ruse ed01fe6db5 Addressed #34 2022-05-30 06:16:10 +01:00
Kayne Ruse cfb8d20ad2 Merge remote-tracking branch 'refs/remotes/origin/main' 2022-05-30 06:10:20 +01:00
Kayne Ruse d44cae397d Updated dependencies, addressed #33 2022-05-30 06:10:01 +01:00
Kayne Ruse b97fff05b3 Added FUNDING.yml 2022-02-13 07:49:49 +11:00
Kayne Ruse 22703bfbcb Swapped day and month around 2022-01-09 15:02:06 +00:00
Kayne Ruse 14a3c9eabe Added timestamps to the chatbox 2022-01-09 06:39:13 +00:00
Kayne Ruse 8e90a4a540 Trying a fix 2022-01-08 04:59:21 +00:00
Kayne Ruse 1d3c94a1aa Shortened the code with a ternary 2022-01-06 14:26:30 +00:00
Kayne Ruse ca5e79ccf3 Merge pull request #32 from kcampbelljr/macos-docs
Macos docs
2022-01-07 01:15:36 +11:00
Kayne Ruse 03acce1907 Removed an unused dependency 2022-01-06 14:00:51 +00:00
Keith Campbell c0b7280533 removed macos, updated code on original configure script, testing.. 2022-01-05 23:23:09 -05:00
Keith Campbell 2925cce7ca created a copy of the configure script and made the changes needed for now, should we add a macos check to the original configure script instead? 2022-01-04 23:51:02 -05:00
Kayne Ruse b90670b922 Tweaked credits 2022-01-03 19:21:57 +00:00
Kayne Ruse 290f25f898 Added prox to the credits 2022-01-03 08:45:27 +00:00
Kayne Ruse 3cdef433f9 Tweaked moderator privileges 2022-01-02 18:11:11 +00:00
Kayne Ruse 53c8ddab54 Removed rehype-raw 2021-12-30 14:28:10 +00:00
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
57 changed files with 6208 additions and 15306 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
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: krgamestudios
ko_fi: krgamestudios
custom: ["https://www.paypal.com/donate/?hosted_button_id=73Q82T2ZHV8AA"]
+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
+40 -33
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
```
@@ -34,7 +39,7 @@ docker-compose up --build
To set up this template in development mode:
1. Ensure mariadb is running in your development environment
2. Run `mariadb sql/create_database.sql` as the root user
2. Run `mariadb tools/create_database.sql` as the root user
3. Run `npm install`
4. Run `cp .envdev .env` and enter your details into the `.env` file
5. Execute `npm run dev`
@@ -42,15 +47,28 @@ 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
- HttpOnly cookies for security
- 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 +76,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?)
+11 -9
View File
@@ -2,14 +2,16 @@
import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOM from 'react-dom/client';
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>
<App />
</TokenProvider>,
document.querySelector('#root')
);
ReactDOM
.createRoot(document.getElementById('root'))
.render(
<TokenProvider>
<App />
</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;
-105
View File
@@ -1,105 +0,0 @@
import React, { useEffect, useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
import DeleteAccount from '../panels/delete-account';
const Account = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
//grab the user's info
useEffect(() => {
authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
}
})
.then(blob => blob.json())
.then(json => contactRef.current.checked = json.contact)
.catch(e => console.error(e))
;
}, []);
//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);
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>
<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>
</div>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount className='constricted' />
</div>
);
};
const update = async (password, retype, contact, tokenFetch) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'PATCH',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
contact
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null];
}
}
export default Account;
-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;
-107
View File
@@ -1,107 +0,0 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const LogIn = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const emailRef = useRef();
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);
}
//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} />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<button type='submit'>Login</button>
</form>
</div>
);
};
//DOCS: returns two values: err and authTokens
const handleSubmit = async (email, password) => {
email = email.trim();
const err = handleValidation(email, password);
if (err) {
return [err, false];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
email,
password,
})
});
//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];
};
//returns an error message, or null on success
const handleValidation = (email, password) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
return null;
};
export default LogIn;
-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;
-129
View File
@@ -1,129 +0,0 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
//utilities
const validateEmail = require('../../../common/utilities/validate-email');
const validateUsername = require('../../../common/utilities/validate-username');
const SignUp = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const emailRef = useRef();
const usernameRef = useRef();
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = 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);
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<div>
<label htmlFor='email'>Email:</label>
<input type='email' name='email' ref={emailRef} />
</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>
);
};
const handleSubmit = async (email, username, password, retype, contact) => {
email = email.trim();
username = username.trim();
const err = handleValidation(email, username, password, retype);
if (err) {
return [err];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
email,
username,
password,
contact
})
});
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, username, password, retype) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (!validateUsername(username)) {
return 'invalid username';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
if (password !== retype) {
return 'passwords do not match';
}
return null;
};
export default SignUp;
-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;
@@ -1,87 +0,0 @@
import React, { useState, useEffect, createContext } from 'react';
import decode from 'jwt-decode';
export const TokenContext = createContext();
const TokenProvider = props => {
const [accessToken, setAccessToken] = useState('');
const [refreshToken, setRefreshToken] = useState('');
//make the access and refresh tokens persist between reloads
useEffect(() => {
setAccessToken(localStorage.getItem("accessToken") || '');
setRefreshToken(localStorage.getItem("refreshToken") || '');
}, [])
useEffect(() => {
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
}, [accessToken, refreshToken]);
//wrap the default fetch function
const tokenFetch = async (url, options) => {
//use this?
let bearer = accessToken;
//if expired (10 minutes, normally)
const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
if (expired) {
//ping the auth server for a new token
const response = await fetch(`${process.env.AUTH_URI}/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;
//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, 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) }}>
{props.children}
</TokenContext.Provider>
)
};
export default TokenProvider;
-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
+101
View File
@@ -0,0 +1,101 @@
import React, { useEffect, useContext, useRef } from 'react';
import { Link, Navigate } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import DeleteAccount from './panels/delete-account';
const Account = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Navigate to='/' />;
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
//grab the user's info
useEffect(() => {
authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`)
.then(blob => blob.json())
.then(json => contactRef.current.checked = json.contact)
.catch(e => console.error(e))
;
}, []);
//render the thing
return (
<>
<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;
}
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} />
<span>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</span>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount />
</div>
<Link to='/' className='text centered'>Return Home</Link>\
</div>
</div>
</div>
</>
);
};
const update = async (password, retype, contact, tokenFetch) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
contact
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null];
}
}
export default Account;
+109
View File
@@ -0,0 +1,109 @@
import React, { useContext, useRef } from 'react';
import { Link, Navigate } 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 => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Navigate to='/' />;
}
//refs
const emailRef = useRef();
const passwordRef = useRef();
return (
<>
<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, accessToken] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
//save auth tokens and redirect
if (accessToken) {
authTokens.setAccessToken(accessToken);
return <Navigate to='/' />;
}
}
}>
<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>
</>
);
};
//DOCS: returns two values: err and authTokens
const handleSubmit = async (email, password) => {
email = email.trim();
const err = handleValidation(email, password);
if (err) {
return [err, false];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password,
}),
credentials: 'include'
});
//handle errors
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.error(err);
return [err, false];
}
//return the new auth tokens
const accessToken = await result.text();
return [null, accessToken];
};
//returns an error message, or null on success
const handleValidation = (email, password) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
return null;
};
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>
);
};
@@ -36,7 +38,6 @@ const handleSubmit = async (password, authTokens) => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'DELETE',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -50,14 +51,7 @@ const handleSubmit = async (password, authTokens) => {
//force a logout
const result2 = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, {
method: 'DELETE',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: authTokens.refreshToken
})
method: 'DELETE'
});
if (!result2.ok) {
@@ -65,7 +59,6 @@ const handleSubmit = async (password, authTokens) => {
}
authTokens.setAccessToken('');
authTokens.setRefreshToken('');
return [null];
};
+29
View File
@@ -0,0 +1,29 @@
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'
});
//any problems?
if (!result.ok) {
console.error(await result.text());
} else {
authTokens.setAccessToken('');
}
}}>Logout</Link>
</>
);
};
export default Logout;
+97
View File
@@ -0,0 +1,97 @@
import React, { useContext, useRef } from 'react';
import { Link, useNavigate } 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 => {
//history
const navigate = useNavigate();
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
navigate("/");
}
//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) {
navigate("/");
}
}
}>
<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'
},
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;
+87
View File
@@ -0,0 +1,87 @@
import React, { useContext, useRef } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const Reset = props => {
//params
const [params, setParams] = useSearchParams(); //the URLSearchParams API
//history
const navigate = useNavigate();
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken || !params.has('email') || !params.has('token')) {
navigate("/");
}
//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, params);
if (err) {
alert(err);
return;
}
alert('Details updated'); //TODO: replace with a message from the auth server
//redirect
if (redirect) {
navigate("/");
}
}}>
<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, params) => {
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=${params.get('email')}&token=${params.get('token')}`, {
method: 'PATCH',
headers: {
'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;
+129
View File
@@ -0,0 +1,129 @@
import React, { useContext, useRef } from 'react';
import { Link, useNavigate } 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 validateUsername = require('../../../common/utilities/validate-username');
const Signup = props => {
//history
const navigate = useNavigate();
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
navigate("/");
}
//refs
const emailRef = useRef();
const usernameRef = useRef();
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
const signupRef = useRef();
return (
<>
<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) {
navigate("/");
}
}
}>
<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>
</>
);
};
const handleSubmit = async (email, username, password, retype, contact) => {
email = email.trim();
username = username.trim();
const err = handleValidation(email, username, password, retype);
if (err) {
return [err];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
username,
password,
contact
})
});
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, username, password, retype) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (!validateUsername(username)) {
return 'invalid username';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
if (password !== retype) {
return 'passwords do not match';
}
return null;
};
export default Signup;
+37
View File
@@ -0,0 +1,37 @@
import React, { useContext } from 'react';
import { Link, Navigate } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
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 <Navigate to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Administration Tools</h1>
<br />
<GrantAdmin />
<br />
<GrantMod />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Admin;
+43
View File
@@ -0,0 +1,43 @@
import React, { useContext } from 'react';
import { Link, Navigate } 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 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 <Navigate to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Moderation Tools</h1>
<NewsPublisher />
<br />
<NewsEditor />
<br />
<BanUser />
<br />
<ChatReports />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Mod;
@@ -0,0 +1,61 @@
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'
},
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,62 @@
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(() => {
authTokens.tokenFetch(`${process.env.CHAT_URI}/admin/reports`)
.then(res => res.json())
.then(json => {
setReports(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'
},
body: JSON.stringify({ chatlogIndex })
});
setReports(reports => reports.filter(report => report.chatlogIndex != chatlogIndex));
};
export default ChatReports;
@@ -0,0 +1,70 @@
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'
},
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,70 @@
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'
},
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
@@ -17,57 +17,40 @@ const NewsEditor = props => {
const [index, setIndex] = useState(null);
//run once
useEffect(async () => {
const result = await fetch(`${process.env.NEWS_URI}/news/metadata?limit=999`, {
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());
}
useEffect(() => {
fetch(`${process.env.NEWS_URI}/news/metadata?limit=999`)
.then(res => res.json())
.then(json => {
setArticles(json)
})
;
}, []);
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;
<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}`);
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);
}
}}
placeholder='Select Article'
/>
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>
<form onSubmit={async evt => {
<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 +61,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 => {
@@ -122,8 +94,7 @@ const handleSubmit = async (title, author, body, index, tokenFetch) => {
const result = await tokenFetch(`${process.env.NEWS_URI}/news/${index}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
@@ -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>
@@ -57,8 +46,7 @@ const handleSubmit = async (title, author, body, tokenFetch) => {
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
'Content-Type': 'application/json'
},
body: JSON.stringify({
title,
+60
View File
@@ -0,0 +1,60 @@
//react
import React, { useContext, Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { TokenContext } from './utilities/token-provider';
//styling
import '../styles/styles.css';
//common components
import Footer from './panels/footer';
import PopupChat from './panels/popup-chat';
//lazy wrappers
const Homepage = lazy(() => import('./homepage'));
const Signup = lazy(() => import('./accounts/signup'));
const Login = lazy(() => import('./accounts/login'));
const Account = lazy(() => import('./accounts/account'));
const Dashboard = lazy(() => import('./dashboard'));
const Recover = lazy(() => import('./accounts/recover'));
const Reset = lazy(() => import('./accounts/reset'));
const Admin = lazy(() => import('./administration/admin'));
const Mod = lazy(() => import('./administration/mod'));
const PrivacyPolicy = lazy(() => import('./static/privacy-policy'));
const Credits = lazy(() => import('./static/credits'));
const NotFound = lazy(() => import('./not-found'));
const App = props => {
const authTokens = useContext(TokenContext);
//default render
return (
<BrowserRouter>
<Suspense>
<Routes>
<Route exact path='/' element={<Homepage />} />
<Route path='/signup' element={<Signup />} />
<Route path='/login' element={<Login />} />
<Route path='/account' element={<Account />} />
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/recover' element={<Recover />} />
<Route path='/reset' element={<Reset />} />
<Route path='/admin' element={<Admin />} />
<Route path='/mod' element={<Mod />} />
<Route path='/privacypolicy' element={<PrivacyPolicy />} />
<Route path='/credits' element={<Credits />} />
<Route path='*' element={<NotFound />} />
</Routes>
</Suspense>
{ authTokens.accessToken ? <PopupChat /> : <></> }
<Footer />
</BrowserRouter>
);
};
export default App;
+34
View File
@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Link, Navigate } 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 <Navigate 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, Navigate } 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 <Navigate 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-2022<Break /><Link to='/privacypolicy'>Privacy Policy</Link><Break /><Link to='/credits'>Credits</Link></p>
</footer>
);
};
export default Footer;
+44
View File
@@ -0,0 +1,44 @@
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`, {
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) => {
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;
+144
View File
@@ -0,0 +1,144 @@
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({ createdAt: (new Date(Date.now())).toISOString(), username: username, text: inputRef.current.value });
}
inputRef.current.value = '';
};
//render each line
const processLine = (line, index, accessToken) => {
//utility functions
const isValidDate = d => {
return d instanceof Date && !isNaN(d);
};
const isToday = d => {
const now = new Date(Date.now());
return d.getDate() == now.getDate() && d.getMonth() == now.getMonth() && d.getFullYear() == now.getFullYear();
};
const isThisYear = d => {
const now = new Date(Date.now());
return d.getFullYear() == now.getFullYear();
};
//parse the date
const date = new Date(line.createdAt);
//split it up so we can format each field individually
const year = `${date.getFullYear()}`;
const month = `${date.getMonth() + 1}`;
const day = `${date.getDate()}`;
const hours = `${date.getHours()}`;
const minutes = `${date.getMinutes()}`.padStart(2, '0');
//combine into the final timestamp
const timestamp = !isValidDate(date) ? '' : isToday(date) ? `${hours}:${minutes}` : isThisYear(date) ? `${month}/${day}` : `${year}`;
//generate the content string
let content = <div className='content row'>{timestamp.length > 0 ? <span className='timestamp col'>{timestamp}</span> : null }<span className='inner col'>{line.username ? <span className='username'>{line.username}: </span> : ''}{line.text ? <span className='text'>{line.text}</span> : ''}</span></div>;
//decorators
if (line.emphasis) {
content = <em>{content}</em>;
}
if (line.strong) {
content = <strong>{content}</strong>;
}
return <li key={index} className='line table noCollapse'>{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;
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Static = props => {
return (
<div className='page central'>
<header>
<h1 className='text centered'>Credits</h1>
</header>
<h2 className='text centered'>MERN-template</h2>
<p>The <a href='https://github.com/krgamestudios/MERN-template'>MERN-template</a> developed by Kayne Ruse, KR Game Studios</p>
<Link className='text centered' to='/'>Return Home</Link>
</div>
);
};
export default Static;
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Static = props => {
return (
<div className='page central'>
<header>
<h1 className="text centered">Privacy Policy</h1>
<Link className='text centered' to='/'>Return Home</Link>
</header>
</div>
);
};
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;
+119
View File
@@ -0,0 +1,119 @@
import React, { useState, useEffect, createContext } from 'react';
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('');
//force a logout under certain conditions
const forceLogout = () => {
localStorage.removeItem("accessToken");
setAccessToken("");
};
//make the access token persist between reloads
useEffect(() => {
setAccessToken(localStorage.getItem("accessToken") || '');
}, []);
//update the stored copies
useEffect(() => {
localStorage.setItem("accessToken", accessToken);
}, [accessToken]);
//wrap the default fetch function
const tokenFetch = async (url, options) => {
//use this?
let bearer = accessToken;
//if expired (10 minutes, normally)
const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
if (expired) {
//BUGFIX: if logging out, just skip over the refresh token
if (url === `${process.env.AUTH_URI}/auth/logout`) {
return fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${bearer}`
},
credentials: 'include'
});
}
//ping the auth server for a new access token
const response = await fetch(`${process.env.AUTH_URI}/auth/token`, {
method: 'POST',
credentials: 'include'
});
//any errors, throw them
if (!response.ok) {
if (response.status == 403) {
forceLogout();
}
throw `${response.status}: ${await response.text()}`;
}
//save the new auth stuff (setting bearer as well)
const newAuth = await response.json();
setAccessToken(newAuth.accessToken);
bearer = newAuth.accessToken;
}
//finally, delegate to fetch
return fetch(url, {
...(options || {}),
headers: {
...(options || { headers: {} }).headers,
'Authorization': `Bearer ${bearer}`
},
credentials: 'include'
});
};
//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`, {
method: 'POST',
credentials: 'include'
});
//any errors, throw them
if (!response.ok) {
if (response.status == 403) {
forceLogout();
}
throw `${response.status}: ${await response.text()}`;
}
//save the new auth stuff (setting bearer as well)
const newAuth = await response.json();
setAccessToken(newAuth.accessToken);
//finally
return cb(newAuth.accessToken);
} else {
return cb(accessToken);
}
};
return (
<TokenContext.Provider value={{ accessToken, setAccessToken, tokenFetch, tokenCallback, getPayload: () => decode(accessToken) }}>
{props.children}
</TokenContext.Provider>
)
};
export default TokenProvider;
+115
View File
@@ -0,0 +1,115 @@
/* 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 .timestamp {
max-width: 44px;
}
.chat .inner {
flex: 1 !important;
display: inline-block !important;
flex-direction: row !important;
}
.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;
}
}
+1 -1
View File
@@ -22,6 +22,6 @@
<meta property="og:description" content="" />
</head>
<body>
<div id = "root"></div>
<div id="root"></div>
</body>
</html>
+69 -19
View File
@@ -40,11 +40,20 @@ impelented are:
* auth-server
* news-server
* chat-server
See https://github.com/krgamestudios/MERN-template/wiki for help.
`
);
//determine local computer address for mac user vs everyone else
let macUser = '';
while (macUser.toLowerCase() !== 'yes' && macUser.toLowerCase() !== 'no') {
macUser = await question('Will the MERN Template be running locally on a MacOS system? (yes or no)', '');
}
const localAddress = macUser ? 'localhost' : '%';
//project configuration
const projectName = await question('Project Name', 'template');
const projectWebAddress = await question('Project Web Address', 'example.com');
@@ -56,13 +65,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 authPostValidationHookArray = await question('Auth Post Validation Hook Array', '');
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 +81,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 +116,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 +133,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 +141,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 +159,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 +189,8 @@ services:
environment:
- WEB_PROTOCOL=https
- WEB_ADDRESS=${authWebAddress}
- HOOK_POST_VALIDATION_ARRAY=${authPostValidationHookArray}
- WEB_RESET_ADDRESS=${authResetAddress}
- WEB_PORT=${authPort}
- DB_HOSTNAME=database
- DB_DATABASE=${authName}
@@ -192,9 +210,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 +255,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,32 +278,39 @@ networks:
`;
const dockerfile = `
FROM node:15
FROM node:18
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"]
`;
const sqlfile = `
CREATE DATABASE IF NOT EXISTS ${projectName};
CREATE USER IF NOT EXISTS '${projectDBUser}'@'%' IDENTIFIED BY '${projectDBPass}';
GRANT ALL PRIVILEGES ON ${projectName}.* TO '${projectDBUser}'@'%';
CREATE USER IF NOT EXISTS '${projectDBUser}'@'${localAddress}' IDENTIFIED BY '${projectDBPass}';
GRANT ALL PRIVILEGES ON ${projectName}.* TO '${projectDBUser}'@'${localAddress}';
CREATE DATABASE IF NOT EXISTS ${newsName};
CREATE USER IF NOT EXISTS '${newsDBUser}'@'%' IDENTIFIED BY '${newsDBPass}';
GRANT ALL PRIVILEGES ON ${newsName}.* TO '${newsDBUser}'@'%';
CREATE USER IF NOT EXISTS '${newsDBUser}'@'${localAddress}' IDENTIFIED BY '${newsDBPass}';
GRANT ALL PRIVILEGES ON ${newsName}.* TO '${newsDBUser}'@'${localAddress}';
CREATE DATABASE IF NOT EXISTS ${authName};
CREATE USER IF NOT EXISTS '${authDBUser}'@'%' IDENTIFIED BY '${authDBPass}';
GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'%';
CREATE USER IF NOT EXISTS '${authDBUser}'@'${localAddress}' IDENTIFIED BY '${authDBPass}';
GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'${localAddress}';
CREATE DATABASE IF NOT EXISTS ${chatName};
CREATE USER IF NOT EXISTS '${chatDBUser}'@'${localAddress}' IDENTIFIED BY '${chatDBPass}';
GRANT ALL PRIVILEGES ON ${chatName}.* TO '${chatDBUser}'@'${localAddress}';
FLUSH PRIVILEGES;
`;
fs.writeFileSync('docker-compose.yml', ymlfile);
fs.writeFileSync('Dockerfile', dockerfile);
fs.writeFileSync('startup.sql', sqlfile);
+3926 -14131
View File
File diff suppressed because it is too large Load Diff
+34 -31
View File
@@ -1,6 +1,6 @@
{
"name": "mern-template",
"version": "1.0.0",
"version": "1.4.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,34 @@
},
"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-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"concurrently": "^5.3.0",
"dateformat": "^4.5.1",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"html-webpack-plugin": "^5.0.0-alpha.14",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^8.3.0",
"clean-webpack-plugin": "^4.0.0",
"compression-webpack-plugin": "^10.0.0",
"concurrently": "^7.6.0",
"css-loader": "^6.7.2",
"dateformat": "^5.0.3",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"html-webpack-plugin": "^5.5.0",
"jwt-decode": "^3.1.2",
"mariadb": "^2.5.2",
"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-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"
"mariadb": "^3.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^6.3.0",
"react-router-dom": "^6.4.3",
"react-select": "^5.6.1",
"sequelize": "^6.25.8",
"socket.io-client": "^4.5.4",
"style-loader": "^3.3.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-cli": "^4.10.0"
},
"devDependencies": {
"nodemon": "^2.0.7",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-dev-server": "^3.11.2"
"nodemon": "^2.0.20",
"webpack-dev-server": "^4.11.1"
}
}
+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');
+41 -48
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,18 +33,14 @@ 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: /\.(md)$/,
use: [
{
loader: 'raw-loader'
},
],
test: /\.(css)$/,
use: ['style-loader', 'css-loader']
},
]
},
@@ -51,9 +48,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 +64,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
}
}
};