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
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
*.lcov *.lcov
# nyc test coverage # nyc test coverage
.nyc_output .nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
# Bower dependency directory (https://bower.io/) # Bower dependency directory (https://bower.io/)
bower_components bower_components
# node-waf configuration # node-waf configuration
.lock-wscript .lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directories # Dependency directories
node_modules/ node_modules/
jspm_packages/ jspm_packages/
# TypeScript v1 declaration files # TypeScript v1 declaration files
typings/ typings/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# Optional npm cache directory # Optional npm cache directory
.npm .npm
# Optional eslint cache # Optional eslint cache
.eslintcache .eslintcache
# Microbundle cache # Microbundle cache
.rpt2_cache/ .rpt2_cache/
.rts2_cache_cjs/ .rts2_cache_cjs/
.rts2_cache_es/ .rts2_cache_es/
.rts2_cache_umd/ .rts2_cache_umd/
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack' # Output of 'npm pack'
*.tgz *.tgz
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# dotenv environment variables file # dotenv environment variables file
.env .env
.env.test .env.test
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache
# Next.js build output # Next.js build output
.next .next
# Nuxt.js build / generate output # Nuxt.js build / generate output
.nuxt .nuxt
dist dist
# Gatsby files # Gatsby files
.cache/ .cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js # 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 # https://nextjs.org/blog/next-9-1#public-directory-support
# public # public
# vuepress build output # vuepress build output
.vuepress/dist .vuepress/dist
# Serverless directories # Serverless directories
.serverless/ .serverless/
# FuseBox cache # FuseBox cache
.fusebox/ .fusebox/
# DynamoDB Local files # DynamoDB Local files
.dynamodb/ .dynamodb/
# TernJS port file # TernJS port file
.tern-port .tern-port
# Output files # Output files
public/*.html public/*.html
public/*.js public/*.js
public/*.css public/*.css
public/*.map public/*.map
public/*.gz public/*.gz
public/*.txt
letsencrypt/
mysql/ letsencrypt/
Dockerfile mysql/
docker-compose.yml Dockerfile
startup.sql docker-compose.yml
startup.sql
+94 -91
View File
@@ -1,91 +1,94 @@
# MERN-template # 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.
* React The primary technology involved is:
* Nodejs
* MariaDB (with Sequelize) * React
* Docker (with docker-compose) * Nodejs
* MariaDB (with Sequelize)
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. * Docker (with docker-compose)
This template is released under the zlib license (see LICENSE). 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.
See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation. This template is released under the zlib license (see LICENSE).
# Microservices See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation.
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). # Microservices
* News Server: https://github.com/krgamestudios/news-server 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
* Chat Server: https://github.com/krgamestudios/chat-server * News Server: https://github.com/krgamestudios/news-server
* Auth Server: https://github.com/krgamestudios/auth-server
# Setup Deployment * Chat Server: https://github.com/krgamestudios/chat-server
A clean install is this easy: # Setup Deployment
``` A clean install is this easy:
git clone https://github.com/krgamestudios/MERN-template.git
cd MERN-template ```
npm install git clone https://github.com/krgamestudios/MERN-template.git
node configure-script.js cd MERN-template
docker-compose up --build node configure-script.js
``` docker-compose up --build
```
# Setup Development
# Setup Development
To set up this template in development mode:
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 1. Ensure mariadb is running in your development environment
3. Run `npm install` 2. Run `mariadb sql/create_database.sql` as the root user
4. Run `cp .envdev .env` and enter your details into the `.env` file 3. Run `npm install`
5. Execute `npm run dev` 4. Run `cp .envdev .env` and enter your details into the `.env` file
6. Navigate to `http://localhost:3001` in your web browser 5. Execute `npm run dev`
6. Navigate to `http://localhost:3001` in your web browser
# Features List
# Features List
- Mainly one language across the codebase (JavaScript)
- Full documentation - Mainly one language across the codebase (JavaScript)
- Setup tutorial - Full documentation
- Fully Featured Account System (as a microservice) - Setup tutorial
- Email validation - Fully Featured Account System (as a microservice)
- Logging in and out - Email validation
- Account deletion - Logging in and out
- Password management - Account deletion
- JSON web token authentication - Password management
- Fully Featured News Blog (as a microservice) - JSON web token authentication
- Publish, edit or delete articles as needed - Optional post validation hook
- Secured via admin panel - Fully Featured News Blog (as a microservice)
- Fully Featured Chat System (as a microservice) - Publish, edit or delete articles as needed
- Available when logged in - Secured via admin panel
- Chat logs saved to the database - Fully Featured Chat System (as a microservice)
- Room-based chat (type `/room name` to access a specific room) - Available when logged in
- Moderation tools - Chat logs saved to the database
- Permanently banning users - Room-based chat (type `/room name` to access a specific room)
- Chat-muting users for a time period - Moderation tools
- Users reporting offensive chat-content - Permanently banning users
- Easy To Use Configuration Script - Chat-muting users for a time period
- Sets up everything via docker - Users reporting offensive chat-content
- A default admin account (if desired) - Easy To Use Configuration Script
- Sets up everything via docker
# Coming Soon - A default admin account (if desired)
- Full documentation # Coming Soon
- Modding tutorials
- Full documentation
# Coming Eventually - Modding tutorials
- Fully Featured News Blog (as a microservice)
- Fully Featured News Blog (as a microservice) - Individual pages for news articles
- Restore deleted articles
- Undo edits # Coming Eventually
- Fully Featured Chat System (as a microservice)
- Custom emoji - Fully Featured News Blog (as a microservice)
- Private messaging - Restore deleted articles
- Broadcasting to all channels - Undo edits
- Badges next to usernames - Fully Featured Chat System (as a microservice)
- Better compression for client files - Custom emoji
- Backend for leaderboards (modding tutorial?) - Private messaging
- Backend for energy systems (modding tutorial?) - Broadcasting to all channels
- Backend for items, shops, trading and currency - 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 React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './components/app'; import App from './pages/app';
import TokenProvider from './components/utilities/token-provider'; import TokenProvider from './pages/utilities/token-provider';
ReactDOM.render( ReactDOM.render(
<TokenProvider> <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) [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 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 { TokenContext } from '../utilities/token-provider';
import DeleteAccount from '../panels/delete-account'; import DeleteAccount from './panels/delete-account';
const Account = props => { const Account = props => {
//context //context
@@ -35,42 +37,42 @@ const Account = props => {
//render the thing //render the thing
return ( return (
<div className='page'> <>
<h1 className='centered'>Account</h1> <ApplyToBody className='dashboard' />
<form className='constricted' onSubmit={async evt => { <div className='page'>
evt.preventDefault(); <div className='central panel centered middle'>
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch); <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) { if (err) {
alert(err); alert(err);
return; return;
} }
alert('Details updated'); alert('Details updated');
passwordRef.current.value = retypeRef.current.value = ''; passwordRef.current.value = retypeRef.current.value = '';
}}> }}>
<div> <input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<div> <input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<label htmlFor='password'>Change Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div> <span>
<label htmlFor='retype'>Retype Password:</label> <label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='password' name='retype' ref={retypeRef} /> <input type='checkbox' name='contact' ref={contactRef} />
</div> </span>
<div> <button type='submit'>Update Information</button>
<label htmlFor='contact'>Allow Promotional Emails:</label> </form>
<input type='checkbox' name='contact' ref={contactRef} /> <DeleteAccount />
</div>
<Link to='/' className='text centered'>Return Home</Link>\
</div> </div>
</div> </div>
</div>
<button type='submit'>Update Information</button> </>
</form>
<DeleteAccount className='constricted' />
</div>
); );
}; };
@@ -1,11 +1,13 @@
import React, { useContext, useRef } from 'react'; 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'; import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email'); const validateEmail = require('../../../common/utilities/validate-email');
const LogIn = props => { const Login = props => {
//context //context
const authTokens = useContext(TokenContext); const authTokens = useContext(TokenContext);
@@ -19,39 +21,40 @@ const LogIn = props => {
const passwordRef = useRef(); const passwordRef = useRef();
return ( return (
<div className='page'> <>
<h1 className='centered'>Login</h1> <ApplyToBody className='dashboard' />
<form className='constricted' onSubmit={ <div className='page'>
async evt => { <div className='central panel centered middle'>
//on submit <div className='panel'>
evt.preventDefault(); <h1 className='text centered'>Login</h1>
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value); <form className='constrained' onSubmit={
if (err) { async evt => {
alert(err); //on submit
} evt.preventDefault();
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
//save auth tokens and redirect //save auth tokens and redirect
if (newTokens) { if (newTokens) {
authTokens.setAccessToken(newTokens.accessToken); authTokens.setAccessToken(newTokens.accessToken);
authTokens.setRefreshToken(newTokens.refreshToken); authTokens.setRefreshToken(newTokens.refreshToken);
props.history.push('/'); props.history.push('/');
} }
} }
}> }>
<div> <input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<label htmlFor="email">Email:</label> <input type='password' name='password' placeholder='********' ref={passwordRef} />
<input type="email" name="email" ref={emailRef} /> <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>
</div>
<div> </>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<button type='submit'>Login</button>
</form>
</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 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 //DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed
const DeleteAccount = props => { const DeleteAccount = props => {
@@ -9,25 +9,27 @@ const DeleteAccount = props => {
const passwordRef = useRef(); const passwordRef = useRef();
if (!open) { if (!open) {
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button> return (
<button onClick={() => setOpen(true)}>Delete Account</button>
);
} }
return ( return (
<form className={props.className} onSubmit={async evt => { <div className='panel centered middle'>
evt.preventDefault(); <h2 className='text centered'>Delete Your Account?</h2>
const [err] = await handleSubmit(passwordRef.current.value, authTokens); <form className='constrained' onSubmit={async evt => {
if (err) { evt.preventDefault();
alert(err); 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} /> <input type="password" name="password" placeholder='Password' ref={passwordRef} />
</div>
<button type='submit'>Delete Account</button> <button type='submit' style={{backgroundColor: 'red'}}>Delete Account</button>
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button> <button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
</form> </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 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'; import { TokenContext } from '../utilities/token-provider';
@@ -20,32 +22,34 @@ const Recover = props => {
const recoverRef = useRef(); const recoverRef = useRef();
return ( return (
<div className='page'> <>
<h1 className='centered'>Recover Password</h1> <ApplyToBody className='dashboard' />
<form className='constricted' onSubmit={ <div className='page'>
async evt => { //on submit <div className='central panel centered middle'>
recoverRef.current.disabled = true; <h1 className='text centered'>Forgot Password</h1>
evt.preventDefault(); <form className='constrained' onSubmit={
const [result, redirect] = await handleSubmit(emailRef.current.value); async evt => { //on submit
if (result) { recoverRef.current.disabled = true;
alert(result); evt.preventDefault();
recoverRef.current.disabled = false; const [result, redirect] = await handleSubmit(emailRef.current.value);
} if (result) {
alert(result);
recoverRef.current.disabled = false;
}
//redirect //redirect
if (redirect) { if (redirect) {
props.history.push('/'); props.history.push('/');
} }
} }
}> }>
<div> <input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<label htmlFor='email'>Enter Your Email:</label> <button type='submit' ref={recoverRef}>Recover Password</button>
<input type='email' name='email' ref={emailRef} /> </form>
<Link to='/' className='text centered'>Return Home</Link>
</div> </div>
</div>
<button type='submit' ref={recoverRef}>Recover Password</button> </>
</form>
</div>
); );
}; };
@@ -1,7 +1,9 @@
import React, { useContext, useRef } from 'react'; 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 queryString from 'query-string';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../utilities/token-provider';
const Reset = props => { const Reset = props => {
@@ -19,43 +21,38 @@ const Reset = props => {
//refs //refs
const passwordRef = useRef(); const passwordRef = useRef();
const retypeRef = useRef(); const retypeRef = useRef();
const resetRef = useRef();
//render the thing //render the thing
return ( return (
<div className='page'> <>
<h1 className='centered'>Reset Password</h1> <ApplyToBody className='dashboard' />
<form className='constricted' onSubmit={async evt => { <div className='page'>
evt.preventDefault(); <div className='central panel centered middle'>
const [err] = await update(passwordRef.current.value, retypeRef.current.value, query); <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) { if (err) {
alert(err); alert(err);
return; return;
} }
alert('Details updated'); alert('Details updated'); //TODO: replace with a message from the auth server
//redirect //redirect
if (redirect) { if (redirect) {
props.history.push('/'); props.history.push('/');
} }
}}> }}>
<div> <input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<div> <input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<label htmlFor='password'>Enter New Password:</label> <button type='submit'>Update Information</button>
<input type='password' name='password' ref={passwordRef} /> </form>
</div> <Link to='/' className='text centered'>Return Home</Link>
<div>
<label htmlFor='retype'>Retype New Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
</div> </div>
</div>
<button type='submit'>Update Information</button> </>
</form>
</div>
); );
}; };
@@ -82,7 +79,7 @@ const update = async (password, retype, query) => {
if (!result.ok) { if (!result.ok) {
return [`${await result.status}: ${await result.text()}`]; return [`${await result.status}: ${await result.text()}`];
} else { } else {
return [null]; return [null, true];
} }
} }
@@ -1,5 +1,7 @@
import React, { useContext, useRef } from 'react'; 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'; import { TokenContext } from '../utilities/token-provider';
@@ -7,7 +9,7 @@ import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email'); const validateEmail = require('../../../common/utilities/validate-email');
const validateUsername = require('../../../common/utilities/validate-username'); const validateUsername = require('../../../common/utilities/validate-username');
const SignUp = props => { const Signup = props => {
//context //context
const authTokens = useContext(TokenContext); const authTokens = useContext(TokenContext);
@@ -25,52 +27,45 @@ const SignUp = props => {
const signupRef = useRef(); const signupRef = useRef();
return ( return (
<div className='page'> <>
<h1 className='centered'>Signup</h1> <ApplyToBody className='dashboard' />
<form className='constricted' onSubmit={ <div className='page'>
async evt => { //on submit <div className='central panel centered middle'>
signupRef.current.disabled = true; <h1 className='text centered'>Signup</h1>
evt.preventDefault(); <form className='constrained' onSubmit={
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked); async evt => { //on submit
if (result) { signupRef.current.disabled = true;
alert(result); evt.preventDefault();
signupRef.current.disabled = false; 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 //redirect
if (redirect) { if (redirect) {
props.history.push('/'); props.history.push('/');
} }
} }
}> }>
<div>
<label htmlFor='email'>Email:</label> <input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='email' name='email' 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>
</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>
); );
}; };
@@ -129,4 +124,4 @@ const handleValidation = (email, username, password, retype) => {
return null; 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 React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const BanUser = props => { const BanUser = props => {
//context //context
@@ -10,13 +10,10 @@ const BanUser = props => {
const usernameRef = useRef(); const usernameRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>Permanently Ban User</h2> <h2 className='text centered'>Permanently Ban User</h2>
<form> <form className='constrained'>
<div> <input type='text' name='username' placeholder='Username' ref={usernameRef} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
evt.preventDefault(); 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 React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const GrantAdmin = props => { const GrantAdmin = props => {
//context //context
@@ -10,13 +10,10 @@ const GrantAdmin = props => {
const usernameRef = useRef(); const usernameRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>Grant Admin Privileges</h2> <h2 className='text centered'>Grant Admin Privileges</h2>
<form> <form className='constrained'>
<div> <input type='text' name='username' placeholder='Username' ref={usernameRef} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
evt.preventDefault(); evt.preventDefault();
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react'; import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const GrantMod = props => { const GrantMod = props => {
//context //context
@@ -10,13 +10,10 @@ const GrantMod = props => {
const usernameRef = useRef(); const usernameRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>Grant Moderation Privileges</h2> <h2 className='text centered'>Grant Moderation Privileges</h2>
<form> <form className='constrained'>
<div> <input type='text' name='username' placeholder='Username' ref={usernameRef} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
evt.preventDefault(); evt.preventDefault();
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useContext, useRef } from 'react'; import React, { useState, useEffect, useContext, useRef } from 'react';
import Select from 'react-dropdown-select'; import Select from 'react-dropdown-select';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const NewsEditor = props => { const NewsEditor = props => {
//context //context
@@ -36,38 +36,36 @@ const NewsEditor = props => {
}, []); }, []);
return ( return (
<div> <div className='panel'>
<h2 className='centered'>News Editor</h2> <h2 className='text centered'>News Editor</h2>
<div> <Select
<label htmlFor='article'>Article: </label> options={articles.map(article => { return { label: article.title, value: article.index }; })}
<Select onChange={async values => {
options={articles.map(article => { return { label: article.title, value: article.index }; })} //fetch this article
onChange={async values => { const index = values[0].value;
//fetch this article
const index = values[0].value;
const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`, { const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`, {
headers: { headers: {
'Access-Control-Allow-Origin': '*' 'Access-Control-Allow-Origin': '*'
}
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
const article = await result.json();
titleRef.current.value = article.title;
authorRef.current.value = article.author;
bodyRef.current.value = article.body;
setIndex(index);
} }
}} });
/>
</div>
<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 //onSubmit
evt.preventDefault(); evt.preventDefault();
const [err] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, index, authTokens.tokenFetch); 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}`); alert(`Edited as article index ${index}`);
} }
}}> }}>
<div> <input type='text' name='title' placeholder='Title' ref={titleRef} />
<label htmlFor='title'>Title: </label> <input type='text' name='author' placeholder='Author' ref={authorRef} />
<input type='text' name='title' ref={titleRef} /> <textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
</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>
<button type='submit'>Update</button> <button type='submit'>Update</button>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
@@ -1,6 +1,6 @@
import React, { useContext, useRef } from 'react'; import React, { useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const NewsPublisher = props => { const NewsPublisher = props => {
//context //context
@@ -12,34 +12,23 @@ const NewsPublisher = props => {
const bodyRef = useRef(); const bodyRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>News Publisher</h2> <h2 className='text centered'>News Publisher</h2>
<form onSubmit={async evt => { <form className='constrained' onSubmit={async evt => {
//on submit //on submit
evt.preventDefault(); evt.preventDefault();
const [err, index] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, authTokens.tokenFetch); const [err, index] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, authTokens.tokenFetch);
if (err) { if (err) {
alert(err); alert(err);
} else { } 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}`); alert(`Published as article index ${index}`);
} }
}}> }}>
<div> <input type='text' name='title' placeholder='Title' ref={titleRef} />
<label htmlFor='title'>Title: </label> <input type='text' name='author' placeholder='Author' ref={authorRef} />
<input type='text' name='title' ref={titleRef} /> <textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
</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>
<button type='submit'>Publish</button> <button type='submit'>Publish</button>
</form> </form>
</div> </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 React, { useState, useEffect, useRef } from 'react';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import MarkdownPanel from '../utilities/markdown-panel';
const NewsFeed = props => { const NewsFeed = props => {
const [articles, setArticles] = useState([]); const [articles, setArticles] = useState([]);
const aborter = useRef(new AbortController()); //BUGFIX: double-renders = double fetches + react update after unmount const aborter = useRef(new AbortController()); //BUGFIX: double-renders = double fetches + react update after unmount
@@ -24,33 +26,21 @@ const NewsFeed = props => {
}, []); }, []);
return ( return (
<div> <div className='panel'>
<h1 className='centered'>News Feed</h1> <h1 className='text centered'>News Feed</h1>
{(articles || []).map((article, index) => { {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
return ( return (
<div key={index}> <div key={index} className='panel'>
<hr /> <hr />
<h2>{article.title}</h2> <h2>{article.title}</h2>
<p>Written by <strong>{article.author}</strong>, { <br />
<p><em>Written by <strong>{article.author}</strong>, {
article.edits > 0 ? article.edits > 0 ?
<span>Last Updated {dateFormat(article.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`})</span> : <span>Last Updated {dateFormat(article.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`})</span> :
<span>Published {dateFormat(article.createdAt, 'fullDate')}</span> <span>Published {dateFormat(article.createdAt, 'fullDate')}</span>
}</p> }</em></p>
<p style={{whiteSpace: 'pre-wrap'}}>{article.body}</p> <br />
<MarkdownPanel style={{whiteSpace: 'pre-wrap'}} content={article.body} />
</div> </div>
); );
})} })}
@@ -4,11 +4,12 @@ import { io } from 'socket.io-client';
import '../../styles/popup-chat.css'; 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 socket = io(`${process.env.CHAT_URI}/chat`);
const PopupChat = props => { const PopupChat = props => {
const [open, setOpen] = useState(false); 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 inputRef = useRef();
const sendRef = useRef(); const sendRef = useRef();
@@ -96,7 +97,7 @@ const processLine = (line, index, accessToken) => {
content = <strong>{content}</strong>; 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) => { 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)) .catch(e => console.error(e))
; ;
}, []); }, []);
} else }
//assume raw info //assume raw info
if (!contentHook) { else if (!contentHook) {
setContentHook(props.content); setContentHook(props.content);
} }
return ( 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(); export const TokenContext = createContext();
//DOCS: tokenFetch() and tokenCallback() are actually closures here
const TokenProvider = props => { const TokenProvider = props => {
//state to be used
const [accessToken, setAccessToken] = useState(''); const [accessToken, setAccessToken] = useState('');
const [refreshToken, setRefreshToken] = useState(''); const [refreshToken, setRefreshToken] = useState('');
@@ -13,6 +16,7 @@ const TokenProvider = props => {
setRefreshToken(localStorage.getItem("refreshToken") || ''); setRefreshToken(localStorage.getItem("refreshToken") || '');
}, []); }, []);
//update the stored copies
useEffect(() => { useEffect(() => {
localStorage.setItem("accessToken", accessToken); localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken); localStorage.setItem("refreshToken", refreshToken);
@@ -27,6 +31,21 @@ const TokenProvider = props => {
const expired = new Date(decode(accessToken).exp * 1000) < Date.now(); const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
if (expired) { 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 //ping the auth server for a new token
const response = await fetch(`${process.env.AUTH_URI}/auth/token`, { const response = await fetch(`${process.env.AUTH_URI}/auth/token`, {
method: 'POST', method: 'POST',
@@ -50,21 +69,6 @@ const TokenProvider = props => {
setAccessToken(newAuth.accessToken); setAccessToken(newAuth.accessToken);
setRefreshToken(newAuth.refreshToken); setRefreshToken(newAuth.refreshToken);
bearer = newAuth.accessToken; 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 //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 { .chat {
position: fixed; position: fixed;
bottom: 23px; bottom: 3.6em; /* Allow space for the footer */
right: 28px; right: 28px;
width: 280px; width: 280px;
border: solid; border: solid;
@@ -8,71 +16,76 @@
border-width: 2px; border-width: 2px;
background-color: #CCC; background-color: #CCC;
display: inline-block; display: inline-block;
max-height: calc(50vh - 23px);
} }
.chat > button.open { .chat button.open {
color: white; color: white;
background-color: grey; background-color: grey;
} }
.chat > button.send { .chat button.send {
color: white; color: white;
background-color: green; background-color: green;
border-style: solid;
border-width: 2px;
border-color: darkslategray;
} }
.chat > button.close { .chat button.close {
color: black; color: black;
background-color: red; background-color: red;
border-color: maroon;
border-width: 2px;
} }
.chat > button { .chat button {
width: 100%; width: 100%;
height: 2em; height: 2em;
opacity: 0.8; opacity: 0.8;
border: unset;
} }
.chat > button:hover { .chat button:hover {
opacity: 1; opacity: 1;
} }
.chat > .input { .chat .input {
width: calc(100% - 10px); width: 100%;
height: 2em; height: 2em;
} }
.chat > .log { .chat .log {
min-height: 300px; min-height: 280px;
} }
.chat > .log > .scrollable > .line { .chat .line {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
.chat > .log > .scrollable > .line > .report { .chat .report {
color: red; color: red;
display: none; display: none;
} }
.chat > .log > .scrollable > .line:hover { .chat .line:hover {
background-color: #BBB; background-color: #BBB;
} }
.chat > .log > .scrollable > .line:hover > .report { .chat .line:hover .report {
display: flex; display: flex;
} }
.chat > .log > .scrollable > .line > .content > .username { .chat .username {
font-weight: bold; font-weight: bold;
} }
.chat > .log > .scrollable { .chat .scrollable {
margin: 0; margin: 0;
padding: 10px; padding: 10px;
min-height: 280px; min-height: 280px;
max-height: calc(50vh - 23px - 20px - 6em); max-height: 180px;
overflow-x: wrap; overflow-x: wrap;
overflow-y: scroll; overflow-y: scroll;
} }
@@ -80,3 +93,13 @@
.chat ul { .chat ul {
list-style: none; 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 //auth configuration
const authName = await question('Auth Name', 'auth'); const authName = await question('Auth Name', 'auth');
const authWebAddress = await question('Auth Web Address', `${authName}.${projectWebAddress}`); 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 authResetAddress = await question('Auth Reset Addr', `${projectWebAddress}/reset`);
const authDBUser = await question('Auth DB Username', authName); const authDBUser = await question('Auth DB Username', authName);
const authDBPass = await question('Auth DB Password', 'charizard'); const authDBPass = await question('Auth DB Password', 'charizard');
@@ -180,6 +181,7 @@ services:
environment: environment:
- WEB_PROTOCOL=https - WEB_PROTOCOL=https
- WEB_ADDRESS=${authWebAddress} - WEB_ADDRESS=${authWebAddress}
- HOOK_POST_VALIDATION=${authPostValidationHook}
- WEB_RESET_ADDRESS=${authResetAddress} - WEB_RESET_ADDRESS=${authResetAddress}
- WEB_PORT=${authPort} - WEB_PORT=${authPort}
- DB_HOSTNAME=database - DB_HOSTNAME=database
@@ -245,11 +247,14 @@ services:
- --api.insecure=false - --api.insecure=false
- --providers.docker=true - --providers.docker=true
- --providers.docker.exposedbydefault=false - --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 - --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.tlschallenge=true - --certificatesresolvers.myresolver.acme.tlschallenge=true
- --certificatesresolvers.myresolver.acme.email=${supportEmail} - --certificatesresolvers.myresolver.acme.email=${supportEmail}
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- traefik.docker.network=app-network
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
@@ -265,7 +270,7 @@ networks:
`; `;
const dockerfile = ` const dockerfile = `
FROM node:15 FROM node:16
WORKDIR "/app" WORKDIR "/app"
COPY . /app COPY . /app
RUN mkdir /app/public 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", "name": "mern-template",
"version": "1.0.2", "version": "1.1.0",
"description": "A website template using the MERN stack.", "description": "A website template using the MERN stack.",
"main": "server/server.js", "main": "server/server.js",
"scripts": { "scripts": {
"start": "npm run build && node server/server.js", "start": "npm run build && node server/server.js",
"build": "npm run build:server && npm run build:client", "build": "npm run build:server && npm run build:client",
"build:server": "exit 0", "build:server": "exit 0",
"build:client": "webpack --env=production --config webpack.config.js", "build:client": "webpack --env=production --config webpack.config.js",
"dev": "concurrently npm:watch:server npm:watch:client", "dev": "concurrently npm:watch:server npm:watch:client",
"watch:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'", "watch:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'",
"watch:client": "webpack serve --env=development --config webpack.config.js", "watch:client": "webpack serve --env=development --config webpack.config.js",
"analyzer": "webpack --env=production --analyzer --config webpack.config.js" "analyze": "webpack --env=production --env=analyze --config webpack.config.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/KRGameStudios/MERN-template.git" "url": "git+https://github.com/KRGameStudios/MERN-template.git"
}, },
"author": "Kayne Ruse", "author": "Kayne Ruse",
"license": "ISC", "license": "ISC",
"bugs": { "bugs": {
"url": "https://github.com/KRGameStudios/MERN-template/issues" "url": "https://github.com/KRGameStudios/MERN-template/issues"
}, },
"homepage": "https://github.com/KRGameStudios/MERN-template#readme", "homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": { "dependencies": {
"@babel/core": "^7.14.8", "@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8", "@babel/preset-env": "^7.14.8",
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@loadable/component": "^5.15.0", "@loadable/component": "^5.15.0",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0", "clean-webpack-plugin": "^3.0.0",
"concurrently": "^6.2.0", "compression-webpack-plugin": "^8.0.1",
"css-loader": "^6.2.0", "concurrently": "^6.2.0",
"dateformat": "^4.5.1", "css-loader": "^6.2.0",
"dotenv": "^10.0.0", "dateformat": "^4.5.1",
"express": "^4.17.1", "dotenv": "^10.0.0",
"html-webpack-plugin": "^5.3.2", "express": "^4.17.1",
"jwt-decode": "^3.1.2", "html-webpack-plugin": "^5.3.2",
"mariadb": "^2.5.4", "jwt-decode": "^3.1.2",
"query-string": "^7.0.1", "mariadb": "^2.5.4",
"raw-loader": "^4.0.2", "query-string": "^7.0.1",
"react": "^17.0.2", "raw-loader": "^4.0.2",
"react-dom": "^17.0.2", "react": "^17.0.2",
"react-dropdown-select": "^4.7.4", "react-dom": "^17.0.2",
"react-markdown": "^6.0.2", "react-dropdown-select": "^4.7.4",
"react-router": "^5.2.0", "react-markdown": "^6.0.2",
"react-router-dom": "^5.2.0", "react-router": "^5.2.0",
"rehype-raw": "^5.1.0", "react-router-dom": "^5.2.0",
"sequelize": "^6.6.5", "rehype-raw": "^5.1.0",
"socket.io-client": "^4.1.3", "sequelize": "^6.6.5",
"style-loader": "^3.2.1", "socket.io-client": "^4.1.3",
"webpack": "^5.46.0", "style-loader": "^3.2.1",
"webpack-bundle-analyzer": "^4.4.2", "webpack": "^5.46.0",
"webpack-cli": "^4.7.2" "webpack-bundle-analyzer": "^4.4.2",
}, "webpack-cli": "^4.7.2"
"devDependencies": { },
"nodemon": "^2.0.12", "devDependencies": {
"webpack-dev-server": "^3.11.2" "nodemon": "^2.0.12",
} "webpack-dev-server": "^4.6.0"
} }
}
+15
View File
@@ -12,6 +12,21 @@ const server = require('http').Server(app);
//config //config
app.use(express.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 //database connection
const database = require('./database'); const database = require('./database');
+21 -38
View File
@@ -2,23 +2,24 @@
const { DefinePlugin } = require('webpack'); const { DefinePlugin } = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
//libraries //libraries
const path = require('path'); const path = require('path');
//the exported config function //the exported config function
module.exports = ({ production, analyzer }) => { module.exports = ({ production, analyze }) => {
return { return {
mode: production ? "production" : "development", mode: production ? "production" : "development",
entry: path.resolve(__dirname, 'client', 'client.jsx'), entry: path.resolve(__dirname, 'client', 'client.jsx'),
output: { output: {
path: path.resolve(__dirname, 'public'), path: path.resolve(__dirname, 'public'),
publicPath: '/',
filename: '[name].[chunkhash].js', filename: '[name].[chunkhash].js',
sourceMapFilename: '[name].[chunkhash].js.map' sourceMapFilename: '[name].[chunkhash].js.map'
}, },
devtool: production ? 'source-map' : 'eval-source-map', devtool: production ? false : 'eval-source-map',
resolve: { resolve: {
extensions: ['.js', '.jsx'] extensions: ['.js', '.jsx']
}, },
@@ -71,46 +72,28 @@ module.exports = ({ production, analyzer }) => {
removeAttributeQuotes: production removeAttributeQuotes: production
} }
}), }),
new CompressionPlugin({
filename: "[path][base].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$/,
minRatio: 0.8
}),
new BundleAnalyzerPlugin({ new BundleAnalyzerPlugin({
analyzerMode: analyzer ? 'server' : 'disabled' analyzerMode: analyze ? 'server' : 'disabled'
}) })
], ],
devServer: { 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, hot: true,
injectHot: true host: 'localhost',
}, port: 3001,
watchOptions: { client: {
ignored: /(node_modules)/ overlay: {
errors: true,
warnings: true,
},
},
static: '/public'
} }
} }
}; };