Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb1590bae7 | |||
| 5f7b9dda3a | |||
| 051f3dfb2a | |||
| 85456e0892 | |||
| 6130337846 | |||
| ac99f3bf38 | |||
| 20e94db628 | |||
| bcb4a37f5a | |||
| 3b0d3c87b1 | |||
| 51a116503d | |||
| eb370663d2 | |||
| 462116d980 | |||
| af06ddc06d | |||
| f937ee47db | |||
| 6d0dd419ca | |||
| 2919467dff | |||
| 269caac88c | |||
| 69c297fa74 | |||
| 0f538be3e5 | |||
| f85b6e8793 | |||
| 2af9532930 | |||
| 191da50740 | |||
| f24c7990f6 | |||
| 7a48780f50 | |||
| 33952a9896 | |||
| 8076b0cc40 | |||
| e216474196 | |||
| 2532bf1867 | |||
| 93a3c30e81 | |||
| ae8c82e83a | |||
| bc6a795750 | |||
| 9947ef13c1 | |||
| d3f0b1ac7d | |||
| b5f9c45a1b | |||
| 5189415e1a | |||
| be793ae2ff | |||
| 19f5c20056 | |||
| 7923f51aae | |||
| 793e54e334 | |||
| 4fa54668e6 | |||
| ff0230b77f | |||
| 6b53bee033 | |||
| 4280319443 | |||
| 9788552d0c | |||
| 3fd76375dd | |||
| 9c8ece5c06 | |||
| 9201e9374b | |||
| 8c8b78462f | |||
| 07e578b4c4 | |||
| 732eeb933e | |||
| 104e15d714 | |||
| 2c9871d82a | |||
| f5f44ae9f7 | |||
| 1c3d24575e | |||
| 13e3ce6db8 | |||
| 8561219542 | |||
| e288a43519 |
@@ -1,9 +1,15 @@
|
|||||||
WEB_PORT=3000
|
WEB_PORT=3000
|
||||||
|
|
||||||
DB_HOSTNAME=database
|
DB_HOSTNAME=localhost
|
||||||
DB_DATABASE=template
|
DB_DATABASE=template
|
||||||
DB_USERNAME=template
|
DB_USERNAME=template
|
||||||
DB_PASSWORD=pikachu
|
DB_PASSWORD=pikachu
|
||||||
|
|
||||||
|
# Select a "TZ database name" that suits your needs: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
DB_TIMEZONE=Australia/Sydney
|
DB_TIMEZONE=Australia/Sydney
|
||||||
|
|
||||||
|
# Give this any value to enable database logging (such as "true")
|
||||||
|
DB_LOGGING=
|
||||||
|
|
||||||
|
# Make sure this value matches the system that you connect to
|
||||||
SECRET_ACCESS=access
|
SECRET_ACCESS=access
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
The primary technology involved is:
|
||||||
|
|
||||||
* React
|
* React
|
||||||
* Nodejs
|
* Nodejs
|
||||||
* MariaDB (with Sequelize)
|
* MariaDB (with Sequelize)
|
||||||
* Docker
|
* Docker (with docker-compose)
|
||||||
|
|
||||||
This template is designed to support the development of persistent browser based games (PBBGs), but it, and it's component microservices, can be used elsewhere.
|
This template is 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).
|
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
|
# 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).
|
There are external components to this template referred to as "microservices". These can be omitted entirely by simply removing the React components that access them. These are also available via [docker hub](https://hub.docker.com/u/krgamestudios).
|
||||||
|
|
||||||
* Auth Server: https://github.com/krgamestudios/auth-server
|
|
||||||
* News Server: https://github.com/krgamestudios/news-server
|
* News Server: https://github.com/krgamestudios/news-server
|
||||||
|
* Auth Server: https://github.com/krgamestudios/auth-server
|
||||||
* Chat Server: https://github.com/krgamestudios/chat-server
|
* Chat Server: https://github.com/krgamestudios/chat-server
|
||||||
|
|
||||||
# Setup Deployment
|
# Setup Deployment
|
||||||
@@ -25,6 +29,7 @@ A clean install is this easy:
|
|||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/krgamestudios/MERN-template.git
|
git clone https://github.com/krgamestudios/MERN-template.git
|
||||||
|
cd MERN-template
|
||||||
node configure-script.js
|
node configure-script.js
|
||||||
docker-compose up --build
|
docker-compose up --build
|
||||||
```
|
```
|
||||||
@@ -42,15 +47,26 @@ To set up this template in development mode:
|
|||||||
|
|
||||||
# Features List
|
# Features List
|
||||||
|
|
||||||
- Fully Featured Account System
|
- Mainly one language across the codebase (JavaScript)
|
||||||
|
- Full documentation
|
||||||
|
- Setup tutorial
|
||||||
|
- Fully Featured Account System (as a microservice)
|
||||||
- Email validation
|
- Email validation
|
||||||
- Logging in and out
|
- Logging in and out
|
||||||
- Account deletion
|
- Account deletion
|
||||||
- Password management
|
- Password management
|
||||||
- JSON web token authentication
|
- JSON web token authentication
|
||||||
- News Blog
|
- Fully Featured News Blog (as a microservice)
|
||||||
- Optional microservice
|
- Publish, edit or delete articles as needed
|
||||||
- Secure publishing and editing of articles via admin panel
|
- 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
|
- Easy To Use Configuration Script
|
||||||
- Sets up everything via docker
|
- Sets up everything via docker
|
||||||
- A default admin account (if desired)
|
- A default admin account (if desired)
|
||||||
@@ -58,31 +74,19 @@ To set up this template in development mode:
|
|||||||
# Coming Soon
|
# Coming Soon
|
||||||
|
|
||||||
- Full documentation
|
- Full documentation
|
||||||
- Setup tutorial
|
- Modding tutorials
|
||||||
- Modding tutorial
|
|
||||||
- Fully Featured Chat System
|
|
||||||
- Optional microservice
|
|
||||||
- Chat logs
|
|
||||||
- Custom emoji
|
|
||||||
- Global and room-based chat
|
|
||||||
- Private messaging?
|
|
||||||
- Broadcasting to all channels
|
|
||||||
- Badges next to usernames?
|
|
||||||
- Moderation tools for banning, suspending, or chat-banning users
|
|
||||||
|
|
||||||
# Coming Eventually
|
# 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
|
- Better compression for client files
|
||||||
- Backend for energy systems
|
- Backend for leaderboards (modding tutorial?)
|
||||||
- Backend for leaderboards
|
- Backend for energy systems (modding tutorial?)
|
||||||
- Backend for items, shops, trading and currency
|
- Backend for items, shops, trading and currency
|
||||||
|
|
||||||
# Gmail Email Settings
|
|
||||||
|
|
||||||
If you decide to use gmail as your email provider (as I do), then use the following `.env` settings:
|
|
||||||
|
|
||||||
MAIL_SMTP=smtp.gmail.com
|
|
||||||
MAIL_USERNAME=you@gmail.com
|
|
||||||
MAIL_PASSWORD=yourpassword
|
|
||||||
|
|
||||||
you'll also need to enable "less secure apps" for the specified email address. Remember - don't ever commit the `.env` file! You might even want to create a dedicated email address just for your project.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
//react
|
//react
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { BrowserRouter, Switch } from 'react-router-dom';
|
import { BrowserRouter, Switch } from 'react-router-dom';
|
||||||
|
import { TokenContext } from './utilities/token-provider';
|
||||||
|
|
||||||
//library components
|
//library components
|
||||||
import LazyRoute from './lazy-route';
|
import LazyRoute from './utilities/lazy-route';
|
||||||
import Markdown from './panels/markdown';
|
import Markdown from './panels/markdown';
|
||||||
|
|
||||||
//styling
|
//styling
|
||||||
//import a styling template here
|
//import a styling template here
|
||||||
|
|
||||||
//common components
|
//common components
|
||||||
import Header from './panels/header.jsx';
|
import Header from './panels/header';
|
||||||
import Footer from './panels/footer.jsx';
|
import Footer from './panels/footer';
|
||||||
|
import PopupChat from './panels/popup-chat';
|
||||||
|
|
||||||
const App = props => {
|
const App = props => {
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
//default render
|
//default render
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -25,13 +29,18 @@ const App = props => {
|
|||||||
<LazyRoute path='/login' component={() => import('./pages/login')} />
|
<LazyRoute path='/login' component={() => import('./pages/login')} />
|
||||||
<LazyRoute path='/account' component={() => import('./pages/account')} />
|
<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='/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='/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='/credits' component={async () => () => <Markdown content={require('../markdown/credits.md').default} />} />
|
||||||
|
|
||||||
<LazyRoute path='*' component={() => import('./pages/not-found')} />
|
<LazyRoute path='*' component={() => import('./pages/not-found')} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
{ authTokens.accessToken ? <PopupChat /> : <></> }
|
||||||
<Footer />
|
<Footer />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
import Loadable from 'react-loadable';
|
|
||||||
|
|
||||||
const Loading = props => {
|
|
||||||
if (props.error) {
|
|
||||||
return <p>{props.error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.timedOut) {
|
|
||||||
return (
|
|
||||||
<div className='page'>
|
|
||||||
<p>Page Timed Out</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.pastDelay) {
|
|
||||||
return (
|
|
||||||
<div className='page'>
|
|
||||||
<p>Page Loading...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LazyRoute = lazyProps => {
|
|
||||||
const component = Loadable({
|
|
||||||
loader: lazyProps.component,
|
|
||||||
loading: Loading,
|
|
||||||
timeout: 10000
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Route {...lazyProps} component={component} />
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LazyRoute;
|
|
||||||
@@ -37,7 +37,7 @@ const Account = props => {
|
|||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Account</h1>
|
<h1 className='centered'>Account</h1>
|
||||||
<form className='constricted' onSubmit={async evt => {
|
<form className='constrained' onSubmit={async evt => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
|
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ const Account = props => {
|
|||||||
<button type='submit'>Update Information</button>
|
<button type='submit'>Update Information</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DeleteAccount className='constricted' />
|
<DeleteAccount className='constrained' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,23 +5,26 @@ import { TokenContext } from '../utilities/token-provider';
|
|||||||
|
|
||||||
import NewsPublisher from '../panels/news-publisher';
|
import NewsPublisher from '../panels/news-publisher';
|
||||||
import NewsEditor from '../panels/news-editor';
|
import NewsEditor from '../panels/news-editor';
|
||||||
import PrivilegeEditor from '../panels/privilege-editor';
|
|
||||||
|
import GrantAdmin from '../panels/grant-admin';
|
||||||
|
import GrantMod from '../panels/grant-mod';
|
||||||
|
|
||||||
const Admin = props => {
|
const Admin = props => {
|
||||||
//context
|
//context
|
||||||
const authTokens = useContext(TokenContext);
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
//misplaced? (admin only)
|
//misplaced? (admin only)
|
||||||
if (!authTokens.accessToken || authTokens.getPayload().privilege != 'administrator') {
|
if (!authTokens.accessToken || !authTokens.getPayload().admin) {
|
||||||
return <Redirect to='/' />;
|
return <Redirect to='/' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Administration</h1>
|
<h1 className='centered'>Administration Tools</h1>
|
||||||
<NewsPublisher />
|
<NewsPublisher />
|
||||||
<NewsEditor />
|
<NewsEditor />
|
||||||
<PrivilegeEditor />
|
<GrantAdmin />
|
||||||
|
<GrantMod />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const LogIn = props => {
|
|||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Login</h1>
|
<h1 className='centered'>Login</h1>
|
||||||
<form className='constricted' onSubmit={
|
<form className='constrained' onSubmit={
|
||||||
async evt => {
|
async evt => {
|
||||||
//on submit
|
//on submit
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useContext, useRef } from 'react';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
|
//utilities
|
||||||
|
const validateEmail = require('../../../common/utilities/validate-email');
|
||||||
|
|
||||||
|
const Recover = props => {
|
||||||
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//misplaced?
|
||||||
|
if (authTokens.accessToken) {
|
||||||
|
return <Redirect to='/' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
//refs
|
||||||
|
const emailRef = useRef();
|
||||||
|
const recoverRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='page'>
|
||||||
|
<h1 className='centered'>Recover Password</h1>
|
||||||
|
<form className='constrained panel' 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} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='submit' ref={recoverRef}>Recover Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (email) => {
|
||||||
|
email = email.trim();
|
||||||
|
|
||||||
|
const err = handleValidation(email);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
return [err];
|
||||||
|
}
|
||||||
|
|
||||||
|
//send to the auth server
|
||||||
|
const result = await fetch(`${process.env.AUTH_URI}/auth/recover`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
|
console.error(err);
|
||||||
|
return [err, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [await result.text(), true];
|
||||||
|
};
|
||||||
|
|
||||||
|
//returns an error message, or null on success
|
||||||
|
const handleValidation = (email) => {
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
return 'invalid email';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Recover;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useContext, useRef } from 'react';
|
||||||
|
import { Redirect } from 'react-router-dom';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
|
const Reset = props => {
|
||||||
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//query
|
||||||
|
const query = queryString.parse(props.location.search);
|
||||||
|
|
||||||
|
//misplaced?
|
||||||
|
if (authTokens.accessToken || !query.email || !query.token) {
|
||||||
|
return <Redirect to='/' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
//refs
|
||||||
|
const passwordRef = useRef();
|
||||||
|
const retypeRef = useRef();
|
||||||
|
const resetRef = useRef();
|
||||||
|
|
||||||
|
//render the thing
|
||||||
|
return (
|
||||||
|
<div className='page'>
|
||||||
|
<h1 className='centered'>Reset Password</h1>
|
||||||
|
<form className='constrained panel' onSubmit={async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const [err, redirect] = await update(passwordRef.current.value, retypeRef.current.value, query);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Details updated');
|
||||||
|
|
||||||
|
//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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='submit'>Update Information</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = async (password, retype, query) => {
|
||||||
|
if (password != retype) {
|
||||||
|
return ['Passwords do not match'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password && password.length < 8) {
|
||||||
|
return ['Password is too short'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetch(`${process.env.AUTH_URI}/auth/reset?email=${query.email}&token=${query.token}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: password ? password : null,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return [`${await result.status}: ${await result.text()}`];
|
||||||
|
} else {
|
||||||
|
return [null, true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reset;
|
||||||
@@ -22,16 +22,19 @@ const SignUp = props => {
|
|||||||
const passwordRef = useRef();
|
const passwordRef = useRef();
|
||||||
const retypeRef = useRef();
|
const retypeRef = useRef();
|
||||||
const contactRef = useRef();
|
const contactRef = useRef();
|
||||||
|
const signupRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='page'>
|
<div className='page'>
|
||||||
<h1 className='centered'>Signup</h1>
|
<h1 className='centered'>Signup</h1>
|
||||||
<form className='constricted' onSubmit={
|
<form className='constrained' onSubmit={
|
||||||
async evt => { //on submit
|
async evt => { //on submit
|
||||||
|
signupRef.current.disabled = true;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
|
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
|
||||||
if (result) {
|
if (result) {
|
||||||
alert(result);
|
alert(result);
|
||||||
|
signupRef.current.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//redirect
|
//redirect
|
||||||
@@ -65,7 +68,7 @@ const SignUp = props => {
|
|||||||
<input type='checkbox' name='contact' ref={contactRef} />
|
<input type='checkbox' name='contact' ref={contactRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type='submit'>Signup</button>
|
<button type='submit' ref={signupRef}>Signup</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useRef, useContext } from 'react';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
|
const BanUser = props => {
|
||||||
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//ref
|
||||||
|
const usernameRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className='centered'>Permanently Ban User</h2>
|
||||||
|
<form>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='username'>Username:</label>
|
||||||
|
<input type='text' name='username' ref={usernameRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='button' onClick={async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const yes = confirm('Permanently ban this user from the website?');
|
||||||
|
|
||||||
|
if (!yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch);
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
usernameRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}>Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonPress = async (username, tokenFetch) => {
|
||||||
|
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/banuser`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
|
console.log(err);
|
||||||
|
return [err, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, true];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BanUser;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useRef, useContext } from 'react';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
|
const GrantAdmin = props => {
|
||||||
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//ref
|
||||||
|
const usernameRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className='centered'>Grant Admin Privileges</h2>
|
||||||
|
<form>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='username'>Username:</label>
|
||||||
|
<input type='text' name='username' ref={usernameRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='button' onClick={async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'POST');
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
alert('admin set');
|
||||||
|
usernameRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}>Submit</button>
|
||||||
|
|
||||||
|
<button type='button' onClick={async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'DELETE');
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
alert('admin removed');
|
||||||
|
usernameRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}>Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonPress = async (username, tokenFetch, method) => {
|
||||||
|
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/admin`, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
|
console.log(err);
|
||||||
|
return [err, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, true];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GrantAdmin;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useRef, useContext } from 'react';
|
||||||
|
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
|
||||||
|
const GrantMod = props => {
|
||||||
|
//context
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
//ref
|
||||||
|
const usernameRef = useRef();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className='centered'>Grant Moderation Privileges</h2>
|
||||||
|
<form>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='username'>Username:</label>
|
||||||
|
<input type='text' name='username' ref={usernameRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='button' onClick={async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'POST');
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
alert('mod set');
|
||||||
|
usernameRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}>Submit</button>
|
||||||
|
|
||||||
|
<button type='button' onClick={async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const [err, result] = await handleButtonPress(usernameRef.current.value, authTokens.tokenFetch, 'DELETE');
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
alert(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
alert('mod removed');
|
||||||
|
usernameRef.current.value = '';
|
||||||
|
}
|
||||||
|
}}>Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleButtonPress = async (username, tokenFetch, method) => {
|
||||||
|
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/mod`, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const err = `${result.status}: ${await result.text()}`;
|
||||||
|
console.log(err);
|
||||||
|
return [err, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, true];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GrantMod;
|
||||||
@@ -9,6 +9,8 @@ const Visitor = () => {
|
|||||||
<Link to='/signup'>Sign Up</Link>
|
<Link to='/signup'>Sign Up</Link>
|
||||||
<span> - </span>
|
<span> - </span>
|
||||||
<Link to='/login'>Log In</Link>
|
<Link to='/login'>Log In</Link>
|
||||||
|
<span> - </span>
|
||||||
|
<Link to='/recover'>Recover</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -21,7 +23,7 @@ const Member = () => {
|
|||||||
<Link to='/account'>Account</Link>
|
<Link to='/account'>Account</Link>
|
||||||
<span> - </span>
|
<span> - </span>
|
||||||
|
|
||||||
{ authTokens.getPayload().privilege == 'administrator' ?
|
{ authTokens.getPayload().admin ?
|
||||||
<span>
|
<span>
|
||||||
<Link to='/admin'>Admin</Link>
|
<Link to='/admin'>Admin</Link>
|
||||||
<span> - </span>
|
<span> - </span>
|
||||||
@@ -29,6 +31,14 @@ const Member = () => {
|
|||||||
<span />
|
<span />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{ authTokens.getPayload().mod ?
|
||||||
|
<span>
|
||||||
|
<Link to='/mod'>Moderation</Link>
|
||||||
|
<span> - </span>
|
||||||
|
</span>:
|
||||||
|
<span />
|
||||||
|
}
|
||||||
|
|
||||||
{ /* Logout button logs you out of the server too */ }
|
{ /* Logout button logs you out of the server too */ }
|
||||||
<Link to='/' onClick={async () => {
|
<Link to='/' onClick={async () => {
|
||||||
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, { //NOTE: this gets overwritten as a bugfix
|
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, { //NOTE: this gets overwritten as a bugfix
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import ReactMarkdown from 'react-markdown/with-html';
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
|
||||||
const Markdown = props => {
|
const Markdown = props => {
|
||||||
//content?
|
//content?
|
||||||
@@ -27,7 +28,7 @@ const Markdown = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown escapeHtml={false} props={{...props}}>{contentHook}</ReactMarkdown>
|
<ReactMarkdown rehypePlugins={[rehypeRaw]} props={{...props}}>{contentHook}</ReactMarkdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import dateFormat from 'dateformat';
|
import dateFormat from 'dateformat';
|
||||||
|
|
||||||
//DOCS: props.uri is the address of a live news-server
|
|
||||||
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
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(() => {
|
||||||
const result = await fetch(`${process.env.NEWS_URI}/news`, {
|
//this... um...
|
||||||
|
fetch(`${process.env.NEWS_URI}/news`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Access-Control-Allow-Origin': '*'
|
'Access-Control-Allow-Origin': '*'
|
||||||
},
|
},
|
||||||
});
|
signal: aborter.current.signal //oh dear
|
||||||
|
})
|
||||||
|
.then(blob => blob.json())
|
||||||
|
.then(json => setArticles(json))
|
||||||
|
.catch(e => null) //swallow errors
|
||||||
|
;
|
||||||
|
|
||||||
if (!result.ok) {
|
return () => aborter.current.abort(); //This is an ugly, ugly solution, but it's the only one that works
|
||||||
const err = `${result.status}: ${await result.text()}`;
|
|
||||||
console.log(err);
|
|
||||||
alert(err);
|
|
||||||
} else {
|
|
||||||
setArticles(await result.json());
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className='centered'>News Feed</h1>
|
<h1 className='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}>
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useContext } from 'react';
|
||||||
|
import { TokenContext } from '../utilities/token-provider';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
|
import '../../styles/popup-chat.css';
|
||||||
|
|
||||||
|
const socket = io(`${process.env.CHAT_URI}/chat`);
|
||||||
|
|
||||||
|
const PopupChat = props => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [chatlog, setChatlog] = useState([]);
|
||||||
|
|
||||||
|
const inputRef = useRef();
|
||||||
|
const sendRef = useRef();
|
||||||
|
const endRef = useRef();
|
||||||
|
|
||||||
|
const authTokens = useContext(TokenContext);
|
||||||
|
|
||||||
|
const pushChatlog = line => setChatlog(prevChatlog => [...prevChatlog, line]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket.on('message', message => pushChatlog(message));
|
||||||
|
socket.on('backlog', messages => setChatlog(prev => [...prev, ...messages]));
|
||||||
|
socket.on('disconnect', reason => pushChatlog({ emphasis: true, text: 'Lost connection' }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
endRef.current.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [chatlog, open]);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<div className='chat'>
|
||||||
|
<button type='button' className='open' onClick={() => authTokens.tokenCallback(accessToken => handleOpen(setOpen, accessToken))}>Open Chat</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='chat'>
|
||||||
|
<div className='log'>
|
||||||
|
<ul className='scrollable'>
|
||||||
|
{chatlog.map((line, index) => processLine(line, index, authTokens.accessToken))}
|
||||||
|
<li ref={endRef} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<input type='text' className='input' placeholder='message' onKeyPress={evt => evt.key == 'Enter' ? sendRef.current.click() : ''} ref={inputRef} />
|
||||||
|
<button type='button' className='send' onClick={() => authTokens.tokenCallback(accessToken => handleSend(inputRef, pushChatlog, authTokens.getPayload().username, accessToken))} ref={sendRef}>Send</button>
|
||||||
|
<button type='button' className='close' onClick={() => handleClose(setOpen)}>Close Chat</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//handlers
|
||||||
|
const handleOpen = (setOpen, accessToken) => {
|
||||||
|
setOpen(true);
|
||||||
|
|
||||||
|
socket.emit('open chat', {
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = setOpen => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = (inputRef, pushChatlog, username, accessToken) => {
|
||||||
|
if (inputRef.current.value == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit('message', {
|
||||||
|
accessToken,
|
||||||
|
text: inputRef.current.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!inputRef.current.value.startsWith('/')) {
|
||||||
|
pushChatlog({ username: username, text: inputRef.current.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
inputRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
//render each line
|
||||||
|
const processLine = (line, index, accessToken) => {
|
||||||
|
let content = <div className='content'>{line.username ? <span className='username'>{line.username}: </span> : ''}{line.text ? <span className='text'>{line.text}</span> : ''}</div>;
|
||||||
|
|
||||||
|
//decorators
|
||||||
|
if (line.emphasis) {
|
||||||
|
content = <em>{content}</em>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.strong) {
|
||||||
|
content = <strong>{content}</strong>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li key={index} className='line'>{content}<div className='report'><a onClick={() => processReport(line, accessToken)} style={{ display: line.index && !line.notification ? 'flex' : 'none' }}>!!!</a></div></li>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processReport = (line, accessToken) => {
|
||||||
|
const yes = confirm('Report this message?');
|
||||||
|
|
||||||
|
if (yes) {
|
||||||
|
socket.emit('report', {
|
||||||
|
accessToken,
|
||||||
|
index: line.index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupChat;
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import React, { useState, useRef, useContext } from 'react';
|
|
||||||
import Select from 'react-dropdown-select';
|
|
||||||
|
|
||||||
import { TokenContext } from '../utilities/token-provider';
|
|
||||||
|
|
||||||
const PrivilegeEditor = props => {
|
|
||||||
//context
|
|
||||||
const authTokens = useContext(TokenContext);
|
|
||||||
|
|
||||||
//state
|
|
||||||
const [privilege, setPrivilege] = useState('normal');
|
|
||||||
|
|
||||||
//ref
|
|
||||||
const usernameRef = useRef();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className='centered'>Privilege Editor</h2>
|
|
||||||
<form onSubmit={async evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
const [err, result] = await handleSubmit(usernameRef.current.value, privilege, authTokens.tokenFetch);
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
alert(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
alert('Privilege set');
|
|
||||||
usernameRef.current.value = '';
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
<label htmlFor='username'>Username:</label>
|
|
||||||
<input type='text' name='username' ref={usernameRef} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
options={[{ label: 'administrator', value: 1 }, { label: 'moderator', value: 2 }, { label: 'alpha', value: 3 }, { label: 'beta', value: 4 }, { label: 'gamma', value: 5 }, { label: 'normal', value: 6 }]}
|
|
||||||
onChange={values => setPrivilege(values[0].label)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button type='submit'>Change</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (username, privilege, tokenFetch) => {
|
|
||||||
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/privilege`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Access-Control-Allow-Origin': '*'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username,
|
|
||||||
privilege
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.ok) {
|
|
||||||
const err = `${result.status}: ${await result.text()}`;
|
|
||||||
console.log(err);
|
|
||||||
return [err, false];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [null, true];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PrivilegeEditor;
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import loadable from '@loadable/component';
|
||||||
|
|
||||||
|
const LazyRoute = props => {
|
||||||
|
const { component, ...lazyProps } = props;
|
||||||
|
|
||||||
|
const lazyComponent = loadable(component);
|
||||||
|
|
||||||
|
return <Route {...lazyProps} component={lazyComponent} />
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LazyRoute;
|
||||||
@@ -11,7 +11,7 @@ const TokenProvider = props => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccessToken(localStorage.getItem("accessToken") || '');
|
setAccessToken(localStorage.getItem("accessToken") || '');
|
||||||
setRefreshToken(localStorage.getItem("refreshToken") || '');
|
setRefreshToken(localStorage.getItem("refreshToken") || '');
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("accessToken", accessToken);
|
localStorage.setItem("accessToken", accessToken);
|
||||||
@@ -77,8 +77,44 @@ const TokenProvider = props => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//access the refreshed token via callback
|
||||||
|
const tokenCallback = async (cb) => {
|
||||||
|
//if expired (10 minutes, normally)
|
||||||
|
const expired = new Date(decode(accessToken).exp * 1000) < Date.now();
|
||||||
|
|
||||||
|
if (expired) {
|
||||||
|
//ping the auth server for a new token
|
||||||
|
const response = await fetch(`${process.env.AUTH_URI}/auth/token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: refreshToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
//any errors, throw them
|
||||||
|
if (!response.ok) {
|
||||||
|
throw `${response.status}: ${await response.text()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//save the new auth stuff (setting bearer as well)
|
||||||
|
const newAuth = await response.json();
|
||||||
|
|
||||||
|
setAccessToken(newAuth.accessToken);
|
||||||
|
setRefreshToken(newAuth.refreshToken);
|
||||||
|
|
||||||
|
//finally
|
||||||
|
return cb(newAuth.accessToken);
|
||||||
|
} else {
|
||||||
|
return cb(accessToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TokenContext.Provider value={{ accessToken, refreshToken, setAccessToken, setRefreshToken, tokenFetch, getPayload: () => decode(accessToken) }}>
|
<TokenContext.Provider value={{ accessToken, refreshToken, setAccessToken, setRefreshToken, tokenFetch, tokenCallback, getPayload: () => decode(accessToken) }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</TokenContext.Provider>
|
</TokenContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
.chat {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 23px;
|
||||||
|
right: 28px;
|
||||||
|
width: 280px;
|
||||||
|
border: solid;
|
||||||
|
border-color: black;
|
||||||
|
border-width: 2px;
|
||||||
|
background-color: #CCC;
|
||||||
|
display: inline-block;
|
||||||
|
max-height: calc(50vh - 23px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > button.open {
|
||||||
|
color: white;
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > button.send {
|
||||||
|
color: white;
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > button.close {
|
||||||
|
color: black;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > button {
|
||||||
|
width: 100%;
|
||||||
|
height: 2em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .input {
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log > .scrollable > .line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log > .scrollable > .line > .report {
|
||||||
|
color: red;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log > .scrollable > .line:hover {
|
||||||
|
background-color: #BBB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log > .scrollable > .line:hover > .report {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log > .scrollable > .line > .content > .username {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat > .log > .scrollable {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 280px;
|
||||||
|
max-height: calc(50vh - 23px - 20px - 6em);
|
||||||
|
overflow-x: wrap;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
+46
-10
@@ -40,6 +40,7 @@ impelented are:
|
|||||||
|
|
||||||
* auth-server
|
* auth-server
|
||||||
* news-server
|
* news-server
|
||||||
|
* chat-server
|
||||||
|
|
||||||
See https://github.com/krgamestudios/MERN-template/wiki for help.
|
See https://github.com/krgamestudios/MERN-template/wiki for help.
|
||||||
`
|
`
|
||||||
@@ -56,13 +57,14 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
|
|||||||
const newsName = await question('News Name', 'news');
|
const newsName = await question('News Name', 'news');
|
||||||
const newsWebAddress = await question('News Web Address', `${newsName}.${projectWebAddress}`);
|
const newsWebAddress = await question('News Web Address', `${newsName}.${projectWebAddress}`);
|
||||||
const newsDBUser = await question('News DB Username', newsName);
|
const newsDBUser = await question('News DB Username', newsName);
|
||||||
const newsDBPass = await question('News DB Password', 'charizard');
|
const newsDBPass = await question('News DB Password', 'venusaur');
|
||||||
|
|
||||||
//auth configuration
|
//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 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', 'venusaur');
|
const authDBPass = await question('Auth DB Password', 'charizard');
|
||||||
|
|
||||||
const emailSMTP = await question('Email SMTP', 'smtp.example.com');
|
const emailSMTP = await question('Email SMTP', 'smtp.example.com');
|
||||||
const emailUser = await question('Email Address', 'foobar@example.com');
|
const emailUser = await question('Email Address', 'foobar@example.com');
|
||||||
@@ -70,6 +72,10 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
|
|||||||
const emailPhysical = await question('Physical Mailing Address', '');
|
const emailPhysical = await question('Physical Mailing Address', '');
|
||||||
|
|
||||||
//chat goes here
|
//chat goes here
|
||||||
|
const chatName = await question('Chat Name', 'chat');
|
||||||
|
const chatWebAddress = await question('Chat Web Address', `${chatName}.${projectWebAddress}`);
|
||||||
|
const chatDBUser = await question('Chat DB Username', chatName);
|
||||||
|
const chatDBPass = await question('Chat DB Password', 'blastoise');
|
||||||
|
|
||||||
//database configuration
|
//database configuration
|
||||||
const dbRootPassword = await question('Database Root Password', 'password');
|
const dbRootPassword = await question('Database Root Password', 'password');
|
||||||
@@ -101,7 +107,7 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
|
|||||||
const projectPort = 3000;
|
const projectPort = 3000;
|
||||||
const newsPort = 3100;
|
const newsPort = 3100;
|
||||||
const authPort = 3200;
|
const authPort = 3200;
|
||||||
//const chatPort = 3300;
|
const chatPort = 3300;
|
||||||
|
|
||||||
const ymlfile = `
|
const ymlfile = `
|
||||||
version: "3.6"
|
version: "3.6"
|
||||||
@@ -118,7 +124,7 @@ services:
|
|||||||
- traefik.http.routers.${projectName}router.service=${projectName}service@docker
|
- traefik.http.routers.${projectName}router.service=${projectName}service@docker
|
||||||
- traefik.http.services.${projectName}service.loadbalancer.server.port=${projectPort}
|
- traefik.http.services.${projectName}service.loadbalancer.server.port=${projectPort}
|
||||||
environment:
|
environment:
|
||||||
- WEB_PORT=3000
|
- WEB_PORT=${projectPort}
|
||||||
- DB_HOSTNAME=database
|
- DB_HOSTNAME=database
|
||||||
- DB_DATABASE=${projectName}
|
- DB_DATABASE=${projectName}
|
||||||
- DB_USERNAME=${projectDBUser}
|
- DB_USERNAME=${projectDBUser}
|
||||||
@@ -126,6 +132,7 @@ services:
|
|||||||
- DB_TIMEZONE=${dbTimeZone}
|
- DB_TIMEZONE=${dbTimeZone}
|
||||||
- NEWS_URI=https://${newsWebAddress}
|
- NEWS_URI=https://${newsWebAddress}
|
||||||
- AUTH_URI=https://${authWebAddress}
|
- AUTH_URI=https://${authWebAddress}
|
||||||
|
- CHAT_URI=https://${chatWebAddress}
|
||||||
- SECRET_ACCESS=${accessToken}
|
- SECRET_ACCESS=${accessToken}
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
@@ -143,9 +150,9 @@ services:
|
|||||||
- traefik.http.routers.${newsName}router.entrypoints=websecure
|
- traefik.http.routers.${newsName}router.entrypoints=websecure
|
||||||
- traefik.http.routers.${newsName}router.tls.certresolver=myresolver
|
- traefik.http.routers.${newsName}router.tls.certresolver=myresolver
|
||||||
- traefik.http.routers.${newsName}router.service=${newsName}service@docker
|
- traefik.http.routers.${newsName}router.service=${newsName}service@docker
|
||||||
- traefik.http.services.${newsName}service.loadbalancer.server.port=3100
|
- traefik.http.services.${newsName}service.loadbalancer.server.port=${newsPort}
|
||||||
environment:
|
environment:
|
||||||
- WEB_PORT=3100
|
- WEB_PORT=${newsPort}
|
||||||
- DB_HOSTNAME=database
|
- DB_HOSTNAME=database
|
||||||
- DB_DATABASE=${newsName}
|
- DB_DATABASE=${newsName}
|
||||||
- DB_USERNAME=${newsDBUser}
|
- DB_USERNAME=${newsDBUser}
|
||||||
@@ -173,6 +180,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- WEB_PROTOCOL=https
|
- WEB_PROTOCOL=https
|
||||||
- WEB_ADDRESS=${authWebAddress}
|
- WEB_ADDRESS=${authWebAddress}
|
||||||
|
- WEB_RESET_ADDRESS=${authResetAddress}
|
||||||
- WEB_PORT=${authPort}
|
- WEB_PORT=${authPort}
|
||||||
- DB_HOSTNAME=database
|
- DB_HOSTNAME=database
|
||||||
- DB_DATABASE=${authName}
|
- DB_DATABASE=${authName}
|
||||||
@@ -193,8 +201,30 @@ services:
|
|||||||
- database
|
- database
|
||||||
- traefik
|
- traefik
|
||||||
|
|
||||||
#chat:
|
${chatName}:
|
||||||
# image: krgamestudios/chat-server
|
image: krgamestudios/chat-server:latest
|
||||||
|
ports:
|
||||||
|
- ${chatPort}
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.${chatName}router.rule=Host(\`${chatWebAddress}\`)
|
||||||
|
- traefik.http.routers.${chatName}router.entrypoints=websecure
|
||||||
|
- traefik.http.routers.${chatName}router.tls.certresolver=myresolver
|
||||||
|
- traefik.http.routers.${chatName}router.service=${chatName}service@docker
|
||||||
|
- traefik.http.services.${chatName}service.loadbalancer.server.port=${chatPort}
|
||||||
|
environment:
|
||||||
|
- WEB_PORT=${chatPort}
|
||||||
|
- DB_HOSTNAME=database
|
||||||
|
- DB_DATABASE=${chatName}
|
||||||
|
- DB_USERNAME=${chatDBUser}
|
||||||
|
- DB_PASSWORD=${chatDBPass}
|
||||||
|
- DB_TIMEZONE=${dbTimeZone}
|
||||||
|
- SECRET_ACCESS=${accessToken}
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- traefik
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: mariadb
|
image: mariadb
|
||||||
@@ -237,10 +267,12 @@ networks:
|
|||||||
const dockerfile = `
|
const dockerfile = `
|
||||||
FROM node:15
|
FROM node:15
|
||||||
WORKDIR "/app"
|
WORKDIR "/app"
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
RUN mkdir /app/public
|
||||||
|
RUN chown node:node /app/public
|
||||||
|
RUN npm install --production
|
||||||
EXPOSE ${projectPort}
|
EXPOSE ${projectPort}
|
||||||
|
USER node
|
||||||
ENTRYPOINT ["bash", "-c"]
|
ENTRYPOINT ["bash", "-c"]
|
||||||
CMD ["sleep 10 && npm start"]
|
CMD ["sleep 10 && npm start"]
|
||||||
`;
|
`;
|
||||||
@@ -258,6 +290,10 @@ CREATE DATABASE IF NOT EXISTS ${authName};
|
|||||||
CREATE USER IF NOT EXISTS '${authDBUser}'@'%' IDENTIFIED BY '${authDBPass}';
|
CREATE USER IF NOT EXISTS '${authDBUser}'@'%' IDENTIFIED BY '${authDBPass}';
|
||||||
GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'%';
|
GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'%';
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS ${chatName};
|
||||||
|
CREATE USER IF NOT EXISTS '${chatDBUser}'@'%' IDENTIFIED BY '${chatDBPass}';
|
||||||
|
GRANT ALL PRIVILEGES ON ${chatName}.* TO '${chatDBUser}'@'%';
|
||||||
|
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Generated
+6807
-4625
File diff suppressed because it is too large
Load Diff
+23
-19
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mern-template",
|
"name": "mern-template",
|
||||||
"version": "1.0.0",
|
"version": "1.0.4",
|
||||||
"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": {
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"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"
|
"analyzer": "webpack --env=production --analyzer --config webpack.config.js"
|
||||||
},
|
},
|
||||||
@@ -24,34 +24,38 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
"homepage": "https://github.com/KRGameStudios/MERN-template#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.14.8",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.14.8",
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.14.5",
|
||||||
|
"@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": "^5.3.0",
|
"concurrently": "^6.2.0",
|
||||||
|
"css-loader": "^6.2.0",
|
||||||
"dateformat": "^4.5.1",
|
"dateformat": "^4.5.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^10.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"html-webpack-plugin": "^5.0.0-alpha.14",
|
"html-webpack-plugin": "^5.3.2",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"mariadb": "^2.5.2",
|
"mariadb": "^2.5.4",
|
||||||
|
"query-string": "^7.0.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-dropdown-select": "^4.7.4",
|
"react-dropdown-select": "^4.7.4",
|
||||||
"react-loadable": "^5.5.0",
|
"react-markdown": "^6.0.2",
|
||||||
"react-markdown": "^5.0.3",
|
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"sequelize": "^6.4.0",
|
"rehype-raw": "^5.1.0",
|
||||||
"universal-cookie": "^4.0.4",
|
"sequelize": "^6.6.5",
|
||||||
"webpack": "^5.15.0",
|
"socket.io-client": "^4.1.3",
|
||||||
"webpack-cli": "^4.3.1"
|
"style-loader": "^3.2.1",
|
||||||
|
"webpack": "^5.46.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.4.2",
|
||||||
|
"webpack-cli": "^4.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.12",
|
||||||
"webpack-bundle-analyzer": "^4.3.0",
|
|
||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-dev-server": "^3.11.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME
|
|||||||
host: process.env.DB_HOSTNAME,
|
host: process.env.DB_HOSTNAME,
|
||||||
dialect: 'mariadb',
|
dialect: 'mariadb',
|
||||||
timezone: process.env.DB_TIMEZONE,
|
timezone: process.env.DB_TIMEZONE,
|
||||||
logging: false
|
logging: process.env.DB_LOGGING ? console.log : false
|
||||||
});
|
});
|
||||||
|
|
||||||
sequelize.sync();
|
sequelize.sync();
|
||||||
|
|||||||
+1
-2
@@ -8,10 +8,9 @@ const path = require('path');
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = require('http').Server(app);
|
const server = require('http').Server(app);
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
|
|
||||||
//config
|
//config
|
||||||
app.use(bodyParser.json());
|
app.use(express.json());
|
||||||
|
|
||||||
//database connection
|
//database connection
|
||||||
const database = require('./database');
|
const database = require('./database');
|
||||||
|
|||||||
+9
-5
@@ -18,7 +18,7 @@ module.exports = ({ production, analyzer }) => {
|
|||||||
filename: '[name].[chunkhash].js',
|
filename: '[name].[chunkhash].js',
|
||||||
sourceMapFilename: '[name].[chunkhash].js.map'
|
sourceMapFilename: '[name].[chunkhash].js.map'
|
||||||
},
|
},
|
||||||
devtool: 'eval-source-map',
|
devtool: production ? 'source-map' : 'eval-source-map',
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx']
|
extensions: ['.js', '.jsx']
|
||||||
},
|
},
|
||||||
@@ -32,11 +32,15 @@ module.exports = ({ production, analyzer }) => {
|
|||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
options: {
|
options: {
|
||||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||||
plugins: ['react-loadable/babel', '@babel/plugin-syntax-dynamic-import']
|
plugins: ['@babel/plugin-syntax-dynamic-import']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(css)$/,
|
||||||
|
use: ['style-loader', 'css-loader']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(md)$/,
|
test: /\.(md)$/,
|
||||||
use: [
|
use: [
|
||||||
@@ -51,9 +55,9 @@ module.exports = ({ production, analyzer }) => {
|
|||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
'process.env': {
|
'process.env': {
|
||||||
'PRODUCTION': production,
|
'PRODUCTION': production,
|
||||||
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.eggtrainer.com"',
|
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.krgamestudios.com"',
|
||||||
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.eggtrainer.com"',
|
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.krgamestudios.com"',
|
||||||
// 'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.eggtrainer.com"',
|
'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.krgamestudios.com"',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new CleanWebpackPlugin({
|
new CleanWebpackPlugin({
|
||||||
|
|||||||
Reference in New Issue
Block a user