Compare commits

..

26 Commits

Author SHA1 Message Date
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
47 changed files with 3010 additions and 6051 deletions
+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
+94 -91
View File
@@ -1,91 +1,94 @@
# MERN-template
A website template using the MERN stack. The primary technology involved is:
* React
* Nodejs
* MariaDB (with Sequelize)
* 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).
* 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
A clean install is this easy:
```
git clone https://github.com/krgamestudios/MERN-template.git
cd MERN-template
npm install
node configure-script.js
docker-compose up --build
```
# Setup Development
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
3. Run `npm install`
4. Run `cp .envdev .env` and enter your details into the `.env` file
5. Execute `npm run dev`
6. Navigate to `http://localhost:3001` in your web browser
# Features List
- 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
- 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)
# Coming Soon
- Full documentation
- Modding tutorials
# Coming Eventually
- 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
- Better compression for client files
- Backend for leaderboards (modding tutorial?)
- Backend for energy systems (modding tutorial?)
- Backend for items, shops, trading and currency
# MERN-template
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 (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).
* 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
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
```
# Setup Development
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
3. Run `npm install`
4. Run `cp .envdev .env` and enter your details into the `.env` file
5. Execute `npm run dev`
6. Navigate to `http://localhost:3001` in your web browser
# Features List
- 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
- 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)
# Coming Soon
- Full documentation
- Modding tutorials
- Fully Featured News Blog (as a microservice)
- Individual pages for news articles
# Coming Eventually
- Fully Featured News Blog (as a microservice)
- Restore deleted articles
- Undo edits
- Fully Featured Chat System (as a microservice)
- Custom emoji
- Private messaging
- Broadcasting to all channels
- Badges next to usernames
- Backend for leaderboards (modding tutorial?)
- Backend for energy systems (modding tutorial?)
- Backend for items, shops, trading and currency (modding tutorial?)
+2 -2
View File
@@ -4,8 +4,8 @@ import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import TokenProvider from './components/utilities/token-provider';
import App from './pages/app';
import TokenProvider from './pages/utilities/token-provider';
ReactDOM.render(
<TokenProvider>
-49
View File
@@ -1,49 +0,0 @@
//react
import React, { useContext } from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import { TokenContext } from './utilities/token-provider';
//library components
import LazyRoute from './utilities/lazy-route';
import Markdown from './panels/markdown';
//styling
//import a styling template here
//common components
import Header from './panels/header';
import Footer from './panels/footer';
import PopupChat from './panels/popup-chat';
const App = props => {
const authTokens = useContext(TokenContext);
//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='/recover' component={() => import('./pages/recover')} />
<LazyRoute path='/reset' component={() => import('./pages/reset')} />
<LazyRoute path='/admin' component={() => import('./pages/admin')} />
<LazyRoute path='/mod' component={() => import('./pages/mod')} />
<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>
{ authTokens.accessToken ? <PopupChat /> : <></> }
<Footer />
</BrowserRouter>
);
};
export default App;
-32
View File
@@ -1,32 +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 GrantAdmin from '../panels/grant-admin';
import GrantMod from '../panels/grant-mod';
const Admin = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !authTokens.getPayload().admin) {
return <Redirect to='/' />;
}
return (
<div className='page'>
<h1 className='centered'>Administration Tools</h1>
<NewsPublisher />
<NewsEditor />
<GrantAdmin />
<GrantMod />
</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;
-27
View File
@@ -1,27 +0,0 @@
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
import ChatReports from '../panels/chat-reports';
import BanUser from '../panels/ban-user';
const Mod = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !(authTokens.getPayload().admin || authTokens.getPayload().mod)) {
return <Redirect to='/' />;
}
return (
<div className='page'>
<h1 className='centered'>Moderation Tools</h1>
<ChatReports />
<BanUser />
</div>
);
};
export default Mod;
-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;
-67
View File
@@ -1,67 +0,0 @@
import React, { useState, useEffect, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider';
import dateFormat from 'dateformat';
const ChatReports = props => {
const [reports, setReports] = useState([]);
const authTokens = useContext(TokenContext);
useEffect(async () => {
const result = await authTokens.tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
}
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
setReports(await result.json());
}
}, []);
return (
<table>
<thead>
<tr>
<th>Date</th>
<th>Username</th>
<th>Room Name</th>
<th>Content</th>
<th>Reported By</th>
</tr>
</thead>
<tbody>
{reports.map((report, index) => (
<tr key={index}>
<td>{dateFormat(report.chatlog.createdAt, 'yyyy-mm-dd, H:MM:ss')}</td>
<td>{report.chatlog.username}</td>
<td>{report.chatlog.room}</td>
<td>{report.chatlog.text}</td>
<td>{report.reporter.join(', ')}</td>
<td><button onClick={() => deleteReportsFor(report.chatlogIndex, authTokens.tokenFetch, setReports)}>Delete</button></td>
</tr>
))}
</tbody>
</table>
);
};
const deleteReportsFor = (chatlogIndex, tokenFetch, setReports) => {
tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ chatlogIndex })
});
setReports(reports => reports.filter(report => report.chatlogIndex != chatlogIndex));
};
export default ChatReports;
-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;
-78
View File
@@ -1,78 +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>
<span> - </span>
<Link to='/recover'>Recover</Link>
</div>
);
};
const Member = () => {
const authTokens = useContext(TokenContext);
return (
<div>
<Link to='/account'>Account</Link>
<span> - </span>
{ authTokens.getPayload().admin ?
<span>
<Link to='/admin'>Admin</Link>
<span> - </span>
</span>:
<span />
}
{ authTokens.getPayload().mod ?
<span>
<Link to='/mod'>Moderation</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;
+6 -2
View File
@@ -1,6 +1,10 @@
# Credits
<header>
<h1 class="text centered">Credits</h1>
</header>
MERN Template developed by Kayne Ruse, KR Game Studios
## MERN-template
The MERN-template developed by Kayne Ruse, KR Game Studios
[https://github.com/krgamestudios/MERN-template](https://github.com/krgamestudios/MERN-template)
+3 -1
View File
@@ -1,2 +1,4 @@
# Privacy Policy
<header>
<h1 class="text centered">Privacy Policy</h1>
</header>
@@ -1,9 +1,11 @@
import React, { useEffect, useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import DeleteAccount from '../panels/delete-account';
import DeleteAccount from './panels/delete-account';
const Account = props => {
//context
@@ -35,42 +37,42 @@ const Account = props => {
//render the thing
return (
<div className='page'>
<h1 className='centered'>Account</h1>
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<div className='panel'>
<h1 className='text centered'>Account</h1>
<div className='panel'>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
if (err) {
alert(err);
return;
}
if (err) {
alert(err);
return;
}
alert('Details updated');
passwordRef.current.value = retypeRef.current.value = '';
}}>
<div>
<div>
<label htmlFor='password'>Change Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
alert('Details updated');
passwordRef.current.value = retypeRef.current.value = '';
}}>
<input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
<span>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</span>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
<button type='submit'>Update Information</button>
</form>
<DeleteAccount />
</div>
<Link to='/' className='text centered'>Return Home</Link>\
</div>
</div>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount className='constricted' />
</div>
</div>
</>
);
};
@@ -1,11 +1,13 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const LogIn = props => {
const Login = props => {
//context
const authTokens = useContext(TokenContext);
@@ -19,39 +21,40 @@ const LogIn = props => {
const passwordRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Login</h1>
<form className='constricted' onSubmit={
async evt => {
//on submit
evt.preventDefault();
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<div className='panel'>
<h1 className='text centered'>Login</h1>
<form className='constrained' onSubmit={
async evt => {
//on submit
evt.preventDefault();
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
//save auth tokens and redirect
if (newTokens) {
authTokens.setAccessToken(newTokens.accessToken);
authTokens.setRefreshToken(newTokens.refreshToken);
//save auth tokens and redirect
if (newTokens) {
authTokens.setAccessToken(newTokens.accessToken);
authTokens.setRefreshToken(newTokens.refreshToken);
props.history.push('/');
}
}
}>
<div>
<label htmlFor="email">Email:</label>
<input type="email" name="email" ref={emailRef} />
props.history.push('/');
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='password' name='password' placeholder='********' ref={passwordRef} />
<button type='submit'>Login</button>
</form>
<Link to='/recover' className='text centered'>Forgot Password?</Link>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<button type='submit'>Login</button>
</form>
</div>
</div>
</>
);
};
@@ -104,4 +107,4 @@ const handleValidation = (email, password) => {
};
export default LogIn;
export default Login;
@@ -1,6 +1,6 @@
import React, { useState, useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
//DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed
const DeleteAccount = props => {
@@ -9,25 +9,27 @@ const DeleteAccount = props => {
const passwordRef = useRef();
if (!open) {
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button>
return (
<button onClick={() => setOpen(true)}>Delete Account</button>
);
}
return (
<form className={props.className} onSubmit={async evt => {
evt.preventDefault();
const [err] = await handleSubmit(passwordRef.current.value, authTokens);
if (err) {
alert(err);
}
}}>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<div className='panel centered middle'>
<h2 className='text centered'>Delete Your Account?</h2>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err] = await handleSubmit(passwordRef.current.value, authTokens);
if (err) {
alert(err);
}
}}>
<input type="password" name="password" placeholder='Password' ref={passwordRef} />
<button type='submit'>Delete Account</button>
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
</form>
<button type='submit' style={{backgroundColor: 'red'}}>Delete Account</button>
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
</form>
</div>
);
};
+37
View File
@@ -0,0 +1,37 @@
import React, { useContext, useRef } from 'react';
import { Link } from 'react-router-dom';
import { TokenContext } from '../../utilities/token-provider';
//TODO: make this an ACTUAL BUTTON
const Logout = () => {
const authTokens = useContext(TokenContext);
return (
<>
{ /* Logout logs you out of the server too */ }
<Link to='/' onClick={async () => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, { //NOTE: this gets overwritten as a bugfix
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
token: authTokens.refreshToken
})
});
//any problems?
if (!result.ok) {
console.error(await result.text());
} else {
authTokens.setAccessToken('');
authTokens.setRefreshToken('');
}
}}>Logout</Link>
</>
);
};
export default Logout;
@@ -1,5 +1,7 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
@@ -20,32 +22,34 @@ const Recover = props => {
const recoverRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Recover Password</h1>
<form className='constricted' 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;
}
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Forgot Password</h1>
<form className='constrained' onSubmit={
async evt => { //on submit
recoverRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value);
if (result) {
alert(result);
recoverRef.current.disabled = false;
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<div>
<label htmlFor='email'>Enter Your Email:</label>
<input type='email' name='email' ref={emailRef} />
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<button type='submit' ref={recoverRef}>Recover Password</button>
</form>
<Link to='/' className='text centered'>Return Home</Link>
</div>
<button type='submit' ref={recoverRef}>Recover Password</button>
</form>
</div>
</div>
</>
);
};
@@ -1,7 +1,9 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import queryString from 'query-string';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const Reset = props => {
@@ -19,43 +21,38 @@ const Reset = props => {
//refs
const passwordRef = useRef();
const retypeRef = useRef();
const resetRef = useRef();
//render the thing
return (
<div className='page'>
<h1 className='centered'>Reset Password</h1>
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, query);
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Reset Password</h1>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err, redirect] = await update(passwordRef.current.value, retypeRef.current.value, query);
if (err) {
alert(err);
return;
}
if (err) {
alert(err);
return;
}
alert('Details updated');
alert('Details updated'); //TODO: replace with a message from the auth server
//redirect
if (redirect) {
props.history.push('/');
}
}}>
<div>
<div>
<label htmlFor='password'>Enter New Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div>
<label htmlFor='retype'>Retype New Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
//redirect
if (redirect) {
props.history.push('/');
}
}}>
<input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<button type='submit'>Update Information</button>
</form>
<Link to='/' className='text centered'>Return Home</Link>
</div>
<button type='submit'>Update Information</button>
</form>
</div>
</div>
</>
);
};
@@ -82,7 +79,7 @@ const update = async (password, retype, query) => {
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null];
return [null, true];
}
}
@@ -1,5 +1,7 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
@@ -7,7 +9,7 @@ import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const validateUsername = require('../../../common/utilities/validate-username');
const SignUp = props => {
const Signup = props => {
//context
const authTokens = useContext(TokenContext);
@@ -25,52 +27,45 @@ const SignUp = props => {
const signupRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Signup</h1>
<form className='constricted' 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;
}
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Signup</h1>
<form className='constrained' onSubmit={
async evt => { //on submit
signupRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
if (result) {
alert(result);
signupRef.current.disabled = false;
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<div>
<label htmlFor='email'>Email:</label>
<input type='email' name='email' ref={emailRef} />
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='text' name='username' placeholder='Username' ref={usernameRef} />
<input type='password' name='password' placeholder='********' ref={passwordRef} />
<input type='password' name='retype' placeholder='********' ref={retypeRef} />
<span>
<label htmlFor='contact'>Allow Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} defaultChecked='true' />
</span>
<button type='submit' ref={signupRef}>Signup</button>
</form>
<Link to='/recover' className='text centered'>Forgot Password?</Link>
<Link to='/' className='text centered'>Return Home</Link>
</div>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<div>
<label htmlFor='password'>Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</div>
<button type='submit' ref={signupRef}>Signup</button>
</form>
</div>
</div>
</>
);
};
@@ -129,4 +124,4 @@ const handleValidation = (email, username, password, retype) => {
return null;
};
export default SignUp;
export default Signup;
+43
View File
@@ -0,0 +1,43 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import NewsPublisher from './panels/news-publisher';
import NewsEditor from './panels/news-editor';
import GrantAdmin from './panels/grant-admin';
import GrantMod from './panels/grant-mod';
const Admin = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !authTokens.getPayload().admin) {
return <Redirect to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Administration Tools</h1>
<NewsPublisher />
<br />
<NewsEditor />
<br />
<GrantAdmin />
<br />
<GrantMod />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Admin;
+35
View File
@@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import ChatReports from './panels/chat-reports';
import BanUser from './panels/ban-user';
const Mod = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !(authTokens.getPayload().admin || authTokens.getPayload().mod)) {
return <Redirect to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Moderation Tools</h1>
<BanUser />
<ChatReports />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Mod;
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const BanUser = props => {
//context
@@ -10,13 +10,10 @@ const BanUser = props => {
const usernameRef = useRef();
return (
<div>
<h2 className='centered'>Permanently Ban User</h2>
<form>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<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();
@@ -0,0 +1,71 @@
import React, { useState, useEffect, useContext } from 'react';
import { TokenContext } from '../../utilities/token-provider';
import dateFormat from 'dateformat';
const ChatReports = props => {
const [reports, setReports] = useState([]);
const authTokens = useContext(TokenContext);
useEffect(async () => {
const result = await authTokens.tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
}
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
setReports(await result.json());
}
}, []);
return (
<div className='panel' style={{minWidth: '100%'}}>
<h2 className='text centered'>Chat Reports</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Username</th>
<th className='mobile hide'>Room Name</th>
<th>Content</th>
<th>Reported By</th>
<th className='mobile hide'>Delete</th>
</tr>
</thead>
<tbody>
{reports.map((report, index) => (
<tr key={index}>
<td className='text centered'>{dateFormat(report.chatlog.createdAt, 'yyyy-mm-dd, H:MM:ss')}</td>
<td className='text centered'>{report.chatlog.username}</td>
<td className='text mobile hide centered'>{report.chatlog.room}</td>
<td className='text centered'>{report.chatlog.text}</td>
<td className='text centered'>{report.reporter.join(', ')}</td>
<td className='text mobile hide centered'><button onClick={() => deleteReportsFor(report.chatlogIndex, authTokens.tokenFetch, setReports)}>Delete</button></td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const deleteReportsFor = (chatlogIndex, tokenFetch, setReports) => {
tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ chatlogIndex })
});
setReports(reports => reports.filter(report => report.chatlogIndex != chatlogIndex));
};
export default ChatReports;
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const GrantAdmin = props => {
//context
@@ -10,13 +10,10 @@ const GrantAdmin = props => {
const usernameRef = useRef();
return (
<div>
<h2 className='centered'>Grant Admin Privileges</h2>
<form>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<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();
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const GrantMod = props => {
//context
@@ -10,13 +10,10 @@ const GrantMod = props => {
const usernameRef = useRef();
return (
<div>
<h2 className='centered'>Grant Moderation Privileges</h2>
<form>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<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();
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useContext, useRef } from 'react';
import Select from 'react-dropdown-select';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const NewsEditor = props => {
//context
@@ -36,38 +36,36 @@ const NewsEditor = props => {
}, []);
return (
<div>
<h2 className='centered'>News Editor</h2>
<div>
<label htmlFor='article'>Article: </label>
<Select
options={articles.map(article => { return { label: article.title, value: article.index }; })}
onChange={async values => {
//fetch this article
const index = values[0].value;
<div className='panel'>
<h2 className='text centered'>News Editor</h2>
<Select
options={articles.map(article => { return { label: article.title, value: article.index }; })}
onChange={async values => {
//fetch this article
const index = values[0].value;
const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`, {
headers: {
'Access-Control-Allow-Origin': '*'
}
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
const article = await result.json();
titleRef.current.value = article.title;
authorRef.current.value = article.author;
bodyRef.current.value = article.body;
setIndex(index);
const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`, {
headers: {
'Access-Control-Allow-Origin': '*'
}
}}
/>
</div>
});
<form onSubmit={async evt => {
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
const article = await result.json();
titleRef.current.value = article.title;
authorRef.current.value = article.author;
bodyRef.current.value = article.body;
setIndex(index);
}
}}
placeholder='Select Article'
/>
<form className='constrained' onSubmit={async evt => {
//onSubmit
evt.preventDefault();
const [err] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, index, authTokens.tokenFetch);
@@ -78,20 +76,9 @@ const NewsEditor = props => {
alert(`Edited as article index ${index}`);
}
}}>
<div>
<label htmlFor='title'>Title: </label>
<input type='text' name='title' ref={titleRef} />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={authorRef} />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
</div>
<input type='text' name='title' placeholder='Title' ref={titleRef} />
<input type='text' name='author' placeholder='Author' ref={authorRef} />
<textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
<button type='submit'>Update</button>
<button type='button' onClick={async evt => {
@@ -1,6 +1,6 @@
import React, { useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider';
import { TokenContext } from '../../utilities/token-provider';
const NewsPublisher = props => {
//context
@@ -12,34 +12,23 @@ 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>
</div>
+48
View File
@@ -0,0 +1,48 @@
//react
import React, { useContext } from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import { TokenContext } from './utilities/token-provider';
//library components
import LazyRoute from './utilities/lazy-route';
import MarkdownPage from './utilities/markdown-page';
//styling
import '../styles/styles.css';
//common components
import Footer from './panels/footer';
import PopupChat from './panels/popup-chat';
const App = props => {
const authTokens = useContext(TokenContext);
//default render
return (
<BrowserRouter>
<Switch>
<LazyRoute exact path='/' component={() => import('./homepage')} />
<LazyRoute path='/signup' component={() => import('./accounts/signup')} />
<LazyRoute path='/login' component={() => import('./accounts/login')} />
<LazyRoute path='/account' component={() => import('./accounts/account')} />
<LazyRoute path='/dashboard' component={() => import('./dashboard')} />
<LazyRoute path='/recover' component={() => import('./accounts/recover')} />
<LazyRoute path='/reset' component={() => import('./accounts/reset')} />
<LazyRoute path='/admin' component={() => import('./administration/admin')} />
<LazyRoute path='/mod' component={() => import('./administration/mod')} />
<LazyRoute path='/privacypolicy' component={async () => () => <MarkdownPage content={require('../markdown/privacy-policy.md').default} />} />
<LazyRoute path='/credits' component={async () => () => <MarkdownPage content={require('../markdown/credits.md').default} />} />
<LazyRoute path='*' component={() => import('./not-found')} />
</Switch>
{ authTokens.accessToken ? <PopupChat /> : <></> }
<Footer />
</BrowserRouter>
);
};
export default App;
+35
View File
@@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from './utilities/apply-to-body';
import { TokenContext } from './utilities/token-provider';
import MarkdownPanel from './utilities/markdown-panel';
import Logout from './accounts/panels/logout';
const Dashboard = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Redirect to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<Link to='/account'>Account</Link>
{ authTokens.getPayload().admin ? <Link to='/admin' className='text centered'>Admin</Link> : <></> }
{ authTokens.getPayload().mod ? <Link to='/mod' className='text centered'>Mod</Link> : <></> }
<Logout />
</div>
</div>
</>
);
};
export default Dashboard;
+46
View File
@@ -0,0 +1,46 @@
import React, { useContext } from 'react';
import { Link, Redirect } from 'react-router-dom';
import ApplyToBody from './utilities/apply-to-body';
import { TokenContext } from './utilities/token-provider';
import MarkdownPanel from './utilities/markdown-panel';
import NewsFeed from './panels/news-feed';
const HomePage = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/dashboard' />;
}
return (
<>
<ApplyToBody className='homepage' />
<div className='page'>
<div className='panel above'>
<header>
<h1 className='text centered'>MERN Template</h1>
<h2 className='text centered'>This is the MERN-template</h2>
</header>
<div className='panel centered middle'>
<Link to='/signup'><button>Sign Up</button></Link>
<Link to='/login'><button>Login</button></Link>
</div>
</div>
<div className='panel below'>
<div className='central'>
<NewsFeed />
</div>
</div>
</div>
</>
);
};
export default HomePage;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Link } from 'react-router-dom';
import ApplyToBody from './utilities/apply-to-body';
const NotFound = props => {
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Page Not Found</h1>
<br />
<Link className='text centered' to='/'>Return Home</Link>
</div>
</div>
</>
);
};
export default NotFound;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Break = () => {
return (
<>
<span className='mobile hide'> - </span>
<br className='mobile show' />
</>
);
}
const Footer = () => {
return (
<footer>
<p className='text centered'>© <a href='https://krgamestudios.com'>KR Game Studios</a> 2020-2021<Break /><Link to='/privacypolicy'>Privacy Policy</Link><Break /><Link to='/credits'>Credits</Link></p>
</footer>
);
};
export default Footer;
@@ -1,6 +1,8 @@
import React, { useState, useEffect, useRef } from 'react';
import dateFormat from 'dateformat';
import MarkdownPanel from '../utilities/markdown-panel';
const NewsFeed = props => {
const [articles, setArticles] = useState([]);
const aborter = useRef(new AbortController()); //BUGFIX: double-renders = double fetches + react update after unmount
@@ -24,33 +26,21 @@ const NewsFeed = props => {
}, []);
return (
<div>
<h1 className='centered'>News Feed</h1>
{(articles || []).map((article, index) => {
//BUGFIX: check for empty data
if (!article.title) {
return article.title = '';
}
if (!article.author) {
return article.author = '';
}
if (!article.body) {
return article.body = '';
}
//render
<div className='panel'>
<h1 className='text centered'>News Feed</h1>
{articles.map((article, index) => {
return (
<div key={index}>
<div key={index} className='panel'>
<hr />
<h2>{article.title}</h2>
<p>Written by <strong>{article.author}</strong>, {
<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>
}</p>
<p style={{whiteSpace: 'pre-wrap'}}>{article.body}</p>
}</em></p>
<br />
<MarkdownPanel style={{whiteSpace: 'pre-wrap'}} content={article.body} />
</div>
);
})}
@@ -4,11 +4,12 @@ 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([]);
const [chatlog, setChatlog] = useState([{ emphasis: true, text: 'If chat doesn\'t load, reload the page' }]);
const inputRef = useRef();
const sendRef = useRef();
@@ -96,7 +97,7 @@ const processLine = (line, index, accessToken) => {
content = <strong>{content}</strong>;
}
return <li key={index} className='line'>{content}<div className='report'><a onClick={() => processReport(line, accessToken)} style={{ display: line.index && !line.notification ? 'flex' : 'none' }}>!!!</a></div></li>;
return <li key={index} className='line'>{content}<a className='report' onClick={() => processReport(line, accessToken)}>!!!</a></li>;
};
const processReport = (line, accessToken) => {
+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;
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import { Link } from 'react-router-dom';
import ApplyToBody from '../utilities/apply-to-body';
import MarkdownPanel from './markdown-panel';
const MarkdownPage = props => {
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel'>
<MarkdownPanel uri={props.uri} content={props.content} />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
)
};
export default MarkdownPage;
@@ -20,15 +20,15 @@ const Markdown = props => {
.catch(e => console.error(e))
;
}, []);
} else
}
//assume raw info
if (!contentHook) {
else if (!contentHook) {
setContentHook(props.content);
}
return (
<ReactMarkdown rehypePlugins={[rehypeRaw]} escapeHtml={false} props={{...props}}>{contentHook}</ReactMarkdown>
<ReactMarkdown rehypePlugins={[rehypeRaw]} props={{...props}}>{contentHook}</ReactMarkdown>
);
};
@@ -3,7 +3,10 @@ import decode from 'jwt-decode';
export const TokenContext = createContext();
//DOCS: tokenFetch() and tokenCallback() are actually closures here
const TokenProvider = props => {
//state to be used
const [accessToken, setAccessToken] = useState('');
const [refreshToken, setRefreshToken] = useState('');
@@ -13,6 +16,7 @@ const TokenProvider = props => {
setRefreshToken(localStorage.getItem("refreshToken") || '');
}, []);
//update the stored copies
useEffect(() => {
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken);
@@ -27,6 +31,21 @@ const TokenProvider = props => {
const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
if (expired) {
//BUGFIX: if logging out, just skip over the refresh token
if (url === `${process.env.AUTH_URI}/auth/logout`) {
return fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Authorization': `Bearer ${bearer}`
},
body: JSON.stringify({
token: refreshToken
})
});
}
//ping the auth server for a new token
const response = await fetch(`${process.env.AUTH_URI}/auth/token`, {
method: 'POST',
@@ -50,21 +69,6 @@ const TokenProvider = props => {
setAccessToken(newAuth.accessToken);
setRefreshToken(newAuth.refreshToken);
bearer = newAuth.accessToken;
//BUGFIX: logging out correctly requires the new refresh token
if (url == `${process.env.AUTH_URI}/auth/logout`) {
return fetch(`${process.env.AUTH_URI}/auth/logout`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Authorization': `Bearer ${bearer}`
},
body: JSON.stringify({
token: newAuth.refreshToken
})
});
}
}
//finally, delegate to fetch
+41 -18
View File
@@ -1,6 +1,14 @@
/* 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: 23px;
bottom: 3.6em; /* Allow space for the footer */
right: 28px;
width: 280px;
border: solid;
@@ -8,71 +16,76 @@
border-width: 2px;
background-color: #CCC;
display: inline-block;
max-height: calc(50vh - 23px);
}
.chat > button.open {
.chat button.open {
color: white;
background-color: grey;
}
.chat > button.send {
.chat button.send {
color: white;
background-color: green;
border-style: solid;
border-width: 2px;
border-color: darkslategray;
}
.chat > button.close {
.chat button.close {
color: black;
background-color: red;
border-color: maroon;
border-width: 2px;
}
.chat > button {
.chat button {
width: 100%;
height: 2em;
opacity: 0.8;
border: unset;
}
.chat > button:hover {
.chat button:hover {
opacity: 1;
}
.chat > .input {
width: calc(100% - 10px);
.chat .input {
width: 100%;
height: 2em;
}
.chat > .log {
min-height: 300px;
.chat .log {
min-height: 280px;
}
.chat > .log > .scrollable > .line {
.chat .line {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.chat > .log > .scrollable > .line > .report {
.chat .report {
color: red;
display: none;
}
.chat > .log > .scrollable > .line:hover {
.chat .line:hover {
background-color: #BBB;
}
.chat > .log > .scrollable > .line:hover > .report {
.chat .line:hover .report {
display: flex;
}
.chat > .log > .scrollable > .line > .content > .username {
.chat .username {
font-weight: bold;
}
.chat > .log > .scrollable {
.chat .scrollable {
margin: 0;
padding: 10px;
min-height: 280px;
max-height: calc(50vh - 23px - 20px - 6em);
max-height: 180px;
overflow-x: wrap;
overflow-y: scroll;
}
@@ -80,3 +93,13 @@
.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;
}
}
+7 -2
View File
@@ -62,6 +62,7 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
//auth configuration
const authName = await question('Auth Name', 'auth');
const authWebAddress = await question('Auth Web Address', `${authName}.${projectWebAddress}`);
const authPostValidationHook = await question('Auth Post Validation Hook', '');
const authResetAddress = await question('Auth Reset Addr', `${projectWebAddress}/reset`);
const authDBUser = await question('Auth DB Username', authName);
const authDBPass = await question('Auth DB Password', 'charizard');
@@ -180,6 +181,7 @@ services:
environment:
- WEB_PROTOCOL=https
- WEB_ADDRESS=${authWebAddress}
- HOOK_POST_VALIDATION=${authPostValidationHook}
- WEB_RESET_ADDRESS=${authResetAddress}
- WEB_PORT=${authPort}
- DB_HOSTNAME=database
@@ -245,11 +247,14 @@ services:
- --api.insecure=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entrypoints.web.http.redirections.entrypoint.permanent=true
- --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.tlschallenge=true
- --certificatesresolvers.myresolver.acme.email=${supportEmail}
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- traefik.docker.network=app-network
ports:
- 80:80
- 443:443
@@ -265,7 +270,7 @@ networks:
`;
const dockerfile = `
FROM node:15
FROM node:16
WORKDIR "/app"
COPY . /app
RUN mkdir /app/public
+1640 -5114
View File
File diff suppressed because it is too large Load Diff
+62 -61
View File
@@ -1,61 +1,62 @@
{
"name": "mern-template",
"version": "1.0.2",
"description": "A website template using the MERN stack.",
"main": "server/server.js",
"scripts": {
"start": "npm run build && node server/server.js",
"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"
},
"repository": {
"type": "git",
"url": "git+https://github.com/KRGameStudios/MERN-template.git"
},
"author": "Kayne Ruse",
"license": "ISC",
"bugs": {
"url": "https://github.com/KRGameStudios/MERN-template/issues"
},
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": {
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8",
"@babel/preset-react": "^7.14.5",
"@loadable/component": "^5.15.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"concurrently": "^6.2.0",
"css-loader": "^6.2.0",
"dateformat": "^4.5.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"html-webpack-plugin": "^5.3.2",
"jwt-decode": "^3.1.2",
"mariadb": "^2.5.4",
"query-string": "^7.0.1",
"raw-loader": "^4.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-dropdown-select": "^4.7.4",
"react-markdown": "^6.0.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"rehype-raw": "^5.1.0",
"sequelize": "^6.6.5",
"socket.io-client": "^4.1.3",
"style-loader": "^3.2.1",
"webpack": "^5.46.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2"
},
"devDependencies": {
"nodemon": "^2.0.12",
"webpack-dev-server": "^3.11.2"
}
}
{
"name": "mern-template",
"version": "1.1.0",
"description": "A website template using the MERN stack.",
"main": "server/server.js",
"scripts": {
"start": "npm run build && node server/server.js",
"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",
"analyze": "webpack --env=production --env=analyze --config webpack.config.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/KRGameStudios/MERN-template.git"
},
"author": "Kayne Ruse",
"license": "ISC",
"bugs": {
"url": "https://github.com/KRGameStudios/MERN-template/issues"
},
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": {
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8",
"@babel/preset-react": "^7.14.5",
"@loadable/component": "^5.15.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"compression-webpack-plugin": "^8.0.1",
"concurrently": "^6.2.0",
"css-loader": "^6.2.0",
"dateformat": "^4.5.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"html-webpack-plugin": "^5.3.2",
"jwt-decode": "^3.1.2",
"mariadb": "^2.5.4",
"query-string": "^7.0.1",
"raw-loader": "^4.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-dropdown-select": "^4.7.4",
"react-markdown": "^6.0.2",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"rehype-raw": "^5.1.0",
"sequelize": "^6.6.5",
"socket.io-client": "^4.1.3",
"style-loader": "^3.2.1",
"webpack": "^5.46.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2"
},
"devDependencies": {
"nodemon": "^2.0.12",
"webpack-dev-server": "^4.6.0"
}
}
+15
View File
@@ -12,6 +12,21 @@ const server = require('http').Server(app);
//config
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');
+21 -38
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, 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: production ? 'source-map' : 'eval-source-map',
devtool: production ? false : 'eval-source-map',
resolve: {
extensions: ['.js', '.jsx']
},
@@ -71,46 +72,28 @@ 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,
},
},
static: '/public'
}
}
};