Compare commits

..

47 Commits

Author SHA1 Message Date
Kayne Ruse eb370663d2 Tweaked some display 2021-07-29 00:37:23 +10:00
Kayne Ruse 462116d980 Fixed whitespace 2021-07-28 23:18:22 +10:00
Kayne Ruse af06ddc06d Working on password recovery 2021-07-28 23:01:41 +10:00
Kayne Ruse f937ee47db Removed universal-cookie, fixed package.json versioning 2021-07-26 05:05:39 +01:00
Kayne Ruse 6d0dd419ca Fixed #22 2021-07-26 13:48:08 +10:00
Kayne Ruse 2919467dff Fixed #23 2021-07-26 13:38:33 +10:00
Kayne Ruse 269caac88c Tweaked README.md 2021-07-25 20:35:34 +01:00
Kayne Ruse 69c297fa74 Stupid line endings 2021-07-25 19:22:17 +10:00
Kayne Ruse 0f538be3e5 Fixed #20 2021-07-25 18:53:14 +10:00
Kayne Ruse f85b6e8793 Fixed passwords 2021-07-25 18:52:46 +10:00
Kayne Ruse 2af9532930 Fixed deploy bug, properly 2021-07-24 14:01:20 +01:00
Kayne Ruse 191da50740 Deploy bug 2021-07-24 22:20:17 +10:00
Kayne Ruse f24c7990f6 Fixed line-endings 2021-07-24 20:23:18 +10:00
Kayne Ruse 7a48780f50 Update package.json 2021-07-23 21:25:24 +10:00
Kayne Ruse 33952a9896 Changed chat line id to index 2021-07-23 21:24:19 +10:00
Kayne Ruse 8076b0cc40 Pointed to new dev-services 2021-07-22 21:48:42 +01:00
Kayne Ruse e216474196 Bumped version number 2021-07-15 08:53:57 +10:00
Kayne Ruse 2532bf1867 Updated packages to fix vulnerabilities 2021-07-15 08:52:54 +10:00
Kayne Ruse 93a3c30e81 Fixed the database hostname
It's supposed to point to localhost when developing locally.
2021-04-08 04:21:31 +10:00
Kayne Ruse ae8c82e83a Fixed embedded source-map error 2021-04-07 05:00:16 +10:00
Kayne Ruse bc6a795750 WHOOPS THAT WAS A MISTAKE 2021-04-07 03:18:48 +10:00
Kayne Ruse 9947ef13c1 Added a workaround for a mysql bug 2021-04-07 02:21:44 +10:00
Kayne Ruse d3f0b1ac7d Fixed WEB_PORT setting 2021-04-03 03:59:08 +11:00
Kayne Ruse b5f9c45a1b Updated README.md 2021-03-28 08:56:02 +11:00
Kayne Ruse 5189415e1a Implemented permabans 2021-03-28 08:33:26 +11:00
Kayne Ruse be793ae2ff Chat report table working 2021-03-28 07:58:52 +11:00
Kayne Ruse 19f5c20056 "Fixed" errors on logout, read more
I really don't like this solution.

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

