Compare commits

..

115 Commits

Author SHA1 Message Date
Kayne Ruse c154f777b4 Updated copyright year 2025-12-10 11:48:23 +11:00
Kayne Ruse 20377359c6 webpack-bundle-analyzer is NOT optional 2025-12-10 11:28:03 +11:00
Kayne Ruse e259f9b1ba Removed the use 'timezone' in the generated 'configure-script.js' 2025-12-10 11:16:38 +11:00
Kayne Ruse 377fe8f605 Updated syntax for docker and CI files 2025-12-10 10:22:05 +11:00
Kayne Ruse fb52787b26 Updated webpack-bundle-analyzer to v5.1 2025-12-10 09:52:29 +11:00
Kayne Ruse 356d0a3638 Updated dependencies 2025-12-10 09:03:46 +11:00
dependabot[bot] 756680888b Bump react-router from 7.9.5 to 7.9.6
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.9.5 to 7.9.6.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.6/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 08:06:07 +11:00
dependabot[bot] 63a9790265 Bump webpack from 5.102.1 to 5.103.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.102.1 to 5.103.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.102.1...v5.103.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.103.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 08:05:32 +11:00
dependabot[bot] edd60307e9 Bump html-webpack-plugin from 5.6.4 to 5.6.5
Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 5.6.4 to 5.6.5.
- [Release notes](https://github.com/jantimon/html-webpack-plugin/releases)
- [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v5.6.4...v5.6.5)

---
updated-dependencies:
- dependency-name: html-webpack-plugin
  dependency-version: 5.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 08:05:07 +11:00
dependabot[bot] ddf1fc7979 Bump nodemon from 3.1.10 to 3.1.11
Bumps [nodemon](https://github.com/remy/nodemon) from 3.1.10 to 3.1.11.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.1.10...v3.1.11)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-version: 3.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 08:04:24 +11:00
dependabot[bot] 78dcaed0aa Bump react-router from 7.9.4 to 7.9.5
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.9.4 to 7.9.5.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.5/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-05 06:38:09 +11:00
dependabot[bot] f93e1749f7 Bump @babel/preset-env from 7.28.3 to 7.28.5
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.28.3 to 7.28.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-28 17:52:37 +11:00
dependabot[bot] 44a9dbfe13 Bump @babel/preset-react from 7.27.1 to 7.28.5
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.27.1 to 7.28.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-react)

---
updated-dependencies:
- dependency-name: "@babel/preset-react"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-28 17:47:27 +11:00
dependabot[bot] 225890dfd5 Bump @babel/core from 7.28.4 to 7.28.5
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.28.4 to 7.28.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-28 17:46:52 +11:00
dependabot[bot] baebd2d594 Bump webpack from 5.101.3 to 5.102.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.101.3 to 5.102.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.101.3...v5.102.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.102.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 22:27:46 +11:00
dependabot[bot] a2ef968ddf Bump react-router from 7.9.1 to 7.9.3
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.9.1 to 7.9.3.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.9.3/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 22:23:28 +11:00
dependabot[bot] 88f44a412b Bump react-dom from 19.1.1 to 19.2.0
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.1.1 to 19.2.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.0/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 22:20:26 +11:00
dependabot[bot] 09b08cd791 Bump dotenv from 17.2.2 to 17.2.3
Bumps [dotenv](https://github.com/motdotla/dotenv) from 17.2.2 to 17.2.3.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.2.2...v17.2.3)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-version: 17.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-12 22:20:03 +11:00
Kayne Ruse 2964c1e448 Updated dependencies
* Removed 'react-router-dom' as no longer needed

Linked to:

* news-server v1.7.4
* auth-server v1.8.10
* chat-server v1.5.4
2025-09-18 10:48:24 +10:00
dependabot[bot] a2344947f8 Bump react-router-dom from 7.8.2 to 7.9.1
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.8.2 to 7.9.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.9.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-18 10:08:51 +10:00
Kayne Ruse 52b755fe16 Bump dependencies 2025-09-10 14:02:40 +10:00
dependabot[bot] b1c31731ee Bump @babel/core from 7.28.0 to 7.28.4
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.28.0 to 7.28.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 12:58:02 +10:00
dependabot[bot] b8cc43c556 Bump html-webpack-plugin from 5.6.3 to 5.6.4
Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 5.6.3 to 5.6.4.
- [Release notes](https://github.com/jantimon/html-webpack-plugin/releases)
- [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v5.6.3...v5.6.4)

---
updated-dependencies:
- dependency-name: html-webpack-plugin
  dependency-version: 5.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 12:53:11 +10:00
dependabot[bot] 24b9ff3b5f Bump dotenv from 17.2.1 to 17.2.2
Bumps [dotenv](https://github.com/motdotla/dotenv) from 17.2.1 to 17.2.2.
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v17.2.1...v17.2.2)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-version: 17.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 12:49:46 +10:00
dependabot[bot] a38b8922a0 Bump @babel/preset-env from 7.28.0 to 7.28.3
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.28.0 to 7.28.3.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 12:49:13 +10:00
dependabot[bot] fcedc00612 Bump react-router-dom from 7.7.1 to 7.8.2
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.7.1 to 7.8.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.8.2/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.8.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 12:48:11 +10:00
Kayne Ruse da4316dccc Experimenting with dependabot 2025-09-09 18:48:01 +10:00
Kayne Ruse faacb8b8c7 Fixed CORS configuration 2025-07-29 14:40:24 +10:00
Kayne Ruse c48fcba994 Commented out MacOS check in configure-script.js 2025-07-29 12:49:50 +10:00
Kayne Ruse 56e3151db0 Fixed a missing import 2025-07-29 11:02:02 +10:00
Kayne Ruse 8aeaa46332 Updated dependencies, removed server demo 2025-07-26 04:03:32 +10:00
Ratstail91 cd34f0db5c Fully tested the remote database
Added default host for the admin account
2024-05-03 09:22:45 +10:00
Ratstail91 ab9e7456fb UNTESTED: Updated all dependencies 2024-05-03 07:07:32 +10:00
Ratstail91 90a4fc1a6a UNTESTED: Added database port as a configurable option
Also updated license field in package.json
2024-04-15 21:02:58 +10:00
Ratstail91 42fa9e27e7 Updated dependencies 2024-04-15 17:11:18 +10:00
Ratstail91 0c462d0e6c Tweaks to token provider 2023-12-24 06:43:42 +11:00
Ratstail91 fce018d19b Fixed Date API bug 2023-12-24 02:48:53 +11:00
Ratstail91 6717dbdef6 Missed a reference to page limit 2023-12-24 00:30:11 +11:00
Ratstail91 39c3e55bd5 Updated to support PAGE_SIZE in news-server 2023-12-23 23:40:10 +11:00
Ratstail91 c4c836edb7 Updated libraries, docker engine version, docker distro version
Also updated copyright year, since it's nearly time.
2023-12-23 22:40:41 +11:00
Kayne Ruse fe089e13f1 Gotcha! 2023-06-27 04:24:50 +10:00
Kayne Ruse c3a322fe2a Updated dependencies, bumped patch version 2023-06-26 23:02:57 +10:00
Kayne Ruse bac1063de5 Fixed a logout bug 2023-06-26 22:26:27 +10:00
Kayne Ruse 2dcafd200d Updated dependencies 2023-05-29 07:59:12 +10:00
Kayne Ruse a18ea7fe67 Updated dependencies 2023-05-03 21:25:21 +10:00
Kayne Ruse b6e8d7ad7a Updated dependencies 2023-03-25 02:08:25 +11:00
Kayne Ruse 1c4d87622c A few updates 2023-03-19 01:24:13 +11:00
Kayne Ruse 2d9f29e694 Updated dependencies 2023-02-21 09:30:23 +11:00
Kayne Ruse 4b6eb2271c Updated dependencies, License 2023-01-12 08:09:04 +11:00
Kayne Ruse 56fc50d3b9 Bumped version number 2023-01-04 12:57:53 +00:00
Kayne Ruse d29e3397a6 Switched to a slim docker distro 2023-01-04 23:52:11 +11:00
Kayne Ruse 335c7008aa Bumped version number 2022-12-31 19:19:38 +00:00
Kayne Ruse e89b0645ca Fixed URLSearchParams 2022-12-04 12:08:12 +00:00
Kayne Ruse 36e79a513f Fixed reset page 2022-12-04 11:48:23 +00:00
Kayne Ruse 6ef0affcf6 Fixed useEffect() usage 2022-12-01 12:48:04 +00:00
Kayne Ruse cb0c1284bf You're now kicked out to the root page when the refresh token expires 2022-12-01 12:05:43 +00:00
Kayne Ruse 5cf4b66894 Fixed redirect issue 2022-11-29 04:53:29 +00:00
Kayne Ruse 45cf281c91 Updated dependencies 2022-11-13 02:07:15 +00:00
Kayne Ruse 2794b4c724 Updated dependencies 2022-11-13 01:56:14 +00:00
Kayne Ruse b1f49a4166 Removed a dependency 2022-08-01 11:09:35 +01:00
Kayne Ruse 4cbf67dcbb Updated dependencies 2022-08-01 10:34:35 +01:00
Kayne Ruse fd29385cf8 Update README.md 2022-07-28 03:47:23 +10:00
Kayne Ruse 8e81dccef6 Added cookies 2022-07-26 10:18:49 +01:00
Kayne Ruse adeb8c4267 Updated dependencies 2022-07-23 11:45:33 +01:00
Kayne Ruse eb6c3a40d7 Added hook array to auth config 2022-06-15 23:50:58 +01:00
Kayne Ruse 4d4a0b5401 Updated dependencies 2022-06-10 17:07:56 +01:00
Kayne Ruse 490860159e Cleaned up usage of fetch 2022-06-10 16:53:42 +01:00
Kayne Ruse ed01fe6db5 Addressed #34 2022-05-30 06:16:10 +01:00
Kayne Ruse cfb8d20ad2 Merge remote-tracking branch 'refs/remotes/origin/main' 2022-05-30 06:10:20 +01:00
Kayne Ruse d44cae397d Updated dependencies, addressed #33 2022-05-30 06:10:01 +01:00
Kayne Ruse b97fff05b3 Added FUNDING.yml 2022-02-13 07:49:49 +11:00
Kayne Ruse 22703bfbcb Swapped day and month around 2022-01-09 15:02:06 +00:00
Kayne Ruse 14a3c9eabe Added timestamps to the chatbox 2022-01-09 06:39:13 +00:00
Kayne Ruse 8e90a4a540 Trying a fix 2022-01-08 04:59:21 +00:00
Kayne Ruse 1d3c94a1aa Shortened the code with a ternary 2022-01-06 14:26:30 +00:00
Kayne Ruse ca5e79ccf3 Merge pull request #32 from kcampbelljr/macos-docs
Macos docs
2022-01-07 01:15:36 +11:00
Kayne Ruse 03acce1907 Removed an unused dependency 2022-01-06 14:00:51 +00:00
Keith Campbell c0b7280533 removed macos, updated code on original configure script, testing.. 2022-01-05 23:23:09 -05:00
Keith Campbell 2925cce7ca created a copy of the configure script and made the changes needed for now, should we add a macos check to the original configure script instead? 2022-01-04 23:51:02 -05:00
Kayne Ruse b90670b922 Tweaked credits 2022-01-03 19:21:57 +00:00
Kayne Ruse 290f25f898 Added prox to the credits 2022-01-03 08:45:27 +00:00
Kayne Ruse 3cdef433f9 Tweaked moderator privileges 2022-01-02 18:11:11 +00:00
Kayne Ruse 53c8ddab54 Removed rehype-raw 2021-12-30 14:28:10 +00:00
Kayne Ruse 4e94c5338d Removed client-side markdown rendering 2021-12-30 13:21:34 +00:00
Kayne Ruse 55ff5765c6 Swapped out a library 2021-12-27 03:34:00 +00:00
Kayne Ruse d486059430 Updated webpack.config.js 2021-12-26 16:12:23 +00:00
Kayne Ruse 9d8c948dbb Updated dependencies 2021-12-24 07:37:43 +00:00
Kayne Ruse b21fa8db9e Tweaked webpack.config.js 2021-12-20 13:52:43 +00:00
Kayne Ruse c130b74e2d Added an example proxy route to webpack.config.js 2021-12-20 07:33:01 +00:00
Kayne Ruse d37b93d5f7 Updated webpack-dev-server to 4.6.0 2021-12-20 07:07:36 +00:00
Kayne Ruse 829cb2e3da Disabled source map in prod 2021-12-20 06:19:23 +00:00
Kayne Ruse 29f0dbb1ca Fixed HTTPS redirection 2021-12-15 18:39:37 +00:00
Kayne Ruse 13ad7d2435 Updated README.md 2021-12-11 16:57:48 +00:00
Kayne Ruse e30853e0cd Updated configure-script.js 2021-12-11 11:38:17 +00:00
Kayne Ruse 1e16a96f86 Updated README.md 2021-12-11 05:01:10 +00:00
Kayne Ruse cff73107b2 Updated package-lock.json 2021-11-17 06:13:41 +00:00
Kayne Ruse 76417747b3 Bumped node to version 16 LTS 2021-11-17 04:58:08 +00:00
Kayne Ruse b8323723ed Updated package-lock.json 2021-11-15 22:37:51 +00:00
Kayne Ruse 415b2f32f1 Bumped version number 2021-08-24 08:02:17 +01:00
Kayne Ruse a0dbe0aee1 Imported the directory structure from egg trainer 2021-08-22 02:22:28 +10:00
Kayne Ruse f415a7ece2 Fixed analyzer 2021-08-20 21:21:18 +01:00
Kayne Ruse 9c863f309f Fixed webpack config 2021-08-21 05:49:01 +10:00
Kayne Ruse 7547b1717e Added gzip compression for JS files 2021-08-21 05:36:23 +10:00
Kayne Ruse 22e6286e0a Webpack config tweak 2021-08-16 07:44:32 +10:00
Kayne Ruse c766c43223 No longer needs the refreshToken to logout 2021-08-15 00:54:28 +10:00
Kayne Ruse bb1590bae7 Bumped version number 2021-08-10 17:39:09 +10:00
Kayne Ruse 5f7b9dda3a Tweaked README.md 2021-08-07 13:54:39 +10:00
Kayne Ruse 051f3dfb2a Updated README.md 2021-07-30 10:20:14 +10:00
Kayne Ruse 85456e0892 Minor markdown tweak 2021-07-30 03:36:40 +10:00
Kayne Ruse 6130337846 Fixed classNames 2021-07-30 01:34:06 +10:00
Kayne Ruse ac99f3bf38 Fixed CSS classes 2021-07-29 22:13:17 +10:00
Kayne Ruse 20e94db628 I really hate these line endings 2021-07-29 21:15:32 +10:00
Kayne Ruse bcb4a37f5a Tweak 2021-07-29 21:13:53 +10:00
Kayne Ruse 3b0d3c87b1 Tweak 2021-07-28 15:52:50 +01:00
Kayne Ruse 51a116503d Bumped version number 2021-07-28 15:43:09 +01:00
60 changed files with 6855 additions and 17854 deletions
+2
View File
@@ -1,6 +1,8 @@
WEB_PORT=3000 WEB_PORT=3000
DB_HOSTNAME=localhost DB_HOSTNAME=localhost
DB_PORTNAME=3306
DB_DATABASE=template DB_DATABASE=template
DB_USERNAME=template DB_USERNAME=template
DB_PASSWORD=pikachu DB_PASSWORD=pikachu
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: krgamestudios
ko_fi: krgamestudios
custom: ["https://www.paypal.com/donate/?hosted_button_id=73Q82T2ZHV8AA"]
+10
View File
@@ -0,0 +1,10 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
+119 -118
View File
@@ -1,118 +1,119 @@
# Logs # Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log* lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
*.lcov *.lcov
# nyc test coverage # nyc test coverage
.nyc_output .nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
# Bower dependency directory (https://bower.io/) # Bower dependency directory (https://bower.io/)
bower_components bower_components
# node-waf configuration # node-waf configuration
.lock-wscript .lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html) # Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directories # Dependency directories
node_modules/ node_modules/
jspm_packages/ jspm_packages/
# TypeScript v1 declaration files # TypeScript v1 declaration files
typings/ typings/
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# Optional npm cache directory # Optional npm cache directory
.npm .npm
# Optional eslint cache # Optional eslint cache
.eslintcache .eslintcache
# Microbundle cache # Microbundle cache
.rpt2_cache/ .rpt2_cache/
.rts2_cache_cjs/ .rts2_cache_cjs/
.rts2_cache_es/ .rts2_cache_es/
.rts2_cache_umd/ .rts2_cache_umd/
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack' # Output of 'npm pack'
*.tgz *.tgz
# Yarn Integrity file # Yarn Integrity file
.yarn-integrity .yarn-integrity
# dotenv environment variables file # dotenv environment variables file
.env .env
.env.test .env.test
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache
# Next.js build output # Next.js build output
.next .next
# Nuxt.js build / generate output # Nuxt.js build / generate output
.nuxt .nuxt
dist dist
# Gatsby files # Gatsby files
.cache/ .cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js # Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support # https://nextjs.org/blog/next-9-1#public-directory-support
# public # public
# vuepress build output # vuepress build output
.vuepress/dist .vuepress/dist
# Serverless directories # Serverless directories
.serverless/ .serverless/
# FuseBox cache # FuseBox cache
.fusebox/ .fusebox/
# DynamoDB Local files # DynamoDB Local files
.dynamodb/ .dynamodb/
# TernJS port file # TernJS port file
.tern-port .tern-port
# Output files # Output files
public/*.html public/*.html
public/*.js public/*.js
public/*.css public/*.css
public/*.map public/*.map
public/*.gz public/*.gz
public/*.txt
letsencrypt/
mysql/ letsencrypt/
Dockerfile mysql/
docker-compose.yml Dockerfile
startup.sql docker-compose.yml
startup.sql
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2021 Kayne Ruse, KR Game Studios Copyright (c) 2021-2023 Kayne Ruse, KR Game Studios
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
+96 -91
View File
@@ -1,91 +1,96 @@
# MERN-template # MERN-template
A website template using the MERN stack. The primary technology involved is: A website template using the MERN stack. It is geared towards Persistent Browser Based Games (think neopets), but is flexible enough for a number of different uses.
* React The primary technology involved is:
* Nodejs
* MariaDB (with Sequelize) * React
* Docker (with docker-compose) * Nodejs
* MariaDB (with Sequelize)
This template is designed to support the development of persistent browser based games (PBBGs), but it, and it's component microservices, can be used elsewhere. * Docker (with docker compose)
This template is released under the zlib license (see LICENSE). This template is designed to support the development of persistent browser based games (PBBGs), but it, and it's component microservices, can be used elsewhere.
See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation. This template is released under the zlib license (see LICENSE).
# Microservices See the [github wiki](https://github.com/krgamestudios/MERN-template/wiki) for full documentation.
There are external components to this template referred to as "microservices". These can be omitted entirely by simply removing the React components that access them. These are also available via [docker hub](https://hub.docker.com/u/krgamestudios). # Microservices
* News Server: https://github.com/krgamestudios/news-server There are external components to this template referred to as "microservices". These can be omitted entirely by simply removing the React components that access them. These are also available via [docker hub](https://hub.docker.com/u/krgamestudios).
* Auth Server: https://github.com/krgamestudios/auth-server
* Chat Server: https://github.com/krgamestudios/chat-server * News Server: https://github.com/krgamestudios/news-server
* Auth Server: https://github.com/krgamestudios/auth-server
# Setup Deployment * Chat Server: https://github.com/krgamestudios/chat-server
A clean install is this easy: # Setup Deployment
``` A clean install is this easy:
git clone https://github.com/krgamestudios/MERN-template.git
cd MERN-template ```
npm install git clone https://github.com/krgamestudios/MERN-template.git
node configure-script.js cd MERN-template
docker-compose up --build node configure-script.js
``` docker compose up --build
```
# Setup Development
# Setup Development
To set up this template in development mode:
To set up this template in development mode:
1. Ensure mariadb is running in your development environment
2. Run `mariadb sql/create_database.sql` as the root user 1. Ensure mariadb is running in your development environment
3. Run `npm install` 2. Run `mariadb tools/create_database.sql` as the root user
4. Run `cp .envdev .env` and enter your details into the `.env` file 3. Run `npm install`
5. Execute `npm run dev` 4. Run `cp .envdev .env` and enter your details into the `.env` file
6. Navigate to `http://localhost:3001` in your web browser 5. Execute `npm run dev`
6. Navigate to `http://localhost:3001` in your web browser
# Features List 7. Repeat this process for each microservice (linked above)
- Mainly one language across the codebase (JavaScript) # Features List
- Full documentation
- Setup tutorial - Mainly one language across the codebase (JavaScript)
- Fully Featured Account System (as a microservice) - Full documentation
- Email validation - Setup tutorial
- Logging in and out - Fully Featured Account System (as a microservice)
- Account deletion - Email validation
- Password management - Logging in and out
- JSON web token authentication - Account deletion
- Fully Featured News Blog (as a microservice) - Password management
- Publish, edit or delete articles as needed - JSON web token authentication
- Secured via admin panel - HttpOnly cookies for security
- Fully Featured Chat System (as a microservice) - Optional post validation hook
- Available when logged in - Fully Featured News Blog (as a microservice)
- Chat logs saved to the database - Publish, edit or delete articles as needed
- Room-based chat (type `/room name` to access a specific room) - Secured via admin panel
- Moderation tools - Fully Featured Chat System (as a microservice)
- Permanently banning users - Available when logged in
- Chat-muting users for a time period - Chat logs saved to the database
- Users reporting offensive chat-content - Room-based chat (type `/room name` to access a specific room)
- Easy To Use Configuration Script - Moderation tools
- Sets up everything via docker - Permanently banning users
- A default admin account (if desired) - Chat-muting users for a time period
- Users reporting offensive chat-content
# Coming Soon - Easy To Use Configuration Script
- Sets up everything via docker
- Full documentation - A default admin account (if desired)
- Modding tutorials
# Coming Soon
# Coming Eventually
- Full documentation
- Fully Featured News Blog (as a microservice) - Modding tutorials
- Restore deleted articles - Fully Featured News Blog (as a microservice)
- Undo edits - Individual pages for news articles
- Fully Featured Chat System (as a microservice)
- Custom emoji # Coming Eventually
- Private messaging
- Broadcasting to all channels - Fully Featured News Blog (as a microservice)
- Badges next to usernames - Restore deleted articles
- Better compression for client files - Undo edits
- Backend for leaderboards (modding tutorial?) - Fully Featured Chat System (as a microservice)
- Backend for energy systems (modding tutorial?) - Custom emoji
- Backend for items, shops, trading and currency - Private messaging
- Broadcasting to all channels
- Badges next to usernames
- Backend for leaderboards (modding tutorial?)
- Backend for energy systems (modding tutorial?)
- Backend for items, shops, trading and currency (modding tutorial?)
+11 -12
View File
@@ -1,15 +1,14 @@
//polyfills
import 'regenerator-runtime/runtime';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom/client';
import App from './components/app'; import App from './pages/app';
import TokenProvider from './components/utilities/token-provider'; import TokenProvider from './pages/utilities/token-provider';
ReactDOM.render( ReactDOM
<TokenProvider> .createRoot(document.getElementById('root'))
<App /> .render(
</TokenProvider>, <TokenProvider>
document.querySelector('#root') <App />
); </TokenProvider>
)
;
-49
View File
@@ -1,49 +0,0 @@
//react
import React, { useContext } from 'react';
import { BrowserRouter, Switch } from 'react-router-dom';
import { TokenContext } from './utilities/token-provider';
//library components
import LazyRoute from './utilities/lazy-route';
import Markdown from './panels/markdown';
//styling
//import a styling template here
//common components
import Header from './panels/header';
import Footer from './panels/footer';
import PopupChat from './panels/popup-chat';
const App = props => {
const authTokens = useContext(TokenContext);
//default render
return (
<BrowserRouter>
<Header />
<Switch>
<LazyRoute exact path='/' component={() => import('./pages/homepage')} />
<LazyRoute path='/signup' component={() => import('./pages/signup')} />
<LazyRoute path='/login' component={() => import('./pages/login')} />
<LazyRoute path='/account' component={() => import('./pages/account')} />
<LazyRoute path='/recover' component={() => import('./pages/recover')} />
<LazyRoute path='/reset' component={() => import('./pages/reset')} />
<LazyRoute path='/admin' component={() => import('./pages/admin')} />
<LazyRoute path='/mod' component={() => import('./pages/mod')} />
<LazyRoute path='/privacypolicy' component={async () => () => <Markdown content={require('../markdown/privacy-policy.md').default} />} />
<LazyRoute path='/credits' component={async () => () => <Markdown content={require('../markdown/credits.md').default} />} />
<LazyRoute path='*' component={() => import('./pages/not-found')} />
</Switch>
{ authTokens.accessToken ? <PopupChat /> : <></> }
<Footer />
</BrowserRouter>
);
};
export default App;
-105
View File
@@ -1,105 +0,0 @@
import React, { useEffect, useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
import DeleteAccount from '../panels/delete-account';
const Account = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
//grab the user's info
useEffect(() => {
authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
}
})
.then(blob => blob.json())
.then(json => contactRef.current.checked = json.contact)
.catch(e => console.error(e))
;
}, []);
//render the thing
return (
<div className='page'>
<h1 className='centered'>Account</h1>
<form className='constricted' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
if (err) {
alert(err);
return;
}
alert('Details updated');
passwordRef.current.value = retypeRef.current.value = '';
}}>
<div>
<div>
<label htmlFor='password'>Change Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</div>
</div>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount className='constricted' />
</div>
);
};
const update = async (password, retype, contact, tokenFetch) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'PATCH',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
contact
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null];
}
}
export default Account;
-32
View File
@@ -1,32 +0,0 @@
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
import NewsPublisher from '../panels/news-publisher';
import NewsEditor from '../panels/news-editor';
import GrantAdmin from '../panels/grant-admin';
import GrantMod from '../panels/grant-mod';
const Admin = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !authTokens.getPayload().admin) {
return <Redirect to='/' />;
}
return (
<div className='page'>
<h1 className='centered'>Administration Tools</h1>
<NewsPublisher />
<NewsEditor />
<GrantAdmin />
<GrantMod />
</div>
);
};
export default Admin;
-14
View File
@@ -1,14 +0,0 @@
import React from 'react';
import NewsFeed from '../panels/news-feed';
const HomePage = props => {
return (
<div className='page'>
<p>This is the MERN template homepage.</p>
<NewsFeed />
</div>
);
};
export default HomePage;
-107
View File
@@ -1,107 +0,0 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const LogIn = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const emailRef = useRef();
const passwordRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Login</h1>
<form className='constricted' onSubmit={
async evt => {
//on submit
evt.preventDefault();
const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
//save auth tokens and redirect
if (newTokens) {
authTokens.setAccessToken(newTokens.accessToken);
authTokens.setRefreshToken(newTokens.refreshToken);
props.history.push('/');
}
}
}>
<div>
<label htmlFor="email">Email:</label>
<input type="email" name="email" ref={emailRef} />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" name="password" ref={passwordRef} />
</div>
<button type='submit'>Login</button>
</form>
</div>
);
};
//DOCS: returns two values: err and authTokens
const handleSubmit = async (email, password) => {
email = email.trim();
const err = handleValidation(email, password);
if (err) {
return [err, false];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
email,
password,
})
});
//handle errors
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.error(err);
return [err, false];
}
//return the new auth tokens
const newTokens = await result.json();
return [null, newTokens];
};
//returns an error message, or null on success
const handleValidation = (email, password) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
return null;
};
export default LogIn;
-27
View File
@@ -1,27 +0,0 @@
import React, { useContext } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
import ChatReports from '../panels/chat-reports';
import BanUser from '../panels/ban-user';
const Mod = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !(authTokens.getPayload().admin || authTokens.getPayload().mod)) {
return <Redirect to='/' />;
}
return (
<div className='page'>
<h1 className='centered'>Moderation Tools</h1>
<ChatReports />
<BanUser />
</div>
);
};
export default Mod;
-11
View File
@@ -1,11 +0,0 @@
import React from 'react';
const NotFound = props => {
return (
<div className='page'>
<h1 className='middle centered'>Page Not Found</h1>
</div>
);
};
export default NotFound;
-89
View File
@@ -1,89 +0,0 @@
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;
-132
View File
@@ -1,132 +0,0 @@
import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
//utilities
const validateEmail = require('../../../common/utilities/validate-email');
const validateUsername = require('../../../common/utilities/validate-username');
const SignUp = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Redirect to='/' />;
}
//refs
const emailRef = useRef();
const usernameRef = useRef();
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
const signupRef = useRef();
return (
<div className='page'>
<h1 className='centered'>Signup</h1>
<form className='constricted' onSubmit={
async evt => { //on submit
signupRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
if (result) {
alert(result);
signupRef.current.disabled = false;
}
//redirect
if (redirect) {
props.history.push('/');
}
}
}>
<div>
<label htmlFor='email'>Email:</label>
<input type='email' name='email' ref={emailRef} />
</div>
<div>
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<div>
<label htmlFor='password'>Password:</label>
<input type='password' name='password' ref={passwordRef} />
</div>
<div>
<label htmlFor='retype'>Retype Password:</label>
<input type='password' name='retype' ref={retypeRef} />
</div>
<div>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</div>
<button type='submit' ref={signupRef}>Signup</button>
</form>
</div>
);
};
const handleSubmit = async (email, username, password, retype, contact) => {
email = email.trim();
username = username.trim();
const err = handleValidation(email, username, password, retype);
if (err) {
return [err];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
email,
username,
password,
contact
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.error(err);
return [err, false];
}
return [await result.text(), true];
};
//returns an error message, or null on success
const handleValidation = (email, username, password, retype) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (!validateUsername(username)) {
return 'invalid username';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
if (password !== retype) {
return 'passwords do not match';
}
return null;
};
export default SignUp;
-67
View File
@@ -1,67 +0,0 @@
import React, { useState, useEffect, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider';
import dateFormat from 'dateformat';
const ChatReports = props => {
const [reports, setReports] = useState([]);
const authTokens = useContext(TokenContext);
useEffect(async () => {
const result = await authTokens.tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*'
}
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
setReports(await result.json());
}
}, []);
return (
<table>
<thead>
<tr>
<th>Date</th>
<th>Username</th>
<th>Room Name</th>
<th>Content</th>
<th>Reported By</th>
</tr>
</thead>
<tbody>
{reports.map((report, index) => (
<tr key={index}>
<td>{dateFormat(report.chatlog.createdAt, 'yyyy-mm-dd, H:MM:ss')}</td>
<td>{report.chatlog.username}</td>
<td>{report.chatlog.room}</td>
<td>{report.chatlog.text}</td>
<td>{report.reporter.join(', ')}</td>
<td><button onClick={() => deleteReportsFor(report.chatlogIndex, authTokens.tokenFetch, setReports)}>Delete</button></td>
</tr>
))}
</tbody>
</table>
);
};
const deleteReportsFor = (chatlogIndex, tokenFetch, setReports) => {
tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({ chatlogIndex })
});
setReports(reports => reports.filter(report => report.chatlogIndex != chatlogIndex));
};
export default ChatReports;
-12
View File
@@ -1,12 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Footer = () => {
return (
<footer>
<p className='centered'>MERN template designed by <a href='https://krgamestudios.com'>Kayne Ruse, KR Game Studios</a> - <Link to='/privacypolicy'>Privacy Policy</Link> - <Link to='/credits'>Credits</Link></p>
</footer>
);
};
export default Footer;
-78
View File
@@ -1,78 +0,0 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { TokenContext } from '../utilities/token-provider';
const Visitor = () => {
return (
<div>
<Link to='/signup'>Sign Up</Link>
<span> - </span>
<Link to='/login'>Log In</Link>
<span> - </span>
<Link to='/recover'>Recover</Link>
</div>
);
};
const Member = () => {
const authTokens = useContext(TokenContext);
return (
<div>
<Link to='/account'>Account</Link>
<span> - </span>
{ authTokens.getPayload().admin ?
<span>
<Link to='/admin'>Admin</Link>
<span> - </span>
</span>:
<span />
}
{ authTokens.getPayload().mod ?
<span>
<Link to='/mod'>Moderation</Link>
<span> - </span>
</span>:
<span />
}
{ /* Logout button logs you out of the server too */ }
<Link to='/' onClick={async () => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, { //NOTE: this gets overwritten as a bugfix
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
token: authTokens.refreshToken
})
});
//any problems?
if (!result.ok) {
console.error(await result.text());
} else {
authTokens.setAccessToken('');
authTokens.setRefreshToken('');
}
}}>Log out</Link>
</div>
);
};
const Header = () => {
const authTokens = useContext(TokenContext);
return (
<header>
<h1><Link to='/'>MERN Template</Link></h1>
{ authTokens.accessToken ? <Member /> : <Visitor /> }
</header>
);
};
export default Header;
-35
View File
@@ -1,35 +0,0 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
const Markdown = props => {
//content?
let [contentHook, setContentHook] = useState(null);
//check arguments
if (!props.content) {
if (!props.uri) {
throw 'Markdown requires either content or uri prop';
}
//once
useEffect(() => {
fetch(props.uri)
.then(blob => blob.text())
.then(blob => setContentHook(blob))
.catch(e => console.error(e))
;
}, []);
} else
//assume raw info
if (!contentHook) {
setContentHook(props.content);
}
return (
<ReactMarkdown rehypePlugins={[rehypeRaw]} escapeHtml={false} props={{...props}}>{contentHook}</ReactMarkdown>
);
};
export default Markdown;
@@ -1,13 +0,0 @@
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;
-6
View File
@@ -1,6 +0,0 @@
# Credits
MERN Template developed by Kayne Ruse, KR Game Studios
[https://github.com/krgamestudios/MERN-template](https://github.com/krgamestudios/MERN-template)
-2
View File
@@ -1,2 +0,0 @@
# Privacy Policy
+101
View File
@@ -0,0 +1,101 @@
import React, { useEffect, useContext, useRef } from 'react';
import { Link, Navigate } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import DeleteAccount from './panels/delete-account';
const Account = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Navigate to='/' />;
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
//grab the user's info
useEffect(() => {
authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`)
.then(blob => blob.json())
.then(json => contactRef.current.checked = json.contact)
.catch(e => console.error(e))
;
}, []);
//render the thing
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<div className='panel'>
<h1 className='text centered'>Account</h1>
<div className='panel'>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err] = await update(passwordRef.current.value, retypeRef.current.value, contactRef.current.checked, authTokens.tokenFetch);
if (err) {
alert(err);
return;
}
alert('Details updated');
passwordRef.current.value = retypeRef.current.value = '';
}}>
<input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<span>
<label htmlFor='contact'>Allow Promotional Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} />
</span>
<button type='submit'>Update Information</button>
</form>
<DeleteAccount />
</div>
<Link to='/' className='text centered'>Return Home</Link>\
</div>
</div>
</div>
</>
);
};
const update = async (password, retype, contact, tokenFetch) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
contact
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null];
}
}
export default Account;
+109
View File
@@ -0,0 +1,109 @@
import React, { useContext, useRef } from 'react';
import { Link, Navigate } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email');
const Login = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Navigate to='/' />;
}
//refs
const emailRef = useRef();
const passwordRef = useRef();
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<div className='panel'>
<h1 className='text centered'>Login</h1>
<form className='constrained' onSubmit={
async evt => {
//on submit
evt.preventDefault();
const [err, accessToken] = await handleSubmit(emailRef.current.value, passwordRef.current.value);
if (err) {
alert(err);
}
//save auth tokens and redirect
if (accessToken) {
authTokens.setAccessToken(accessToken);
return <Navigate to='/' />;
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='password' name='password' placeholder='********' ref={passwordRef} />
<button type='submit'>Login</button>
</form>
<Link to='/recover' className='text centered'>Forgot Password?</Link>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</div>
</>
);
};
//DOCS: returns two values: err and authTokens
const handleSubmit = async (email, password) => {
email = email.trim();
const err = handleValidation(email, password);
if (err) {
return [err, false];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password,
}),
credentials: 'include'
});
//handle errors
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.error(err);
return [err, false];
}
//return the new auth tokens
const accessToken = await result.text();
return [null, accessToken];
};
//returns an error message, or null on success
const handleValidation = (email, password) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
return null;
};
export default Login;
@@ -1,6 +1,6 @@
import React, { useState, useContext, useRef } from 'react'; import React, { useState, useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
//DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed //DOCS: isolated the delete account button into it's own panel, so it can be easily moved as needed
const DeleteAccount = props => { const DeleteAccount = props => {
@@ -9,25 +9,27 @@ const DeleteAccount = props => {
const passwordRef = useRef(); const passwordRef = useRef();
if (!open) { if (!open) {
return <button onClick={() => setOpen(true)} className={props.className}>Delete Account</button> return (
<button onClick={() => setOpen(true)}>Delete Account</button>
);
} }
return ( return (
<form className={props.className} onSubmit={async evt => { <div className='panel centered middle'>
evt.preventDefault(); <h2 className='text centered'>Delete Your Account?</h2>
const [err] = await handleSubmit(passwordRef.current.value, authTokens); <form className='constrained' onSubmit={async evt => {
if (err) { evt.preventDefault();
alert(err); const [err] = await handleSubmit(passwordRef.current.value, authTokens);
} if (err) {
}}> alert(err);
<div> }
<label htmlFor="password">Password:</label> }}>
<input type="password" name="password" ref={passwordRef} /> <input type="password" name="password" placeholder='Password' ref={passwordRef} />
</div>
<button type='submit'>Delete Account</button> <button type='submit' style={{backgroundColor: 'red'}}>Delete Account</button>
<button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button> <button type='cancel' onClick={() => { passwordRef.current.value = ''; setOpen(false); }}>Cancel</button>
</form> </form>
</div>
); );
}; };
@@ -36,7 +38,6 @@ const handleSubmit = async (password, authTokens) => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`, { const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/account`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
@@ -50,14 +51,7 @@ const handleSubmit = async (password, authTokens) => {
//force a logout //force a logout
const result2 = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, { const result2 = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, {
method: 'DELETE', method: 'DELETE'
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: authTokens.refreshToken
})
}); });
if (!result2.ok) { if (!result2.ok) {
@@ -65,7 +59,6 @@ const handleSubmit = async (password, authTokens) => {
} }
authTokens.setAccessToken(''); authTokens.setAccessToken('');
authTokens.setRefreshToken('');
return [null]; return [null];
}; };
+29
View File
@@ -0,0 +1,29 @@
import React, { useContext, useRef } from 'react';
import { Link } from 'react-router';
import { TokenContext } from '../../utilities/token-provider';
//TODO: make this an ACTUAL BUTTON
const Logout = () => {
const authTokens = useContext(TokenContext);
return (
<>
{ /* Logout logs you out of the server too */ }
<Link to='/' onClick={async () => {
const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/auth/logout`, { //NOTE: this gets overwritten as a bugfix
method: 'DELETE'
});
//any problems?
if (!result.ok) {
console.error(await result.text());
} else {
authTokens.setAccessToken('');
}
}}>Logout</Link>
</>
);
};
export default Logout;
@@ -1,5 +1,7 @@
import React, { useContext, useRef } from 'react'; import React, { useContext, useRef } from 'react';
import { Redirect } from 'react-router-dom'; import { Link, useNavigate } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../utilities/token-provider';
@@ -7,12 +9,15 @@ import { TokenContext } from '../utilities/token-provider';
const validateEmail = require('../../../common/utilities/validate-email'); const validateEmail = require('../../../common/utilities/validate-email');
const Recover = props => { const Recover = props => {
//history
const navigate = useNavigate();
//context //context
const authTokens = useContext(TokenContext); const authTokens = useContext(TokenContext);
//misplaced? //misplaced?
if (authTokens.accessToken) { if (authTokens.accessToken) {
return <Redirect to='/' />; navigate("/");
} }
//refs //refs
@@ -20,32 +25,34 @@ const Recover = props => {
const recoverRef = useRef(); const recoverRef = useRef();
return ( return (
<div className='page'> <>
<h1 className='centered'>Recover Password</h1> <ApplyToBody className='dashboard' />
<form className='constricted' onSubmit={ <div className='page'>
async evt => { //on submit <div className='central panel centered middle'>
recoverRef.current.disabled = true; <h1 className='text centered'>Forgot Password</h1>
evt.preventDefault(); <form className='constrained' onSubmit={
const [result, redirect] = await handleSubmit(emailRef.current.value); async evt => { //on submit
if (result) { recoverRef.current.disabled = true;
alert(result); evt.preventDefault();
recoverRef.current.disabled = false; const [result, redirect] = await handleSubmit(emailRef.current.value);
} if (result) {
alert(result);
recoverRef.current.disabled = false;
}
//redirect //redirect
if (redirect) { if (redirect) {
props.history.push('/'); navigate("/");
} }
} }
}> }>
<div> <input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<label htmlFor='email'>Enter Your Email:</label> <button type='submit' ref={recoverRef}>Recover Password</button>
<input type='email' name='email' ref={emailRef} /> </form>
<Link to='/' className='text centered'>Return Home</Link>
</div> </div>
</div>
<button type='submit' ref={recoverRef}>Recover Password</button> </>
</form>
</div>
); );
}; };
@@ -62,8 +69,7 @@ const handleSubmit = async (email) => {
const result = await fetch(`${process.env.AUTH_URI}/auth/recover`, { const result = await fetch(`${process.env.AUTH_URI}/auth/recover`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ body: JSON.stringify({
email email
+87
View File
@@ -0,0 +1,87 @@
import React, { useContext, useRef } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
const Reset = props => {
//params
const [params, setParams] = useSearchParams(); //the URLSearchParams API
//history
const navigate = useNavigate();
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken || !params.has('email') || !params.has('token')) {
navigate("/");
}
//refs
const passwordRef = useRef();
const retypeRef = useRef();
//render the thing
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Reset Password</h1>
<form className='constrained' onSubmit={async evt => {
evt.preventDefault();
const [err, redirect] = await update(passwordRef.current.value, retypeRef.current.value, params);
if (err) {
alert(err);
return;
}
alert('Details updated'); //TODO: replace with a message from the auth server
//redirect
if (redirect) {
navigate("/");
}
}}>
<input type='password' name='password' placeholder='New Password' ref={passwordRef} />
<input type='password' name='retype' placeholder='Retype New Password' ref={retypeRef} />
<button type='submit'>Update Information</button>
</form>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
const update = async (password, retype, params) => {
if (password != retype) {
return ['Passwords do not match'];
}
if (password && password.length < 8) {
return ['Password is too short'];
}
const result = await fetch(`${process.env.AUTH_URI}/auth/reset?email=${params.get('email')}&token=${params.get('token')}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
password: password ? password : null,
})
});
if (!result.ok) {
return [`${await result.status}: ${await result.text()}`];
} else {
return [null, true];
}
}
export default Reset;
+129
View File
@@ -0,0 +1,129 @@
import React, { useContext, useRef } from 'react';
import { Link, useNavigate } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
//utilities
const validateEmail = require('../../../common/utilities/validate-email');
const validateUsername = require('../../../common/utilities/validate-username');
const Signup = props => {
//history
const navigate = useNavigate();
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
navigate("/");
}
//refs
const emailRef = useRef();
const usernameRef = useRef();
const passwordRef = useRef();
const retypeRef = useRef();
const contactRef = useRef();
const signupRef = useRef();
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Signup</h1>
<form className='constrained' onSubmit={
async evt => { //on submit
signupRef.current.disabled = true;
evt.preventDefault();
const [result, redirect] = await handleSubmit(emailRef.current.value, usernameRef.current.value, passwordRef.current.value, retypeRef.current.value, contactRef.current.checked);
if (result) {
alert(result);
signupRef.current.disabled = false;
}
//redirect
if (redirect) {
navigate("/");
}
}
}>
<input type='email' name='email' placeholder='your@email.com' ref={emailRef} />
<input type='text' name='username' placeholder='Username' ref={usernameRef} />
<input type='password' name='password' placeholder='********' ref={passwordRef} />
<input type='password' name='retype' placeholder='********' ref={retypeRef} />
<span>
<label htmlFor='contact'>Allow Emails:</label>
<input type='checkbox' name='contact' ref={contactRef} defaultChecked='true' />
</span>
<button type='submit' ref={signupRef}>Signup</button>
</form>
<Link to='/recover' className='text centered'>Forgot Password?</Link>
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
const handleSubmit = async (email, username, password, retype, contact) => {
email = email.trim();
username = username.trim();
const err = handleValidation(email, username, password, retype);
if (err) {
return [err];
}
//send to the auth server
const result = await fetch(`${process.env.AUTH_URI}/auth/signup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
username,
password,
contact
})
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.error(err);
return [err, false];
}
return [await result.text(), true];
};
//returns an error message, or null on success
const handleValidation = (email, username, password, retype) => {
if (!validateEmail(email)) {
return 'invalid email';
}
if (!validateUsername(username)) {
return 'invalid username';
}
if (password.length < 8) {
return 'invalid password (Must be at least 8 characters long)';
}
if (password !== retype) {
return 'passwords do not match';
}
return null;
};
export default Signup;
+37
View File
@@ -0,0 +1,37 @@
import React, { useContext } from 'react';
import { Link, Navigate } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import GrantAdmin from './panels/grant-admin';
import GrantMod from './panels/grant-mod';
const Admin = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !authTokens.getPayload().admin) {
return <Navigate to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Administration Tools</h1>
<br />
<GrantAdmin />
<br />
<GrantMod />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Admin;
+43
View File
@@ -0,0 +1,43 @@
import React, { useContext } from 'react';
import { Link, Navigate } from 'react-router';
import ApplyToBody from '../utilities/apply-to-body';
import { TokenContext } from '../utilities/token-provider';
import NewsPublisher from './panels/news-publisher';
import NewsEditor from './panels/news-editor';
import ChatReports from './panels/chat-reports';
import BanUser from './panels/ban-user';
const Mod = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced? (admin only)
if (!authTokens.accessToken || !(authTokens.getPayload().admin || authTokens.getPayload().mod)) {
return <Navigate to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page panel'>
<div className='central panel'>
<h1 className='text centered'>Moderation Tools</h1>
<NewsPublisher />
<br />
<NewsEditor />
<br />
<BanUser />
<br />
<ChatReports />
<Link to='/' className='text centered'>Return Home</Link>
</div>
</div>
</>
);
};
export default Mod;
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react'; import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const BanUser = props => { const BanUser = props => {
//context //context
@@ -10,13 +10,10 @@ const BanUser = props => {
const usernameRef = useRef(); const usernameRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>Permanently Ban User</h2> <h2 className='text centered'>Permanently Ban User</h2>
<form> <form className='constrained'>
<div> <input type='text' name='username' placeholder='Username' ref={usernameRef} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
evt.preventDefault(); evt.preventDefault();
@@ -45,8 +42,7 @@ const handleButtonPress = async (username, tokenFetch) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/banuser`, { const result = await tokenFetch(`${process.env.AUTH_URI}/admin/banuser`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ body: JSON.stringify({
username username
@@ -0,0 +1,62 @@
import React, { useState, useEffect, useContext } from 'react';
import { TokenContext } from '../../utilities/token-provider';
import dateFormat from 'dateformat';
const ChatReports = props => {
const [reports, setReports] = useState([]);
const authTokens = useContext(TokenContext);
useEffect(() => {
authTokens.tokenFetch(`${process.env.CHAT_URI}/admin/reports`)
.then(res => res.json())
.then(json => {
setReports(json);
})
;
}, []);
return (
<div className='panel' style={{minWidth: '100%'}}>
<h2 className='text centered'>Chat Reports</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Username</th>
<th className='mobile hide'>Room Name</th>
<th>Content</th>
<th>Reported By</th>
<th className='mobile hide'>Delete</th>
</tr>
</thead>
<tbody>
{reports.map((report, index) => (
<tr key={index}>
<td className='text centered'>{dateFormat(report.chatlog.createdAt, 'yyyy-mm-dd, H:MM:ss')}</td>
<td className='text centered'>{report.chatlog.username}</td>
<td className='text mobile hide centered'>{report.chatlog.room}</td>
<td className='text centered'>{report.chatlog.text}</td>
<td className='text centered'>{report.reporter.join(', ')}</td>
<td className='text mobile hide centered'><button onClick={() => deleteReportsFor(report.chatlogIndex, authTokens.tokenFetch, setReports)}>Delete</button></td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const deleteReportsFor = (chatlogIndex, tokenFetch, setReports) => {
tokenFetch(`${process.env.CHAT_URI}/admin/reports`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ chatlogIndex })
});
setReports(reports => reports.filter(report => report.chatlogIndex != chatlogIndex));
};
export default ChatReports;
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react'; import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const GrantAdmin = props => { const GrantAdmin = props => {
//context //context
@@ -10,13 +10,10 @@ const GrantAdmin = props => {
const usernameRef = useRef(); const usernameRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>Grant Admin Privileges</h2> <h2 className='text centered'>Grant Admin Privileges</h2>
<form> <form className='constrained'>
<div> <input type='text' name='username' placeholder='Username' ref={usernameRef} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
evt.preventDefault(); evt.preventDefault();
@@ -54,8 +51,7 @@ const handleButtonPress = async (username, tokenFetch, method) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/admin`, { const result = await tokenFetch(`${process.env.AUTH_URI}/admin/admin`, {
method: method, method: method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ body: JSON.stringify({
username username
@@ -1,6 +1,6 @@
import React, { useRef, useContext } from 'react'; import React, { useRef, useContext } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const GrantMod = props => { const GrantMod = props => {
//context //context
@@ -10,13 +10,10 @@ const GrantMod = props => {
const usernameRef = useRef(); const usernameRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>Grant Moderation Privileges</h2> <h2 className='text centered'>Grant Moderation Privileges</h2>
<form> <form className='constrained'>
<div> <input type='text' name='username' placeholder='Username' ref={usernameRef} />
<label htmlFor='username'>Username:</label>
<input type='text' name='username' ref={usernameRef} />
</div>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
evt.preventDefault(); evt.preventDefault();
@@ -54,8 +51,7 @@ const handleButtonPress = async (username, tokenFetch, method) => {
const result = await tokenFetch(`${process.env.AUTH_URI}/admin/mod`, { const result = await tokenFetch(`${process.env.AUTH_URI}/admin/mod`, {
method: method, method: method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ body: JSON.stringify({
username username
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useContext, useRef } from 'react'; import React, { useState, useEffect, useContext, useRef } from 'react';
import Select from 'react-dropdown-select'; import Select from 'react-select';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const NewsEditor = props => { const NewsEditor = props => {
//context //context
@@ -17,57 +17,40 @@ const NewsEditor = props => {
const [index, setIndex] = useState(null); const [index, setIndex] = useState(null);
//run once //run once
useEffect(async () => { useEffect(() => {
const result = await fetch(`${process.env.NEWS_URI}/news/metadata?limit=999`, { fetch(`${process.env.NEWS_URI}/news/metadata?page_size=999`)
method: 'GET', .then(res => res.json())
headers: { .then(json => {
'Content-Type': 'application/json', setArticles(json)
'Access-Control-Allow-Origin': '*' })
}, ;
});
if (!result.ok) {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
setArticles(await result.json());
}
}, []); }, []);
return ( return (
<div> <div className='panel'>
<h2 className='centered'>News Editor</h2> <h2 className='text centered'>News Editor</h2>
<div> <Select
<label htmlFor='article'>Article: </label> options={articles.map(article => { return { label: article.title, index: article.index }; })}
<Select onChange={async ({index}) => {
options={articles.map(article => { return { label: article.title, value: article.index }; })} //fetch this article
onChange={async values => { const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`);
//fetch this article
const index = values[0].value;
const result = await fetch(`${process.env.NEWS_URI}/news/archive/${index}`, { if (!result.ok) {
headers: { const err = `${result.status}: ${await result.text()}`;
'Access-Control-Allow-Origin': '*' console.log(err);
} alert(err);
}); } else {
const article = await result.json();
titleRef.current.value = article.title;
authorRef.current.value = article.author;
bodyRef.current.value = article.body;
setIndex(index);
}
}}
placeholder='Select Article'
/>
if (!result.ok) { <form className='constrained' onSubmit={async evt => {
const err = `${result.status}: ${await result.text()}`;
console.log(err);
alert(err);
} else {
const article = await result.json();
titleRef.current.value = article.title;
authorRef.current.value = article.author;
bodyRef.current.value = article.body;
setIndex(index);
}
}}
/>
</div>
<form onSubmit={async evt => {
//onSubmit //onSubmit
evt.preventDefault(); evt.preventDefault();
const [err] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, index, authTokens.tokenFetch); const [err] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, index, authTokens.tokenFetch);
@@ -78,20 +61,9 @@ const NewsEditor = props => {
alert(`Edited as article index ${index}`); alert(`Edited as article index ${index}`);
} }
}}> }}>
<div> <input type='text' name='title' placeholder='Title' ref={titleRef} />
<label htmlFor='title'>Title: </label> <input type='text' name='author' placeholder='Author' ref={authorRef} />
<input type='text' name='title' ref={titleRef} /> <textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={authorRef} />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
</div>
<button type='submit'>Update</button> <button type='submit'>Update</button>
<button type='button' onClick={async evt => { <button type='button' onClick={async evt => {
@@ -122,8 +94,7 @@ const handleSubmit = async (title, author, body, index, tokenFetch) => {
const result = await tokenFetch(`${process.env.NEWS_URI}/news/${index}`, { const result = await tokenFetch(`${process.env.NEWS_URI}/news/${index}`, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ body: JSON.stringify({
title, title,
@@ -1,6 +1,6 @@
import React, { useContext, useRef } from 'react'; import React, { useContext, useRef } from 'react';
import { TokenContext } from '../utilities/token-provider'; import { TokenContext } from '../../utilities/token-provider';
const NewsPublisher = props => { const NewsPublisher = props => {
//context //context
@@ -12,33 +12,22 @@ const NewsPublisher = props => {
const bodyRef = useRef(); const bodyRef = useRef();
return ( return (
<div> <div className='panel'>
<h2 className='centered'>News Publisher</h2> <h2 className='text centered'>News Publisher</h2>
<form onSubmit={async evt => { <form className='constrained' onSubmit={async evt => {
//on submit //on submit
evt.preventDefault(); evt.preventDefault();
const [err, index] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, authTokens.tokenFetch); const [err, index] = await handleSubmit(titleRef.current.value, authorRef.current.value, bodyRef.current.value, authTokens.tokenFetch);
if (err) { if (err) {
alert(err); alert(err);
} else { } else {
titleRef.current.value = authorRef.current.value = bodyRef.current.value = ''; titleRef.current.value = authorRef.current.value = bodyRef.current.value = ''; //TODO: null bug here?
alert(`Published as article index ${index}`); alert(`Published as article index ${index}`);
} }
}}> }}>
<div> <input type='text' name='title' placeholder='Title' ref={titleRef} />
<label htmlFor='title'>Title: </label> <input type='text' name='author' placeholder='Author' ref={authorRef} />
<input type='text' name='title' ref={titleRef} /> <textarea name='body' rows='10' cols='150' placeholder='Body of the article goes here...' ref={bodyRef} />
</div>
<div>
<label htmlFor='author'>Author: </label>
<input type='text' name='author' ref={authorRef} />
</div>
<div>
<label htmlFor='body'>Body: </label>
<textarea name='body' rows='10' cols='150' ref={bodyRef} />
</div>
<button type='submit'>Publish</button> <button type='submit'>Publish</button>
</form> </form>
@@ -57,8 +46,7 @@ const handleSubmit = async (title, author, body, tokenFetch) => {
{ {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ body: JSON.stringify({
title, title,
+60
View File
@@ -0,0 +1,60 @@
//react
import React, { useContext, Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router';
import { TokenContext } from './utilities/token-provider';
//styling
import '../styles/styles.css';
//common components
import Footer from './panels/footer';
import PopupChat from './panels/popup-chat';
//lazy wrappers
const Homepage = lazy(() => import('./homepage'));
const Signup = lazy(() => import('./accounts/signup'));
const Login = lazy(() => import('./accounts/login'));
const Account = lazy(() => import('./accounts/account'));
const Dashboard = lazy(() => import('./dashboard'));
const Recover = lazy(() => import('./accounts/recover'));
const Reset = lazy(() => import('./accounts/reset'));
const Admin = lazy(() => import('./administration/admin'));
const Mod = lazy(() => import('./administration/mod'));
const PrivacyPolicy = lazy(() => import('./static/privacy-policy'));
const Credits = lazy(() => import('./static/credits'));
const NotFound = lazy(() => import('./not-found'));
const App = props => {
const authTokens = useContext(TokenContext);
//default render
return (
<BrowserRouter>
<Suspense>
<Routes>
<Route exact path='/' element={<Homepage />} />
<Route path='/signup' element={<Signup />} />
<Route path='/login' element={<Login />} />
<Route path='/account' element={<Account />} />
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/recover' element={<Recover />} />
<Route path='/reset' element={<Reset />} />
<Route path='/admin' element={<Admin />} />
<Route path='/mod' element={<Mod />} />
<Route path='/privacypolicy' element={<PrivacyPolicy />} />
<Route path='/credits' element={<Credits />} />
<Route path='*' element={<NotFound />} />
</Routes>
</Suspense>
{ authTokens.accessToken ? <PopupChat /> : <></> }
<Footer />
</BrowserRouter>
);
};
export default App;
+34
View File
@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Link, Navigate } from 'react-router';
import ApplyToBody from './utilities/apply-to-body';
import { TokenContext } from './utilities/token-provider';
import Logout from './accounts/panels/logout';
const Dashboard = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (!authTokens.accessToken) {
return <Navigate to='/' />;
}
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<Link to='/account'>Account</Link>
{ authTokens.getPayload().admin ? <Link to='/admin' className='text centered'>Admin</Link> : <></> }
{ authTokens.getPayload().mod ? <Link to='/mod' className='text centered'>Mod</Link> : <></> }
<Logout />
</div>
</div>
</>
);
};
export default Dashboard;
+45
View File
@@ -0,0 +1,45 @@
import React, { useContext } from 'react';
import { Link, Navigate } from 'react-router';
import ApplyToBody from './utilities/apply-to-body';
import { TokenContext } from './utilities/token-provider';
import NewsFeed from './panels/news-feed';
const HomePage = props => {
//context
const authTokens = useContext(TokenContext);
//misplaced?
if (authTokens.accessToken) {
return <Navigate to='/dashboard' />;
}
return (
<>
<ApplyToBody className='homepage' />
<div className='page'>
<div className='panel above'>
<header>
<h1 className='text centered'>MERN Template</h1>
<h2 className='text centered'>This is the MERN-template</h2>
</header>
<div className='panel centered middle'>
<Link to='/signup'><button>Sign Up</button></Link>
<Link to='/login'><button>Login</button></Link>
</div>
</div>
<div className='panel below'>
<div className='central'>
<NewsFeed />
</div>
</div>
</div>
</>
);
};
export default HomePage;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Link } from 'react-router';
import ApplyToBody from './utilities/apply-to-body';
const NotFound = props => {
return (
<>
<ApplyToBody className='dashboard' />
<div className='page'>
<div className='central panel centered middle'>
<h1 className='text centered'>Page Not Found</h1>
<br />
<Link className='text centered' to='/'>Return Home</Link>
</div>
</div>
</>
);
};
export default NotFound;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { Link } from 'react-router';
const Break = () => {
return (
<>
<span className='mobile hide'> - </span>
<br className='mobile show' />
</>
);
}
const Footer = () => {
return (
<footer>
<p className='text centered'>© <a href='https://krgamestudios.com'>KR Game Studios</a> 2020-2025<Break /><Link to='/privacypolicy'>Privacy Policy</Link><Break /><Link to='/credits'>Credits</Link></p>
</footer>
);
};
export default Footer;
@@ -8,11 +8,6 @@ const NewsFeed = props => {
useEffect(() => { useEffect(() => {
//this... um... //this... um...
fetch(`${process.env.NEWS_URI}/news`, { fetch(`${process.env.NEWS_URI}/news`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
signal: aborter.current.signal //oh dear signal: aborter.current.signal //oh dear
}) })
.then(blob => blob.json()) .then(blob => blob.json())
@@ -24,33 +19,21 @@ const NewsFeed = props => {
}, []); }, []);
return ( return (
<div> <div className='panel'>
<h1 className='centered'>News Feed</h1> <h1 className='text centered'>News Feed</h1>
{(articles || []).map((article, index) => { {articles.map((article, index) => {
//BUGFIX: check for empty data
if (!article.title) {
return article.title = '';
}
if (!article.author) {
return article.author = '';
}
if (!article.body) {
return article.body = '';
}
//render
return ( return (
<div key={index}> <div key={index} className='panel'>
<hr /> <hr />
<h2>{article.title}</h2> <h2>{article.title}</h2>
<p>Written by <strong>{article.author}</strong>, { <br />
<p><em>Written by <strong>{article.author}</strong>, {
article.edits > 0 ? article.edits > 0 ?
<span>Last Updated {dateFormat(article.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`})</span> : <span>Last Updated {dateFormat(article.updatedAt, 'fullDate')} ({`${article.edits} edit${article.edits > 1 ? 's': ''}`})</span> :
<span>Published {dateFormat(article.createdAt, 'fullDate')}</span> <span>Published {dateFormat(article.createdAt, 'fullDate')}</span>
}</p> }</em></p>
<p style={{whiteSpace: 'pre-wrap'}}>{article.body}</p> <br />
<div dangerouslySetInnerHTML={{ __html: article.rendered }} />
</div> </div>
); );
})} })}
@@ -4,11 +4,12 @@ import { io } from 'socket.io-client';
import '../../styles/popup-chat.css'; import '../../styles/popup-chat.css';
//TODO: I very much need to move this out of global state
const socket = io(`${process.env.CHAT_URI}/chat`); const socket = io(`${process.env.CHAT_URI}/chat`);
const PopupChat = props => { const PopupChat = props => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [chatlog, setChatlog] = useState([]); const [chatlog, setChatlog] = useState([{ emphasis: true, text: 'If chat doesn\'t load, reload the page' }]);
const inputRef = useRef(); const inputRef = useRef();
const sendRef = useRef(); const sendRef = useRef();
@@ -77,7 +78,7 @@ const handleSend = (inputRef, pushChatlog, username, accessToken) => {
}); });
if (!inputRef.current.value.startsWith('/')) { if (!inputRef.current.value.startsWith('/')) {
pushChatlog({ username: username, text: inputRef.current.value }); pushChatlog({ createdAt: (new Date(Date.now())).toISOString(), username: username, text: inputRef.current.value });
} }
inputRef.current.value = ''; inputRef.current.value = '';
@@ -85,7 +86,36 @@ const handleSend = (inputRef, pushChatlog, username, accessToken) => {
//render each line //render each line
const processLine = (line, index, accessToken) => { 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>; //utility functions
const isValidDate = d => {
return d instanceof Date && !isNaN(d);
};
const isToday = d => {
const now = new Date(Date.now());
return d.getDate() == now.getDate() && d.getMonth() == now.getMonth() && d.getFullYear() == now.getFullYear();
};
const isThisYear = d => {
const now = new Date(Date.now());
return d.getFullYear() == now.getFullYear();
};
//parse the date
const date = new Date(line.createdAt);
//split it up so we can format each field individually
const year = `${date.getFullYear()}`;
const month = `${date.getMonth() + 1}`;
const day = `${date.getDate()}`;
const hours = `${date.getHours()}`;
const minutes = `${date.getMinutes()}`.padStart(2, '0');
//combine into the final timestamp
const timestamp = !isValidDate(date) ? '' : isToday(date) ? `${hours}:${minutes}` : isThisYear(date) ? `${month}/${day}` : `${year}`;
//generate the content string
let content = <div className='content row'>{timestamp.length > 0 ? <span className='timestamp col'>{timestamp}</span> : null }<span className='inner col'>{line.username ? <span className='username'>{line.username}: </span> : ''}{line.text ? <span className='text'>{line.text}</span> : ''}</span></div>;
//decorators //decorators
if (line.emphasis) { if (line.emphasis) {
@@ -96,7 +126,8 @@ const processLine = (line, index, accessToken) => {
content = <strong>{content}</strong>; content = <strong>{content}</strong>;
} }
return <li key={index} className='line'>{content}<div className='report'><a onClick={() => processReport(line, accessToken)} style={{ display: line.index && !line.notification ? 'flex' : 'none' }}>!!!</a></div></li>;
return <li key={index} className='line table noCollapse'>{content}<a className='report' onClick={() => processReport(line, accessToken)}>!!!</a></li>;
}; };
const processReport = (line, accessToken) => { const processReport = (line, accessToken) => {
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Link } from 'react-router';
const Static = props => {
return (
<div className='page central'>
<header>
<h1 className='text centered'>Credits</h1>
</header>
<h2 className='text centered'>MERN-template</h2>
<p>The <a href='https://github.com/krgamestudios/MERN-template'>MERN-template</a> developed by Kayne Ruse, KR Game Studios</p>
<Link className='text centered' to='/'>Return Home</Link>
</div>
);
};
export default Static;
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { Link } from 'react-router';
const Static = props => {
return (
<div className='page central'>
<header>
<h1 className="text centered">Privacy Policy</h1>
<Link className='text centered' to='/'>Return Home</Link>
</header>
</div>
);
};
export default Static;
+19
View File
@@ -0,0 +1,19 @@
import React, { useEffect } from 'react';
//applies the classname of 'body'
const ApplyToBody = (props) => {
useEffect(() => {
document.body.classList.add(props.className);
return () => {
document.body.classList.remove(props.className);
};
}, []);
return (
<></>
);
};
export default ApplyToBody;
@@ -1,22 +1,29 @@
import React, { useState, useEffect, createContext } from 'react'; import React, { useState, useEffect, createContext } from 'react';
import decode from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
export const TokenContext = createContext(); export const TokenContext = createContext();
const TokenProvider = props => { //DOCS: tokenFetch() and tokenCallback() are actually closures here
const [accessToken, setAccessToken] = useState('');
const [refreshToken, setRefreshToken] = useState('');
//make the access and refresh tokens persist between reloads const TokenProvider = props => {
//state to be used
const [accessToken, setAccessToken] = useState('');
//force a logout under certain conditions
const forceLogout = () => {
localStorage.removeItem("accessToken");
setAccessToken("");
};
//make the access token persist between reloads
useEffect(() => { useEffect(() => {
setAccessToken(localStorage.getItem("accessToken") || ''); setAccessToken(localStorage.getItem("accessToken") || '');
setRefreshToken(localStorage.getItem("refreshToken") || '');
}, []); }, []);
//update the stored copies
useEffect(() => { useEffect(() => {
localStorage.setItem("accessToken", accessToken); localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", refreshToken); }, [accessToken]);
}, [accessToken, refreshToken]);
//wrap the default fetch function //wrap the default fetch function
const tokenFetch = async (url, options) => { const tokenFetch = async (url, options) => {
@@ -24,23 +31,34 @@ const TokenProvider = props => {
let bearer = accessToken; let bearer = accessToken;
//if expired (10 minutes, normally) //if expired (10 minutes, normally)
const expired = new Date(decode(accessToken).exp * 1000) < Date.now(); const expired = new Date(jwtDecode(accessToken).exp) < Date.now() / 1000;
if (expired) { if (expired) {
//ping the auth server for a new token //BUGFIX: if logging out, just skip over the refresh token
if (url === `${process.env.AUTH_URI}/auth/logout`) {
return fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${bearer}`
},
credentials: 'include'
});
}
//ping the auth server for a new access token
const response = await fetch(`${process.env.AUTH_URI}/auth/token`, { const response = await fetch(`${process.env.AUTH_URI}/auth/token`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Authorization': `Bearer ${bearer}`
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ credentials: 'include'
token: refreshToken
})
}); });
//any errors, throw them //any errors, throw them
if (!response.ok) { if (!response.ok) {
if (response.status == 403) {
forceLogout();
}
throw `${response.status}: ${await response.text()}`; throw `${response.status}: ${await response.text()}`;
} }
@@ -48,23 +66,7 @@ const TokenProvider = props => {
const newAuth = await response.json(); const newAuth = await response.json();
setAccessToken(newAuth.accessToken); setAccessToken(newAuth.accessToken);
setRefreshToken(newAuth.refreshToken);
bearer = newAuth.accessToken; bearer = newAuth.accessToken;
//BUGFIX: logging out correctly requires the new refresh token
if (url == `${process.env.AUTH_URI}/auth/logout`) {
return fetch(`${process.env.AUTH_URI}/auth/logout`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Authorization': `Bearer ${bearer}`
},
body: JSON.stringify({
token: newAuth.refreshToken
})
});
}
} }
//finally, delegate to fetch //finally, delegate to fetch
@@ -73,30 +75,34 @@ const TokenProvider = props => {
headers: { headers: {
...(options || { headers: {} }).headers, ...(options || { headers: {} }).headers,
'Authorization': `Bearer ${bearer}` 'Authorization': `Bearer ${bearer}`
} },
credentials: 'include'
}); });
}; };
//access the refreshed token via callback //access the refreshed token via callback
const tokenCallback = async (cb) => { const tokenCallback = async (cb) => {
//use this?
let bearer = accessToken;
//if expired (10 minutes, normally) //if expired (10 minutes, normally)
const expired = new Date(decode(accessToken).exp * 1000) < Date.now(); const expired = new Date(jwtDecode(accessToken).exp) < Date.now() / 1000;
if (expired) { if (expired) {
//ping the auth server for a new token //ping the auth server for a new token
const response = await fetch(`${process.env.AUTH_URI}/auth/token`, { const response = await fetch(`${process.env.AUTH_URI}/auth/token`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Authorization': `Bearer ${bearer}`
'Access-Control-Allow-Origin': '*'
}, },
body: JSON.stringify({ credentials: 'include'
token: refreshToken
})
}); });
//any errors, throw them //any errors, throw them
if (!response.ok) { if (!response.ok) {
if (response.status == 403) {
forceLogout();
}
throw `${response.status}: ${await response.text()}`; throw `${response.status}: ${await response.text()}`;
} }
@@ -104,7 +110,6 @@ const TokenProvider = props => {
const newAuth = await response.json(); const newAuth = await response.json();
setAccessToken(newAuth.accessToken); setAccessToken(newAuth.accessToken);
setRefreshToken(newAuth.refreshToken);
//finally //finally
return cb(newAuth.accessToken); return cb(newAuth.accessToken);
@@ -114,7 +119,7 @@ const TokenProvider = props => {
}; };
return ( return (
<TokenContext.Provider value={{ accessToken, refreshToken, setAccessToken, setRefreshToken, tokenFetch, tokenCallback, getPayload: () => decode(accessToken) }}> <TokenContext.Provider value={{ accessToken, setAccessToken, tokenFetch, tokenCallback, getPayload: () => jwtDecode(accessToken) }}>
{props.children} {props.children}
</TokenContext.Provider> </TokenContext.Provider>
) )
+51 -18
View File
@@ -1,6 +1,14 @@
/* clear from the rest of the CSS files */
.chat button, .chat input {
border-radius: unset !important;
font-size: unset !important;
margin: unset !important;
}
.chat { .chat {
position: fixed; position: fixed;
bottom: 23px; bottom: 3.6em; /* Allow space for the footer */
right: 28px; right: 28px;
width: 280px; width: 280px;
border: solid; border: solid;
@@ -8,71 +16,86 @@
border-width: 2px; border-width: 2px;
background-color: #CCC; background-color: #CCC;
display: inline-block; display: inline-block;
max-height: calc(50vh - 23px);
} }
.chat > button.open { .chat button.open {
color: white; color: white;
background-color: grey; background-color: grey;
} }
.chat > button.send { .chat button.send {
color: white; color: white;
background-color: green; background-color: green;
border-style: solid;
border-width: 2px;
border-color: darkslategray;
} }
.chat > button.close { .chat button.close {
color: black; color: black;
background-color: red; background-color: red;
border-color: maroon;
border-width: 2px;
} }
.chat > button { .chat button {
width: 100%; width: 100%;
height: 2em; height: 2em;
opacity: 0.8; opacity: 0.8;
border: unset;
} }
.chat > button:hover { .chat button:hover {
opacity: 1; opacity: 1;
} }
.chat > .input { .chat .input {
width: calc(100% - 10px); width: 100%;
height: 2em; height: 2em;
} }
.chat > .log { .chat .log {
min-height: 300px; min-height: 280px;
} }
.chat > .log > .scrollable > .line { .chat .line {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
} }
.chat > .log > .scrollable > .line > .report { .chat .report {
color: red; color: red;
display: none; display: none;
} }
.chat > .log > .scrollable > .line:hover { .chat .line:hover {
background-color: #BBB; background-color: #BBB;
} }
.chat > .log > .scrollable > .line:hover > .report { .chat .line:hover .report {
display: flex; display: flex;
} }
.chat > .log > .scrollable > .line > .content > .username { .chat .timestamp {
max-width: 44px;
}
.chat .inner {
flex: 1 !important;
display: inline-block !important;
flex-direction: row !important;
}
.chat .username {
font-weight: bold; font-weight: bold;
} }
.chat > .log > .scrollable { .chat .scrollable {
margin: 0; margin: 0;
padding: 10px; padding: 10px;
min-height: 280px; min-height: 280px;
max-height: calc(50vh - 23px - 20px - 6em); max-height: 180px;
overflow-x: wrap; overflow-x: wrap;
overflow-y: scroll; overflow-y: scroll;
} }
@@ -80,3 +103,13 @@
.chat ul { .chat ul {
list-style: none; list-style: none;
} }
@media screen and (max-width: 768px) {
.chat {
position: unset;
bottom: unset;
right: unset;
width: calc(100% + 20px);
margin-left: -10px;
}
}
+323
View File
@@ -0,0 +1,323 @@
/* global defaults */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body, #root {
font: 12pt Helvetica, Arial;
min-width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
background-color: #fefefe;
color: #010101;
}
h1 {
font-size: 40pt;
font-weight: bold;
}
h2 {
font-size: 24pt;
font-weight: bold;
}
h3 {
font-size: 16pt;
font-weight: bold;
}
ul {
list-style-type: disc;
list-style-position: inside;
padding-bottom: .5em;
}
p {
margin-bottom: 0 !important;
padding-bottom: 1em;
}
pre {
padding: 5px;
margin-bottom: 1em;
background-color: lightgray;
overflow-x: scroll;
}
a {
color: blue;
text-decoration: none;
}
blockquote {
padding-top: 0.5em;
padding-left: 1em;
margin-bottom: 1em;
border-left: 3px solid #ccc;
}
.text.left {
text-align: left;
}
.text.centered {
text-align: center;
}
.text.right {
text-align: right;
}
.centered {
justify-content: center;
}
.middle {
align-items: center;
}
/* header */
header {
flex: 0 1 auto;
margin-top: 1em;
margin-bottom: 1em;
justify-self: flex-start;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* footer */
footer {
padding-top: 0.5em;
flex: 0 1 auto;
justify-self: flex-end;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
/* central display */
.central {
padding: 0 10px;
margin: 0 20%;
min-height: calc(100vh - 3.6em);
}
@media screen and (max-width: 768px) {
.central {
margin: 0;
}
}
/* components */
.page {
flex: 1 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.page.centered {
justify-content: center;
}
.page.middle {
align-items: center;
}
.panel {
flex: 0 1 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-bottom: 1em;
}
.panel.centered {
justify-content: center;
}
.panel.middle {
align-items: middle;
}
button:disabled {
opacity: 0.5;
}
input, button {
text-indent: 0.3em;
border-radius: 0.2em;
font-size: 1.8em;
margin: 0.2em;
padding: 0.2em 0;
}
textarea {
margin-left: 0.5em;
margin-right: -0.4em;
max-height: none !important;
resize: vertical;
overflow: auto;
}
/* "constrained" means reusable input area */
.constrained {
flex: 0 1 auto;
align-self: center;
max-width: 480px;
display: flex;
flex-direction: column;
margin-bottom: 1em;
}
.constrained > * {
flex: 1 0 auto;
max-height: 2em;
display: flex;
flex-direction: row;
}
.constrained button, button.constrained {
display: inline-block;
}
.constrained label {
font-size: 1.8em;
text-indent: 0.4em;
}
@media screen and (max-width: 480px) {
.constrained {
max-width: 100vw;
}
}
/* flexbox tables */
.table {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.table .row {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
@media screen and (max-width: 480px) {
.table .row {
flex-direction: column;
}
}
@media screen and (max-width: 768px) {
.table .row.tabletCollapse {
flex-direction: column;
}
}
.table .row .col {
flex: 1 1 1%;
display: flex;
flex-direction: column;
min-width: 0;
}
.table .row .col.double {
flex: 2 1 2%;
}
.table .row .col.half {
flex: 0.5 1 0.5%;
}
@media screen and (max-width: 480px) {
.table .row .col.double {
flex: 2 1 2.5%;
}
}
.table.noCollapse .row, .table .row.noCollapse {
flex-direction: row;
}
/* mobile control */
.mobile.show {
display: none;
}
@media screen and (max-width: 480px) {
.mobile.centered {
text-align: initial;
}
.mobile.show {
display: inline-block;
}
.mobile.hide {
display: none;
}
.mobile.centered {
text-align: center;
}
/* hybrid of table and mobile control */
.mobile.hide.col {
display: none;
}
.mobile.col.half {
flex: 0.5;
}
}
/* tablet control */
.tablet.show {
display: none;
}
@media screen and (max-width: 768px) {
.tablet.centered {
text-align: initial;
}
.tablet.show {
display: inline-block;
}
.tablet.hide {
display: none;
}
.tablet.centered {
text-align: center;
}
/* hybrid of table and tablet control */
.tablet.hide.col {
display: none;
}
.tablet.col.half {
flex: 0.5;
}
}
+1 -1
View File
@@ -22,6 +22,6 @@
<meta property="og:description" content="" /> <meta property="og:description" content="" />
</head> </head>
<body> <body>
<div id = "root"></div> <div id="root"></div>
</body> </body>
</html> </html>
+148 -38
View File
@@ -36,7 +36,7 @@ const question = (prompt, def = null) => {
Currently, all microservices are mandatory; you'll have to mess with the result Currently, all microservices are mandatory; you'll have to mess with the result
and the source code if you wish to be more selective. Microservices currently and the source code if you wish to be more selective. Microservices currently
impelented are: implemented are:
* auth-server * auth-server
* news-server * news-server
@@ -46,24 +46,89 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
` `
); );
// //determine local computer address for mac user vs everyone else
// let macUser = '';
// while (macUser.toLowerCase() !== 'yes' && macUser.toLowerCase() !== 'no') {
// macUser = await question('Will the MERN-Template be running locally on a MacOS system? (yes or no, this only alters startup.sql)', '');
// }
// const localAddress = macUser === 'yes' ? 'localhost' : '%';
const localAddress = '%';
//project configuration //project configuration
const projectName = await question('Project Name', 'template'); const projectName = await question('Project Name', 'template');
const projectWebAddress = await question('Project Web Address', 'example.com'); const projectWebAddress = await question('Project Web Address', 'example.com');
const corsWebOrigin = await question('CORS Web Origin', `https://${projectWebAddress}`);
let projectDBLocation = '';
while (typeof projectDBLocation != 'string' || /^[le]/i.test(projectDBLocation[0]) == false) {
projectDBLocation = await question('Project [l]ocal or [e]xternal database?');
}
let projectDBHost = '';
let projectDBPort = '';
if (/^[l]/i.test(projectDBLocation[0])) {
projectDBHost = 'database';
projectDBPort = '3306';
}
else {
projectDBHost = await question('Project DB Host');
projectDBPort = await question('Project DB Port', '3306');
}
const projectDBUser = await question('Project DB Username', projectName); const projectDBUser = await question('Project DB Username', projectName);
const projectDBPass = await question('Project DB Password', 'pikachu'); const projectDBPass = await question('Project DB Password', 'pikachu');
//news configuration //news configuration
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);
let newsDBLocation = '';
while (typeof newsDBLocation != 'string' || /^[le]/i.test(newsDBLocation[0]) == false) {
newsDBLocation = await question('News [l]ocal or [e]xternal database?');
}
let newsDBHost = '';
let newsDBPort = '';
if (/^[l]/i.test(newsDBLocation[0])) {
newsDBHost = 'database';
newsDBPort = '3306';
}
else {
newsDBHost = await question('News DB Host');
newsDBPort = await question('News DB Port', '3306');
}
const newsDBUser = await question('News DB Username', newsName);
const newsDBPass = await question('News DB Password', 'venusaur'); 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 authPostValidationHookArray = await question('Auth Post Validation Hook Array', '');
const authResetAddress = await question('Auth Reset Addr', `${projectWebAddress}/reset`); const authResetAddress = await question('Auth Reset Addr', `${projectWebAddress}/reset`);
const authDBUser = await question('Auth DB Username', authName);
let authDBLocation = '';
while (typeof authDBLocation != 'string' || /^[le]/i.test(authDBLocation[0]) == false) {
authDBLocation = await question('Auth [l]ocal or [e]xternal database?');
}
let authDBHost = '';
let authDBPort = '';
if (/^[l]/i.test(authDBLocation[0])) {
authDBHost = 'database';
authDBPort = '3306';
}
else {
authDBHost = await question('Auth DB Host');
authDBPort = await question('Auth DB Port', '3306');
}
const authDBUser = await question('Auth DB Username', authName);
const authDBPass = await question('Auth DB Password', 'charizard'); 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');
@@ -74,7 +139,25 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
//chat goes here //chat goes here
const chatName = await question('Chat Name', 'chat'); const chatName = await question('Chat Name', 'chat');
const chatWebAddress = await question('Chat Web Address', `${chatName}.${projectWebAddress}`); const chatWebAddress = await question('Chat Web Address', `${chatName}.${projectWebAddress}`);
const chatDBUser = await question('Chat DB Username', chatName);
let chatDBLocation = '';
while (typeof chatDBLocation != 'string' || /^[le]/i.test(chatDBLocation[0]) == false) {
chatDBLocation = await question('Chat [l]ocal or [e]xternal database?');
}
let chatDBHost = '';
let chatDBPort = '';
if (/^[l]/i.test(chatDBLocation[0])) {
chatDBHost = 'database';
chatDBPort = '3306';
}
else {
chatDBHost = await question('Chat DB Host');
chatDBPort = await question('Chat DB Port', '3306');
}
const chatDBUser = await question('Chat DB Username', chatName);
const chatDBPass = await question('Chat DB Password', 'blastoise'); const chatDBPass = await question('Chat DB Password', 'blastoise');
//database configuration //database configuration
@@ -90,32 +173,34 @@ See https://github.com/krgamestudios/MERN-template/wiki for help.
//MUST be at least 8 chars //MUST be at least 8 chars
let tmpPass = ''; let tmpPass = '';
let tmpHost = '';
while (defaultUser && tmpPass.length < 8) { while (defaultUser && tmpPass.length < 8) {
console.log('--All passwords must be at least 8 characters long--'); console.log('--All passwords must be at least 8 characters long--');
tmpPass = await question('Default Admin Pass', ''); tmpPass = await question('Default Admin Pass', '');
tmpHost = await question('Default Admin Host', '');
} }
const defaultPass = tmpPass; const defaultPass = tmpPass;
const defaultHost = tmpHost;
if (defaultUser) { if (defaultUser) {
console.log(`Default user email will be: ${defaultUser}@${authWebAddress}`); console.log(`Default user email will be: ${defaultUser}@${defaultHost}`);
} }
//traefic configuration //traefic configuration
const supportEmail = await question('Support Email', emailUser); const supportEmail = await question('Support Email', emailUser);
//misc. configuration //misc. configuration
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"
services: services:
${projectName}: ${projectName}:
build: . build: .
ports: ports:
- "${projectPort}" - ${projectPort}
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.${projectName}router.rule=Host(\`${projectWebAddress}\`) - traefik.http.routers.${projectName}router.rule=Host(\`${projectWebAddress}\`)
@@ -124,8 +209,10 @@ 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_ORIGIN=${corsWebOrigin}
- WEB_PORT=${projectPort} - WEB_PORT=${projectPort}
- DB_HOSTNAME=database - DB_HOSTNAME=${projectDBHost}
- DB_PORTNAME=${projectDBPort}
- DB_DATABASE=${projectName} - DB_DATABASE=${projectName}
- DB_USERNAME=${projectDBUser} - DB_USERNAME=${projectDBUser}
- DB_PASSWORD=${projectDBPass} - DB_PASSWORD=${projectDBPass}
@@ -134,10 +221,12 @@ services:
- AUTH_URI=https://${authWebAddress} - AUTH_URI=https://${authWebAddress}
- CHAT_URI=https://${chatWebAddress} - CHAT_URI=https://${chatWebAddress}
- SECRET_ACCESS=${accessToken} - SECRET_ACCESS=${accessToken}
volumes:
- /etc/localtime:/etc/localtime:ro
networks: networks:
- app-network - app-network
depends_on: depends_on:${ projectDBHost != 'database' ? '' : `
- database - database`}
- traefik - traefik
${newsName}: ${newsName}:
@@ -152,18 +241,22 @@ services:
- 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=${newsPort} - traefik.http.services.${newsName}service.loadbalancer.server.port=${newsPort}
environment: environment:
- WEB_ORIGIN=${corsWebOrigin}
- WEB_PORT=${newsPort} - WEB_PORT=${newsPort}
- DB_HOSTNAME=database - DB_HOSTNAME=${newsDBHost}
- DB_PORTNAME=${newsDBPort}
- DB_DATABASE=${newsName} - DB_DATABASE=${newsName}
- DB_USERNAME=${newsDBUser} - DB_USERNAME=${newsDBUser}
- DB_PASSWORD=${newsDBPass} - DB_PASSWORD=${newsDBPass}
- DB_TIMEZONE=${dbTimeZone} - DB_TIMEZONE=${dbTimeZone}
- QUERY_LIMIT=10 - PAGE_SIZE=10
- SECRET_ACCESS=${accessToken} - SECRET_ACCESS=${accessToken}
volumes:
- /etc/localtime:/etc/localtime:ro
networks: networks:
- app-network - app-network
depends_on: depends_on:${ newsDBHost != 'database' ? '' : `
- database - database`}
- traefik - traefik
${authName}: ${authName}:
@@ -178,11 +271,14 @@ services:
- traefik.http.routers.${authName}router.service=${authName}service@docker - traefik.http.routers.${authName}router.service=${authName}service@docker
- traefik.http.services.${authName}service.loadbalancer.server.port=${authPort} - traefik.http.services.${authName}service.loadbalancer.server.port=${authPort}
environment: environment:
- WEB_ORIGIN=${corsWebOrigin}
- WEB_PROTOCOL=https - WEB_PROTOCOL=https
- WEB_ADDRESS=${authWebAddress} - WEB_ADDRESS=${authWebAddress}
- HOOK_POST_VALIDATION_ARRAY=${authPostValidationHookArray}
- WEB_RESET_ADDRESS=${authResetAddress} - WEB_RESET_ADDRESS=${authResetAddress}
- WEB_PORT=${authPort} - WEB_PORT=${authPort}
- DB_HOSTNAME=database - DB_HOSTNAME=${authDBHost}
- DB_PORTNAME=${authDBPort}
- DB_DATABASE=${authName} - DB_DATABASE=${authName}
- DB_USERNAME=${authDBUser} - DB_USERNAME=${authDBUser}
- DB_PASSWORD=${authDBPass} - DB_PASSWORD=${authDBPass}
@@ -192,13 +288,16 @@ services:
- MAIL_PASSWORD=${emailPass} - MAIL_PASSWORD=${emailPass}
- MAIL_PHYSICAL=${emailPhysical} - MAIL_PHYSICAL=${emailPhysical}
- ADMIN_DEFAULT_USERNAME=${defaultUser} - ADMIN_DEFAULT_USERNAME=${defaultUser}
- ADMIN_DEFAULT_HOSTNAME=${defaultHost}
- ADMIN_DEFAULT_PASSWORD=${defaultPass} - ADMIN_DEFAULT_PASSWORD=${defaultPass}
- SECRET_ACCESS=${accessToken} - SECRET_ACCESS=${accessToken}
- SECRET_REFRESH=${refreshToken} - SECRET_REFRESH=${refreshToken}
volumes:
- /etc/localtime:/etc/localtime:ro
networks: networks:
- app-network - app-network
depends_on: depends_on:${ authDBHost != 'database' ? '' : `
- database - database`}
- traefik - traefik
${chatName}: ${chatName}:
@@ -213,19 +312,24 @@ services:
- traefik.http.routers.${chatName}router.service=${chatName}service@docker - traefik.http.routers.${chatName}router.service=${chatName}service@docker
- traefik.http.services.${chatName}service.loadbalancer.server.port=${chatPort} - traefik.http.services.${chatName}service.loadbalancer.server.port=${chatPort}
environment: environment:
- WEB_ORIGIN=${corsWebOrigin}
- WEB_PORT=${chatPort} - WEB_PORT=${chatPort}
- DB_HOSTNAME=database - DB_HOSTNAME=${chatDBHost}
- DB_PORTNAME=${chatDBPort}
- DB_DATABASE=${chatName} - DB_DATABASE=${chatName}
- DB_USERNAME=${chatDBUser} - DB_USERNAME=${chatDBUser}
- DB_PASSWORD=${chatDBPass} - DB_PASSWORD=${chatDBPass}
- DB_TIMEZONE=${dbTimeZone} - DB_TIMEZONE=${dbTimeZone}
- SECRET_ACCESS=${accessToken} - SECRET_ACCESS=${accessToken}
volumes:
- /etc/localtime:/etc/localtime:ro
networks: networks:
- app-network - app-network
depends_on: depends_on:${ chatDBHost != 'database' ? '' : `
- database - database`}
- traefik - traefik
${ [projectDBHost, newsDBHost, authDBHost, chatDBHost].some(x => x == "database") == false ? '' : `
database: database:
image: mariadb image: mariadb
restart: always restart: always
@@ -234,28 +338,33 @@ services:
volumes: volumes:
- ./mysql:/var/lib/mysql - ./mysql:/var/lib/mysql
- ./startup.sql:/docker-entrypoint-initdb.d/startup.sql:ro - ./startup.sql:/docker-entrypoint-initdb.d/startup.sql:ro
- /etc/localtime:/etc/localtime:ro
networks: networks:
- app-network - app-network
`}
traefik: traefik:
image: traefik:v2.4 image: traefik:latest
container_name: traefik container_name: traefik
command: command:
- --log.level=ERROR - --log.level=ERROR
- --api.insecure=false - --api.insecure=false
- --providers.docker=true - --providers.docker=true
- --providers.docker.exposedbydefault=false - --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entryPoint.to=websecure
- --entrypoints.web.http.redirections.entryPoint.scheme=https
- --entrypoints.web.http.redirections.entrypoint.permanent=true
- --entrypoints.websecure.address=:443 - --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.tlschallenge=true - --certificatesresolvers.myresolver.acme.tlschallenge=true
- --certificatesresolvers.myresolver.acme.email=${supportEmail} - --certificatesresolvers.myresolver.acme.email=${supportEmail}
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- traefik.docker.network=app-network
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
volumes: volumes:
- ./letsencrypt:/letsencrypt - ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
- /etc/localtime:/etc/localtime:ro
networks: networks:
- app-network - app-network
@@ -265,12 +374,12 @@ networks:
`; `;
const dockerfile = ` const dockerfile = `
FROM node:15 FROM node:22-bookworm-slim
WORKDIR "/app" WORKDIR "/app"
COPY . /app COPY . /app
RUN mkdir /app/public RUN mkdir -p /app/public
RUN chown node:node /app/public RUN chown node:node /app/public
RUN npm install --production RUN npm install --omit=dev
EXPOSE ${projectPort} EXPOSE ${projectPort}
USER node USER node
ENTRYPOINT ["bash", "-c"] ENTRYPOINT ["bash", "-c"]
@@ -279,24 +388,25 @@ CMD ["sleep 10 && npm start"]
const sqlfile = ` const sqlfile = `
CREATE DATABASE IF NOT EXISTS ${projectName}; CREATE DATABASE IF NOT EXISTS ${projectName};
CREATE USER IF NOT EXISTS '${projectDBUser}'@'%' IDENTIFIED BY '${projectDBPass}'; CREATE USER IF NOT EXISTS '${projectDBUser}'@'${localAddress}' IDENTIFIED BY '${projectDBPass}';
GRANT ALL PRIVILEGES ON ${projectName}.* TO '${projectDBUser}'@'%'; GRANT ALL PRIVILEGES ON ${projectName}.* TO '${projectDBUser}'@'${localAddress}';
CREATE DATABASE IF NOT EXISTS ${newsName}; CREATE DATABASE IF NOT EXISTS ${newsName};
CREATE USER IF NOT EXISTS '${newsDBUser}'@'%' IDENTIFIED BY '${newsDBPass}'; CREATE USER IF NOT EXISTS '${newsDBUser}'@'${localAddress}' IDENTIFIED BY '${newsDBPass}';
GRANT ALL PRIVILEGES ON ${newsName}.* TO '${newsDBUser}'@'%'; GRANT ALL PRIVILEGES ON ${newsName}.* TO '${newsDBUser}'@'${localAddress}';
CREATE DATABASE IF NOT EXISTS ${authName}; CREATE DATABASE IF NOT EXISTS ${authName};
CREATE USER IF NOT EXISTS '${authDBUser}'@'%' IDENTIFIED BY '${authDBPass}'; CREATE USER IF NOT EXISTS '${authDBUser}'@'${localAddress}' IDENTIFIED BY '${authDBPass}';
GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'%'; GRANT ALL PRIVILEGES ON ${authName}.* TO '${authDBUser}'@'${localAddress}';
CREATE DATABASE IF NOT EXISTS ${chatName}; CREATE DATABASE IF NOT EXISTS ${chatName};
CREATE USER IF NOT EXISTS '${chatDBUser}'@'%' IDENTIFIED BY '${chatDBPass}'; CREATE USER IF NOT EXISTS '${chatDBUser}'@'${localAddress}' IDENTIFIED BY '${chatDBPass}';
GRANT ALL PRIVILEGES ON ${chatName}.* TO '${chatDBUser}'@'%'; GRANT ALL PRIVILEGES ON ${chatName}.* TO '${chatDBUser}'@'${localAddress}';
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
`; `;
fs.writeFileSync('docker-compose.yml', ymlfile); fs.writeFileSync('docker-compose.yml', ymlfile);
fs.writeFileSync('Dockerfile', dockerfile); fs.writeFileSync('Dockerfile', dockerfile);
fs.writeFileSync('startup.sql', sqlfile); fs.writeFileSync('startup.sql', sqlfile);
+4934 -16432
View File
File diff suppressed because it is too large Load Diff
+56 -61
View File
@@ -1,61 +1,56 @@
{ {
"name": "mern-template", "name": "mern-template",
"version": "1.0.2", "version": "1.5.5",
"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:dev:server npm:dev:client",
"watch:server": "nodemon ./* --ext js,jsx,json --ignore 'node_modules/*'", "dev:server": "nodemon --ext js,jsx,json --ignore 'node_modules/*'",
"watch:client": "webpack serve --env=development --config webpack.config.js", "dev:client": "webpack serve --config webpack.config.js",
"analyzer": "webpack --env=production --analyzer --config webpack.config.js" "analyze": "webpack --env=production --env=analyze --config webpack.config.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/KRGameStudios/MERN-template.git" "url": "git+https://github.com/KRGameStudios/MERN-template.git"
}, },
"author": "Kayne Ruse", "author": "Kayne Ruse",
"license": "ISC", "license": "Zlib",
"bugs": { "bugs": {
"url": "https://github.com/KRGameStudios/MERN-template/issues" "url": "https://github.com/KRGameStudios/MERN-template/issues"
}, },
"homepage": "https://github.com/KRGameStudios/MERN-template#readme", "homepage": "https://github.com/KRGameStudios/MERN-template#readme",
"dependencies": { "dependencies": {
"@babel/core": "^7.14.8", "@babel/core": "^7.28.5",
"@babel/preset-env": "^7.14.8", "@babel/preset-env": "^7.28.5",
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.28.5",
"@loadable/component": "^5.15.0", "babel-loader": "^10.0.0",
"babel-loader": "^8.2.2", "clean-webpack-plugin": "^4.0.0",
"clean-webpack-plugin": "^3.0.0", "compression-webpack-plugin": "^11.1.0",
"concurrently": "^6.2.0", "concurrently": "^9.2.1",
"css-loader": "^6.2.0", "css-loader": "^7.1.2",
"dateformat": "^4.5.1", "dateformat": "^5.0.3",
"dotenv": "^10.0.0", "dotenv": "^17.2.3",
"express": "^4.17.1", "express": "^5.2.1",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.6.5",
"jwt-decode": "^3.1.2", "jwt-decode": "^4.0.0",
"mariadb": "^2.5.4", "mariadb": "^3.4.5",
"query-string": "^7.0.1", "react": "^19.2.1",
"raw-loader": "^4.0.2", "react-dom": "^19.2.1",
"react": "^17.0.2", "react-router": "^7.10.1",
"react-dom": "^17.0.2", "react-select": "^5.10.2",
"react-dropdown-select": "^4.7.4", "sequelize": "^6.37.7",
"react-markdown": "^6.0.2", "socket.io-client": "^4.8.1",
"react-router": "^5.2.0", "style-loader": "^4.0.0",
"react-router-dom": "^5.2.0", "webpack": "^5.103.0",
"rehype-raw": "^5.1.0", "webpack-bundle-analyzer": "^5.1.0",
"sequelize": "^6.6.5", "webpack-cli": "^6.0.1"
"socket.io-client": "^4.1.3", },
"style-loader": "^3.2.1", "devDependencies": {
"webpack": "^5.46.0", "nodemon": "^3.1.11",
"webpack-bundle-analyzer": "^4.4.2", "webpack-dev-server": "^5.2.2"
"webpack-cli": "^4.7.2" }
}, }
"devDependencies": {
"nodemon": "^2.0.12",
"webpack-dev-server": "^3.11.2"
}
}
+2 -3
View File
@@ -2,11 +2,10 @@ const Sequelize = require('sequelize');
const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME, process.env.DB_PASSWORD, { const sequelize = new Sequelize(process.env.DB_DATABASE, process.env.DB_USERNAME, process.env.DB_PASSWORD, {
host: process.env.DB_HOSTNAME, host: process.env.DB_HOSTNAME,
port: process.env.DB_PORTNAME,
dialect: 'mariadb', dialect: 'mariadb',
timezone: process.env.DB_TIMEZONE, timezone: process.env.DB_TIMEZONE,
logging: process.env.DB_LOGGING ? console.log : false logging: process.env.DB_LOGGING ? console.log : false
}); });
sequelize.sync(); module.exports = sequelize;
module.exports = sequelize;
+17 -1
View File
@@ -12,6 +12,21 @@ const server = require('http').Server(app);
//config //config
app.use(express.json()); app.use(express.json());
//handle compressed files (middleware)
app.get('/{*any}.js', (req, res, next) => {
req.url = req.url + '.gz';
res.set('Content-Encoding', 'gzip');
res.set('Content-Type', 'text/javascript');
next();
});
app.get('/{*any}.css', (req, res, next) => {
req.url = req.url + '.gz';
res.set('Content-Encoding', 'gzip');
res.set('Content-Type', 'text/css');
next();
});
//database connection //database connection
const database = require('./database'); const database = require('./database');
@@ -19,7 +34,7 @@ const database = require('./database');
app.use('/', express.static(path.resolve(__dirname, '..', 'public'))); app.use('/', express.static(path.resolve(__dirname, '..', 'public')));
//fallback to the index file //fallback to the index file
app.get('*', (req, res) => { app.get('/{*any}', (req, res) => {
res.sendFile(path.resolve(__dirname, '..', 'public' , 'index.html')); res.sendFile(path.resolve(__dirname, '..', 'public' , 'index.html'));
}); });
@@ -27,4 +42,5 @@ app.get('*', (req, res) => {
server.listen(process.env.WEB_PORT || 3000, async (err) => { server.listen(process.env.WEB_PORT || 3000, async (err) => {
await database.sync(); await database.sync();
console.log(`listening to localhost:${process.env.WEB_PORT || 3000}`); console.log(`listening to localhost:${process.env.WEB_PORT || 3000}`);
console.log(`database located at ${process.env.DB_HOSTNAME || '<default>'}:${process.env.DB_PORTNAME || '<default>'}`);
}); });
-10
View File
@@ -1,10 +0,0 @@
#This file only needs to be run once, during initial development setup
#This file isnt needed for actual deployment
#Create the development database
CREATE DATABASE IF NOT EXISTS template;
USE template;
#Create the database user
CREATE USER IF NOT EXISTS 'template'@'%' IDENTIFIED BY 'pikachu';
GRANT ALL PRIVILEGES ON template.* TO 'template'@'%';
+5
View File
@@ -0,0 +1,5 @@
#use this while debugging
CREATE DATABASE template;
CREATE USER 'template'@'%' IDENTIFIED BY 'pikachu';
GRANT ALL PRIVILEGES ON template.* TO 'template'@'%';
+39 -50
View File
@@ -2,23 +2,24 @@
const { DefinePlugin } = require('webpack'); const { DefinePlugin } = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
//libraries //libraries
const path = require('path'); const path = require('path');
//the exported config function //the exported config function
module.exports = ({ production, analyzer }) => { module.exports = ({ production, analyze }) => {
return { return {
mode: production ? "production" : "development", mode: production ? "production" : "development",
entry: path.resolve(__dirname, 'client', 'client.jsx'), entry: path.resolve(__dirname, 'client', 'client.jsx'),
output: { output: {
path: path.resolve(__dirname, 'public'), path: path.resolve(__dirname, 'public'),
publicPath: '/',
filename: '[name].[chunkhash].js', filename: '[name].[chunkhash].js',
sourceMapFilename: '[name].[chunkhash].js.map' sourceMapFilename: '[name].[chunkhash].js.map'
}, },
devtool: production ? 'source-map' : 'eval-source-map', devtool: production ? false : 'eval-source-map',
resolve: { resolve: {
extensions: ['.js', '.jsx'] extensions: ['.js', '.jsx']
}, },
@@ -32,7 +33,6 @@ 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: ['@babel/plugin-syntax-dynamic-import']
} }
} }
] ]
@@ -41,23 +41,15 @@ module.exports = ({ production, analyzer }) => {
test: /\.(css)$/, test: /\.(css)$/,
use: ['style-loader', 'css-loader'] use: ['style-loader', 'css-loader']
}, },
{
test: /\.(md)$/,
use: [
{
loader: 'raw-loader'
},
],
},
] ]
}, },
plugins: [ plugins: [
new DefinePlugin({ new DefinePlugin({
'process.env': { 'process.env': {
'PRODUCTION': production, 'PRODUCTION': production,
'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"https://dev-news.krgamestudios.com"', 'NEWS_URI': production ? `"${process.env.NEWS_URI}"` : '"http://localhost:3100"',
'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"https://dev-auth.krgamestudios.com"', 'AUTH_URI': production ? `"${process.env.AUTH_URI}"` : '"http://localhost:3200"',
'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"https://dev-chat.krgamestudios.com"', 'CHAT_URI': production ? `"${process.env.CHAT_URI}"` : '"http://localhost:3300"',
} }
}), }),
new CleanWebpackPlugin({ new CleanWebpackPlugin({
@@ -71,46 +63,43 @@ module.exports = ({ production, analyzer }) => {
removeAttributeQuotes: production removeAttributeQuotes: production
} }
}), }),
new CompressionPlugin({
filename: "[path][base].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$/,
minRatio: 0.8
}),
new BundleAnalyzerPlugin({ new BundleAnalyzerPlugin({
analyzerMode: analyzer ? 'server' : 'disabled' analyzerMode: analyze ? 'server' : 'disabled'
}) })
], ],
devServer: { devServer: {
contentBase: path.resolve(__dirname, 'public'),
compress: true,
port: 3001,
proxy: {
'/api/': 'http://localhost:3000/'
},
overlay: {
errors: true
},
stats: {
colors: true,
hash: false,
version: false,
timings: false,
assets: false,
chunks: false,
modules: false,
reasons: false,
children: false,
source: false,
errors: true,
errorDetails: false,
warnings: true,
publicPath: false
},
host: '0.0.0.0',
disableHostCheck: true,
clientLogLevel: 'silent',
historyApiFallback: true,
hot: true, hot: true,
injectHot: true host: 'localhost',
}, port: 3001,
watchOptions: { client: {
ignored: /(node_modules)/ overlay: {
errors: true,
warnings: true,
},
},
watchFiles: {
options: {
ignored: ['node_modules/**']
}
},
proxy: [
{
context: ['/api'],
target: 'http://localhost:3000',
}
],
static: '/public',
historyApiFallback: true
} }
} }
}; };