Related to krgamestudios/MERN-template#16
2021-03-22 16:39:44 +11:00
Kayne Ruse 9c8ece5c06 Implemented DB_LOGGING 2021-03-20 05:13:12 +11:00
Kayne Ruse 9201e9374b Updated README.md 2021-03-19 20:39:52 +11:00
Kayne Ruse 8c8b78462f Updated README.md 2021-03-19 18:49:01 +11:00
Kayne Ruse 07e578b4c4 Updated README.md 2021-03-19 18:47:17 +11:00
Kayne Ruse 732eeb933e Tweak 2021-03-18 05:26:21 +11:00
Kayne Ruse 104e15d714 Stupid tabs 2021-03-18 05:21:26 +11:00
Kayne Ruse 2c9871d82a Tweak 2021-03-18 05:17:12 +11:00
Kayne Ruse f5f44ae9f7 Updated configure-script.js with chat info 2021-03-18 05:15:32 +11:00
Kayne Ruse 1c3d24575e Added backlog to chat 2021-03-18 02:40:22 +11:00
Kayne Ruse 13e3ce6db8 Chat is working with a local chat-server 2021-03-17 16:52:14 +11:00
Kayne Ruse 8561219542 Wrote CSS for a chatbox 2021-03-17 03:44:38 +11:00
Kayne Ruse e288a43519 Tweaked README.md 2021-03-17 00:56:33 +11:00
27 changed files with 7831 additions and 4935 deletions
+8 -2
View File
@@ -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
SECRET_ACCESS=access # Give this any value to enable database logging (such as "true")
DB_LOGGING=
# Make sure this value matches the system that you connect to
SECRET_ACCESS=access
+91 -88
View File
@@ -1,88 +1,91 @@
# MERN-template # MERN-template
A website template using the MERN stack. The primary technology involved is: A website template using the MERN stack. 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).
# 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
* Auth Server: https://github.com/krgamestudios/auth-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).
* News Server: https://github.com/krgamestudios/news-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
node configure-script.js ```
docker-compose up --build git clone https://github.com/krgamestudios/MERN-template.git
``` cd MERN-template
npm install
# Setup Development node configure-script.js
docker-compose up --build
To set up this template in development mode: ```
1. Ensure mariadb is running in your development environment # Setup Development
2. Run `mariadb sql/create_database.sql` as the root user
3. Run `npm install` To set up this template in development mode:
4. Run `cp .envdev .env` and enter your details into the `.env` file
5. Execute `npm run dev` 1. Ensure mariadb is running in your development environment
6. Navigate to `http://localhost:3001` in your web browser 2. Run `mariadb sql/create_database.sql` as the root user
3. Run `npm install`
# Features List 4. Run `cp .envdev .env` and enter your details into the `.env` file
5. Execute `npm run dev`
- Fully Featured Account System 6. Navigate to `http://localhost:3001` in your web browser
- Email validation
- Logging in and out # Features List
- Account deletion
- Password management - Mainly one language across the codebase (JavaScript)
- JSON web token authentication - Full documentation
- News Blog - Setup tutorial
- Optional microservice - Fully Featured Account System (as a microservice)
- Secure publishing and editing of articles via admin panel - Email validation
- Easy To Use Configuration Script - Logging in and out
- Sets up everything via docker - Account deletion
- A default admin account (if desired) - Password management
- JSON web token authentication
# Coming Soon - Fully Featured News Blog (as a microservice)
- Publish, edit or delete articles as needed
- Full documentation - Secured via admin panel
- Setup tutorial - Fully Featured Chat System (as a microservice)
- Modding tutorial - Available when logged in
- Fully Featured Chat System - Chat logs saved to the database
- Optional microservice - Room-based chat (type `/room name` to access a specific room)
- Chat logs - Moderation tools
- Custom emoji - Permanently banning users
- Global and room-based chat - Chat-muting users for a time period
- Private messaging? - Users reporting offensive chat-content
- Broadcasting to all channels - Easy To Use Configuration Script
- Badges next to usernames? - Sets up everything via docker
- Moderation tools for banning, suspending, or chat-banning users - A default admin account (if desired)
# Coming Eventually # Coming Soon
- Better compression for client files
- Backend for energy systems - Full documentation
- Backend for leaderboards - Modding tutorials
- Backend for items, shops, trading and currency
# Coming Eventually
# Gmail Email Settings
- Fully Featured News Blog (as a microservice)
If you decide to use gmail as your email provider (as I do), then use the following `.env` settings: - Restore deleted articles
- Undo edits
MAIL_SMTP=smtp.gmail.com - Fully Featured Chat System (as a microservice)
MAIL_USERNAME=you@gmail.com - Custom emoji
MAIL_PASSWORD=yourpassword - Private messaging
- Broadcasting to all channels
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. - Badges next to usernames
- Better compression for client files
- Backend for leaderboards (modding tutorial?)
- Backend for energy systems (modding tutorial?)
- Backend for items, shops, trading and currency
+13 -4
View File
@@ -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>
); );
-39
View File
@@ -1,39 +0,0 @@
import React from 'react';
import { Route } from 'react-router-dom';
import Loadable from 'react-loadable';
const Loading = props => {
if (props.error) {
return <p>{props.error}</p>;
}
if (props.timedOut) {
return (
<div className='page'>
<p>Page Timed Out</p>
</div>
);
}
if (props.pastDelay) {
return (
<div className='page'>
<p>Page Loading...</p>
</div>
);
}
return null;
};
const LazyRoute = lazyProps => {
const component = Loadable({
loader: lazyProps.component,
loading: Loading,
timeout: 10000
});
return <Route {...lazyProps} component={component} />
};
export default LazyRoute;
+7 -4
View File
@@ -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>
); );
}; };
+27
View File
@@ -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;
+91
View File
@@ -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='constricted' onSubmit={
async evt => { //on submit
recoverRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value);
if (result) {
alert(result);
recoverRef.current.disabled = false;
}
//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;
+89
View File
@@ -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='constricted' onSubmit={async evt => {
evt.preventDefault();
const [err] = 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];
}
}
export default Reset;
+4 -1
View File
@@ -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='constricted' 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>
); );
+65
View File
@@ -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;
+67
View File
@@ -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;
+74
View File
@@ -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;
+74
View File
@@ -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;
+11 -1
View File
@@ -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
+3 -2
View File
@@ -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]} escapeHtml={false} props={{...props}}>{contentHook}</ReactMarkdown>
); );
}; };
+27 -13
View File
@@ -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 />
+113
View File
@@ -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;
+38 -2
View File
@@ -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>
) )
+82
View File
@@ -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;
}
+47 -11
View File
@@ -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}
@@ -192,9 +200,31 @@ services:
depends_on: depends_on:
- 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;
`; `;
+6815 -4633
View File
File diff suppressed because it is too large Load Diff
+61 -57
View File
@@ -1,57 +1,61 @@
{ {
"name": "mern-template", "name": "mern-template",
"version": "1.0.0", "version": "1.0.2",
"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" "analyzer": "webpack --env=production --analyzer --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.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",
"babel-loader": "^8.2.2", "@loadable/component": "^5.15.0",
"clean-webpack-plugin": "^3.0.0", "babel-loader": "^8.2.2",
"concurrently": "^5.3.0", "clean-webpack-plugin": "^3.0.0",
"dateformat": "^4.5.1", "concurrently": "^6.2.0",
"dotenv": "^8.2.0", "css-loader": "^6.2.0",
"express": "^4.17.1", "dateformat": "^4.5.1",
"html-webpack-plugin": "^5.0.0-alpha.14", "dotenv": "^10.0.0",
"jwt-decode": "^3.1.2", "express": "^4.17.1",
"mariadb": "^2.5.2", "html-webpack-plugin": "^5.3.2",
"raw-loader": "^4.0.2", "jwt-decode": "^3.1.2",
"react": "^17.0.1", "mariadb": "^2.5.4",
"react-dom": "^17.0.1", "query-string": "^7.0.1",
"react-dropdown-select": "^4.7.4", "raw-loader": "^4.0.2",
"react-loadable": "^5.5.0", "react": "^17.0.2",
"react-markdown": "^5.0.3", "react-dom": "^17.0.2",
"react-router": "^5.2.0", "react-dropdown-select": "^4.7.4",
"react-router-dom": "^5.2.0", "react-markdown": "^6.0.2",
"sequelize": "^6.4.0", "react-router": "^5.2.0",
"universal-cookie": "^4.0.4", "react-router-dom": "^5.2.0",
"webpack": "^5.15.0", "rehype-raw": "^5.1.0",
"webpack-cli": "^4.3.1" "sequelize": "^6.6.5",
}, "socket.io-client": "^4.1.3",
"devDependencies": { "style-loader": "^3.2.1",
"nodemon": "^2.0.7", "webpack": "^5.46.0",
"webpack-bundle-analyzer": "^4.3.0", "webpack-bundle-analyzer": "^4.4.2",
"webpack-dev-server": "^3.11.2" "webpack-cli": "^4.7.2"
} },
} "devDependencies": {
"nodemon": "^2.0.12",
"webpack-dev-server": "^3.11.2"
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME
host: process.env.DB_HOSTNAME, 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
View File
@@ -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
View File
@@ -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({