From 52e850146f8ac1fe684d06a06bd72bee0d41b0c2 Mon Sep 17 00:00:00 2001 From: fcaps <48691346+fcaps@users.noreply.github.com> Date: Fri, 1 Dec 2023 21:48:53 +0100 Subject: [PATCH] init m2m client (#504) AppKernel, ServiceContainer, eslinter fixes, m2mclient, report player fixes and some minor issues --- .editorconfig | 2 +- .env.example | 2 + .env.faf-stack | 3 +- Dockerfile | 2 +- ExpressApp.js | 4 - README.md | 6 +- express.js | 9 - fafApp.js | 144 ---------- grunt/concurrent.js | 14 +- grunt/nodemon.js | 27 +- grunt/run.js | 4 +- grunt/sass.js | 40 +-- grunt/watch.js | 10 +- lib/JavaApiClientFactory.js | 69 ----- lib/LoggedInUserService.js | 15 - package.json | 5 +- public/styles/awesomplete.css | 104 +++++++ routes/middleware.js | 77 ------ routes/views/account/get/activate.js | 18 -- routes/views/account/get/changeEmail.js | 14 - routes/views/account/get/changePassword.js | 14 - routes/views/account/get/changeUsername.js | 14 - routes/views/account/get/checkUsername.js | 19 -- .../views/account/get/confirmPasswordReset.js | 18 -- routes/views/account/get/connectSteam.js | 45 --- routes/views/account/get/createAccount.js | 27 -- routes/views/account/get/linkGog.js | 50 ---- routes/views/account/get/linkSteam.js | 33 --- routes/views/account/get/register.js | 16 -- routes/views/account/get/report.js | 107 ------- .../views/account/get/requestPasswordReset.js | 34 --- routes/views/account/get/resync.js | 34 --- routes/views/account/post/activate.js | 55 ---- routes/views/account/post/changeEmail.js | 58 ---- routes/views/account/post/changePassword.js | 63 ----- routes/views/account/post/changeUsername.js | 54 ---- .../account/post/confirmPasswordReset.js | 56 ---- routes/views/account/post/error.js | 21 -- routes/views/account/post/linkGog.js | 75 ----- routes/views/account/post/register.js | 90 ------ routes/views/account/post/report.js | 212 -------------- .../account/post/requestPasswordReset.js | 56 ---- routes/views/checkUsername.js | 19 -- routes/views/clans/get/accept_invite.js | 95 ------- routes/views/clans/get/create.js | 52 ---- routes/views/clans/get/manage.js | 134 --------- routes/views/clans/post/create.js | 130 --------- routes/views/clans/post/destroy.js | 111 -------- routes/views/clans/post/invite.js | 156 ----------- routes/views/clans/post/join.js | 87 ------ routes/views/clans/post/kick.js | 96 ------- routes/views/clans/post/leave.js | 111 -------- routes/views/clans/post/transfer.js | 159 ----------- routes/views/clans/post/update.js | 156 ----------- routes/views/dataRouter.js | 38 --- routes/views/leaderboardRouter.js | 61 ---- routes/views/news.js | 53 ---- routes/views/staticMarkdownRouter.js | 23 -- scripts/cron-jobs.js | 27 -- src/backend/AppKernel.js | 143 ++++++++++ src/backend/ExpressApp.js | 4 + {config => src/backend/config}/app.js | 9 +- .../cron-jobs/leaderboardCacheCrawler.js | 45 +++ .../cron-jobs/wordpressCacheCrawler.js | 50 ++++ .../dependency-injection/AppContainer.js | 54 ++++ .../dependency-injection/RequestContainer.js | 19 ++ .../RequestContainerCompilerPass.js | 24 ++ src/backend/index.js | 11 + src/backend/middleware/webpackAsset.js | 12 + src/backend/routes/middleware.js | 17 ++ .../routes/views/account/get/activate.js | 16 ++ .../routes/views/account/get/changeEmail.js | 12 + .../views/account/get/changePassword.js | 12 + .../views/account/get/changeUsername.js | 12 + .../routes/views/account/get/checkUsername.js | 19 ++ .../views/account/get/confirmPasswordReset.js | 16 ++ .../routes/views/account/get/connectSteam.js | 50 ++++ .../routes/views/account/get/createAccount.js | 24 ++ .../routes/views/account/get/linkGog.js | 45 +++ .../routes/views/account/get/linkSteam.js | 32 +++ .../routes/views/account/get/register.js | 14 + .../routes/views/account/get/report.js | 99 +++++++ .../views/account/get/requestPasswordReset.js | 35 +++ .../routes/views/account/get/resync.js | 32 +++ .../routes/views/account/post/activate.js | 53 ++++ .../routes/views/account/post/changeEmail.js | 52 ++++ .../views/account/post/changePassword.js | 58 ++++ .../views/account/post/changeUsername.js | 50 ++++ .../account/post/confirmPasswordReset.js | 53 ++++ .../routes/views/account/post/error.js | 21 ++ .../routes/views/account/post/linkGog.js | 69 +++++ .../routes/views/account/post/register.js | 77 ++++++ .../routes/views/account/post/report.js | 126 +++++++++ .../account/post/requestPasswordReset.js | 54 ++++ .../backend/routes}/views/accountRouter.js | 6 +- {routes => src/backend/routes}/views/auth.js | 8 +- src/backend/routes/views/checkUsername.js | 19 ++ .../backend/routes}/views/clanRouter.js | 4 +- .../routes/views/clans/get/accept_invite.js | 92 +++++++ src/backend/routes/views/clans/get/create.js | 53 ++++ src/backend/routes/views/clans/get/manage.js | 129 +++++++++ src/backend/routes/views/clans/post/create.js | 96 +++++++ .../routes/views/clans/post/destroy.js | 96 +++++++ src/backend/routes/views/clans/post/invite.js | 143 ++++++++++ src/backend/routes/views/clans/post/join.js | 93 +++++++ src/backend/routes/views/clans/post/kick.js | 76 +++++ src/backend/routes/views/clans/post/leave.js | 93 +++++++ .../routes/views/clans/post/transfer.js | 153 +++++++++++ src/backend/routes/views/clans/post/update.js | 145 ++++++++++ src/backend/routes/views/dataRouter.js | 46 ++++ .../backend/routes}/views/defaultRouter.js | 4 +- src/backend/routes/views/leaderboardRouter.js | 48 ++++ src/backend/routes/views/news.js | 49 ++++ .../routes/views/staticMarkdownRouter.js | 23 ++ src/backend/security/bootPassport.js | 45 +++ {lib => src/backend/services}/ApiErrors.js | 0 {lib => src/backend/services}/CacheService.js | 4 +- src/backend/services/JavaApiClientFactory.js | 72 +++++ src/backend/services/JavaApiM2MClient.js | 51 ++++ .../services}/LeaderboardRepository.js | 26 +- .../backend/services}/LeaderboardService.js | 15 +- {lib => src/backend/services}/MutexService.js | 36 +-- {lib => src/backend/services}/Scheduler.js | 10 +- .../backend/services}/UserRepository.js | 2 +- src/backend/services/UserService.js | 26 ++ .../backend/services}/WordpressRepository.js | 65 ++--- .../backend/services}/WordpressService.js | 42 +-- .../services}/WordpressServiceFactory.js | 8 +- .../backend/templates}/layouts/default.pug | 0 src/backend/templates/mixins/flash-error.pug | 7 + .../templates}/mixins/flash-messages.pug | 2 +- .../templates}/mixins/form/account.pug | 0 .../templates}/views/account/activate.pug | 0 .../templates}/views/account/changeEmail.pug | 0 .../views/account/changePassword.pug | 0 .../views/account/changeUsername.pug | 0 .../views/account/confirmPasswordReset.pug | 0 .../views/account/confirmResyncAccount.pug | 0 .../views/account/createAccount.pug | 0 .../templates}/views/account/linkGog.pug | 0 .../templates}/views/account/linkSteam.pug | 0 .../templates}/views/account/register.pug | 0 .../templates}/views/account/report.pug | 45 +-- .../views/account/requestPasswordReset.pug | 0 .../backend/templates}/views/ai.pug | 0 .../templates}/views/campaign-missions.pug | 0 .../backend/templates}/views/clans.pug | 0 .../templates}/views/clans/accept_invite.pug | 0 .../backend/templates}/views/clans/create.pug | 0 .../backend/templates}/views/clans/manage.pug | 0 .../templates}/views/clans/seeClan.pug | 0 .../templates}/views/competitive_nav.pug | 0 .../templates}/views/content-creators.pug | 0 .../backend/templates}/views/contribution.pug | 0 .../backend/templates}/views/donation.pug | 0 .../backend/templates}/views/errors/404.pug | 0 .../backend/templates}/views/errors/500.pug | 0 .../views/errors/503-known-issue.pug | 0 .../backend/templates}/views/faf-teams.pug | 0 .../backend/templates}/views/index.pug | 0 .../backend/templates}/views/leaderboards.pug | 0 .../backend/templates}/views/markdown.pug | 0 .../backend/templates}/views/markdown/cg.md | 0 .../templates}/views/markdown/privacy-fr.md | 0 .../templates}/views/markdown/privacy-ru.md | 0 .../templates}/views/markdown/privacy.de.md | 0 .../templates}/views/markdown/privacy.md | 0 .../templates}/views/markdown/rules.md | 0 .../templates}/views/markdown/tos-fr.md | 0 .../templates}/views/markdown/tos-ru.md | 0 .../backend/templates}/views/markdown/tos.md | 0 .../backend/templates}/views/news.pug | 0 .../backend/templates}/views/newsArticle.pug | 0 .../backend/templates}/views/newshub.pug | 0 .../backend/templates}/views/play.pug | 0 .../backend/templates}/views/scfa-vs-faf.pug | 0 .../templates}/views/tutorials-guides.pug | 0 src/frontend/js/entrypoint/leaderboards.js | 5 +- src/frontend/js/entrypoint/report.js | 58 ++-- tests/JavaApiClient.test.js | 31 ++- tests/LeaderboardService.test.js | 4 +- tests/MutexService.test.js | 2 +- .../IsAuthenticatedMiddleware.test.js | 16 +- tests/integration/NewsRouter.test.js | 15 +- tests/integration/accountRouter.test.js | 13 +- tests/integration/asyncErrorHandler.test.js | 2 +- tests/integration/clanRouter.test.js | 11 +- tests/integration/defaultRouter.test.js | 14 +- tests/integration/leaderboardRouter.test.js | 23 +- tests/integration/markdownRouter.test.js | 14 +- tests/integration/servicesMiddleware.test.js | 60 ---- tests/setup.js | 19 +- webpack.config.js | 40 +-- yarn.lock | 260 +++++++++++++++++- 194 files changed, 3644 insertions(+), 3422 deletions(-) delete mode 100644 ExpressApp.js delete mode 100644 express.js delete mode 100644 fafApp.js delete mode 100644 lib/JavaApiClientFactory.js delete mode 100644 lib/LoggedInUserService.js create mode 100644 public/styles/awesomplete.css delete mode 100755 routes/middleware.js delete mode 100644 routes/views/account/get/activate.js delete mode 100644 routes/views/account/get/changeEmail.js delete mode 100644 routes/views/account/get/changePassword.js delete mode 100644 routes/views/account/get/changeUsername.js delete mode 100644 routes/views/account/get/checkUsername.js delete mode 100644 routes/views/account/get/confirmPasswordReset.js delete mode 100644 routes/views/account/get/connectSteam.js delete mode 100644 routes/views/account/get/createAccount.js delete mode 100644 routes/views/account/get/linkGog.js delete mode 100644 routes/views/account/get/linkSteam.js delete mode 100644 routes/views/account/get/register.js delete mode 100644 routes/views/account/get/report.js delete mode 100644 routes/views/account/get/requestPasswordReset.js delete mode 100644 routes/views/account/get/resync.js delete mode 100644 routes/views/account/post/activate.js delete mode 100644 routes/views/account/post/changeEmail.js delete mode 100644 routes/views/account/post/changePassword.js delete mode 100644 routes/views/account/post/changeUsername.js delete mode 100644 routes/views/account/post/confirmPasswordReset.js delete mode 100644 routes/views/account/post/error.js delete mode 100644 routes/views/account/post/linkGog.js delete mode 100644 routes/views/account/post/register.js delete mode 100644 routes/views/account/post/report.js delete mode 100644 routes/views/account/post/requestPasswordReset.js delete mode 100644 routes/views/checkUsername.js delete mode 100644 routes/views/clans/get/accept_invite.js delete mode 100644 routes/views/clans/get/create.js delete mode 100755 routes/views/clans/get/manage.js delete mode 100755 routes/views/clans/post/create.js delete mode 100755 routes/views/clans/post/destroy.js delete mode 100644 routes/views/clans/post/invite.js delete mode 100644 routes/views/clans/post/join.js delete mode 100755 routes/views/clans/post/kick.js delete mode 100755 routes/views/clans/post/leave.js delete mode 100755 routes/views/clans/post/transfer.js delete mode 100644 routes/views/clans/post/update.js delete mode 100644 routes/views/dataRouter.js delete mode 100644 routes/views/leaderboardRouter.js delete mode 100644 routes/views/news.js delete mode 100644 routes/views/staticMarkdownRouter.js delete mode 100644 scripts/cron-jobs.js create mode 100644 src/backend/AppKernel.js create mode 100644 src/backend/ExpressApp.js rename {config => src/backend/config}/app.js (78%) create mode 100644 src/backend/cron-jobs/leaderboardCacheCrawler.js create mode 100644 src/backend/cron-jobs/wordpressCacheCrawler.js create mode 100644 src/backend/dependency-injection/AppContainer.js create mode 100644 src/backend/dependency-injection/RequestContainer.js create mode 100644 src/backend/dependency-injection/RequestContainerCompilerPass.js create mode 100644 src/backend/index.js create mode 100644 src/backend/middleware/webpackAsset.js create mode 100755 src/backend/routes/middleware.js create mode 100644 src/backend/routes/views/account/get/activate.js create mode 100644 src/backend/routes/views/account/get/changeEmail.js create mode 100644 src/backend/routes/views/account/get/changePassword.js create mode 100644 src/backend/routes/views/account/get/changeUsername.js create mode 100644 src/backend/routes/views/account/get/checkUsername.js create mode 100644 src/backend/routes/views/account/get/confirmPasswordReset.js create mode 100644 src/backend/routes/views/account/get/connectSteam.js create mode 100644 src/backend/routes/views/account/get/createAccount.js create mode 100644 src/backend/routes/views/account/get/linkGog.js create mode 100644 src/backend/routes/views/account/get/linkSteam.js create mode 100644 src/backend/routes/views/account/get/register.js create mode 100644 src/backend/routes/views/account/get/report.js create mode 100644 src/backend/routes/views/account/get/requestPasswordReset.js create mode 100644 src/backend/routes/views/account/get/resync.js create mode 100644 src/backend/routes/views/account/post/activate.js create mode 100644 src/backend/routes/views/account/post/changeEmail.js create mode 100644 src/backend/routes/views/account/post/changePassword.js create mode 100644 src/backend/routes/views/account/post/changeUsername.js create mode 100644 src/backend/routes/views/account/post/confirmPasswordReset.js create mode 100644 src/backend/routes/views/account/post/error.js create mode 100644 src/backend/routes/views/account/post/linkGog.js create mode 100644 src/backend/routes/views/account/post/register.js create mode 100644 src/backend/routes/views/account/post/report.js create mode 100644 src/backend/routes/views/account/post/requestPasswordReset.js rename {routes => src/backend/routes}/views/accountRouter.js (95%) rename {routes => src/backend/routes}/views/auth.js (81%) create mode 100644 src/backend/routes/views/checkUsername.js rename {routes => src/backend/routes}/views/clanRouter.js (54%) create mode 100644 src/backend/routes/views/clans/get/accept_invite.js create mode 100644 src/backend/routes/views/clans/get/create.js create mode 100755 src/backend/routes/views/clans/get/manage.js create mode 100755 src/backend/routes/views/clans/post/create.js create mode 100755 src/backend/routes/views/clans/post/destroy.js create mode 100644 src/backend/routes/views/clans/post/invite.js create mode 100644 src/backend/routes/views/clans/post/join.js create mode 100755 src/backend/routes/views/clans/post/kick.js create mode 100755 src/backend/routes/views/clans/post/leave.js create mode 100755 src/backend/routes/views/clans/post/transfer.js create mode 100644 src/backend/routes/views/clans/post/update.js create mode 100644 src/backend/routes/views/dataRouter.js rename {routes => src/backend/routes}/views/defaultRouter.js (92%) create mode 100644 src/backend/routes/views/leaderboardRouter.js create mode 100644 src/backend/routes/views/news.js create mode 100644 src/backend/routes/views/staticMarkdownRouter.js create mode 100644 src/backend/security/bootPassport.js rename {lib => src/backend/services}/ApiErrors.js (100%) rename {lib => src/backend/services}/CacheService.js (68%) create mode 100644 src/backend/services/JavaApiClientFactory.js create mode 100644 src/backend/services/JavaApiM2MClient.js rename {lib => src/backend/services}/LeaderboardRepository.js (69%) rename {lib => src/backend/services}/LeaderboardService.js (67%) rename {lib => src/backend/services}/MutexService.js (68%) rename {lib => src/backend/services}/Scheduler.js (64%) rename {lib => src/backend/services}/UserRepository.js (94%) create mode 100644 src/backend/services/UserService.js rename {lib => src/backend/services}/WordpressRepository.js (70%) rename {lib => src/backend/services}/WordpressService.js (84%) rename {lib => src/backend/services}/WordpressServiceFactory.js (64%) rename {templates => src/backend/templates}/layouts/default.pug (100%) create mode 100644 src/backend/templates/mixins/flash-error.pug rename {templates => src/backend/templates}/mixins/flash-messages.pug (85%) rename {templates => src/backend/templates}/mixins/form/account.pug (100%) rename {templates => src/backend/templates}/views/account/activate.pug (100%) rename {templates => src/backend/templates}/views/account/changeEmail.pug (100%) rename {templates => src/backend/templates}/views/account/changePassword.pug (100%) rename {templates => src/backend/templates}/views/account/changeUsername.pug (100%) rename {templates => src/backend/templates}/views/account/confirmPasswordReset.pug (100%) rename {templates => src/backend/templates}/views/account/confirmResyncAccount.pug (100%) rename {templates => src/backend/templates}/views/account/createAccount.pug (100%) rename {templates => src/backend/templates}/views/account/linkGog.pug (100%) rename {templates => src/backend/templates}/views/account/linkSteam.pug (100%) rename {templates => src/backend/templates}/views/account/register.pug (100%) rename {templates => src/backend/templates}/views/account/report.pug (86%) rename {templates => src/backend/templates}/views/account/requestPasswordReset.pug (100%) rename {templates => src/backend/templates}/views/ai.pug (100%) rename {templates => src/backend/templates}/views/campaign-missions.pug (100%) rename {templates => src/backend/templates}/views/clans.pug (100%) rename {templates => src/backend/templates}/views/clans/accept_invite.pug (100%) rename {templates => src/backend/templates}/views/clans/create.pug (100%) rename {templates => src/backend/templates}/views/clans/manage.pug (100%) rename {templates => src/backend/templates}/views/clans/seeClan.pug (100%) rename {templates => src/backend/templates}/views/competitive_nav.pug (100%) rename {templates => src/backend/templates}/views/content-creators.pug (100%) rename {templates => src/backend/templates}/views/contribution.pug (100%) rename {templates => src/backend/templates}/views/donation.pug (100%) rename {templates => src/backend/templates}/views/errors/404.pug (100%) rename {templates => src/backend/templates}/views/errors/500.pug (100%) rename {templates => src/backend/templates}/views/errors/503-known-issue.pug (100%) rename {templates => src/backend/templates}/views/faf-teams.pug (100%) rename {templates => src/backend/templates}/views/index.pug (100%) rename {templates => src/backend/templates}/views/leaderboards.pug (100%) rename {templates => src/backend/templates}/views/markdown.pug (100%) rename {templates => src/backend/templates}/views/markdown/cg.md (100%) rename {templates => src/backend/templates}/views/markdown/privacy-fr.md (100%) rename {templates => src/backend/templates}/views/markdown/privacy-ru.md (100%) rename {templates => src/backend/templates}/views/markdown/privacy.de.md (100%) rename {templates => src/backend/templates}/views/markdown/privacy.md (100%) rename {templates => src/backend/templates}/views/markdown/rules.md (100%) rename {templates => src/backend/templates}/views/markdown/tos-fr.md (100%) rename {templates => src/backend/templates}/views/markdown/tos-ru.md (100%) rename {templates => src/backend/templates}/views/markdown/tos.md (100%) rename {templates => src/backend/templates}/views/news.pug (100%) rename {templates => src/backend/templates}/views/newsArticle.pug (100%) rename {templates => src/backend/templates}/views/newshub.pug (100%) rename {templates => src/backend/templates}/views/play.pug (100%) rename {templates => src/backend/templates}/views/scfa-vs-faf.pug (100%) rename {templates => src/backend/templates}/views/tutorials-guides.pug (100%) delete mode 100644 tests/integration/servicesMiddleware.test.js diff --git a/.editorconfig b/.editorconfig index 78b66ce6..a60e6e87 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true [*] end_of_line = lf charset = utf-8 -trim_trailing_whitespace = false +trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 4 diff --git a/.env.example b/.env.example index e41373f1..cd233a8e 100755 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ API_URL=https://api.test.faforever.com OAUTH_URL=https://hydra.test.faforever.com WP_URL=https://direct.faforever.com CALLBACK=auth +OAUTH_M2M_CLIENT_ID=faf-website-public +OAUTH_M2M_CLIENT_SECRET=banana OAUTH_CLIENT_ID=faf-website OAUTH_CLIENT_SECRET=banana SESSION_SECRET_KEY=banana diff --git a/.env.faf-stack b/.env.faf-stack index 39514b35..d1aa4d2c 100644 --- a/.env.faf-stack +++ b/.env.faf-stack @@ -20,7 +20,8 @@ OAUTH_PUBLIC_URL=http://localhost:4444 # unsing the "xyz" wordpress because the faf-local-stack is just an empty instance without any news etc. WP_URL=https://direct.faforever.xyz - +OAUTH_M2M_CLIENT_ID=faf-website-public +OAUTH_M2M_CLIENT_SECRET=banana OAUTH_CLIENT_ID=faf-website OAUTH_CLIENT_SECRET=banana SESSION_SECRET_KEY=banana diff --git a/Dockerfile b/Dockerfile index 414bfcb1..dd779936 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,5 +19,5 @@ COPY --from=builder --chown=node:node /code /code WORKDIR /code USER node -CMD ["dumb-init", "node", "express.js"] +CMD ["dumb-init", "node", "src/backend/index.js"] diff --git a/ExpressApp.js b/ExpressApp.js deleted file mode 100644 index a05ccb72..00000000 --- a/ExpressApp.js +++ /dev/null @@ -1,4 +0,0 @@ -const express = require('express'); -require('express-async-errors'); - -module.exports = express diff --git a/README.md b/README.md index 813ba768..49d5f6e7 100755 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ Development-Container: `````bash cd ../website # replace path if needed cp -n .env.faf-stack .env -docker compose build +docker compose build docker compose run website yarn install -docker compose up +docker compose up ````` this should start the express-server on http://localhost:8020/. @@ -53,5 +53,3 @@ As of March 2022 the main 4 Languages that are set up on POEditor are: - Russian - French - German - - diff --git a/express.js b/express.js deleted file mode 100644 index cc469501..00000000 --- a/express.js +++ /dev/null @@ -1,9 +0,0 @@ -const fafApp = require('./fafApp') -const express = require('./ExpressApp') -const app = express() - -fafApp.setup(app) -fafApp.loadRouters(app) -fafApp.setupCronJobs() - -fafApp.startServer(app) diff --git a/fafApp.js b/fafApp.js deleted file mode 100644 index 07e1d512..00000000 --- a/fafApp.js +++ /dev/null @@ -1,144 +0,0 @@ -const appConfig = require('./config/app') -const express = require('./ExpressApp') -const bodyParser = require('body-parser') -const session = require('express-session') -const FileStore = require('session-file-store')(session) -const passport = require('passport') -const flash = require('connect-flash') -const middleware = require('./routes/middleware') -const defaultRouter = require("./routes/views/defaultRouter") -const authRouter = require("./routes/views/auth") -const staticMarkdownRouter = require("./routes/views/staticMarkdownRouter") -const newsRouter = require("./routes/views/news") -const leaderboardRouter = require("./routes/views/leaderboardRouter") -const clanRouter = require("./routes/views/clanRouter") -const accountRouter = require("./routes/views/accountRouter") -const dataRouter = require('./routes/views/dataRouter'); -const setupCronJobs = require("./scripts/cron-jobs") -const OidcStrategy = require('passport-openidconnect') -const refresh = require('passport-oauth2-refresh') -const {JavaApiClientFactory} = require('./lib/JavaApiClientFactory') -const UserRepository = require('./lib/UserRepository') - -const copyFlashHandler = (req, res, next) => { - res.locals.message = req.flash(); - next(); -} -const notFoundHandler = (req, res) => { - res.status(404).render('errors/404'); -} - -const errorHandler = (err, req, res, next) => { - console.error('[error] Incoming request to"', req.originalUrl, '"failed with error "', err.toString(), '"') - if (res.headersSent) { - return next(err); - } - - res.status(500).render('errors/500'); -} - -const configureAuth = (app) => { - app.use(passport.initialize()) - app.use(passport.session()) - - passport.serializeUser((user, done) => done(null, user)) - passport.deserializeUser((user, done) => done(null, user)) - - const authStrategy = new OidcStrategy({ - issuer: appConfig.oauth.url + '/', - tokenURL: appConfig.oauth.url + '/oauth2/token', - authorizationURL: appConfig.oauth.publicUrl + '/oauth2/auth', - userInfoURL: appConfig.oauth.url + '/userinfo?schema=openid', - clientID: appConfig.oauth.clientId, - clientSecret: appConfig.oauth.clientSecret, - callbackURL: `${appConfig.host}/${appConfig.oauth.callback}`, - scope: ['openid', 'offline', 'public_profile', 'write_account_data'] - }, async function (iss, sub, profile, jwtClaims, token, refreshToken, params, verified) { - const oAuthPassport = { - token, - refreshToken - } - - const apiClient = JavaApiClientFactory(appConfig.apiUrl, oAuthPassport) - const userRepository = new UserRepository(apiClient) - - userRepository.fetchUser(oAuthPassport).then(user => { - verified(null, user) - }).catch(e => { - console.error('[Error] oAuth verify failed with "' + e.toString() + '"') - verified(null, null) - }) - } - ) - - passport.use(appConfig.oauth.strategy, authStrategy) - refresh.use(appConfig.oauth.strategy, authStrategy) -} - -module.exports.setupCronJobs = () => { - setupCronJobs() -} - -module.exports.startServer = (app) => { - app.listen(appConfig.expressPort, () => { - console.log(`Express listening on port ${appConfig.expressPort}`); - }); -} - -module.exports.loadRouters = (app) => { - app.use('/', defaultRouter) - app.use('/', authRouter) - app.use('/', staticMarkdownRouter) - app.use('/news', newsRouter) - app.use('/leaderboards', leaderboardRouter) - app.use('/clans', clanRouter) - app.use('/account', accountRouter) - app.use('/data', dataRouter) - - app.use(notFoundHandler) - app.use(errorHandler) -} - -module.exports.setup = (app) => { - app.locals.clanInvitations = {} - - app.set('views', 'templates/views') - app.set('view engine', 'pug') - app.set('port', appConfig.expressPort) - - app.use(middleware.initLocals) - - app.use(express.static('public', { - immutable: true, - maxAge: 4 * 60 * 60 * 1000 // 4 hours - })) - - app.use('/dist', express.static('dist', { - immutable: true, - maxAge: 4 * 60 * 60 * 1000 // 4 hours, could be longer since we got cache-busting - })) - - app.use(express.json()) - app.use(bodyParser.json()) - app.use(bodyParser.urlencoded({extended: false})) - - app.use(session({ - resave: false, - saveUninitialized: true, - secret: appConfig.session.key, - store: new FileStore({ - retries: 0, - ttl: appConfig.session.tokenLifespan, - secret: appConfig.session.key - }) - })) - - configureAuth(app) - - app.use(middleware.injectServices) - - app.use(flash()) - app.use(middleware.populatePugGlobals) - app.use(middleware.webpackAsset) - app.use(copyFlashHandler) -} diff --git a/grunt/concurrent.js b/grunt/concurrent.js index 223a3613..84a14aed 100644 --- a/grunt/concurrent.js +++ b/grunt/concurrent.js @@ -1,8 +1,8 @@ module.exports = { - dev: { - tasks: [['run:webpack', 'sass:dev','nodemon'], 'watch'], - options: { - logConcurrentOutput: true - } - } -}; + dev: { + tasks: [['run:webpack', 'sass:dev', 'nodemon'], 'watch'], + options: { + logConcurrentOutput: true + } + } +} diff --git a/grunt/nodemon.js b/grunt/nodemon.js index d877a22d..f3f661fe 100644 --- a/grunt/nodemon.js +++ b/grunt/nodemon.js @@ -1,15 +1,16 @@ -//Runs express script and sets default port to 3000 if environment is not set. +// Runs express script and sets default port to 3000 if environment is not set. module.exports = { - debug: { - options: { - delay: 500, - ignore: [ - 'dist/js/*.js', - 'sessions/**', - 'node_modules/**', - 'grunt/**', - 'Gruntfile.js', - ] + debug: { + script: 'src/backend/index.js', + options: { + delay: 500, + ignore: [ + 'dist/js/*.js', + 'sessions/**', + 'node_modules/**', + 'grunt/**', + 'Gruntfile.js' + ] + } } - } -}; +} diff --git a/grunt/run.js b/grunt/run.js index 4f8f5036..b3c1605c 100644 --- a/grunt/run.js +++ b/grunt/run.js @@ -1,5 +1,5 @@ module.exports = { webpack: { - args: ['node_modules/.bin/webpack'], + args: ['node_modules/.bin/webpack'] } -}; +} diff --git a/grunt/sass.js b/grunt/sass.js index 201cc610..8be0d9f5 100644 --- a/grunt/sass.js +++ b/grunt/sass.js @@ -1,24 +1,24 @@ -const sass = require('dart-sass'); +const sass = require('dart-sass') module.exports = { - dev: { - options: { - implementation: sass, - style: 'expanded', - compass: true + dev: { + options: { + implementation: sass, + style: 'expanded', + compass: true + }, + files: { + 'public/styles/css/site.min.css': 'public/styles/site.sass' + } }, - files: { - 'public/styles/css/site.min.css': 'public/styles/site.sass', + dist: { + options: { + implementation: sass, + style: 'compressed', + compass: true + }, + files: { + 'public/styles/css/site.min.css': 'public/styles/site.sass' + } } - }, - dist: { - options: { - implementation: sass, - style: 'compressed', - compass: true - }, - files: { - 'public/styles/css/site.min.css': 'public/styles/site.sass', - } - } -}; +} diff --git a/grunt/watch.js b/grunt/watch.js index 5c53cf8c..653aa63b 100644 --- a/grunt/watch.js +++ b/grunt/watch.js @@ -3,8 +3,8 @@ module.exports = { files: ['src/frontend/**/*.js'], tasks: ['run:webpack'] }, - sass: { - files: ['public/styles/**/*.{scss,sass}'], - tasks: ['sass:dev'] - } -}; + sass: { + files: ['public/styles/**/*.{scss,sass}'], + tasks: ['sass:dev'] + } +} diff --git a/lib/JavaApiClientFactory.js b/lib/JavaApiClientFactory.js deleted file mode 100644 index d732de79..00000000 --- a/lib/JavaApiClientFactory.js +++ /dev/null @@ -1,69 +0,0 @@ -const { Axios } = require('axios') -const refresh = require('passport-oauth2-refresh') -const { AuthFailed } = require('./ApiErrors') -const appConfig = require('../config/app') - -const getRefreshToken = (oAuthPassport) => { - return new Promise((resolve, reject) => { - refresh.requestNewAccessToken(appConfig.oauth.strategy, oAuthPassport.refreshToken, function (err, accessToken, refreshToken) { - if (err || !accessToken || !refreshToken) { - return reject(new AuthFailed('Failed to refresh token')) - } - - return resolve([accessToken, refreshToken]) - }) - }) -} - -module.exports.JavaApiClientFactory = (javaApiBaseURL, oAuthPassport) => { - if (typeof oAuthPassport !== "object") { - throw new Error("oAuthPassport not an object"); - } - - if (typeof oAuthPassport.refreshToken !== "string") { - throw new Error("oAuthPassport.refreshToken not a string") - } - - if (typeof oAuthPassport.token !== "string") { - throw new Error("oAuthPassport.token not a string") - } - - let tokenRefreshRunning = null - const client = new Axios({ - baseURL: javaApiBaseURL - }) - - client.interceptors.request.use( - async config => { - config.headers = { - Authorization: `Bearer ${oAuthPassport.token}` - } - - return config - }) - - client.interceptors.response.use((res) => { - if (!res.config._refreshTokenRequest && res.config && res.status === 401) { - res.config._refreshTokenRequest = true - - if (!tokenRefreshRunning) { - tokenRefreshRunning = getRefreshToken(oAuthPassport) - } - - return tokenRefreshRunning.then(([token, refreshToken]) => { - oAuthPassport.token = token - oAuthPassport.refreshToken = refreshToken - - return client.request(res.config) - }) - } - - if (res.status === 401) { - throw new AuthFailed('Token no longer valid and refresh did not help') - } - - return res - }) - - return client -} diff --git a/lib/LoggedInUserService.js b/lib/LoggedInUserService.js deleted file mode 100644 index 2945d9cb..00000000 --- a/lib/LoggedInUserService.js +++ /dev/null @@ -1,15 +0,0 @@ -class LoggedInUserService { - constructor (userRepository, request) { - this.request = request - - if (typeof this.request.user !== "object") { - throw new Error("request.user not an object"); - } - } - - getUser() { - return this.request.user - } -} - -module.exports = LoggedInUserService diff --git a/package.json b/package.json index cffe9c70..5335ddaa 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "express-validator": "7.0.1", "moment": "^2.29.4", "node-cache": "^5.1.2", + "node-dependency-injection": "^3.1.2", "node-fetch": "^2.6.7", "npm-check": "^6.0.1", "passport": "^0.6.0", @@ -23,6 +24,7 @@ "request": "2.88.2", "session-file-store": "^1.5.0", "showdown": "^2.1.0", + "simple-oauth2": "^5.0.0", "supertest-session": "^5.0.1", "url-slug": "^4.0.1" }, @@ -43,12 +45,12 @@ "grunt-sass": "3.1.0", "highcharts": "^11.2.0", "jest": "^29.7.0", - "jquery": "^3.7.1", "load-grunt-config": "4.0.1", "load-grunt-tasks": "5.1.0", "nock": "^13.3.8", "octokit": "^3.1.2", "supertest": "^6.3.3", + "typescript": "^5.3.2", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", "webpack-manifest-plugin": "^5.0.0" @@ -58,7 +60,6 @@ "yarn": ">=1.22.0" }, "scripts": { - "start": "node express.js", "test": "jest", "lint": "eslint --ignore-path .gitignore src tests", "lint:fix": "eslint --fix --ignore-path .gitignore src tests" diff --git a/public/styles/awesomplete.css b/public/styles/awesomplete.css new file mode 100644 index 00000000..b8954c69 --- /dev/null +++ b/public/styles/awesomplete.css @@ -0,0 +1,104 @@ +.awesomplete [hidden] { + display: none; +} + +.awesomplete .visually-hidden { + position: absolute; + clip: rect(0, 0, 0, 0); +} + +.awesomplete { + display: inline-block; + position: relative; +} + +.awesomplete > input { + display: block; +} + +.awesomplete > ul { + position: absolute; + left: 0; + z-index: 1; + min-width: 100%; + box-sizing: border-box; + list-style: none; + padding: 0; + margin: 0; + background: #fff; +} + +.awesomplete > ul:empty { + display: none; +} + +.awesomplete > ul { + border-radius: .3em; + margin: .2em 0 0; + background: hsla(0,0%,100%,.9); + background: #3f3f3f; + border: 1px solid rgba(0,0,0,.3); + box-shadow: .05em .2em .6em rgba(0,0,0,.2); + text-shadow: none; +} + +@supports (transform: scale(0)) { + .awesomplete > ul { + transition: .3s cubic-bezier(.4,.2,.5,1.4); + transform-origin: 1.43em -.43em; + } + + .awesomplete > ul[hidden], + .awesomplete > ul:empty { + opacity: 0; + transform: scale(0); + display: block; + transition-timing-function: ease; + } +} + + /* Pointer */ + .awesomplete > ul:before { + content: ""; + position: absolute; + top: -.43em; + left: 1em; + width: 0; height: 0; + padding: .4em; + background: white; + border: inherit; + border-right: 0; + border-bottom: 0; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + } + + .awesomplete > ul > li { + position: relative; + padding: .2em .5em; + cursor: pointer; + } + + .awesomplete > ul > li:hover { + background: hsl(200, 40%, 80%); + color: black; + } + + .awesomplete > ul > li[aria-selected="true"] { + background: hsl(205, 40%, 40%); + color: white; + } + + .awesomplete mark { + background: hsl(65, 100%, 50%); + } + + .awesomplete li:hover mark { + background: hsl(68, 100%, 41%); + } + + .awesomplete li[aria-selected="true"] mark { + background: hsl(86, 100%, 21%); + color: inherit; + } +/*# sourceMappingURL=awesomplete.css.map */ diff --git a/routes/middleware.js b/routes/middleware.js deleted file mode 100755 index 8d31807c..00000000 --- a/routes/middleware.js +++ /dev/null @@ -1,77 +0,0 @@ -const WordpressServiceFactory = require("../lib/WordpressServiceFactory"); -const {JavaApiClientFactory} = require('../lib/JavaApiClientFactory') -const LeaderboardService = require('../lib/LeaderboardService') -const LeaderboardRepository = require('../lib/LeaderboardRepository') -const cacheService = require('../lib/CacheService') -const appConfig = require("../config/app"); -const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) -const fs = require('fs'); -const webpackManifestJS = JSON.parse(fs.readFileSync('dist/js/manifest.json', 'utf8')); -const LoggedInUserService = require('../lib/LoggedInUserService') -const UserRepository = require('../lib/UserRepository'); - - -exports.initLocals = function(req, res, next) { - let locals = res.locals; - locals.navLinks = []; - locals.cNavLinks = []; - next(); -}; - -exports.webpackAsset = (req, res, next) => { - res.locals.webpackAssetJS = (asset) => { - if (asset in webpackManifestJS) { - return webpackManifestJS[asset] - } - - throw new Error('[error] middleware::webpackAsset Failed to find asset "' + asset + '"') - } - - next() -} - -exports.populatePugGlobals = function (req, res, next) { - res.locals.appGlobals = { - loggedInUser: null - } - - if (req.isAuthenticated()) { - res.locals.appGlobals.loggedInUser = req.services.userService.getUser() - } - next() -} - -exports.isAuthenticated = (redirectUrlAfterLogin = null, isApiRequest = false) => { - return (req, res, next) => { - if (req.isAuthenticated()) { - return next() - } - - if (req.xhr || req.headers?.accept?.indexOf('json') > -1 || isApiRequest) { - return res.status(401).json({error: 'Unauthorized'}) - } - - if (req.session) { - req.session.returnTo = redirectUrlAfterLogin || req.originalUrl - } - - return res.redirect('/login') - } -} - -exports.injectServices = (req, res, next) => { - req.services = {} - req.services.wordpressService = wordpressService - - if (req.isAuthenticated()) { - try { - req.services.javaApiClient = JavaApiClientFactory(appConfig.apiUrl, req.user.oAuthPassport) - req.services.userService = new LoggedInUserService(new UserRepository(req.services.javaApiClient), req) - req.services.leaderboardService = new LeaderboardService(cacheService, new LeaderboardRepository(req.services.javaApiClient)) - } catch (e) { - req.logout(() => next(e)) - } - } - - next() -} diff --git a/routes/views/account/get/activate.js b/routes/views/account/get/activate.js deleted file mode 100644 index 6ccd58e4..00000000 --- a/routes/views/account/get/activate.js +++ /dev/null @@ -1,18 +0,0 @@ -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - var flash = null; - - // Render the view - locals.username = req.query.username - locals.token = req.query.token - res.render('account/activate', {flash: flash}); - -}; diff --git a/routes/views/account/get/changeEmail.js b/routes/views/account/get/changeEmail.js deleted file mode 100644 index cfc07fcf..00000000 --- a/routes/views/account/get/changeEmail.js +++ /dev/null @@ -1,14 +0,0 @@ -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - // Render the view - res.render('account/changeEmail'); - -}; diff --git a/routes/views/account/get/changePassword.js b/routes/views/account/get/changePassword.js deleted file mode 100644 index 7bdf4d13..00000000 --- a/routes/views/account/get/changePassword.js +++ /dev/null @@ -1,14 +0,0 @@ -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - // Render the view - res.render('account/changePassword'); - -}; diff --git a/routes/views/account/get/changeUsername.js b/routes/views/account/get/changeUsername.js deleted file mode 100644 index 9caa42ed..00000000 --- a/routes/views/account/get/changeUsername.js +++ /dev/null @@ -1,14 +0,0 @@ -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - // Render the view - res.render('account/changeUsername'); - -}; diff --git a/routes/views/account/get/checkUsername.js b/routes/views/account/get/checkUsername.js deleted file mode 100644 index 6f3b1c7e..00000000 --- a/routes/views/account/get/checkUsername.js +++ /dev/null @@ -1,19 +0,0 @@ -const request = require("request"); - -exports = module.exports = function (req, res) { - const name = req.query.username; - - request(process.env.API_URL + "/data/player?filter=login==" + encodeURI(name), function (error, response, body) { - if (error) { - console.error(error); - return res.status(500).send(error); - } - - try { - let userNameFree = JSON.parse(body).data.length === 0; - return res.status(userNameFree ? 200 : 400).send(userNameFree); - } catch (e) { - return res.status(500).send(e); - } - }); -}; diff --git a/routes/views/account/get/confirmPasswordReset.js b/routes/views/account/get/confirmPasswordReset.js deleted file mode 100644 index 98ff4aec..00000000 --- a/routes/views/account/get/confirmPasswordReset.js +++ /dev/null @@ -1,18 +0,0 @@ -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - var flash = null; - - // Render the view - locals.username = req.query.username - locals.token = req.query.token - res.render('account/confirmPasswordReset', {flash: flash}); - -}; diff --git a/routes/views/account/get/connectSteam.js b/routes/views/account/get/connectSteam.js deleted file mode 100644 index a3229cd4..00000000 --- a/routes/views/account/get/connectSteam.js +++ /dev/null @@ -1,45 +0,0 @@ -const request = require('request'); -let flash = {}; - -exports = module.exports = function (req, res) { - - let locals = res.locals; - - locals.section = 'account'; - var overallRes = res; - - request.post({ - 'url': process.env.API_URL + '/users/buildSteamLinkUrl', - 'headers': {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - form: {callbackUrl: req.protocol + '://' + req.get('host') + '/account/link?done'} - }, function (err, res, body) { - //Must not be valid, check to see if errors, otherwise return generic error. - try { - body = JSON.parse(body); - - if (body.steamUrl) { - return overallRes.redirect(body.steamUrl); - } - - var errorMessages = []; - - for (var i = 0; i < body.errors.length; i++) { - var error = body.errors[i]; - errorMessages.push({msg: error.detail}); - } - - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - overallRes.render('account/linkSteam', {flash: flash}); - - } catch (e) { - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Your steam account was not successfully linked! Please verify you logged into the website correctly.'}]; - flash.type = 'Error!'; - - overallRes.render('account/linkSteam', {flash: flash}); - } - }); -}; diff --git a/routes/views/account/get/createAccount.js b/routes/views/account/get/createAccount.js deleted file mode 100644 index 74c36553..00000000 --- a/routes/views/account/get/createAccount.js +++ /dev/null @@ -1,27 +0,0 @@ -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - if (req.query.token){ - locals.tokenURL = process.env.API_URL+"/users/activate?token="+ req.query.token; - } - else{ - let flash = {}; - flash.type = 'Error!'; - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Invalid or missing account token'}]; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/?flash='+data); - } - - // Render the view - res.render('account/create'); - -}; diff --git a/routes/views/account/get/linkGog.js b/routes/views/account/get/linkGog.js deleted file mode 100644 index fd429f5c..00000000 --- a/routes/views/account/get/linkGog.js +++ /dev/null @@ -1,50 +0,0 @@ -const request = require('request'); -let error = require('../post/error'); - -exports = module.exports = function (req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - let flash = {}; - if (req.query.done !== undefined) { - if (req.query.errors) { - let errors = JSON.parse(req.query.errors); - - flash.class = 'alert-danger'; - flash.messages = errors.map(error => ({msg: error.detail})); - flash.type = 'Error'; - } /*else { - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your GOG account has been linked successfully.'}]; - flash.type = 'Success'; - }*/ - } else { - flash = null; - } - - let overallRes = res; - - request.get({ - url: process.env.API_URL + '/users/buildGogProfileToken', - headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - form: {} - }, function (err, res, body) { - locals.gogToken = 'unable to obtain token'; - if (res === undefined || res.statusCode !== 200) { - flash = {}; - error.parseApiErrors(body, flash); - return overallRes.render('account/linkGog', {flash: flash}); - } - - locals.gogToken = JSON.parse(body).gogToken; - - // Render the view - overallRes.render('account/linkGog', {flash: flash}); - }); -}; diff --git a/routes/views/account/get/linkSteam.js b/routes/views/account/get/linkSteam.js deleted file mode 100644 index 278f783d..00000000 --- a/routes/views/account/get/linkSteam.js +++ /dev/null @@ -1,33 +0,0 @@ -exports = module.exports = function (req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - let flash = {}; - if (req.query.done !== undefined) { - if (req.query.errors) { - let errors = JSON.parse(req.query.errors); - - flash.class = 'alert-danger'; - flash.messages = errors.map(error => ({msg: error.detail})); - flash.type = 'Error'; - } else { - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your steam account has successfully been linked.'}]; - flash.type = 'Success'; - } - } else { - flash = null; - } - - //locals.steam = process.env.API_URL + '/users/linkToSteam'; - locals.steamConnect = req.protocol + '://' + req.get('host') + '/account/connect'; - - // Render the view - res.render('account/linkSteam', {flash: flash}); -}; diff --git a/routes/views/account/get/register.js b/routes/views/account/get/register.js deleted file mode 100644 index 0c4b9bd6..00000000 --- a/routes/views/account/get/register.js +++ /dev/null @@ -1,16 +0,0 @@ -exports = module.exports = function (req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - var flash = null; - - // Render the view - res.render('account/register', {flash: flash, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY}); - -}; diff --git a/routes/views/account/get/report.js b/routes/views/account/get/report.js deleted file mode 100644 index e720b2fe..00000000 --- a/routes/views/account/get/report.js +++ /dev/null @@ -1,107 +0,0 @@ -const request = require('request'); - -exports = module.exports = function (req, res) { - - const maxDescriptionLength = 48; - - const locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - locals.formData = req.body || {}; - locals.game_id = req.query.game_id; // Game_id can be supplied as GET - locals.offenders_names = []; // Offender name aswell - - if (req.query.offenders !== undefined) { - locals.offenders_names = req.query.offenders.split(" "); - } - - var fs = require('fs'); - - - var flash = null; - - if (req.originalUrl === '/report_submitted') { - flash = {}; - - flash.class = 'alert-success'; - flash.messages = [{msg: 'You have successfully submitted your report'}]; - flash.type = 'Success!'; - } else if (req.query.flash) { - let buff = Buffer.from(req.query.flash, 'base64'); - let text = buff.toString('ascii'); - flash = JSON.parse(text); - } - - - request.get( - { - url: process.env.API_URL + '/data/moderationReport?include=reportedUsers,lastModerator&sort=-createTime', - headers: { - 'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token - } - }, - function (err, childRes, body) { - const reports = JSON.parse(body); - locals.reports = []; - for (k in reports.data) { - const report = reports.data[k]; - - let offenders = []; - for (l in report.relationships.reportedUsers.data) { - const offender = report.relationships.reportedUsers.data[l]; - for (m in reports.included) { - const user = reports.included[m]; - if (user.type === "player" && user.id === offender.id) { - offenders.push(user.attributes.login); - } - } - } - - let moderator = ''; - if (report.relationships.lastModerator.data) { - for (l in reports.included) { - const user = reports.included[l]; - if (user.type === "player" && user.id === report.relationships.lastModerator.data.id) { - moderator = user.attributes.login; - break; - } - } - } - - let statusStyle = {}; - switch (report.attributes.reportStatus) { - case "AWAITING": - statusStyle = {'color': '#806A15', 'background-color': '#FAD147'}; - break; - case "PROCESSING": - statusStyle = {'color': '#213A4D', 'background-color': '#3A85BF'}; - break; - case "COMPLETED": - statusStyle = {'color': 'green', 'background-color': 'lightgreen'}; - break; - case "DISCARDED": - statusStyle = {'color': '#444444', 'background-color': '#AAAAAA'}; - break; - } - statusStyle['font-weight'] = 'bold'; - - locals.reports.push({ - 'id': report.id, - 'offenders': offenders.join(" "), - 'creationTime': report.attributes.createTime, - 'game': report.relationships.game.data != null ? '#' + report.relationships.game.data.id : '', - 'lastModerator': moderator, - 'description': report.attributes.reportDescription.substr(0, maxDescriptionLength) + (report.attributes.reportDescription.length > maxDescriptionLength ? '...' : ''), - 'notice': report.attributes.moderatorNotice, - 'status': report.attributes.reportStatus, - 'statusStyle': statusStyle - }); - } - - locals.reportable_members = {}; - res.render('account/report', {flash: flash}) - } - ) -}; diff --git a/routes/views/account/get/requestPasswordReset.js b/routes/views/account/get/requestPasswordReset.js deleted file mode 100644 index 19d6af44..00000000 --- a/routes/views/account/get/requestPasswordReset.js +++ /dev/null @@ -1,34 +0,0 @@ -const axios = require("axios"); -const appConfig = require('../../../../config/app') - -exports = module.exports = async function (req, res) { - const formData = req.body || {}; - - // funky issue: https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express - await new Promise(resolve => process.nextTick(resolve)) - - axios.post(appConfig.apiUrl + '/users/buildSteamPasswordResetUrl', {}, { maxRedirects: 0 }).then(response => { - if (response.status !== 200) { - throw new Error('java-api error') - } - - res.render('account/requestPasswordReset', { - section: 'account', - flash: {}, - steamReset: response.data.steamUrl, - formData: formData, - recaptchaSiteKey: appConfig.recaptchaKey - }) - }).catch(error => { - res.render('account/requestPasswordReset', { - section: 'account', - flash: { - class: 'alert-danger', - messages: 'issue resetting', - type: 'Error!', - }, - formData: formData, - recaptchaSiteKey: appConfig.recaptchaKey - }) - }) -} diff --git a/routes/views/account/get/resync.js b/routes/views/account/get/resync.js deleted file mode 100644 index dfef5f03..00000000 --- a/routes/views/account/get/resync.js +++ /dev/null @@ -1,34 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('../post/error'); - -exports = module.exports = function (req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'account'; - - locals.formData = req.body || {}; - - let overallRes = res; - - request.post({ - url: process.env.API_URL + '/users/resyncAccount', - headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - } else { - // Successfully account resync - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your account was resynced successfully.'}]; - flash.type = 'Success!'; - } - - overallRes.render('account/confirmResyncAccount', {flash: flash}); - } - ); -}; diff --git a/routes/views/account/post/activate.js b/routes/views/account/post/activate.js deleted file mode 100644 index 1dab2677..00000000 --- a/routes/views/account/post/activate.js +++ /dev/null @@ -1,55 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - -exports = module.exports = function (req, res) { - - let locals = res.locals; - locals.username = req.query.username - locals.token = req.query.token - - locals.formData = req.body || {}; - - // validate the input - check('password', 'Password is required').notEmpty(); - check('password', 'Password must be six or more characters').isLength({min: 6}); - check('password', 'Passwords don\'t match').equals(req.body.password_confirm); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/activate', {flash: flash}); - } else { - let token = req.query.token; - let password = req.body.password; - - let overallRes = res; - - //Run post to reset endpoint - request.post({ - url: process.env.API_URL + '/users/activate', - form: {password: password, token: token} - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - return overallRes.render('account/activate', {flash: flash}); - } - - // Successfully reset password - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your account was created successfully.'}]; - flash.type = 'Success!'; - - overallRes.render('account/activate', {flash: flash}); - } - ); - } -}; diff --git a/routes/views/account/post/changeEmail.js b/routes/views/account/post/changeEmail.js deleted file mode 100644 index 11519ea2..00000000 --- a/routes/views/account/post/changeEmail.js +++ /dev/null @@ -1,58 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - - -exports = module.exports = function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - // validate the input - check('email', 'Email is required').notEmpty(); - check('email', 'Email does not appear to be valid').isEmail(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - - // failure - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/changeEmail', {flash: flash}); - - } else { - - // pull the form variables off the request body - - let email = req.body.email; - let password = req.body.password; - - let overallRes = res; - - request.post({ - url: `${process.env.API_URL}/users/changeEmail`, - headers: {'Authorization': `Bearer ${req.services.userService.getUser()?.oAuthPassport.token}`}, - form: {newEmail: email, currentPassword: password} - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - return overallRes.render('account/changeEmail', {flash: flash}); - } - - // Successfully changed email - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your email was set successfully.'}]; - flash.type = 'Success!'; - - overallRes.render('account/changeEmail', {flash: flash}); - }); - } -}; diff --git a/routes/views/account/post/changePassword.js b/routes/views/account/post/changePassword.js deleted file mode 100644 index a5423884..00000000 --- a/routes/views/account/post/changePassword.js +++ /dev/null @@ -1,63 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - -exports = module.exports = function(req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - // validate the input - check('old_password', 'Old Password is required').notEmpty(); - check('old_password', 'Old Password must be six or more characters').isLength({min: 6}); - check('password', 'New Password is required').notEmpty(); - check('password', 'New Password must be six or more characters').isLength({min: 6}); - check('password', 'New Passwords don\'t match').equals(req.body.password_confirm); - check('username', 'Username is required').notEmpty(); - check('username', 'Username must be three or more characters').isLength({min: 3}); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - - // failure - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/changePassword', {flash: flash}); - - } else { - - //Encrypt password before sending it off to endpoint - let newPassword = req.body.password; - let oldPassword = req.body.old_password; - let username = req.body.username; - - let overallRes = res; - - //Run post to reset endpoint - request.post({ - url: process.env.API_URL + '/users/changePassword', - headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - form: {name: username, currentPassword: oldPassword, newPassword: newPassword} - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - return overallRes.render('account/changePassword', {flash: flash}); - } - - // Successfully reset password - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your password was changed successfully. Please use the new password to log in!'}]; - flash.type = 'Success!'; - - overallRes.render('account/changePassword', {flash: flash}); - }); - } -}; diff --git a/routes/views/account/post/changeUsername.js b/routes/views/account/post/changeUsername.js deleted file mode 100644 index 94f27bf1..00000000 --- a/routes/views/account/post/changeUsername.js +++ /dev/null @@ -1,54 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - -exports = module.exports = function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - // validate the input - check('username', 'Username is required').notEmpty(); - check('username', 'Username must be three or more characters').isLength({min: 3}); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - - // failure - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/changeUsername', {flash: flash}); - - } else { - // pull the form variables off the request body - let username = req.body.username; - let overallRes = res; - - //Run post to reset endpoint - request.post({ - url: process.env.API_URL + '/users/changeUsername', - headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - form: {newUsername: username} - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - return overallRes.render('account/changeUsername', {flash: flash}); - } - - // Successfully changed username - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your username was changed successfully. Please use the new username to log in!'}]; - flash.type = 'Success!'; - - overallRes.render('account/changeUsername', {flash: flash}); - }); - } -}; diff --git a/routes/views/account/post/confirmPasswordReset.js b/routes/views/account/post/confirmPasswordReset.js deleted file mode 100644 index cf1290a3..00000000 --- a/routes/views/account/post/confirmPasswordReset.js +++ /dev/null @@ -1,56 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - -exports = module.exports = function (req, res) { - - let locals = res.locals; - locals.username = req.query.username - locals.token = req.query.token - - locals.formData = req.body || {}; - - // validate the input - check('password', 'Password is required').notEmpty(); - check('password', 'Password must be six or more characters').isLength({min: 6}); - check('password', 'Passwords don\'t match').equals(req.body.password_confirm); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/confirmPasswordReset', {flash: flash}); - } else { - let token = req.query.token; - let newPassword = req.body.password; - - let overallRes = res; - - //Run post to reset endpoint - request.post({ - url: process.env.API_URL + '/users/performPasswordReset', - form: {newPassword: newPassword, token: token} - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - return overallRes.render('account/confirmPasswordReset', {flash: flash}); - } - - // Successfully reset password - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your password was changed successfully.'}]; - flash.type = 'Success!'; - - overallRes.render('account/confirmPasswordReset', {flash: flash}); - } -); -} -} -; diff --git a/routes/views/account/post/error.js b/routes/views/account/post/error.js deleted file mode 100644 index 5d2baa9b..00000000 --- a/routes/views/account/post/error.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = { - parseApiErrors: function (body, flash) { - let errorMessages = []; - - try { - let response = JSON.parse(body); - response.errors.forEach(error => errorMessages.push({msg: error.detail})) - } catch (e) { - errorMessages.push({msg: 'An unknown error occurred. Please try again later or ask the support.'}); - console.log("Error on parsing server response: " + body); - } - - if (errorMessages.length === 0) { - errorMessages.push({msg: 'An unknown error occurred. Please try again later or ask the support.'}); - } - - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - } -} diff --git a/routes/views/account/post/linkGog.js b/routes/views/account/post/linkGog.js deleted file mode 100644 index 1a414465..00000000 --- a/routes/views/account/post/linkGog.js +++ /dev/null @@ -1,75 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - -exports = module.exports = function(req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - // validate the input - check('gog_username', 'Username is required').notEmpty(); - check('gog_username', 'Username must be at least 3 characters').isLength({min: 3}); - check('gog_username', 'Username must be at most 100 characters').isLength({max: 100}); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - - // failure - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/linkGog', {flash: flash}); - - } else { - - let gogUsername = req.body.gog_username; // this is obtained from the form field in the mixin, not the pug file of this page! - - let overallRes = res; - - request.post({ - url: process.env.API_URL + '/users/linkToGog', - headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - form: {gogUsername: gogUsername} - }, function (err, res, body) { - - if (res !== undefined && res.statusCode === 200) { - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your accounts were linked successfully.'}]; - flash.type = 'Success!'; - - locals.gogToken = '-'; - overallRes.render('account/linkGog', {flash: flash}); - - } else { - error.parseApiErrors(body, flash); - - // We need the gog token on the error page as well, - // this code literally does the same as linkGog.js, but due to the architectural structure of this application - // it's not possible to extract it into a separate function while saving any code - request.get({ - url: process.env.API_URL + '/users/buildGogProfileToken', - headers: {'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token}, - form: {} - }, function (err, res, body) { - locals.gogToken = 'unable to obtain token'; - if (res === undefined || res.statusCode !== 200) { - flash = {}; - error.parseApiErrors(body, flash); - return overallRes.render('account/linkGog', {flash: flash}); - } - - locals.gogToken = JSON.parse(body).gogToken; - - return overallRes.render('account/linkGog', {flash: flash}); - }); - } - }); - } -}; diff --git a/routes/views/account/post/register.js b/routes/views/account/post/register.js deleted file mode 100644 index 88eee986..00000000 --- a/routes/views/account/post/register.js +++ /dev/null @@ -1,90 +0,0 @@ -let flash = {}; -const request = require('request'); -const ClientOAuth2 = require('client-oauth2'); -const {check, validationResult} = require('express-validator'); -require("dotenv").config(); - -const apiAuth = new ClientOAuth2({ - clientId: process.env.OAUTH_CLIENT_ID, - clientSecret: process.env.OAUTH_CLIENT_SECRET, - accessTokenUri: process.env.API_URL + '/oauth/token', - authorizationUri: process.env.API_URL + '/oauth/authorize', - redirectUri: process.env.HOST + '/callback', - scopes: ['create_user'] -}); - -exports = module.exports = function (req, res) { - let locals = res.locals; - - locals.formData = req.body || {}; - // validate the input - check('username', 'Username is required').notEmpty(); - check('username', 'Username must be three or more characters').isLength({min: 3}); - check('email', 'Email is required').notEmpty(); - check('email', 'Email does not appear to be valid').isEmail(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - - // failure - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/register', {flash: flash}); - - } else { - - // pull the form variables off the request body - let username = req.body.username; - let email = req.body.email; - let recaptchaResponse = req.body["g-recaptcha-response"] - - let overallRes = res; - - //Run post to register endpoint - request.post({ - url: process.env.API_URL + '/users/register', - form: {username: username, email: email, recaptchaResponse: recaptchaResponse} - }, function (err, res, body) { - let resp; - let errorMessages = []; - - if (res.statusCode !== 200) { - try { - resp = JSON.parse(body); - } catch (e) { - errorMessages.push({msg: 'Invalid registration sign up. Please try again later.'}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - return overallRes.render('account/register', {flash: flash, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY}); - } - - // Failed registering user - for (let i = 0; i < resp.errors.length; i++) { - let error = resp.errors[i]; - - errorMessages.push({msg: error.detail}); - } - - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - return overallRes.render('account/register', {flash: flash, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY}); - } - - // Successfully registered user - flash.class = 'alert-success'; - flash.messages = [{msg: 'Please check your email to verify your registration. Then you will be ready to log in!'}]; - flash.type = 'Success!'; - - overallRes.render('account/register', {flash: flash}); - }); - } -}; diff --git a/routes/views/account/post/report.js b/routes/views/account/post/report.js deleted file mode 100644 index 7297a700..00000000 --- a/routes/views/account/post/report.js +++ /dev/null @@ -1,212 +0,0 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url, req) { - return new Promise(function (resolve, reject) { - request(url, { - headers: { - 'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token, - } - }, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error || new Error('report failed with status' + res.statusCode)); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - const bodyKeys = Object.keys(req.body); - - // validate the input - for (i in bodyKeys){ - const element = bodyKeys[i]; - if (!element.startsWith("offender_")) continue; - check(element, "Please indicate the player or players you're reporting").notEmpty(); - } - check('report_description', 'Please describe the incident').notEmpty(); - if (req.body.game_id.length > 0){ - check('game_id', 'Please enter a valid game ID, or nothing. The # is not needed.').optional().isDecimal(); - } - else{ - req.body.game_id = null; - } - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('report?flash=' + data); - } else { - - const isGameReport = req.body.game_id != null; - - let offenders = []; - - for (i in bodyKeys){ - const key = bodyKeys[i]; - if (!key.startsWith("offender_")) continue; - - const offender = req.body[key]; - if (offender.trim().length == 0) continue; - - offenders.push(offender); - } - - // Let's check first that the users exist - let filter = ''; - for (k in offenders){ - filter+= 'login=='+offenders[k]; - if (k < offenders.length-1){ - filter += ','; - } - } - const userFetchRoute = process.env.API_URL+'/data/player?filter='+filter+'&fields[player]=login&page[size]='+offenders.length; - let apiUsers; - try { - const userFetch = await promiseRequest(userFetchRoute, req); - apiUsers = JSON.parse(userFetch); - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Error while submitting the report form: '+e.toString()}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('report?flash='+data); - } - - // Mapping users to their IDs - let foundUsers = {}; - for (k in apiUsers.data){ - const record = apiUsers.data[k]; - if (offenders.indexOf(record.attributes.login) > -1){ - foundUsers[record.attributes.login] = record.id; - } - } - - if (Object.keys(foundUsers).length != offenders.length){ - // someone is missing ! - let missing = []; - for (k in offenders){ - const offenderName = offenders[k]; - if (foundUsers[offenderName] == undefined){ - missing.push(offenderName); - } - } - - flash.class = 'alert-danger'; - flash.messages = [{"msg": "The following users could not be found : " + missing.join(',')}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('report?flash='+data); - - } - - // Checking the game exists - if (isGameReport){ - const gameFetchRoute = process.env.API_URL+'/data/game?filter=id=='+req.body.game_id+'&fields[game]='; /* empty field here to fetch nothing but ID */ - try { - const gameFetch = await promiseRequest(gameFetchRoute, req); - const gameData = JSON.parse(gameFetch); - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The game could not be found. Please check the game ID you provided.'}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('report?flash='+data); - } - } - - // Building report; - let reportedUsers = [] - for (k in foundUsers){ - reportedUsers.push({ - 'type':'player', - 'id':''+foundUsers[k] - }) - } - - let relationShips = { - "reportedUsers": { - "data": reportedUsers - } - }; - - if (isGameReport){ - relationShips.game = {"data":{"type":"game","id":""+req.body.game_id}}; - } - - // Making the report accordingly to what the API expects to receive - const report = - { - "data": [ - { - "type": "moderationReport", - "attributes": { - "gameIncidentTimecode": (req.body.game_timecode.length > 0 ? req.body.game_timecode : null), - "reportDescription": req.body.report_description - }, - "relationships":relationShips - } - ] - } - ; - - //Run post to reset endpoint - request.post({ - url: process.env.API_URL + '/data/moderationReport', - body: JSON.stringify(report), - headers: { - 'Authorization': 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token, - 'Content-Type': 'application/vnd.api+json', - 'Accept': 'application/vnd.api+json' - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 201) { - errorMessages.push({msg: 'Error while submitting the report form'}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('report?flash='+data); - } - - overallRes.redirect('../report_submitted'); - }); - } -} diff --git a/routes/views/account/post/requestPasswordReset.js b/routes/views/account/post/requestPasswordReset.js deleted file mode 100644 index a4d4992a..00000000 --- a/routes/views/account/post/requestPasswordReset.js +++ /dev/null @@ -1,56 +0,0 @@ -let flash = {}; -let request = require('request'); -let error = require('./error'); -const {check, validationResult} = require('express-validator'); - -exports = module.exports = function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - // validate the input - check('usernameOrEmail', 'Username or email is required').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - res.render('account/requestPasswordReset', {flash: flash}); - } else { - let identifier = req.body.usernameOrEmail; - let recaptchaResponse = req.body["g-recaptcha-response"] - - let overallRes = res; - - //Run post to reset endpoint - request.post({ - url: process.env.API_URL + '/users/requestPasswordReset', - form: {identifier: identifier, recaptchaResponse: recaptchaResponse} - }, function (err, res, body) { - - if (res.statusCode !== 200) { - error.parseApiErrors(body, flash); - return overallRes.render('account/requestPasswordReset', { - flash: flash, - recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY - }); - } - - // Successfully reset password - flash.class = 'alert-success'; - flash.messages = [{msg: 'Your password is in the process of being reset, please reset your password by clicking on the link provided in an email.'}]; - flash.type = 'Success!'; - - overallRes.render('account/requestPasswordReset', { - flash: flash, - recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY - }); - }); - } -}; diff --git a/routes/views/checkUsername.js b/routes/views/checkUsername.js deleted file mode 100644 index 6f3b1c7e..00000000 --- a/routes/views/checkUsername.js +++ /dev/null @@ -1,19 +0,0 @@ -const request = require("request"); - -exports = module.exports = function (req, res) { - const name = req.query.username; - - request(process.env.API_URL + "/data/player?filter=login==" + encodeURI(name), function (error, response, body) { - if (error) { - console.error(error); - return res.status(500).send(error); - } - - try { - let userNameFree = JSON.parse(body).data.length === 0; - return res.status(userNameFree ? 200 : 400).send(userNameFree); - } catch (e) { - return res.status(500).send(e); - } - }); -}; diff --git a/routes/views/clans/get/accept_invite.js b/routes/views/clans/get/accept_invite.js deleted file mode 100644 index 7f4a7531..00000000 --- a/routes/views/clans/get/accept_invite.js +++ /dev/null @@ -1,95 +0,0 @@ -const request = require('request'); - -exports = module.exports = function(req, res) { - - var locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'clan'; - let flash = {}; - - if (!req.query.i){ - flash.type = 'Error!'; - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The invitation link is wrong or truncated. Key informations are missing.'}]; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/clans?flash='+data); - } - - const invitationId = req.query.i; - - if (!req.app.locals.clanInvitations[invitationId]){ - flash.type = 'Error!'; - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The invitation link is wrong or truncated. Invite code missing from website clan map.'}]; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/clans?flash='+data); - } - - const invite = req.app.locals.clanInvitations[invitationId]; - const clanId = invite.clan; - - if (req.user.data.attributes.clan != null){ - // User is already in a clan! - return res.redirect(`/clans/${req.user.data.attributes.clan.tag}?member=true`); - } - - const queryUrl = process.env.API_URL - + '/data/clan/'+clanId - + '?include=memberships.player' - + '&fields[clan]=createTime,description,name,tag,updateTime,websiteUrl,founder,leader' - + '&fields[player]=login,updateTime' - ; - - request.get( - { - url: queryUrl - }, - function (err, childRes, body) { - - const clan = JSON.parse(body); - - if (err || !clan.data){ - flash.type = 'Error!'; - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The clan you want to join is invalid or does no longer exist'}]; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('./?flash='+data); - } - - locals.clanName = clan.data.attributes.name; - locals.clanLeaderName = ""; - - for (k in clan.included){ - switch(clan.included[k].type){ - case "player": - const player = clan.included[k]; - - // Getting the leader name - if (player.id == clan.data.relationships.leader.data.id) - { - locals.clanLeaderName = player.attributes.login; - } - - break; - } - } - - const token = invite.token; - locals.acceptURL = `/clans/join?clan_id=${clanId}&token=${token}`; - - // Render the view - res.render('clans/accept_invite'); - } - ); -}; diff --git a/routes/views/clans/get/create.js b/routes/views/clans/get/create.js deleted file mode 100644 index 4b6a13eb..00000000 --- a/routes/views/clans/get/create.js +++ /dev/null @@ -1,52 +0,0 @@ -exports = module.exports = function(req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'clan'; - - const request = require('request'); - - request.get( - { - url: process.env.API_URL + '/clans/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, - function (err, childRes, body) { - - const clanInfo = JSON.parse(body); - if (clanInfo.clan != null){ - res.redirect('/clans/manage'); - return; - } - - locals.formData = req.body || {}; - locals.clan_name = req.query.clan_name || ""; - locals.clan_tag = req.query.clan_tag || ""; - locals.clan_description = req.query.clan_description || ""; - locals.clan_create_time = (new Date()).toUTCString(); - - var flash = null; - - if (req.query.flash){ - let buff = Buffer.from(req.query.flash, 'base64'); - let text = buff.toString('ascii'); - - try{ - flash = JSON.parse(text); - } - catch(e){ - console.error("Parsing error while trying to decode a flash error: " + text); - console.error(e); - flasg = [{msg: "Unknown error"}]; - } - } - - // Render the view - res.render('clans/create', {flash: flash}); - } - ); -}; diff --git a/routes/views/clans/get/manage.js b/routes/views/clans/get/manage.js deleted file mode 100755 index 5bcd4ab6..00000000 --- a/routes/views/clans/get/manage.js +++ /dev/null @@ -1,134 +0,0 @@ -const request = require('request'); - -exports = module.exports = function(req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'clan'; - - let flash = null; - - let clanMembershipId = null; - try{ - clanMembershipId = req.user.data.attributes.clan.membershipId; - } - catch{ - // The user doesnt belong to a clan - res.redirect('/clans'); - return; - } - - // In case the user has just generated an invite link - if (req.query.invitation_id) { - flash = {}; - flash.class = 'alert-invite'; - - flash.messages = [ - {msg: - `

Right click on me and copy the invitation link

Note: It only works for the user you typed.`} - ]; - flash.type = ''; - - } - - - - - request.get( - { - url: - process.env.API_URL - + '/data/clanMembership/'+clanMembershipId+'/clan' - + '?include=memberships.player' - + '&fields[clan]=createTime,description,name,tag,updateTime,websiteUrl,founder,leader' - + '&fields[player]=login,updateTime' - + '&fields[clanMembership]=createTime,player', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, - function (err, childRes, body) { - - const clan = JSON.parse(body); - - if (err || !clan.data){ - flash = {}; - flash.class = 'alert-danger'; - flash.messages = [{msg: "Unknown error while retrieving your clan information"}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/clans?flash='+data); - } - - if (clan.data.relationships.leader.data.id != req.user.data.id){ - // Not the leader! Shouldn't be able to manage stuff - res.redirect(`/clans/${req.user.data.attributes.clan.tag}?member=true`); - return; - } - - locals.clan_name = clan.data.attributes.name; - locals.clan_tag = clan.data.attributes.tag; - locals.clan_description = clan.data.attributes.description; - locals.clan_create_time = clan.data.attributes.createTime; - locals.me = req.user.data.id; - locals.clan_id = clan.data.id; - locals.clan_link = process.env.HOST + "/clans/see?id="+clan.data.id; - - let members = {}; - - for (k in clan.included){ - switch(clan.included[k].type){ - case "player": - const player = clan.included[k]; - if (!members[player.id]) members[player.id] = {}; - members[player.id].id = player.id; - members[player.id].name = player.attributes.login; - - if (clan.data.relationships.founder.data.id == player.id){ - locals.founder_name = player.attributes.login - } - break; - - case "clanMembership": - const membership = clan.included[k]; - const member = membership.relationships.player.data; - if (!members[member.id]) members[member.id] = {}; - members[member.id].id = member.id; - members[member.id].membershipId = membership.id; - members[member.id].joinedAt = membership.attributes.createTime; - break; - - } - } - - locals.clan_members = members; - - if (req.originalUrl == '/clan_created') { - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'You have successfully created your clan'}]; - flash.type = 'Success!'; - } - else if (req.query.flash){ - let buff = Buffer.from(req.query.flash, 'base64'); - let text = buff.toString('ascii'); - try{ - flash = JSON.parse(text); - } - catch(e){ - console.error("Parsing error while trying to decode a flash error: " + text); - console.error(e); - flash = [{msg: "Unknown error"}]; - } - } - - // Render the view - res.render('clans/manage', {flash: flash}); - } - ); -}; diff --git a/routes/views/clans/post/create.js b/routes/views/clans/post/create.js deleted file mode 100755 index 8c43d917..00000000 --- a/routes/views/clans/post/create.js +++ /dev/null @@ -1,130 +0,0 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({max: 3}); - check('clan_description', 'Please add a description for your clan').notEmpty().isLength({max: 1000}); - check('clan_name', "Please indicate your clan's name").notEmpty().isLength({max: 40}); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('create?flash=' + data); - } else { - - const clanName = req.body.clan_name; - const clanTag = req.body.clan_tag; - const clanDescription = req.body.clan_description; - const userId = req.body.user_id; - - // Let's check first that the name or tag are not taken - const clanFetchRoute = process.env.API_URL+'/data/clan?filter=name=="'+clanName+'",tag=="'+clanTag+'"'; - let exists = true; - try { - const httpData = await promiseRequest(clanFetchRoute); - exists = JSON.parse(httpData).data.length > 0; - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Error while creating the clan '+e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('create?flash='+data+'&clan_name='+clanName+'&clan_tag='+clanTag+'&clan_description='+clanDescription+''); - } - - const queryUrl = - process.env.API_URL - + '/clans/create' - + '?name=' + encodeURIComponent(clanName) - + '&tag='+encodeURIComponent(clanTag) - + '&description='+encodeURIComponent(clanDescription) - ; - - //Run post to endpoint - request.post({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode !== 200) { - let msg = 'Error while creating the clan'; - try { - - msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail); - } catch { - } - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('create?flash='+data+'&clan_name='+clanName+'&clan_tag='+clanTag+'&clan_description='+clanDescription+''); - } - - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.attributes.token = req.user.data.attributes.token; - user.data.id = user.data.attributes.userId; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('/clans/manage'); - }); - } - catch{ - console.error("There was an error updating a session after a clan creation"); - } - }); - - }); - } -} diff --git a/routes/views/clans/post/destroy.js b/routes/views/clans/post/destroy.js deleted file mode 100755 index 0caba265..00000000 --- a/routes/views/clans/post/destroy.js +++ /dev/null @@ -1,111 +0,0 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - // Building update query - const queryUrl = - process.env.API_URL - + '/data/clan/' + req.body.clan_id - ; - - //Run post to endpoint - request.delete({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while destroying the clan'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{} - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'The clan was successfully destroyed'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('/clans?flash='+data); - }); - } - catch{ - console.error("There was an error updating a session after a clan destruction"); - } - }); - }); - } -} diff --git a/routes/views/clans/post/invite.js b/routes/views/clans/post/invite.js deleted file mode 100644 index 918dcb21..00000000 --- a/routes/views/clans/post/invite.js +++ /dev/null @@ -1,156 +0,0 @@ -let flash = {}; -const request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - console.error("Call to " + url + " failed: " + error); - reject(error); - } - }); - }); -} - -function setLongTimeout(func, delayMs) { - const maxDelay = 214748364-1; // JS Limit for 32 bit integers - - if (delayMs > maxDelay) { - const remainingDelay = delayMs - maxDelay; - - // we cut it in smaller, edible chunks - setTimeout(() => { - setLongTimeout(func, remainingDelay); - }, maxDelay); - } - else{ - setTimeout(func, delayMs); - } -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - - // validate the input - check('invited_player', 'Please indicate the player name').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - const clanId = req.body.clan_id; - const userName = req.body.invited_player; - - // Let's check first that the player exists - const fetchRoute = process.env.API_URL + '/data/player?filter=login=="' + userName + '"&fields[player]='; - - let exists = true; - let playerData = null; - let playerId = null; - try { - const httpData = await promiseRequest(fetchRoute); - playerData = JSON.parse(httpData).data; - exists = playerData.length > 0; - playerId = playerData[0].id; - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The player ' + userName + " doesn't seem to exist" + e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - const queryUrl = - process.env.API_URL - + '/clans/generateInvitationLink' - + '?clanId=' + encodeURIComponent(clanId) - + '&playerId=' + encodeURIComponent(playerId) - ; - - //Run post to endpoint - request.get({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - if (res.statusCode !== 200) { - - let errorMessages = []; - let msg = 'Error while generating the invite link'; - try { - - msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail); - } catch { - } - - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash='+data); - } - else{ - try{ - const token = JSON.parse(res.body).jwtToken; - - const id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5).toUpperCase(); - - req.app.locals.clanInvitations[id] = { - token:token, - clan:clanId - }; - - // We use timeout here because if we delete the invite link whenver the page is GET, - // then discord and other messaging applications will destroy the link accidentally - // when pre-fetching the page. So we will delete it later. Regardless if the website is restarted all the links will be - // killed instantly, which is fine. They are short lived by design. - const lifespan = process.env.CLAN_INVITES_LIFESPAN_DAYS * 24 * 3600 * 1000; - setLongTimeout(()=>{ - delete req.app.locals.clanInvitations[id]; - console.log(`Killed invitation with id ${id} after having waited ${lifespan} seconds (${process.env.CLAN_INVITES_LIFESPAN_DAYS} days)`); - }, lifespan); - - return overallRes.redirect('manage?invitation_id='+id); - } - catch (e){ - flash.class = 'alert-danger'; - flash.messages = [{msg:"Unkown error while generating the invite link: "+e}]; - flash.type = 'Error!'; - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash='+data); - } - } - }); - } -} diff --git a/routes/views/clans/post/join.js b/routes/views/clans/post/join.js deleted file mode 100644 index b4e51d6b..00000000 --- a/routes/views/clans/post/join.js +++ /dev/null @@ -1,87 +0,0 @@ -const request = require('request'); - -exports = module.exports = function(req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'clan'; - - let flash = {}; - const overallRes = res; - - if (!req.query.token || !req.query.clan_id){ - flash.type = 'Error!'; - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The invitation link is invalid!'}]; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/clans?flash='+data+''); - } - - const token = req.query.token; - const clanId = req.query.clan_id; - - request.post( - { - url: process.env.API_URL + '/clans/joinClan?token='+token, - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, - function (err, childRes, body) { - let flashData; - if (childRes.statusCode == 200 || childRes.statusCode == 201){ - flash.class = 'alert-success'; - flash.messages = [ - {msg: "Welcome to your new clan!"} - ]; - flash.type = 'Success!'; - let buff = Buffer.from(JSON.stringify(flash)); - flashData = buff.toString('base64'); - - // Refreshing user - return request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect(`${user.data.attributes.clan.tag}?member=true&flash=${flashData}`); - }); - } - catch{ - console.error("There was an error updating a session after an user left a clan"); - } - }); - } - else{ - flash.type = 'Error!'; - flash.class = 'alert-danger'; - let msg = 'The invitation is invalid or has expired, or you are already part of a clan'; - try{ - msg += ': '+JSON.stringify(JSON.parse(childRes.body).errors[0].detail); - } catch{} - - flash.messages = [{msg: msg}]; - - let buff = Buffer.from(JSON.stringify(flash)); - flashData = buff.toString('base64'); - - return overallRes.redirect('/clans?flash='+flashData+''); - } - - } - ); -}; diff --git a/routes/views/clans/post/kick.js b/routes/views/clans/post/kick.js deleted file mode 100755 index 13914bf1..00000000 --- a/routes/views/clans/post/kick.js +++ /dev/null @@ -1,96 +0,0 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - check('membership_id', 'Internal error while processing your query: invalid member ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - // Should not happen normally, but you never know - if (req.body.membership_id == req.user.data.attributes.clan.membershipId) errors = [{msg: "You cannot kick yourself"}]; - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - // Building update query - const membershipId = req.body.membership_id; - const queryUrl = - process.env.API_URL - + '/data/clanMembership/' + membershipId - - ; - - //Run post to endpoint - request.delete({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while removing the member'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{} - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'The member was kicked'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - }); - } -} diff --git a/routes/views/clans/post/leave.js b/routes/views/clans/post/leave.js deleted file mode 100755 index 2028675f..00000000 --- a/routes/views/clans/post/leave.js +++ /dev/null @@ -1,111 +0,0 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - check('membership_id', 'Internal error while processing your query: invalid member ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('/clans?flash=' + data); - } else { - - // Building update query - const membershipId = req.body.membership_id; - const queryUrl = `${process.env.API_URL}/data/clanMembership/${membershipId}`; - - //Run post to endpoint - request.delete({ - url: `${process.env.API_URL}/data/clanMembership/${req.user.data.attributes.clan.membershipId}`, - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while leaving the clan'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{ - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - } - - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('/clans?flash='+data); - } - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'You left the clan'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('/clans?flash='+data); - }); - } - catch{ - console.error("There was an error updating a session after an user left a clan"); - } - }); - }); - } -}; diff --git a/routes/views/clans/post/transfer.js b/routes/views/clans/post/transfer.js deleted file mode 100755 index 82cf178a..00000000 --- a/routes/views/clans/post/transfer.js +++ /dev/null @@ -1,159 +0,0 @@ -let flash = {}; -const request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error || `Unexpected status code ${res.statusCode}`); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - - // validate the input - check('transfer_to', 'Please indicate the recipient name').notEmpty(); - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - const clanId = req.body.clan_id; - const userName = req.body.transfer_to; - - // Let's check first that the player exists AND is part of this clan - const fetchRoute = process.env.API_URL+'/data/clan/'+clanId+'?include=memberships.player&fields[player]=login'; - - let playerId = null; - - try { - if (userName === req.user.data.attributes.userName) throw "You cannot transfer your own clan to yourself"; - - const httpData = await promiseRequest(fetchRoute); - clanData = JSON.parse(httpData); - - let members = {}; - - for (k in clanData.included){ - const record = clanData.included[k]; - if (record.type !== "player") continue; - members[record.attributes.login] = record.id; - } - - if (!members[userName]) throw "User does not exist or is not part of the clan"; - playerId = members[userName]; - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'There was an error during the transfer to ' + userName + ": "+e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - - // Building update query - const queryUrl = - process.env.API_URL - + '/data/clan/' + clanId - ; - - const newClanObject = - { - "data": { - "type": "clan", - "id": clanId, - "relationships": { - "leader": { - "data":{ - "id": playerId, - "type": "player" - } - } - } - } - }; - - //Run post to endpoint - request.patch({ - url: queryUrl, - body: JSON.stringify(newClanObject), - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - 'Content-Type': 'application/vnd.api+json' - } - }, function (err, res, body) { - - if (res.statusCode != 204) { - - let errorMessages = []; - let msg = 'Error during the ownership transfer'; - try{ - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{} - - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - else{ - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('see?id='+clanId); - }); - } - catch{ - console.error("There was an error updating a session after a clan transfer"); - } - }); - } - }); - } -} diff --git a/routes/views/clans/post/update.js b/routes/views/clans/post/update.js deleted file mode 100644 index 4822433f..00000000 --- a/routes/views/clans/post/update.js +++ /dev/null @@ -1,156 +0,0 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({max: 3}); - check('clan_description', 'Please add a description for your clan').notEmpty().isLength({max: 1000}); - check('clan_name', "Please indicate your clan's name").notEmpty().isLength({max: 64}); - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - const newName = req.body.clan_name; - const newTag = req.body.clan_tag; - const oldName = req.body.original_clan_name; - const oldTag = req.body.original_clan_tag; - const clanDescription = req.body.clan_description; - const userId = req.body.user_id; - - // Is the name taken ? - try { - let msg = null; - - flash.class = 'alert-danger'; - flash.type = 'Error!'; - - if (oldName != newName){ - const fetchRoute = process.env.API_URL+'/data/clan?filter=name=="'+encodeURIComponent(newName)+'"'; - const data = await promiseRequest(fetchRoute); - const exists = JSON.parse(data).data.length > 0; - - if (exists) msg = "This name is already taken: "+encodeURIComponent(newName); - } - if (oldTag != newTag){ - const fetchRoute = process.env.API_URL+'/data/clan?filter=tag=="'+encodeURIComponent(newTag)+'"'; - const data = await promiseRequest(fetchRoute); - const exists = JSON.parse(data).data.length > 0; - - if (exists) msg = "This tag is already taken: "+encodeURIComponent(newTag); - } - - if (msg){ - flash.messages = [{msg: msg}]; - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash='+data); - } - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Error while updating the clan '+e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - // Building update query - const queryUrl = - process.env.API_URL - + '/data/clan/' + req.body.clan_id - ; - - const newClanObject ={ - "data": { - "type": "clan", - "id": req.body.clan_id, - "attributes": { - "description": clanDescription, - "name": newName, - "tag": newTag - } - } - }; - - - //Run post to endpoint - request.patch({ - url: queryUrl, - body: JSON.stringify(newClanObject), - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - 'Content-Type': 'application/vnd.api+json', - 'Accept': 'application/vnd.api+json' - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while updating the clan'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{} - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'You have successfully updated your clan'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - }); - } -}; diff --git a/routes/views/dataRouter.js b/routes/views/dataRouter.js deleted file mode 100644 index b6b1095e..00000000 --- a/routes/views/dataRouter.js +++ /dev/null @@ -1,38 +0,0 @@ -const express = require('../../ExpressApp') -const router = express.Router(); -const {AcquireTimeoutError} = require('../../lib/MutexService'); - -const getData = async (req, res, name, data) => { - try { - return res.json(data) - } catch (e) { - if (e instanceof AcquireTimeoutError) { - return res.status(503).json({error: 'timeout reached'}) - } - - console.error('[error] dataRouter::get:' + name + '.json failed with "' + e.toString() + '"') - - if (!res.headersSent) { - return res.status(500).json({error: 'unexpected error'}) - } - throw e - } -} -router.get('/newshub.json', async (req, res) => { - getData(req, res, 'newshub', await req.services.wordpressService.getNewshub()) -}) - -router.get('/tournament-news.json', async (req, res) => { - getData(req, res, 'tournament-news', await req.services.wordpressService.getTournamentNews()) -}) -router.get('/faf-teams.json', async (req, res) => { - getData(req, res, 'faf-teams', await req.services.wordpressService.getFafTeams()) -}) - -router.get('/content-creators.json', async (req, res) => { - getData(req, res, 'content-creators', await req.services.wordpressService.getContentCreators()) -}) - - - -module.exports = router diff --git a/routes/views/leaderboardRouter.js b/routes/views/leaderboardRouter.js deleted file mode 100644 index 2f7aa1b7..00000000 --- a/routes/views/leaderboardRouter.js +++ /dev/null @@ -1,61 +0,0 @@ -const express = require('../../ExpressApp') -const router = express.Router(); -const {AcquireTimeoutError} = require('../../lib/MutexService'); -const middlewares = require('../middleware') -const {AuthFailed} = require('../../lib/ApiErrors') - - -const getLeaderboardId = (leaderboardName) => { - const mapping = { - 'global': 1, - '1v1': 2, - '2v2': 3, - '4v4': 4 - } - - if (leaderboardName in mapping) { - return mapping[leaderboardName] - } - - return null -} - -router.get('/', middlewares.isAuthenticated(), (req, res) => { - return res.render('leaderboards') -}) - -router.get('/:leaderboard.json', middlewares.isAuthenticated(null, true), async (req, res) => { - try { - const leaderboardId = getLeaderboardId(req.params.leaderboard ?? null); - - if (leaderboardId === null) { - return res.status(404).json({error: 'Leaderboard "' + req.params.leaderboard + '" does not exist'}) - } - - return res.json(await req.services.leaderboardService.getLeaderboard(leaderboardId)) - } catch (e) { - if (e instanceof AcquireTimeoutError) { - return res.status(503).json({error: 'timeout reached'}) - } - - if (e instanceof AuthFailed) { - req.logout(function(err) { - if (err) { - throw err - } - }) - - return res.status(400).json({error: 'authentication failed, reload site'}) - } - - console.error('[error] leaderboardRouter::get:leaderboard.json failed with "' + e.toString() + '"') - - if (!res.headersSent) { - return res.status(500).json({error: 'unexpected error'}) - } - - throw e - } -}) - -module.exports = router diff --git a/routes/views/news.js b/routes/views/news.js deleted file mode 100644 index 3531807d..00000000 --- a/routes/views/news.js +++ /dev/null @@ -1,53 +0,0 @@ -const express = require('../../ExpressApp') -const router = express.Router(); - -function getNewsArticleBySlug(articles, slug) { - let [newsArticle] = articles.filter((entry) => { - if (entry.slug === slug) { - return entry - } - }) ?? [] - - return newsArticle ?? null -} - -function getNewsArticleByDeprecatedSlug(articles, slug) { - let [newsArticle] = articles.filter((entry) => { - if (entry.bcSlug === slug) { - return entry - } - }) ?? [] - - return newsArticle ?? null -} - -router.get(`/`, async (req, res) => { - res.render('news', {news: await req.services.wordpressService.getNews()}) -}) - -router.get(`/:slug`, async (req, res) => { - const newsArticles = await req.services.wordpressService.getNews() - - const newsArticle = getNewsArticleBySlug(newsArticles, req.params.slug) - - if (newsArticle === null) { - const newsArticleByOldSlug = getNewsArticleByDeprecatedSlug(newsArticles, req.params.slug) - - if (newsArticleByOldSlug) { - // old slug style, here for backward compatibility - res.redirect(301, newsArticleByOldSlug.slug) - - return - } - - res.redirect(req.baseUrl) - - return - } - - res.render('newsArticle', { - newsArticle: newsArticle - }); -}) - -module.exports = router diff --git a/routes/views/staticMarkdownRouter.js b/routes/views/staticMarkdownRouter.js deleted file mode 100644 index 98267e1c..00000000 --- a/routes/views/staticMarkdownRouter.js +++ /dev/null @@ -1,23 +0,0 @@ -const express = require('../../ExpressApp') -const showdown = require('showdown') -const fs = require('fs') -const router = express.Router() - -function markdown(template) { - return (req, res) => { - res.render('markdown', { - content: new showdown.Converter().makeHtml(fs.readFileSync(template, 'utf-8')) - }) - } -} - -router.get('/privacy', markdown('templates/views/markdown/privacy.md')) -router.get('/privacy-fr', markdown('templates/views/markdown/privacy-fr.md')) -router.get('/privacy-ru', markdown('templates/views/markdown/privacy-ru.md')) -router.get('/tos', markdown('templates/views/markdown/tos.md')) -router.get('/tos-fr', markdown('templates/views/markdown/tos-fr.md')) -router.get('/tos-ru', markdown('templates/views/markdown/tos-ru.md')) -router.get('/rules', markdown('templates/views/markdown/rules.md')) -router.get('/cg', markdown('templates/views/markdown/cg.md')) - -module.exports = router diff --git a/scripts/cron-jobs.js b/scripts/cron-jobs.js deleted file mode 100644 index cb5fe181..00000000 --- a/scripts/cron-jobs.js +++ /dev/null @@ -1,27 +0,0 @@ -const appConfig = require("../config/app") -const WordpressServiceFactory = require("../lib/WordpressServiceFactory"); -const Scheduler = require("../lib/Scheduler"); - -const warmupWordpressCache = async () => { - const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) - - const successHandler = (name) => { - console.info(name, 'cache generated') - } - const errorHandler = (e, name) => { - console.error(name, e.toString(), 'cache failed') - } - - wordpressService.getNews(true).then(() => successHandler('getNews')).catch((e) => errorHandler(e, 'getNews')) - wordpressService.getNewshub(true).then(() => successHandler('getNewshub')).catch((e) => errorHandler(e, 'getNewshub')) - wordpressService.getContentCreators(true).then(() => successHandler('getContentCreators')).catch((e) => errorHandler(e, 'getContentCreators')) - wordpressService.getTournamentNews(true).then(() => successHandler('getTournamentNews')).catch((e) => errorHandler(e, 'getTournamentNews')) - wordpressService.getFafTeams(true).then(() => successHandler('getFafTeams')).catch((e) => errorHandler(e, 'getFafTeams')) -} - -module.exports = async () => { - await warmupWordpressCache() - - const wordpressScheduler = new Scheduler('createWordpressCaches', warmupWordpressCache, 60 * 59 * 1000) - wordpressScheduler.start() -} diff --git a/src/backend/AppKernel.js b/src/backend/AppKernel.js new file mode 100644 index 00000000..879f4cbf --- /dev/null +++ b/src/backend/AppKernel.js @@ -0,0 +1,143 @@ +const { appContainer } = require('./dependency-injection/AppContainer') +const { RequestContainer } = require('./dependency-injection/RequestContainer') +const { RequestContainerCompilerPass } = require('./dependency-injection/RequestContainerCompilerPass') +const { webpackAsset } = require('./middleware/webpackAsset') +const { bootPassport } = require('./security/bootPassport') +const express = require('./ExpressApp') +const appConfig = require('./config/app') +const bodyParser = require('body-parser') +const session = require('express-session') +const flash = require('connect-flash') +const FileStore = require('session-file-store')(session) +const wordpressCacheCrawler = require('./cron-jobs/wordpressCacheCrawler') +const leaderboardCacheCrawler = require('./cron-jobs/leaderboardCacheCrawler') +const defaultRouter = require('./routes/views/defaultRouter') +const authRouter = require('./routes/views/auth') +const staticMarkdownRouter = require('./routes/views/staticMarkdownRouter') +const newsRouter = require('./routes/views/news') +const leaderboardRouter = require('./routes/views/leaderboardRouter') +const clanRouter = require('./routes/views/clanRouter') +const accountRouter = require('./routes/views/accountRouter') +const dataRouter = require('./routes/views/dataRouter') + +class AppKernel { + constructor (nodeEnv = 'production') { + this.env = nodeEnv + this.config = appConfig + this.expressApp = null + this.appContainer = null + this.schedulers = [] + } + + async boot () { + await this.compileContainer(this.config) + this.bootstrapExpress() + return this + } + + async compileContainer (config) { + this.appContainer = appContainer(config) + await this.appContainer.compile() + } + + bootstrapExpress () { + this.expressApp = express() + + this.expressApp.locals.clanInvitations = {} + this.expressApp.use((req, res, next) => { + res.locals.navLinks = [] + res.locals.cNavLinks = [] + res.locals.appGlobals = { + loggedInUser: null + } + next() + }) + + this.expressApp.set('views', 'src/backend/templates/views') + this.expressApp.set('view engine', 'pug') + this.expressApp.use(express.static('public', { + immutable: true, + maxAge: 4 * 60 * 60 * 1000 // 4 hours + })) + + this.expressApp.use('/dist', express.static('dist', { + immutable: true, + maxAge: 4 * 60 * 60 * 1000 // 4 hours, could be longer since we got cache-busting + })) + + this.expressApp.use(express.json()) + this.expressApp.use(bodyParser.json()) + this.expressApp.use(bodyParser.urlencoded({ extended: false })) + this.expressApp.use(webpackAsset(this.appContainer.getParameter('webpackManifestJS'))) + + this.expressApp.use(session({ + resave: false, + saveUninitialized: true, + secret: appConfig.session.key, + store: new FileStore({ + retries: 0, + ttl: appConfig.session.tokenLifespan, + secret: appConfig.session.key + }) + })) + bootPassport(this.expressApp, this.config) + + this.expressApp.use(async (req, res, next) => { + req.appContainer = this.appContainer + req.requestContainer = RequestContainer(this.appContainer, req) + req.requestContainer.addCompilerPass(new RequestContainerCompilerPass(this.config, req)) + await req.requestContainer.compile() + + if (req.requestContainer.fafThrownException) { + return next(req.requestContainer.fafThrownException) + } + + next() + }) + + this.expressApp.use(flash()) + this.expressApp.use((req, res, next) => { + res.locals.message = req.flash() + next() + }) + + this.expressApp.use(function (req, res, next) { + if (req.isAuthenticated()) { + res.locals.appGlobals.loggedInUser = req.requestContainer.get('UserService').getUser() + } + next() + }) + } + + startCronJobs () { + this.schedulers.push(leaderboardCacheCrawler(this.appContainer.get('LeaderboardService'))) + this.schedulers.push(wordpressCacheCrawler(this.appContainer.get('WordpressService'))) + } + + loadControllers () { + this.expressApp.use('/', defaultRouter) + this.expressApp.use('/', authRouter) + this.expressApp.use('/', staticMarkdownRouter) + this.expressApp.use('/news', newsRouter) + this.expressApp.use('/leaderboards', leaderboardRouter) + this.expressApp.use('/clans', clanRouter) + this.expressApp.use('/account', accountRouter) + this.expressApp.use('/data', dataRouter) + + this.expressApp.use((req, res) => { + res.status(404).render('errors/404') + }) + this.expressApp.use((err, req, res, next) => { + console.error('[error] Incoming request to"', req.originalUrl, '"failed with error "', err.toString(), '"') + console.error(err.stack) + + if (res.headersSent) { + return next(err) + } + + res.status(500).render('errors/500') + }) + } +} + +module.exports.AppKernel = AppKernel diff --git a/src/backend/ExpressApp.js b/src/backend/ExpressApp.js new file mode 100644 index 00000000..87479c56 --- /dev/null +++ b/src/backend/ExpressApp.js @@ -0,0 +1,4 @@ +const express = require('express') +require('express-async-errors') + +module.exports = express diff --git a/config/app.js b/src/backend/config/app.js similarity index 78% rename from config/app.js rename to src/backend/config/app.js index 85d6d104..b5e3d021 100644 --- a/config/app.js +++ b/src/backend/config/app.js @@ -1,4 +1,4 @@ -require('dotenv').config(); +require('dotenv').config() const oauthUrl = process.env.OAUTH_URL || 'https://hydra.faforever.com' @@ -16,7 +16,12 @@ const appConfig = { clientSecret: process.env.OAUTH_CLIENT_SECRET || '12345', url: oauthUrl, publicUrl: process.env.OAUTH_PUBLIC_URL || oauthUrl, - callback: process.env.CALLBACK || 'callback', + callback: process.env.CALLBACK || 'callback' + }, + m2mOauth: { + clientId: process.env.OAUTH_M2M_CLIENT_ID || 'faf-website-public', + clientSecret: process.env.OAUTH_M2M_CLIENT_SECRET || 'banana', + url: oauthUrl }, apiUrl: process.env.API_URL || 'https://api.faforever.com', wordpressUrl: process.env.WP_URL || 'https://direct.faforever.com', diff --git a/src/backend/cron-jobs/leaderboardCacheCrawler.js b/src/backend/cron-jobs/leaderboardCacheCrawler.js new file mode 100644 index 00000000..7c15f351 --- /dev/null +++ b/src/backend/cron-jobs/leaderboardCacheCrawler.js @@ -0,0 +1,45 @@ +const Scheduler = require('../services/Scheduler') + +const successHandler = (name) => { + console.debug('[debug] Cache updated', { name }) +} +const errorHandler = (e, name) => { + console.error(e.toString(), { name, entrypoint: 'leaderboardCacheCrawler.js' }) + console.error(e.stack) +} + +const warmupLeaderboard = async (leaderboardService) => { + try { + await leaderboardService.getLeaderboard(1, true) + .then(() => successHandler('leaderboardService::getLeaderboard(global)')) + .catch((e) => errorHandler(e, 'leaderboardService::getLeaderboard(global)')) + + await leaderboardService.getLeaderboard(2, true) + .then(() => successHandler('leaderboardService::getLeaderboard(1v1)')) + .catch((e) => errorHandler(e, 'leaderboardService::getLeaderboard(1v1)')) + + await leaderboardService.getLeaderboard(3, true) + .then(() => successHandler('leaderboardService::getLeaderboard(2v2)')) + .catch((e) => errorHandler(e, 'leaderboardService::getLeaderboard(2v2)')) + + await leaderboardService.getLeaderboard(4, true) + .then(() => successHandler('leaderboardService::getLeaderboard(4v4)')) + .catch((e) => errorHandler(e, 'leaderboardService::getLeaderboard(4v4)')) + } catch (e) { + console.error('Error: leaderboardCacheCrawler::warmupLeaderboard failed with "' + e.toString() + '"', { entrypoint: 'leaderboardCacheCrawler.js' }) + console.error(e.stack) + } +} + +/** + * @param {LeaderboardService} leaderboardService + * @return {Scheduler[]} + */ +module.exports = (leaderboardService) => { + warmupLeaderboard(leaderboardService).then(() => {}) + + const leaderboardScheduler = new Scheduler('createLeaderboardCaches', () => warmupLeaderboard(leaderboardService).then(() => {}), 60 * 59 * 1000) + leaderboardScheduler.start() + + return leaderboardScheduler +} diff --git a/src/backend/cron-jobs/wordpressCacheCrawler.js b/src/backend/cron-jobs/wordpressCacheCrawler.js new file mode 100644 index 00000000..d570dddc --- /dev/null +++ b/src/backend/cron-jobs/wordpressCacheCrawler.js @@ -0,0 +1,50 @@ +const Scheduler = require('../services/Scheduler') + +const successHandler = (name) => { + console.debug('[debug] Cache updated', { name }) +} +const errorHandler = (e, name) => { + console.error(e.toString(), { name, entrypoint: 'wordpressCacheCrawler.js' }) + console.error(e.stack) +} + +const warmupWordpressCache = (wordpressService) => { + try { + wordpressService.getNews(true) + .then(() => successHandler('wordpressService::getNews')) + .catch((e) => errorHandler(e, 'wordpressService::getNews')) + + wordpressService.getNewshub(true) + .then(() => successHandler('wordpressService::getNewshub')) + .catch((e) => errorHandler(e, 'wordpressService::getNewshub')) + + wordpressService.getContentCreators(true) + .then(() => successHandler('wordpressService::getContentCreators')) + .catch((e) => errorHandler(e, 'wordpressService::getContentCreators')) + + wordpressService.getTournamentNews(true) + .then(() => successHandler('wordpressService::getTournamentNews')) + .catch((e) => errorHandler(e, 'wordpressService::getTournamentNews')) + + wordpressService.getFafTeams(true) + .then(() => successHandler('wordpressService::getFafTeams')) + .catch((e) => errorHandler(e, 'wordpressService::getFafTeams')) + } catch (e) { + console.error('Error: wordpressCacheCrawler::warmupWordpressCache failed with "' + e.toString() + '"', { entrypoint: 'wordpressCacheCrawler.js' }) + console.error(e.stack) + } +} + +/** + * @param {WordpressService} wordpressService + * @param {LeaderboardService} leaderboardService + * @return {Scheduler[]} + */ +module.exports = (wordpressService, leaderboardService) => { + warmupWordpressCache(wordpressService) + + const wordpressScheduler = new Scheduler('createWordpressCaches', () => warmupWordpressCache(wordpressService), 60 * 59 * 1000) + wordpressScheduler.start() + + return wordpressScheduler +} diff --git a/src/backend/dependency-injection/AppContainer.js b/src/backend/dependency-injection/AppContainer.js new file mode 100644 index 00000000..8dd98701 --- /dev/null +++ b/src/backend/dependency-injection/AppContainer.js @@ -0,0 +1,54 @@ +const { ContainerBuilder, Reference } = require('node-dependency-injection') +const { LeaderboardRepository } = require('../services/LeaderboardRepository') +const { LeaderboardService } = require('../services/LeaderboardService') +const { JavaApiM2MClient } = require('../services/JavaApiM2MClient') +const { WordpressService } = require('../services/WordpressService') +const { WordpressRepository } = require('../services/WordpressRepository') +const NodeCache = require('node-cache') +const { Axios } = require('axios') +const fs = require('fs') +const webpackManifestJS = JSON.parse(fs.readFileSync('dist/js/manifest.json', 'utf8')) + +/** + * @param {object} appConfig + * @return {ContainerBuilder} + */ +module.exports.appContainer = function (appConfig) { + const container = new ContainerBuilder() + + container.setParameter('webpackManifestJS', webpackManifestJS) + + container.register('NodeCache', NodeCache) + .addArgument({ + stdTTL: 300, // use 5 min for all caches if not changed with ttl + checkperiod: 600 // cleanup memory every 10 min + }) + + container.register('WordpressClient', Axios) + .addArgument({ + baseURL: appConfig.wordpressUrl + }) + + container.register('WordpressRepository', WordpressRepository) + .addArgument(new Reference('WordpressClient')) + + container.register('WordpressService', WordpressService) + .addArgument(new Reference('NodeCache')) + .addArgument(new Reference('WordpressRepository')) + + container.register('JavaApiM2MClient') + .addArgument(appConfig.m2mOauth.clientId) + .addArgument(appConfig.m2mOauth.clientSecret) + .addArgument(appConfig.m2mOauth.url) + .addArgument(appConfig.apiUrl) + .setFactory(JavaApiM2MClient, 'createInstance') + + container.register('LeaderboardRepository', LeaderboardRepository) + .addArgument(new Reference('JavaApiM2MClient')) + + container.register('LeaderboardService', LeaderboardService) + .addArgument(new Reference('NodeCache')) + .addArgument(new Reference('LeaderboardRepository')) + + return container +} diff --git a/src/backend/dependency-injection/RequestContainer.js b/src/backend/dependency-injection/RequestContainer.js new file mode 100644 index 00000000..cbcefb06 --- /dev/null +++ b/src/backend/dependency-injection/RequestContainer.js @@ -0,0 +1,19 @@ +const { ContainerBuilder, Reference } = require('node-dependency-injection') +const { UserRepository } = require('../services/UserRepository') +const { UserService } = require('../services/UserService') + +module.exports.RequestContainer = (appContainer, request) => { + const container = new ContainerBuilder() + + container.setParameter('request', request) + + container.register('UserRepository', UserRepository) + .addArgument(new Reference('JavaApiClient')) + .lazy = true + + container.register('JavaApiClient') + .synthetic = true + container.register('UserService', UserService) + + return container +} diff --git a/src/backend/dependency-injection/RequestContainerCompilerPass.js b/src/backend/dependency-injection/RequestContainerCompilerPass.js new file mode 100644 index 00000000..66a62b59 --- /dev/null +++ b/src/backend/dependency-injection/RequestContainerCompilerPass.js @@ -0,0 +1,24 @@ +const { JavaApiClientFactory } = require('../services/JavaApiClientFactory') + +class RequestContainerCompilerPass { + constructor (appConfig, request = null) { + this.request = request + this.appConfig = appConfig + } + + async process (container) { + try { + if (this.request.user) { + container.get('UserService').setUserFromRequest(this.request) + container.set('JavaApiClient', JavaApiClientFactory.createInstance(container.get('UserService'), this.appConfig.apiUrl, this.request.user.oAuthPassport, this.appConfig.oauth.strategy)) + } + + return container + } catch (e) { + // https://github.com/zazoomauro/node-dependency-injection/issues/208 + container.fafThrownException = e + } + } +} + +module.exports.RequestContainerCompilerPass = RequestContainerCompilerPass diff --git a/src/backend/index.js b/src/backend/index.js new file mode 100644 index 00000000..bf8347eb --- /dev/null +++ b/src/backend/index.js @@ -0,0 +1,11 @@ +const { AppKernel } = require('./AppKernel') + +const kernel = new AppKernel() +kernel.boot() + .then((bootedKernel) => { + bootedKernel.startCronJobs() + bootedKernel.loadControllers() + bootedKernel.expressApp.listen(bootedKernel.config.expressPort, () => { + console.log(`Express listening on port ${bootedKernel.config.expressPort}`) + }) + }) diff --git a/src/backend/middleware/webpackAsset.js b/src/backend/middleware/webpackAsset.js new file mode 100644 index 00000000..8a33e54a --- /dev/null +++ b/src/backend/middleware/webpackAsset.js @@ -0,0 +1,12 @@ +module.exports.webpackAsset = (webpackManifestJS) => { + return (req, res, next) => { + res.locals.webpackAssetJS = (asset) => { + if (asset in webpackManifestJS) { + return webpackManifestJS[asset] + } + + throw new Error('[error] middleware::webpackAsset Failed to find asset "' + asset + '"') + } + next() + } +} diff --git a/src/backend/routes/middleware.js b/src/backend/routes/middleware.js new file mode 100755 index 00000000..cfd579b5 --- /dev/null +++ b/src/backend/routes/middleware.js @@ -0,0 +1,17 @@ +exports.isAuthenticated = (redirectUrlAfterLogin = null, isApiRequest = false) => { + return (req, res, next) => { + if (req.isAuthenticated()) { + return next() + } + + if (req.xhr || req.headers?.accept?.indexOf('json') > -1 || isApiRequest) { + return res.status(401).json({ error: 'Unauthorized' }) + } + + if (req.session) { + req.session.returnTo = redirectUrlAfterLogin || req.originalUrl + } + + return res.redirect('/login') + } +} diff --git a/src/backend/routes/views/account/get/activate.js b/src/backend/routes/views/account/get/activate.js new file mode 100644 index 00000000..94c6606e --- /dev/null +++ b/src/backend/routes/views/account/get/activate.js @@ -0,0 +1,16 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + const flash = null + + // Render the view + locals.username = req.query.username + locals.token = req.query.token + res.render('account/activate', { flash }) +} diff --git a/src/backend/routes/views/account/get/changeEmail.js b/src/backend/routes/views/account/get/changeEmail.js new file mode 100644 index 00000000..ae7e8e27 --- /dev/null +++ b/src/backend/routes/views/account/get/changeEmail.js @@ -0,0 +1,12 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + // Render the view + res.render('account/changeEmail') +} diff --git a/src/backend/routes/views/account/get/changePassword.js b/src/backend/routes/views/account/get/changePassword.js new file mode 100644 index 00000000..04351e85 --- /dev/null +++ b/src/backend/routes/views/account/get/changePassword.js @@ -0,0 +1,12 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + // Render the view + res.render('account/changePassword') +} diff --git a/src/backend/routes/views/account/get/changeUsername.js b/src/backend/routes/views/account/get/changeUsername.js new file mode 100644 index 00000000..49592425 --- /dev/null +++ b/src/backend/routes/views/account/get/changeUsername.js @@ -0,0 +1,12 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + // Render the view + res.render('account/changeUsername') +} diff --git a/src/backend/routes/views/account/get/checkUsername.js b/src/backend/routes/views/account/get/checkUsername.js new file mode 100644 index 00000000..1d814e3c --- /dev/null +++ b/src/backend/routes/views/account/get/checkUsername.js @@ -0,0 +1,19 @@ +const request = require('request') + +exports = module.exports = function (req, res) { + const name = req.query.username + + request(process.env.API_URL + '/data/player?filter=login==' + encodeURI(name), function (error, response, body) { + if (error) { + console.error(error) + return res.status(500).send(error) + } + + try { + const userNameFree = JSON.parse(body).data.length === 0 + return res.status(userNameFree ? 200 : 400).send(userNameFree) + } catch (e) { + return res.status(500).send(e) + } + }) +} diff --git a/src/backend/routes/views/account/get/confirmPasswordReset.js b/src/backend/routes/views/account/get/confirmPasswordReset.js new file mode 100644 index 00000000..eb600c43 --- /dev/null +++ b/src/backend/routes/views/account/get/confirmPasswordReset.js @@ -0,0 +1,16 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + const flash = null + + // Render the view + locals.username = req.query.username + locals.token = req.query.token + res.render('account/confirmPasswordReset', { flash }) +} diff --git a/src/backend/routes/views/account/get/connectSteam.js b/src/backend/routes/views/account/get/connectSteam.js new file mode 100644 index 00000000..4d1e2353 --- /dev/null +++ b/src/backend/routes/views/account/get/connectSteam.js @@ -0,0 +1,50 @@ +const request = require('request') +const flash = {} + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.section = 'account' + const overallRes = res + + request.post({ + url: process.env.API_URL + '/users/buildSteamLinkUrl', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token }, + form: { callbackUrl: req.protocol + '://' + req.get('host') + '/account/link?done' } + }, function (err, res, body) { + if (err) { + flash.class = 'alert-danger' + flash.messages = [{ msg: 'Your steam account was not successfully linked! Please verify you logged into the website correctly.' }] + flash.type = 'Error!' + + return overallRes.render('account/linkSteam', { flash }) + } + // Must not be valid, check to see if errors, otherwise return generic error. + try { + body = JSON.parse(body) + + if (body.steamUrl) { + return overallRes.redirect(body.steamUrl) + } + + const errorMessages = [] + + for (let i = 0; i < body.errors.length; i++) { + const error = body.errors[i] + errorMessages.push({ msg: error.detail }) + } + + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + overallRes.render('account/linkSteam', { flash }) + } catch (e) { + flash.class = 'alert-danger' + flash.messages = [{ msg: 'Your steam account was not successfully linked! Please verify you logged into the website correctly.' }] + flash.type = 'Error!' + + overallRes.render('account/linkSteam', { flash }) + } + }) +} diff --git a/src/backend/routes/views/account/get/createAccount.js b/src/backend/routes/views/account/get/createAccount.js new file mode 100644 index 00000000..0e35c4b3 --- /dev/null +++ b/src/backend/routes/views/account/get/createAccount.js @@ -0,0 +1,24 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + if (req.query.token) { + locals.tokenURL = process.env.API_URL + '/users/activate?token=' + req.query.token + } else { + const flash = {} + flash.type = 'Error!' + flash.class = 'alert-danger' + flash.messages = [{ msg: 'Invalid or missing account token' }] + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return res.redirect('/?flash=' + data) + } + + // Render the view + res.render('account/create') +} diff --git a/src/backend/routes/views/account/get/linkGog.js b/src/backend/routes/views/account/get/linkGog.js new file mode 100644 index 00000000..05ab5363 --- /dev/null +++ b/src/backend/routes/views/account/get/linkGog.js @@ -0,0 +1,45 @@ +const request = require('request') +const error = require('../post/error') + +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + let flash = {} + if (req.query.done !== undefined) { + if (req.query.errors) { + const errors = JSON.parse(req.query.errors) + + flash.class = 'alert-danger' + flash.messages = errors.map(error => ({ msg: error.detail })) + flash.type = 'Error' + } + } else { + flash = null + } + + const overallRes = res + + request.get({ + url: process.env.API_URL + '/users/buildGogProfileToken', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token }, + form: {} + }, function (err, res, body) { + locals.gogToken = 'unable to obtain token' + if (err || res.statusCode !== 200) { + flash = {} + error.parseApiErrors(body, flash) + return overallRes.render('account/linkGog', { flash }) + } + + locals.gogToken = JSON.parse(body).gogToken + + // Render the view + overallRes.render('account/linkGog', { flash }) + }) +} diff --git a/src/backend/routes/views/account/get/linkSteam.js b/src/backend/routes/views/account/get/linkSteam.js new file mode 100644 index 00000000..e29f8458 --- /dev/null +++ b/src/backend/routes/views/account/get/linkSteam.js @@ -0,0 +1,32 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + let flash = {} + if (req.query.done !== undefined) { + if (req.query.errors) { + const errors = JSON.parse(req.query.errors) + + flash.class = 'alert-danger' + flash.messages = errors.map(error => ({ msg: error.detail })) + flash.type = 'Error' + } else { + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your steam account has successfully been linked.' }] + flash.type = 'Success' + } + } else { + flash = null + } + + // locals.steam = process.env.API_URL + '/users/linkToSteam'; + locals.steamConnect = req.protocol + '://' + req.get('host') + '/account/connect' + + // Render the view + res.render('account/linkSteam', { flash }) +} diff --git a/src/backend/routes/views/account/get/register.js b/src/backend/routes/views/account/get/register.js new file mode 100644 index 00000000..a955540b --- /dev/null +++ b/src/backend/routes/views/account/get/register.js @@ -0,0 +1,14 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + const flash = null + + // Render the view + res.render('account/register', { flash, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY }) +} diff --git a/src/backend/routes/views/account/get/report.js b/src/backend/routes/views/account/get/report.js new file mode 100644 index 00000000..7dde48c0 --- /dev/null +++ b/src/backend/routes/views/account/get/report.js @@ -0,0 +1,99 @@ +const getReports = async (javaApiClient) => { + const maxDescriptionLength = 48 + + const response = await javaApiClient.get('/data/moderationReport?include=reportedUsers,lastModerator&sort=-createTime') + + if (response.status !== 200) { + return [] + } + + const reports = JSON.parse(response.data) + const cleanReports = [] + for (const k in reports.data) { + const report = reports.data[k] + + const offenders = [] + for (const l in report.relationships.reportedUsers.data) { + const offender = report.relationships.reportedUsers.data[l] + for (const m in reports.included) { + const user = reports.included[m] + if (user.type === 'player' && user.id === offender.id) { + offenders.push(user.attributes.login) + } + } + } + + let moderator = '' + if (report.relationships.lastModerator.data) { + for (const l in reports.included) { + const user = reports.included[l] + if (user.type === 'player' && user.id === report.relationships.lastModerator.data.id) { + moderator = user.attributes.login + break + } + } + } + + let statusStyle = {} + switch (report.attributes.reportStatus) { + case 'AWAITING': + statusStyle = { color: '#806A15', 'background-color': '#FAD147' } + break + case 'PROCESSING': + statusStyle = { color: '#213A4D', 'background-color': '#3A85BF' } + break + case 'COMPLETED': + statusStyle = { color: 'green', 'background-color': 'lightgreen' } + break + case 'DISCARDED': + statusStyle = { color: '#444444', 'background-color': '#AAAAAA' } + break + } + statusStyle['font-weight'] = 'bold' + + cleanReports.push({ + id: report.id, + offenders: offenders.join(' '), + creationTime: report.attributes.createTime, + game: report.relationships.game.data != null ? '#' + report.relationships.game.data.id : '', + lastModerator: moderator, + description: report.attributes.reportDescription.substr(0, maxDescriptionLength) + (report.attributes.reportDescription.length > maxDescriptionLength ? '...' : ''), + notice: report.attributes.moderatorNotice, + status: report.attributes.reportStatus, + statusStyle + }) + } + + return cleanReports +} + +module.exports = async (req, res) => { + let offendersNames = [] + if (req.query.offenders !== undefined) { + offendersNames = req.query.offenders.split(' ') + } + + let flash = null + + if (req.originalUrl === '/report_submitted') { + flash = { + class: 'alert-success ', + messages: { errors: [{ msg: 'You have successfully submitted your report' }] }, + type: 'Success!' + } + } else if (req.query.flash) { + const buff = Buffer.from(req.query.flash, 'base64') + const text = buff.toString('ascii') + flash = JSON.parse(text) + } + + res.render('account/report', { + section: 'account', + formData: req.body || {}, + game_id: req.query.game_id, + flash, + reports: await getReports(req.requestContainer.get('JavaApiClient')), + reportable_members: {}, + offenders_names: offendersNames + }) +} diff --git a/src/backend/routes/views/account/get/requestPasswordReset.js b/src/backend/routes/views/account/get/requestPasswordReset.js new file mode 100644 index 00000000..fc1b9a07 --- /dev/null +++ b/src/backend/routes/views/account/get/requestPasswordReset.js @@ -0,0 +1,35 @@ +const axios = require('axios') +const appConfig = require('../../../../config/app') + +exports = module.exports = async function (req, res) { + const formData = req.body || {} + + // funky issue: https://stackoverflow.com/questions/69169492/async-external-function-leaves-open-handles-jest-supertest-express + await new Promise(resolve => process.nextTick(resolve)) + + axios.post(appConfig.apiUrl + '/users/buildSteamPasswordResetUrl', {}, { maxRedirects: 0 }).then(response => { + if (response.status !== 200) { + throw new Error('java-api error') + } + + res.render('account/requestPasswordReset', { + section: 'account', + flash: {}, + steamReset: response.data.steamUrl, + formData, + recaptchaSiteKey: appConfig.recaptchaKey + }) + }).catch(error => { + console.error(error.toString()) + res.render('account/requestPasswordReset', { + section: 'account', + flash: { + class: 'alert-danger', + messages: 'issue resetting', + type: 'Error!' + }, + formData, + recaptchaSiteKey: appConfig.recaptchaKey + }) + }) +} diff --git a/src/backend/routes/views/account/get/resync.js b/src/backend/routes/views/account/get/resync.js new file mode 100644 index 00000000..a89fb287 --- /dev/null +++ b/src/backend/routes/views/account/get/resync.js @@ -0,0 +1,32 @@ +const flash = {} +const request = require('request') +const error = require('../post/error') + +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'account' + + locals.formData = req.body || {} + + const overallRes = res + + request.post({ + url: process.env.API_URL + '/users/resyncAccount', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + } else { + // Successfully account resync + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your account was resynced successfully.' }] + flash.type = 'Success!' + } + + overallRes.render('account/confirmResyncAccount', { flash }) + } + ) +} diff --git a/src/backend/routes/views/account/post/activate.js b/src/backend/routes/views/account/post/activate.js new file mode 100644 index 00000000..7e311276 --- /dev/null +++ b/src/backend/routes/views/account/post/activate.js @@ -0,0 +1,53 @@ +const flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + locals.username = req.query.username + locals.token = req.query.token + + locals.formData = req.body || {} + + // validate the input + check('password', 'Password is required').notEmpty() + check('password', 'Password must be six or more characters').isLength({ min: 6 }) + check('password', 'Passwords don\'t match').equals(req.body.password_confirm) + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/activate', { flash }) + } else { + const token = req.query.token + const password = req.body.password + + const overallRes = res + + // Run post to reset endpoint + request.post({ + url: process.env.API_URL + '/users/activate', + form: { password, token } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + return overallRes.render('account/activate', { flash }) + } + + // Successfully reset password + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your account was created successfully.' }] + flash.type = 'Success!' + + overallRes.render('account/activate', { flash }) + } + ) + } +} diff --git a/src/backend/routes/views/account/post/changeEmail.js b/src/backend/routes/views/account/post/changeEmail.js new file mode 100644 index 00000000..2f327f37 --- /dev/null +++ b/src/backend/routes/views/account/post/changeEmail.js @@ -0,0 +1,52 @@ +const flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + // validate the input + check('email', 'Email is required').notEmpty() + check('email', 'Email does not appear to be valid').isEmail() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + // failure + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/changeEmail', { flash }) + } else { + // pull the form variables off the request body + + const email = req.body.email + const password = req.body.password + + const overallRes = res + + request.post({ + url: `${process.env.API_URL}/users/changeEmail`, + headers: { Authorization: `Bearer ${req.services.userService.getUser()?.oAuthPassport.token}` }, + form: { newEmail: email, currentPassword: password } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + return overallRes.render('account/changeEmail', { flash }) + } + + // Successfully changed email + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your email was set successfully.' }] + flash.type = 'Success!' + + overallRes.render('account/changeEmail', { flash }) + }) + } +} diff --git a/src/backend/routes/views/account/post/changePassword.js b/src/backend/routes/views/account/post/changePassword.js new file mode 100644 index 00000000..dab8c169 --- /dev/null +++ b/src/backend/routes/views/account/post/changePassword.js @@ -0,0 +1,58 @@ +const flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + // validate the input + check('old_password', 'Old Password is required').notEmpty() + check('old_password', 'Old Password must be six or more characters').isLength({ min: 6 }) + check('password', 'New Password is required').notEmpty() + check('password', 'New Password must be six or more characters').isLength({ min: 6 }) + check('password', 'New Passwords don\'t match').equals(req.body.password_confirm) + check('username', 'Username is required').notEmpty() + check('username', 'Username must be three or more characters').isLength({ min: 3 }) + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + // failure + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/changePassword', { flash }) + } else { + // Encrypt password before sending it off to endpoint + const newPassword = req.body.password + const oldPassword = req.body.old_password + const username = req.body.username + + const overallRes = res + + // Run post to reset endpoint + request.post({ + url: process.env.API_URL + '/users/changePassword', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token }, + form: { name: username, currentPassword: oldPassword, newPassword } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + return overallRes.render('account/changePassword', { flash }) + } + + // Successfully reset password + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your password was changed successfully. Please use the new password to log in!' }] + flash.type = 'Success!' + + overallRes.render('account/changePassword', { flash }) + }) + } +} diff --git a/src/backend/routes/views/account/post/changeUsername.js b/src/backend/routes/views/account/post/changeUsername.js new file mode 100644 index 00000000..d66db6b8 --- /dev/null +++ b/src/backend/routes/views/account/post/changeUsername.js @@ -0,0 +1,50 @@ +const flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + // validate the input + check('username', 'Username is required').notEmpty() + check('username', 'Username must be three or more characters').isLength({ min: 3 }) + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + // failure + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/changeUsername', { flash }) + } else { + // pull the form variables off the request body + const username = req.body.username + const overallRes = res + + // Run post to reset endpoint + request.post({ + url: process.env.API_URL + '/users/changeUsername', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token }, + form: { newUsername: username } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + return overallRes.render('account/changeUsername', { flash }) + } + + // Successfully changed username + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your username was changed successfully. Please use the new username to log in!' }] + flash.type = 'Success!' + + overallRes.render('account/changeUsername', { flash }) + }) + } +} diff --git a/src/backend/routes/views/account/post/confirmPasswordReset.js b/src/backend/routes/views/account/post/confirmPasswordReset.js new file mode 100644 index 00000000..52e42c94 --- /dev/null +++ b/src/backend/routes/views/account/post/confirmPasswordReset.js @@ -0,0 +1,53 @@ +const flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + locals.username = req.query.username + locals.token = req.query.token + + locals.formData = req.body || {} + + // validate the input + check('password', 'Password is required').notEmpty() + check('password', 'Password must be six or more characters').isLength({ min: 6 }) + check('password', 'Passwords don\'t match').equals(req.body.password_confirm) + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/confirmPasswordReset', { flash }) + } else { + const token = req.query.token + const newPassword = req.body.password + + const overallRes = res + + // Run post to reset endpoint + request.post({ + url: process.env.API_URL + '/users/performPasswordReset', + form: { newPassword, token } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + return overallRes.render('account/confirmPasswordReset', { flash }) + } + + // Successfully reset password + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your password was changed successfully.' }] + flash.type = 'Success!' + + overallRes.render('account/confirmPasswordReset', { flash }) + } + ) + } +} diff --git a/src/backend/routes/views/account/post/error.js b/src/backend/routes/views/account/post/error.js new file mode 100644 index 00000000..175092da --- /dev/null +++ b/src/backend/routes/views/account/post/error.js @@ -0,0 +1,21 @@ +module.exports = { + parseApiErrors: function (body, flash) { + const errorMessages = [] + + try { + const response = JSON.parse(body) + response.errors.forEach(error => errorMessages.push({ msg: error.detail })) + } catch (e) { + errorMessages.push({ msg: 'An unknown error occurred. Please try again later or ask the support.' }) + console.log('Error on parsing server response: ' + body) + } + + if (errorMessages.length === 0) { + errorMessages.push({ msg: 'An unknown error occurred. Please try again later or ask the support.' }) + } + + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + } +} diff --git a/src/backend/routes/views/account/post/linkGog.js b/src/backend/routes/views/account/post/linkGog.js new file mode 100644 index 00000000..31fc1c82 --- /dev/null +++ b/src/backend/routes/views/account/post/linkGog.js @@ -0,0 +1,69 @@ +let flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + // validate the input + check('gog_username', 'Username is required').notEmpty() + check('gog_username', 'Username must be at least 3 characters').isLength({ min: 3 }) + check('gog_username', 'Username must be at most 100 characters').isLength({ max: 100 }) + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + // failure + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/linkGog', { flash }) + } else { + const gogUsername = req.body.gog_username // this is obtained from the form field in the mixin, not the pug file of this page! + + const overallRes = res + + request.post({ + url: process.env.API_URL + '/users/linkToGog', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token }, + form: { gogUsername } + }, function (err, res, body) { + if (!err && res.statusCode === 200) { + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your accounts were linked successfully.' }] + flash.type = 'Success!' + + locals.gogToken = '-' + overallRes.render('account/linkGog', { flash }) + } else { + error.parseApiErrors(body, flash) + + // We need the gog token on the error page as well, + // this code literally does the same as linkGog.js, but due to the architectural structure of this application + // it's not possible to extract it into a separate function while saving any code + request.get({ + url: process.env.API_URL + '/users/buildGogProfileToken', + headers: { Authorization: 'Bearer ' + req.services.userService.getUser()?.oAuthPassport.token }, + form: {} + }, function (err, res, body) { + locals.gogToken = 'unable to obtain token' + if (err || res.statusCode !== 200) { + flash = {} + error.parseApiErrors(body, flash) + return overallRes.render('account/linkGog', { flash }) + } + + locals.gogToken = JSON.parse(body).gogToken + + return overallRes.render('account/linkGog', { flash }) + }) + } + }) + } +} diff --git a/src/backend/routes/views/account/post/register.js b/src/backend/routes/views/account/post/register.js new file mode 100644 index 00000000..54f337b8 --- /dev/null +++ b/src/backend/routes/views/account/post/register.js @@ -0,0 +1,77 @@ +const flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') +require('dotenv').config() + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + // validate the input + check('username', 'Username is required').notEmpty() + check('username', 'Username must be three or more characters').isLength({ min: 3 }) + check('email', 'Email is required').notEmpty() + check('email', 'Email does not appear to be valid').isEmail() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + // failure + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/register', { flash }) + } else { + // pull the form variables off the request body + const username = req.body.username + const email = req.body.email + const recaptchaResponse = req.body['g-recaptcha-response'] + + const overallRes = res + + // Run post to register endpoint + request.post({ + url: process.env.API_URL + '/users/register', + form: { username, email, recaptchaResponse } + }, function (err, res, body) { + let resp + const errorMessages = [] + + if (err || res.statusCode !== 200) { + try { + resp = JSON.parse(body) + } catch (e) { + errorMessages.push({ msg: 'Invalid registration sign up. Please try again later.' }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + return overallRes.render('account/register', { flash, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY }) + } + + // Failed registering user + for (let i = 0; i < resp.errors.length; i++) { + const error = resp.errors[i] + + errorMessages.push({ msg: error.detail }) + } + + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + return overallRes.render('account/register', { flash, recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY }) + } + + // Successfully registered user + flash.class = 'alert-success' + flash.messages = [{ msg: 'Please check your email to verify your registration. Then you will be ready to log in!' }] + flash.type = 'Success!' + + overallRes.render('account/register', { flash }) + }) + } +} diff --git a/src/backend/routes/views/account/post/report.js b/src/backend/routes/views/account/post/report.js new file mode 100644 index 00000000..0eb05d3b --- /dev/null +++ b/src/backend/routes/views/account/post/report.js @@ -0,0 +1,126 @@ +const { validationResult, body, matchedData } = require('express-validator') +const getReportRoute = require('../get/report') + +exports = module.exports = [ + body('offender', 'Offender invalid').isString().isLength({ min: 2, max: 50 }).trim().escape(), + body('report_description', 'Please describe the incident').notEmpty().isLength({ min: 2, max: 2000 }).trim().escape(), + body('game_id', 'Please enter a valid game ID, or nothing. The # is not needed.').optional({ values: 'falsy' }).isDecimal(), + async function (req, res) { + const javaApiClient = req.requestContainer.get('JavaApiClient') + const errors = validationResult(req) + + if (!errors.isEmpty()) { + req.query.flash = Buffer.from(JSON.stringify({ + class: 'alert-danger', + messages: errors, + type: 'Error!' + })).toString('base64') + + return getReportRoute(req, res) + } + + const formData = matchedData(req) + + let apiUsers + try { + const userFetch = await javaApiClient.get('/data/player?filter=login==' + encodeURIComponent(formData.offender) + '&fields[player]=login&page[size]=1') + + if (userFetch.status !== 200) { + throw new Error('issues getting players') + } + + apiUsers = JSON.parse(userFetch.data) + } catch (e) { + req.query.flash = Buffer.from(JSON.stringify({ + class: 'alert-danger', + messages: { errors: [{ msg: 'Error while fetching offender' }] }, + type: 'Error!' + })).toString('base64') + + return getReportRoute(req, res) + } + + // Mapping users to their IDs + let offenderId = null + apiUsers.data.forEach((user) => { + if (user.attributes.login.toUpperCase() === formData.offender.toUpperCase()) { + offenderId = user.id + } + }) + + if (!offenderId) { + req.query.flash = Buffer.from(JSON.stringify({ + class: 'alert-danger', + messages: { errors: [{ msg: 'The following user could not be found : ' + formData.offender }] }, + type: 'Error!' + })).toString('base64') + + return getReportRoute(req, res) + } + + // Checking the game exists + if (formData.game_id != null) { + try { + const response = await javaApiClient.get('/data/game?filter=id==' + encodeURIComponent(formData.game_id)) + if (response.status !== 200) { + throw new Error('issues getting game') + } + } catch (e) { + req.query.flash = Buffer.from(JSON.stringify({ + class: 'alert-danger', + messages: { errors: [{ msg: 'The game could not be found. Please check the game ID you provided.' }] }, + type: 'Error!' + })).toString('base64') + + return getReportRoute(req, res) + } + } + + const relationShips = { + reportedUsers: { + data: [{ + type: 'player', + id: '' + offenderId + }] + } + } + + if (formData.game_id != null) { + relationShips.game = { data: { type: 'game', id: '' + formData.game_id } } + } + + const report = + { + data: [ + { + type: 'moderationReport', + attributes: { + gameIncidentTimecode: (formData.game_timecode ? formData.game_timecode : null), + reportDescription: formData.report_description + }, + relationships: relationShips + } + ] + } + + const resp = await javaApiClient.post('/data/moderationReport', JSON.stringify(report), { + headers: { + 'Content-Type': 'application/vnd.api+json', + Accept: 'application/vnd.api+json' + } + }) + + if (resp.status !== 201) { + const apiError = JSON.parse(resp.data) + req.query.flash = Buffer.from(JSON.stringify({ + class: 'alert-danger', + messages: { errors: [{ msg: 'Error while submitting the report form' }, { msg: apiError.errors?.[0]?.detail || 'unknown api error' }] }, + type: 'Error!' + })).toString('base64') + + return getReportRoute(req, res) + } + + res.redirect('../report_submitted') + } +] diff --git a/src/backend/routes/views/account/post/requestPasswordReset.js b/src/backend/routes/views/account/post/requestPasswordReset.js new file mode 100644 index 00000000..7ce18670 --- /dev/null +++ b/src/backend/routes/views/account/post/requestPasswordReset.js @@ -0,0 +1,54 @@ +const flash = {} +const request = require('request') +const error = require('./error') +const { check, validationResult } = require('express-validator') + +exports = module.exports = function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + // validate the input + check('usernameOrEmail', 'Username or email is required').notEmpty() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + res.render('account/requestPasswordReset', { flash }) + } else { + const identifier = req.body.usernameOrEmail + const recaptchaResponse = req.body['g-recaptcha-response'] + + const overallRes = res + + // Run post to reset endpoint + request.post({ + url: process.env.API_URL + '/users/requestPasswordReset', + form: { identifier, recaptchaResponse } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + error.parseApiErrors(body, flash) + return overallRes.render('account/requestPasswordReset', { + flash, + recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY + }) + } + + // Successfully reset password + flash.class = 'alert-success' + flash.messages = [{ msg: 'Your password is in the process of being reset, please reset your password by clicking on the link provided in an email.' }] + flash.type = 'Success!' + + overallRes.render('account/requestPasswordReset', { + flash, + recaptchaSiteKey: process.env.RECAPTCHA_SITE_KEY + }) + }) + } +} diff --git a/routes/views/accountRouter.js b/src/backend/routes/views/accountRouter.js similarity index 95% rename from routes/views/accountRouter.js rename to src/backend/routes/views/accountRouter.js index d4c29e04..28d4330b 100644 --- a/routes/views/accountRouter.js +++ b/src/backend/routes/views/accountRouter.js @@ -1,8 +1,7 @@ const express = require('../../ExpressApp') -const router = express.Router(); +const router = express.Router() const middlewares = require('../middleware') - router.get('/linkGog', middlewares.isAuthenticated(), require('./account/get/linkGog')) router.post('/linkGog', middlewares.isAuthenticated(), require('./account/post/linkGog')) @@ -24,10 +23,9 @@ router.post('/password/confirmReset', require('./account/post/confirmPasswordRes router.get('/requestPasswordReset', require('./account/get/requestPasswordReset')) router.post('/requestPasswordReset', require('./account/post/requestPasswordReset')) -//still used in other applications (user-service, game-client etc.) +// still used in other applications (user-service, game-client etc.) router.get('/password/reset', (req, res) => res.redirect('/account/requestPasswordReset')) - router.get('/register', require('./account/get/register')) router.post('/register', require('./account/post/register')) diff --git a/routes/views/auth.js b/src/backend/routes/views/auth.js similarity index 81% rename from routes/views/auth.js rename to src/backend/routes/views/auth.js index c8393e4a..75d5f00e 100644 --- a/routes/views/auth.js +++ b/src/backend/routes/views/auth.js @@ -3,16 +3,16 @@ const passport = require('passport') const express = require('../../ExpressApp') const router = express.Router() -router.get('/login', passport.authenticate(appConfig.oauth.strategy)); +router.get('/login', passport.authenticate(appConfig.oauth.strategy)) router.get( '/' + appConfig.oauth.callback, - (req, res, next)=>{ + (req, res, next) => { res.locals.returnTo = req.session.returnTo - + return next() }, - passport.authenticate(appConfig.oauth.strategy, {failureRedirect: '/login', failureFlash: true}), + passport.authenticate(appConfig.oauth.strategy, { failureRedirect: '/login', failureFlash: true }), (req, res) => { res.redirect(res.locals.returnTo || '/') } diff --git a/src/backend/routes/views/checkUsername.js b/src/backend/routes/views/checkUsername.js new file mode 100644 index 00000000..1d814e3c --- /dev/null +++ b/src/backend/routes/views/checkUsername.js @@ -0,0 +1,19 @@ +const request = require('request') + +exports = module.exports = function (req, res) { + const name = req.query.username + + request(process.env.API_URL + '/data/player?filter=login==' + encodeURI(name), function (error, response, body) { + if (error) { + console.error(error) + return res.status(500).send(error) + } + + try { + const userNameFree = JSON.parse(body).data.length === 0 + return res.status(userNameFree ? 200 : 400).send(userNameFree) + } catch (e) { + return res.status(500).send(e) + } + }) +} diff --git a/routes/views/clanRouter.js b/src/backend/routes/views/clanRouter.js similarity index 54% rename from routes/views/clanRouter.js rename to src/backend/routes/views/clanRouter.js index c42c8f21..c6974225 100644 --- a/routes/views/clanRouter.js +++ b/src/backend/routes/views/clanRouter.js @@ -1,7 +1,7 @@ const express = require('../../ExpressApp') -const router = express.Router(); +const router = express.Router() // This will be replaced soon, therefor I did not spend time on it -router.get('*', (req, res) => res.status(503).render('errors/503-known-issue')); +router.get('*', (req, res) => res.status(503).render('errors/503-known-issue')) module.exports = router diff --git a/src/backend/routes/views/clans/get/accept_invite.js b/src/backend/routes/views/clans/get/accept_invite.js new file mode 100644 index 00000000..9b11368b --- /dev/null +++ b/src/backend/routes/views/clans/get/accept_invite.js @@ -0,0 +1,92 @@ +const request = require('request') + +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'clan' + const flash = {} + + if (!req.query.i) { + flash.type = 'Error!' + flash.class = 'alert-danger' + flash.messages = [{ msg: 'The invitation link is wrong or truncated. Key informations are missing.' }] + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return res.redirect('/clans?flash=' + data) + } + + const invitationId = req.query.i + + if (!req.app.locals.clanInvitations[invitationId]) { + flash.type = 'Error!' + flash.class = 'alert-danger' + flash.messages = [{ msg: 'The invitation link is wrong or truncated. Invite code missing from website clan map.' }] + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return res.redirect('/clans?flash=' + data) + } + + const invite = req.app.locals.clanInvitations[invitationId] + const clanId = invite.clan + + if (req.user.data.attributes.clan != null) { + // User is already in a clan! + return res.redirect(`/clans/${req.user.data.attributes.clan.tag}?member=true`) + } + + const queryUrl = process.env.API_URL + + '/data/clan/' + clanId + + '?include=memberships.player' + + '&fields[clan]=createTime,description,name,tag,updateTime,websiteUrl,founder,leader' + + '&fields[player]=login,updateTime' + + request.get( + { + url: queryUrl + }, + function (err, childRes, body) { + const clan = JSON.parse(body) + + if (err || !clan.data) { + flash.type = 'Error!' + flash.class = 'alert-danger' + flash.messages = [{ msg: 'The clan you want to join is invalid or does no longer exist' }] + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return res.redirect('./?flash=' + data) + } + + locals.clanName = clan.data.attributes.name + locals.clanLeaderName = '' + + for (const k in clan.included) { + let player = null + switch (clan.included[k].type) { + case 'player': + player = clan.included[k] + + // Getting the leader name + if (player.id === clan.data.relationships.leader.data.id) { + locals.clanLeaderName = player.attributes.login + } + + break + } + } + + const token = invite.token + locals.acceptURL = `/clans/join?clan_id=${clanId}&token=${token}` + + // Render the view + res.render('clans/accept_invite') + } + ) +} diff --git a/src/backend/routes/views/clans/get/create.js b/src/backend/routes/views/clans/get/create.js new file mode 100644 index 00000000..0b2367f1 --- /dev/null +++ b/src/backend/routes/views/clans/get/create.js @@ -0,0 +1,53 @@ +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'clan' + + const request = require('request') + + request.get( + { + url: process.env.API_URL + '/clans/me', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + function (err, childRes, body) { + if (err) { + return res.redirect('/clans/manage') + } + + const clanInfo = JSON.parse(body) + if (clanInfo.clan != null) { + res.redirect('/clans/manage') + return + } + + locals.formData = req.body || {} + locals.clan_name = req.query.clan_name || '' + locals.clan_tag = req.query.clan_tag || '' + locals.clan_description = req.query.clan_description || '' + locals.clan_create_time = (new Date()).toUTCString() + + let flash = null + + if (req.query.flash) { + const buff = Buffer.from(req.query.flash, 'base64') + const text = buff.toString('ascii') + + try { + flash = JSON.parse(text) + } catch (e) { + console.error('Parsing error while trying to decode a flash error: ' + text) + console.error(e) + flash = [{ msg: 'Unknown error' }] + } + } + + // Render the view + res.render('clans/create', { flash }) + } + ) +} diff --git a/src/backend/routes/views/clans/get/manage.js b/src/backend/routes/views/clans/get/manage.js new file mode 100755 index 00000000..2af57b70 --- /dev/null +++ b/src/backend/routes/views/clans/get/manage.js @@ -0,0 +1,129 @@ +const request = require('request') + +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'clan' + + let flash = null + + let clanMembershipId = null + try { + clanMembershipId = req.user.data.attributes.clan.membershipId + } catch { + // The user doesnt belong to a clan + res.redirect('/clans') + return + } + + // In case the user has just generated an invite link + if (req.query.invitation_id) { + flash = {} + flash.class = 'alert-invite' + + flash.messages = [ + { + msg: + `

Right click on me and copy the invitation link

Note: It only works for the user you typed.` + } + ] + flash.type = '' + } + + request.get( + { + url: + process.env.API_URL + + '/data/clanMembership/' + clanMembershipId + '/clan' + + '?include=memberships.player' + + '&fields[clan]=createTime,description,name,tag,updateTime,websiteUrl,founder,leader' + + '&fields[player]=login,updateTime' + + '&fields[clanMembership]=createTime,player', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + function (err, childRes, body) { + const clan = JSON.parse(body) + + if (err || !clan.data) { + flash = {} + flash.class = 'alert-danger' + flash.messages = [{ msg: 'Unknown error while retrieving your clan information' }] + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return res.redirect('/clans?flash=' + data) + } + + if (clan.data.relationships.leader.data.id !== req.user.data.id) { + // Not the leader! Shouldn't be able to manage stuff + res.redirect(`/clans/${req.user.data.attributes.clan.tag}?member=true`) + return + } + + locals.clan_name = clan.data.attributes.name + locals.clan_tag = clan.data.attributes.tag + locals.clan_description = clan.data.attributes.description + locals.clan_create_time = clan.data.attributes.createTime + locals.me = req.user.data.id + locals.clan_id = clan.data.id + locals.clan_link = process.env.HOST + '/clans/see?id=' + clan.data.id + + const members = {} + let player = null + let membership = null + let member = null + + for (const k in clan.included) { + switch (clan.included[k].type) { + case 'player': + player = clan.included[k] + if (!members[player.id]) members[player.id] = {} + members[player.id].id = player.id + members[player.id].name = player.attributes.login + + if (clan.data.relationships.founder.data.id === player.id) { + locals.founder_name = player.attributes.login + } + break + + case 'clanMembership': + membership = clan.included[k] + member = membership.relationships.player.data + if (!members[member.id]) members[member.id] = {} + members[member.id].id = member.id + members[member.id].membershipId = membership.id + members[member.id].joinedAt = membership.attributes.createTime + break + } + } + + locals.clan_members = members + + if (req.originalUrl === '/clan_created') { + flash = {} + flash.class = 'alert-success' + flash.messages = [{ msg: 'You have successfully created your clan' }] + flash.type = 'Success!' + } else if (req.query.flash) { + const buff = Buffer.from(req.query.flash, 'base64') + const text = buff.toString('ascii') + try { + flash = JSON.parse(text) + } catch (e) { + console.error('Parsing error while trying to decode a flash error: ' + text) + console.error(e) + flash = [{ msg: 'Unknown error' }] + } + } + + // Render the view + res.render('clans/manage', { flash }) + } + ) +} diff --git a/src/backend/routes/views/clans/post/create.js b/src/backend/routes/views/clans/post/create.js new file mode 100755 index 00000000..7b352590 --- /dev/null +++ b/src/backend/routes/views/clans/post/create.js @@ -0,0 +1,96 @@ +const flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({ max: 3 }) + check('clan_description', 'Please add a description for your clan').notEmpty().isLength({ max: 1000 }) + check('clan_name', "Please indicate your clan's name").notEmpty().isLength({ max: 40 }) + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('create?flash=' + data) + } else { + const clanName = req.body.clan_name + const clanTag = req.body.clan_tag + const clanDescription = req.body.clan_description + + const queryUrl = + process.env.API_URL + + '/clans/create' + + '?name=' + encodeURIComponent(clanName) + + '&tag=' + encodeURIComponent(clanTag) + + '&description=' + encodeURIComponent(clanDescription) + + // Run post to endpoint + request.post({ + url: queryUrl, + body: '', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, function (err, res, body) { + const errorMessages = [] + + if (err || res.statusCode !== 200) { + let msg = 'Error while creating the clan' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch { + } + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('create?flash=' + data + '&clan_name=' + clanName + '&clan_tag=' + clanTag + '&clan_description=' + clanDescription + '') + } + + // Refreshing user + request.get({ + url: process.env.API_URL + '/me', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + function (err, res, body) { + if (err) { + console.error('There was an error updating a session after a clan creation') + + return + } + try { + const user = JSON.parse(body) + user.data.attributes.token = req.user.data.attributes.token + user.data.id = user.data.attributes.userId + req.logIn(user, function (err) { + if (err) console.error(err) + return overallRes.redirect('/clans/manage') + }) + } catch { + console.error('There was an error updating a session after a clan creation') + } + }) + }) + } +} diff --git a/src/backend/routes/views/clans/post/destroy.js b/src/backend/routes/views/clans/post/destroy.js new file mode 100755 index 00000000..786c878c --- /dev/null +++ b/src/backend/routes/views/clans/post/destroy.js @@ -0,0 +1,96 @@ +let flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } else { + // Building update query + const queryUrl = + process.env.API_URL + + '/data/clan/' + req.body.clan_id + + // Run post to endpoint + request.delete({ + url: queryUrl, + body: '', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, function (err, res, body) { + const errorMessages = [] + + if (err || res.statusCode !== 204) { + let msg = 'Error while destroying the clan' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch {} + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } + + flash = {} + flash.class = 'alert-success' + flash.messages = [{ msg: 'The clan was successfully destroyed' }] + flash.type = 'Success!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + // Refreshing user + request.get({ + url: process.env.API_URL + '/me', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + + function (err, res, body) { + if (err) { + console.error('There was an error updating a session after a clan destruction') + + return + } + try { + const user = JSON.parse(body) + user.data.id = user.data.attributes.userId + user.data.attributes.token = req.user.data.attributes.token + req.logIn(user, function (err) { + if (err) console.error(err) + return overallRes.redirect('/clans?flash=' + data) + }) + } catch { + console.error('There was an error updating a session after a clan destruction') + } + }) + }) + } +} diff --git a/src/backend/routes/views/clans/post/invite.js b/src/backend/routes/views/clans/post/invite.js new file mode 100644 index 00000000..bc6bbe5b --- /dev/null +++ b/src/backend/routes/views/clans/post/invite.js @@ -0,0 +1,143 @@ +const flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +function promiseRequest (url) { + return new Promise(function (resolve, reject) { + request(url, function (error, res, body) { + if (!error && res.statusCode < 300) { + resolve(body) + } else { + console.error('Call to ' + url + ' failed: ' + error) + reject(error) + } + }) + }) +} + +function setLongTimeout (func, delayMs) { + const maxDelay = 214748364 - 1 // JS Limit for 32 bit integers + + if (delayMs > maxDelay) { + const remainingDelay = delayMs - maxDelay + + // we cut it in smaller, edible chunks + setTimeout(() => { + setLongTimeout(func, remainingDelay) + }, maxDelay) + } else { + setTimeout(func, delayMs) + } +} + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('invited_player', 'Please indicate the player name').notEmpty() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } else { + const clanId = req.body.clan_id + const userName = req.body.invited_player + + // Let's check first that the player exists + const fetchRoute = process.env.API_URL + '/data/player?filter=login=="' + userName + '"&fields[player]=' + + let playerData = null + let playerId = null + try { + const httpData = await promiseRequest(fetchRoute) + playerData = JSON.parse(httpData).data + playerId = playerData[0].id + } catch (e) { + flash.class = 'alert-danger' + flash.messages = [{ msg: 'The player ' + userName + " doesn't seem to exist" + e }] + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } + + const queryUrl = + process.env.API_URL + + '/clans/generateInvitationLink' + + '?clanId=' + encodeURIComponent(clanId) + + '&playerId=' + encodeURIComponent(playerId) + + // Run post to endpoint + request.get({ + url: queryUrl, + body: '', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, function (err, res, body) { + if (err || res.statusCode !== 200) { + const errorMessages = [] + let msg = 'Error while generating the invite link' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch { + } + + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + return overallRes.redirect('manage?flash=' + data) + } else { + try { + const token = JSON.parse(res.body).jwtToken + + const id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5).toUpperCase() + + req.app.locals.clanInvitations[id] = { + token, + clan: clanId + } + + // We use timeout here because if we delete the invite link whenver the page is GET, + // then discord and other messaging applications will destroy the link accidentally + // when pre-fetching the page. So we will delete it later. Regardless if the website is restarted all the links will be + // killed instantly, which is fine. They are short lived by design. + const lifespan = process.env.CLAN_INVITES_LIFESPAN_DAYS * 24 * 3600 * 1000 + setLongTimeout(() => { + delete req.app.locals.clanInvitations[id] + console.log(`Killed invitation with id ${id} after having waited ${lifespan} seconds (${process.env.CLAN_INVITES_LIFESPAN_DAYS} days)`) + }, lifespan) + + return overallRes.redirect('manage?invitation_id=' + id) + } catch (e) { + flash.class = 'alert-danger' + flash.messages = [{ msg: 'Unkown error while generating the invite link: ' + e }] + flash.type = 'Error!' + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + return overallRes.redirect('manage?flash=' + data) + } + } + }) + } +} diff --git a/src/backend/routes/views/clans/post/join.js b/src/backend/routes/views/clans/post/join.js new file mode 100644 index 00000000..936d0490 --- /dev/null +++ b/src/backend/routes/views/clans/post/join.js @@ -0,0 +1,93 @@ +const request = require('request') + +exports = module.exports = function (req, res) { + const locals = res.locals + + // locals.section is used to set the currently selected + // item in the header navigation. + locals.section = 'clan' + + const flash = {} + const overallRes = res + + if (!req.query.token || !req.query.clan_id) { + flash.type = 'Error!' + flash.class = 'alert-danger' + flash.messages = [{ msg: 'The invitation link is invalid!' }] + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return res.redirect('/clans?flash=' + data + '') + } + + const token = req.query.token + + request.post( + { + url: process.env.API_URL + '/clans/joinClan?token=' + token, + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + function (err, childRes, body) { + if (err) { + console.error('There was an error at join') + + return + } + + let flashData + if (childRes.statusCode === 200 || childRes.statusCode === 201) { + flash.class = 'alert-success' + flash.messages = [ + { msg: 'Welcome to your new clan!' } + ] + flash.type = 'Success!' + const buff = Buffer.from(JSON.stringify(flash)) + flashData = buff.toString('base64') + + // Refreshing user + return request.get({ + url: process.env.API_URL + '/me', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + + function (err, res, body) { + if (err) { + console.error('There was an error updating a session after an user left a clan') + + return + } + try { + const user = JSON.parse(body) + user.data.id = user.data.attributes.userId + user.data.attributes.token = req.user.data.attributes.token + req.logIn(user, function (err) { + if (err) console.error(err) + return overallRes.redirect(`${user.data.attributes.clan.tag}?member=true&flash=${flashData}`) + }) + } catch { + console.error('There was an error updating a session after an user left a clan') + } + }) + } else { + flash.type = 'Error!' + flash.class = 'alert-danger' + let msg = 'The invitation is invalid or has expired, or you are already part of a clan' + try { + msg += ': ' + JSON.stringify(JSON.parse(childRes.body).errors[0].detail) + } catch {} + + flash.messages = [{ msg }] + + const buff = Buffer.from(JSON.stringify(flash)) + flashData = buff.toString('base64') + + return overallRes.redirect('/clans?flash=' + flashData + '') + } + } + ) +} diff --git a/src/backend/routes/views/clans/post/kick.js b/src/backend/routes/views/clans/post/kick.js new file mode 100755 index 00000000..2969228a --- /dev/null +++ b/src/backend/routes/views/clans/post/kick.js @@ -0,0 +1,76 @@ +let flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty() + check('membership_id', 'Internal error while processing your query: invalid member ID').notEmpty() + + // check the validation object for errors + let errors = validationResult(req) + + // Should not happen normally, but you never know + if (req.body.membership_id === req.user.clan.membershipId) errors = [{ msg: 'You cannot kick yourself' }] + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } else { + // Building update query + const membershipId = req.body.membership_id + const queryUrl = + process.env.API_URL + + '/data/clanMembership/' + membershipId + + // Run post to endpoint + request.delete({ + url: queryUrl, + body: '', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, function (err, res, body) { + const errorMessages = [] + + if (err || res.statusCode !== 204) { + let msg = 'Error while removing the member' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch {} + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } + + flash = {} + flash.class = 'alert-success' + flash.messages = [{ msg: 'The member was kicked' }] + flash.type = 'Success!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + }) + } +} diff --git a/src/backend/routes/views/clans/post/leave.js b/src/backend/routes/views/clans/post/leave.js new file mode 100755 index 00000000..ec8fff2a --- /dev/null +++ b/src/backend/routes/views/clans/post/leave.js @@ -0,0 +1,93 @@ +let flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty() + check('membership_id', 'Internal error while processing your query: invalid member ID').notEmpty() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('/clans?flash=' + data) + } else { + // Building update query + // Run post to endpoint + request.delete({ + url: `${process.env.API_URL}/data/clanMembership/${req.user.clan.membershipId}`, + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, function (err, res, body) { + const errorMessages = [] + + if (err || res.statusCode !== 204) { + let msg = 'Error while leaving the clan' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch { + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + } + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('/clans?flash=' + data) + } + + flash = {} + flash.class = 'alert-success' + flash.messages = [{ msg: 'You left the clan' }] + flash.type = 'Success!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + // Refreshing user + request.get({ + url: process.env.API_URL + '/me', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + + function (err, res, body) { + if (err) { + console.error('There was an error updating a session after an user left a clan') + + return + } + try { + const user = JSON.parse(body) + user.data.id = user.data.attributes.userId + user.data.attributes.token = req.user.data.attributes.token + req.logIn(user, function (err) { + if (err) console.error(err) + return overallRes.redirect('/clans?flash=' + data) + }) + } catch { + console.error('There was an error updating a session after an user left a clan') + } + }) + }) + } +} diff --git a/src/backend/routes/views/clans/post/transfer.js b/src/backend/routes/views/clans/post/transfer.js new file mode 100755 index 00000000..9d12aa35 --- /dev/null +++ b/src/backend/routes/views/clans/post/transfer.js @@ -0,0 +1,153 @@ +const flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +function promiseRequest (url) { + return new Promise(function (resolve, reject) { + request(url, function (error, res, body) { + if (!error && res.statusCode < 300) { + resolve(body) + } else { + reject(error || `Unexpected status code ${res.statusCode}`) + } + }) + }) +} + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('transfer_to', 'Please indicate the recipient name').notEmpty() + check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } else { + const clanId = req.body.clan_id + const userName = req.body.transfer_to + + // Let's check first that the player exists AND is part of this clan + const fetchRoute = process.env.API_URL + '/data/clan/' + clanId + '?include=memberships.player&fields[player]=login' + + let playerId = null + + try { + if (userName === req.user.data.attributes.userName) throw new Error('You cannot transfer your own clan to yourself') + + const httpData = await promiseRequest(fetchRoute) + const clanData = JSON.parse(httpData) + + const members = {} + + for (const k in clanData.included) { + const record = clanData.included[k] + if (record.type !== 'player') continue + members[record.attributes.login] = record.id + } + + if (!members[userName]) throw new Error('User does not exist or is not part of the clan') + playerId = members[userName] + } catch (e) { + flash.class = 'alert-danger' + flash.messages = [{ msg: 'There was an error during the transfer to ' + userName + ': ' + e }] + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } + + // Building update query + const queryUrl = + process.env.API_URL + + '/data/clan/' + clanId + + const newClanObject = + { + data: { + type: 'clan', + id: clanId, + relationships: { + leader: { + data: { + id: playerId, + type: 'player' + } + } + } + } + } + + // Run post to endpoint + request.patch({ + url: queryUrl, + body: JSON.stringify(newClanObject), + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token, + 'Content-Type': 'application/vnd.api+json' + } + }, function (err, res, body) { + if (err || res.statusCode !== 204) { + const errorMessages = [] + let msg = 'Error during the ownership transfer' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch {} + + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } else { + // Refreshing user + request.get({ + url: process.env.API_URL + '/me', + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token + } + }, + + function (err, res, body) { + if (err) { + console.error('There was an error updating a session after a clan transfer:', err) + + return + } + try { + const user = JSON.parse(body) + user.data.id = user.data.attributes.userId + user.data.attributes.token = req.user.data.attributes.token + req.logIn(user, function (err) { + if (err) console.error(err) + return overallRes.redirect('see?id=' + clanId) + }) + } catch { + console.error('There was an error updating a session after a clan transfer') + } + }) + } + }) + } +} diff --git a/src/backend/routes/views/clans/post/update.js b/src/backend/routes/views/clans/post/update.js new file mode 100644 index 00000000..7563b64f --- /dev/null +++ b/src/backend/routes/views/clans/post/update.js @@ -0,0 +1,145 @@ +let flash = {} +const request = require('request') +const { check, validationResult } = require('express-validator') + +function promiseRequest (url) { + return new Promise(function (resolve, reject) { + request(url, function (error, res, body) { + if (!error && res.statusCode < 300) { + resolve(body) + } else { + reject(error) + } + }) + }) +} + +exports = module.exports = async function (req, res) { + const locals = res.locals + + locals.formData = req.body || {} + + const overallRes = res + + // validate the input + check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({ max: 3 }) + check('clan_description', 'Please add a description for your clan').notEmpty().isLength({ max: 1000 }) + check('clan_name', "Please indicate your clan's name").notEmpty().isLength({ max: 64 }) + check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty() + + // check the validation object for errors + const errors = validationResult(req) + + // Must have client side errors to fix + if (!errors.isEmpty()) { + flash.class = 'alert-danger' + flash.messages = errors + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } else { + const newName = req.body.clan_name + const newTag = req.body.clan_tag + const oldName = req.body.original_clan_name + const oldTag = req.body.original_clan_tag + const clanDescription = req.body.clan_description + + // Is the name taken ? + try { + let msg = null + + flash.class = 'alert-danger' + flash.type = 'Error!' + + if (oldName !== newName) { + const fetchRoute = process.env.API_URL + '/data/clan?filter=name=="' + encodeURIComponent(newName) + '"' + const data = await promiseRequest(fetchRoute) + const exists = JSON.parse(data).data.length > 0 + + if (exists) msg = 'This name is already taken: ' + encodeURIComponent(newName) + } + if (oldTag !== newTag) { + const fetchRoute = process.env.API_URL + '/data/clan?filter=tag=="' + encodeURIComponent(newTag) + '"' + const data = await promiseRequest(fetchRoute) + const exists = JSON.parse(data).data.length > 0 + + if (exists) msg = 'This tag is already taken: ' + encodeURIComponent(newTag) + } + + if (msg) { + flash.messages = [{ msg }] + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + return overallRes.redirect('manage?flash=' + data) + } + } catch (e) { + flash.class = 'alert-danger' + flash.messages = [{ msg: 'Error while updating the clan ' + e }] + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } + + // Building update query + const queryUrl = + process.env.API_URL + + '/data/clan/' + req.body.clan_id + + const newClanObject = { + data: { + type: 'clan', + id: req.body.clan_id, + attributes: { + description: clanDescription, + name: newName, + tag: newTag + } + } + } + + // Run post to endpoint + request.patch({ + url: queryUrl, + body: JSON.stringify(newClanObject), + headers: { + Authorization: 'Bearer ' + req.user.data.attributes.token, + 'Content-Type': 'application/vnd.api+json', + Accept: 'application/vnd.api+json' + } + }, function (err, res) { + const errorMessages = [] + + if (err || res.statusCode !== 204) { + let msg = 'Error while updating the clan' + try { + msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail) + } catch {} + errorMessages.push({ msg }) + flash.class = 'alert-danger' + flash.messages = errorMessages + flash.type = 'Error!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + } + + flash = {} + flash.class = 'alert-success' + flash.messages = [{ msg: 'You have successfully updated your clan' }] + flash.type = 'Success!' + + const buff = Buffer.from(JSON.stringify(flash)) + const data = buff.toString('base64') + + return overallRes.redirect('manage?flash=' + data) + }) + } +} diff --git a/src/backend/routes/views/dataRouter.js b/src/backend/routes/views/dataRouter.js new file mode 100644 index 00000000..01676be7 --- /dev/null +++ b/src/backend/routes/views/dataRouter.js @@ -0,0 +1,46 @@ +const express = require('../../ExpressApp') +const router = express.Router() +const { AcquireTimeoutError } = require('../../services/MutexService') + +const getData = async (req, res, name, data) => { + try { + return res.json(data) + } catch (e) { + if (e instanceof AcquireTimeoutError) { + return res.status(503).json({ error: 'timeout reached' }) + } + + console.error('[error] dataRouter::get:' + name + '.json failed with "' + e.toString() + '"') + + if (!res.headersSent) { + return res.status(500).json({ error: 'unexpected error' }) + } + throw e + } +} +router.get('/newshub.json', async (req, res) => { + getData(req, res, 'newshub', await req.appContainer.get('WordpressService').getNewshub()) +}) + +router.get('/tournament-news.json', async (req, res) => { + getData(req, res, 'tournament-news', await req.appContainer.get('WordpressService').getTournamentNews()) +}) +router.get('/faf-teams.json', async (req, res) => { + getData(req, res, 'faf-teams', await req.appContainer.get('WordpressService').getFafTeams()) +}) + +router.get('/content-creators.json', async (req, res) => { + getData(req, res, 'content-creators', await req.appContainer.get('WordpressService').getContentCreators()) +}) + +router.get('/recent-players.json', async (req, res) => { + const rawData = await req.appContainer.get('LeaderboardService').getLeaderboard(1) + const data = rawData.map((item) => ({ + id: item.playerId, + name: item.label + })) + + getData(req, res, 'content-creators', data) +}) + +module.exports = router diff --git a/routes/views/defaultRouter.js b/src/backend/routes/views/defaultRouter.js similarity index 92% rename from routes/views/defaultRouter.js rename to src/backend/routes/views/defaultRouter.js index 4789b9eb..9ff8e7d6 100644 --- a/routes/views/defaultRouter.js +++ b/src/backend/routes/views/defaultRouter.js @@ -1,5 +1,5 @@ const express = require('../../ExpressApp') -const router = express.Router(); +const router = express.Router() router.get('/', (req, res) => res.render('index')) router.get('/newshub', (req, res) => res.render('newshub')) @@ -20,6 +20,6 @@ router.get('/account_activated', (req, res) => res.redirect('/account/register') router.get('/password_resetted', (req, res) => res.redirect('/account/requestPasswordReset')) // this is prob. outdated, but don't know -router.get('/report_submitted', require('./account/get/report')) +router.get('/report_submitted', require('./account/get/report')) module.exports = router diff --git a/src/backend/routes/views/leaderboardRouter.js b/src/backend/routes/views/leaderboardRouter.js new file mode 100644 index 00000000..81b99f6b --- /dev/null +++ b/src/backend/routes/views/leaderboardRouter.js @@ -0,0 +1,48 @@ +const express = require('../../ExpressApp') +const router = express.Router() +const { AcquireTimeoutError } = require('../../services/MutexService') + +const getLeaderboardId = (leaderboardName) => { + const mapping = { + global: 1, + '1v1': 2, + '2v2': 3, + '4v4': 4 + } + + if (leaderboardName in mapping) { + return mapping[leaderboardName] + } + + return null +} + +router.get('/', (req, res) => { + return res.render('leaderboards') +}) + +router.get('/:leaderboard.json', async (req, res) => { + try { + const leaderboardId = getLeaderboardId(req.params.leaderboard ?? null) + + if (leaderboardId === null) { + return res.status(404).json({ error: 'Leaderboard "' + req.params.leaderboard + '" does not exist' }) + } + + return res.json(await req.appContainer.get('LeaderboardService').getLeaderboard(leaderboardId)) + } catch (e) { + if (e instanceof AcquireTimeoutError) { + return res.status(503).json({ error: 'timeout reached' }) + } + + console.error('[error] leaderboardRouter::get:leaderboard.json failed with "' + e.toString() + '"', e.stack) + + if (!res.headersSent) { + return res.status(500).json({ error: 'unexpected error' }) + } + + throw e + } +}) + +module.exports = router diff --git a/src/backend/routes/views/news.js b/src/backend/routes/views/news.js new file mode 100644 index 00000000..8cc2f26d --- /dev/null +++ b/src/backend/routes/views/news.js @@ -0,0 +1,49 @@ +const express = require('../../ExpressApp') +const router = express.Router() + +function getNewsArticleBySlug (articles, slug) { + const [newsArticle] = articles.filter((entry) => { + return entry.slug === slug + }) ?? [] + + return newsArticle ?? null +} + +function getNewsArticleByDeprecatedSlug (articles, slug) { + const [newsArticle] = articles.filter((entry) => { + return entry.bcSlug === slug + }) ?? [] + + return newsArticle ?? null +} + +router.get('/', async (req, res) => { + res.render('news', { news: await req.appContainer.get('WordpressService').getNews() }) +}) + +router.get('/:slug', async (req, res) => { + const newsArticles = await req.appContainer.get('WordpressService').getNews() + + const newsArticle = getNewsArticleBySlug(newsArticles, req.params.slug) + + if (newsArticle === null) { + const newsArticleByOldSlug = getNewsArticleByDeprecatedSlug(newsArticles, req.params.slug) + + if (newsArticleByOldSlug) { + // old slug style, here for backward compatibility + res.redirect(301, newsArticleByOldSlug.slug) + + return + } + + res.redirect(req.baseUrl) + + return + } + + res.render('newsArticle', { + newsArticle + }) +}) + +module.exports = router diff --git a/src/backend/routes/views/staticMarkdownRouter.js b/src/backend/routes/views/staticMarkdownRouter.js new file mode 100644 index 00000000..4bea879c --- /dev/null +++ b/src/backend/routes/views/staticMarkdownRouter.js @@ -0,0 +1,23 @@ +const express = require('../../ExpressApp') +const showdown = require('showdown') +const fs = require('fs') +const router = express.Router() + +function markdown (template) { + return (req, res) => { + res.render('markdown', { + content: new showdown.Converter().makeHtml(fs.readFileSync(template, 'utf-8')) + }) + } +} + +router.get('/privacy', markdown('src/backend/templates/views/markdown/privacy.md')) +router.get('/privacy-fr', markdown('src/backend/templates/views/markdown/privacy-fr.md')) +router.get('/privacy-ru', markdown('src/backend/templates/views/markdown/privacy-ru.md')) +router.get('/tos', markdown('src/backend/templates/views/markdown/tos.md')) +router.get('/tos-fr', markdown('src/backend/templates/views/markdown/tos-fr.md')) +router.get('/tos-ru', markdown('src/backend/templates/views/markdown/tos-ru.md')) +router.get('/rules', markdown('src/backend/templates/views/markdown/rules.md')) +router.get('/cg', markdown('src/backend/templates/views/markdown/cg.md')) + +module.exports = router diff --git a/src/backend/security/bootPassport.js b/src/backend/security/bootPassport.js new file mode 100644 index 00000000..1dd12e81 --- /dev/null +++ b/src/backend/security/bootPassport.js @@ -0,0 +1,45 @@ +const passport = require('passport') +const OidcStrategy = require('passport-openidconnect') +const refresh = require('passport-oauth2-refresh') +const { JavaApiClientFactory } = require('../services/JavaApiClientFactory') +const { UserRepository } = require('../services/UserRepository') +const { UserService } = require('../services/UserService') + +module.exports.bootPassport = (expressApp, appConfig) => { + expressApp.use(passport.initialize()) + expressApp.use(passport.session()) + + passport.serializeUser((user, done) => done(null, user)) + passport.deserializeUser((user, done) => done(null, user)) + + const authStrategy = new OidcStrategy({ + passReqToCallback: true, + issuer: appConfig.oauth.url + '/', + tokenURL: appConfig.oauth.url + '/oauth2/token', + authorizationURL: appConfig.oauth.publicUrl + '/oauth2/auth', + userInfoURL: appConfig.oauth.url + '/userinfo?schema=openid', + clientID: appConfig.oauth.clientId, + clientSecret: appConfig.oauth.clientSecret, + callbackURL: `${appConfig.host}/${appConfig.oauth.callback}`, + scope: ['openid', 'offline', 'public_profile', 'write_account_data'] + }, async function (req, iss, sub, profile, jwtClaims, token, refreshToken, params, verified) { + const oAuthPassport = { + token, + refreshToken + } + + const apiClient = JavaApiClientFactory.createInstance(new UserService(), appConfig.apiUrl, oAuthPassport) + const userRepository = new UserRepository(apiClient) + + userRepository.fetchUser(oAuthPassport).then(user => { + verified(null, user) + }).catch(e => { + console.error('[Error] oAuth verify failed with "' + e.toString() + '"') + verified(null, null) + }) + } + ) + + passport.use(appConfig.oauth.strategy, authStrategy) + refresh.use(appConfig.oauth.strategy, authStrategy) +} diff --git a/lib/ApiErrors.js b/src/backend/services/ApiErrors.js similarity index 100% rename from lib/ApiErrors.js rename to src/backend/services/ApiErrors.js diff --git a/lib/CacheService.js b/src/backend/services/CacheService.js similarity index 68% rename from lib/CacheService.js rename to src/backend/services/CacheService.js index c58be858..a132f241 100644 --- a/lib/CacheService.js +++ b/src/backend/services/CacheService.js @@ -1,4 +1,4 @@ -const NodeCache = require("node-cache"); +const NodeCache = require('node-cache') const cacheService = new NodeCache( { stdTTL: 300, // use 5 min for all caches if not changed with ttl @@ -6,4 +6,4 @@ const cacheService = new NodeCache( } ) -module.exports = cacheService +module.exports.CacheService = cacheService diff --git a/src/backend/services/JavaApiClientFactory.js b/src/backend/services/JavaApiClientFactory.js new file mode 100644 index 00000000..5e749dfd --- /dev/null +++ b/src/backend/services/JavaApiClientFactory.js @@ -0,0 +1,72 @@ +const { Axios } = require('axios') +const refresh = require('passport-oauth2-refresh') +const { AuthFailed } = require('./ApiErrors') + +const getRefreshToken = (strategy, oAuthPassport) => { + return new Promise((resolve, reject) => { + refresh.requestNewAccessToken(strategy, oAuthPassport.refreshToken, function (err, accessToken, refreshToken) { + if (err || !accessToken || !refreshToken) { + return reject(new AuthFailed('Failed to refresh token' + err)) + } + + return resolve([accessToken, refreshToken]) + }) + }) +} + +class JavaApiClientFactory { + static createInstance (userService, javaApiBaseURL, oAuthPassport, strategy) { + if (typeof oAuthPassport !== 'object') { + throw new Error('oAuthPassport not an object') + } + + if (typeof oAuthPassport.refreshToken !== 'string') { + throw new Error('oAuthPassport.refreshToken not a string') + } + + if (typeof oAuthPassport.token !== 'string') { + throw new Error('oAuthPassport.token not a string') + } + + let tokenRefreshRunning = null + const client = new Axios({ + baseURL: javaApiBaseURL + }) + + client.interceptors.request.use( + async config => { + config.headers.Authorization = `Bearer ${oAuthPassport.token}` + + return config + }) + + client.interceptors.response.use((res) => { + if (!res.config._refreshTokenRequest && res.config && res.status === 401) { + res.config._refreshTokenRequest = true + + if (!tokenRefreshRunning) { + tokenRefreshRunning = getRefreshToken(strategy, oAuthPassport) + } + + return tokenRefreshRunning.then(([token, refreshToken]) => { + oAuthPassport.token = token + oAuthPassport.refreshToken = refreshToken + + userService.updatePassport(oAuthPassport) + + return client.request(res.config) + }) + } + + if (res.status === 401) { + throw new AuthFailed('Token no longer valid and refresh did not help') + } + + return res + }) + + return client + } +} + +module.exports.JavaApiClientFactory = JavaApiClientFactory diff --git a/src/backend/services/JavaApiM2MClient.js b/src/backend/services/JavaApiM2MClient.js new file mode 100644 index 00000000..94e7c22e --- /dev/null +++ b/src/backend/services/JavaApiM2MClient.js @@ -0,0 +1,51 @@ +const { Axios } = require('axios') +const { ClientCredentials } = require('simple-oauth2') +const { AuthFailed } = require('./ApiErrors') + +class JavaApiM2MClient { + static createInstance (clientId, clientSecret, host, javaApiBaseURL) { + let passport = null + const axios = new Axios({ + baseURL: javaApiBaseURL + }) + + axios.interceptors.request.use(async (config) => { + if (!passport || passport.expired()) { + passport = await JavaApiM2MClient.getToken(clientId, clientSecret, host) + } + config.headers.Authorization = `Bearer ${passport.token.access_token}` + + return config + }) + + return axios + } + + static getToken (clientId, clientSecret, host) { + const tokenClient = new ClientCredentials({ + client: { + id: clientId, + secret: clientSecret + + }, + auth: { + tokenHost: host, + tokenPath: '/oauth2/token', + revokePath: '/oauth2/revoke' + }, + options: { + authorizationMethod: 'body' + } + }) + + try { + return tokenClient.getToken({ + scope: '' + }) + } catch (error) { + throw new AuthFailed(error.toString()) + } + } +} + +module.exports.JavaApiM2MClient = JavaApiM2MClient diff --git a/lib/LeaderboardRepository.js b/src/backend/services/LeaderboardRepository.js similarity index 69% rename from lib/LeaderboardRepository.js rename to src/backend/services/LeaderboardRepository.js index 1fb50f1f..8b1ab87d 100644 --- a/lib/LeaderboardRepository.js +++ b/src/backend/services/LeaderboardRepository.js @@ -1,20 +1,20 @@ class LeaderboardRepository { - constructor(javaApiClient, monthsInThePast = 12) { + constructor (javaApiClient, monthsInThePast = 12) { this.javaApiClient = javaApiClient this.monthsInThePast = monthsInThePast } - getUpdateTimeForApiEntries() { - const date = new Date(); - date.setMonth(date.getMonth() - this.monthsInThePast); + getUpdateTimeForApiEntries () { + const date = new Date() + date.setMonth(date.getMonth() - this.monthsInThePast) return date.toISOString() } - async fetchLeaderboard(id) { + async fetchLeaderboard (id) { const updateTime = this.getUpdateTimeForApiEntries() - let response = await this.javaApiClient.get(`/data/leaderboardRating?include=player&sort=-rating&filter=leaderboard.id==${id};updateTime=ge=${updateTime}&page[size]=9999`); + const response = await this.javaApiClient.get(`/data/leaderboardRating?include=player&sort=-rating&filter=leaderboard.id==${id};updateTime=ge=${updateTime}&page[size]=9999`) if (response.status !== 200) { throw new Error('LeaderboardRepository::fetchLeaderboard failed with response status "' + response.status + '"') @@ -23,12 +23,12 @@ class LeaderboardRepository { return this.mapResponse(JSON.parse(response.data)) } - mapResponse(data) { + mapResponse (data) { if (typeof data !== 'object' || data === null) { throw new Error('LeaderboardRepository::mapResponse malformed response, not an object') } - if (!data.hasOwnProperty('data')) { + if (!Object.prototype.hasOwnProperty.call(data, 'data')) { throw new Error('LeaderboardRepository::mapResponse malformed response, expected "data"') } @@ -38,29 +38,29 @@ class LeaderboardRepository { return [] } - if (!data.hasOwnProperty('included')) { + if (!Object.prototype.hasOwnProperty.call(data, 'included')) { throw new Error('LeaderboardRepository::mapResponse malformed response, expected "included"') } - let leaderboardData = [] + const leaderboardData = [] data.data.forEach((item, index) => { try { leaderboardData.push({ + playerId: item.id, rating: item.attributes.rating, totalgames: item.attributes.totalGames, wonGames: item.attributes.wonGames, date: item.attributes.updateTime, - label: data.included[index]?.attributes.login || 'unknown user', + label: data.included[index]?.attributes.login || 'unknown user' }) } catch (e) { console.error('LeaderboardRepository::mapResponse failed on item with "' + e.toString() + '"') } - }) return leaderboardData } } -module.exports = LeaderboardRepository +module.exports.LeaderboardRepository = LeaderboardRepository diff --git a/lib/LeaderboardService.js b/src/backend/services/LeaderboardService.js similarity index 67% rename from lib/LeaderboardService.js rename to src/backend/services/LeaderboardService.js index efc86c58..0654e218 100644 --- a/lib/LeaderboardService.js +++ b/src/backend/services/LeaderboardService.js @@ -1,23 +1,22 @@ -const {MutexService} = require("./MutexService"); -const leaderboardTTL = 60 * 60 // 1 hours ttl as a relaxation for https://github.com/FAForever/website/issues/482 +const { MutexService } = require('./MutexService') +const leaderboardTTL = 60 * 60 // 1 hours ttl as a relaxation for https://github.com/FAForever/website/issues/482 class LeaderboardService { - constructor(cacheService, leaderboardRepository, lockTimeout = 3000) { + constructor (cacheService, leaderboardRepository, lockTimeout = 3000) { this.lockTimeout = lockTimeout this.cacheService = cacheService this.mutexService = new MutexService() this.leaderboardRepository = leaderboardRepository } - async getLeaderboard(id) { - + async getLeaderboard (id, ignoreCache = false) { if (typeof (id) !== 'number') { throw new Error('LeaderboardService:getLeaderboard id must be a number') } const cacheKey = 'leaderboard-' + id - if (this.cacheService.has(cacheKey)) { + if (this.cacheService.has(cacheKey) && ignoreCache === false) { return this.cacheService.get(cacheKey) } @@ -29,11 +28,11 @@ class LeaderboardService { await this.mutexService.acquire(async () => { const result = await this.leaderboardRepository.fetchLeaderboard(id) - this.cacheService.set(cacheKey, result, leaderboardTTL); + this.cacheService.set(cacheKey, result, leaderboardTTL) }) return this.getLeaderboard(id) } } -module.exports = LeaderboardService +module.exports.LeaderboardService = LeaderboardService diff --git a/lib/MutexService.js b/src/backend/services/MutexService.js similarity index 68% rename from lib/MutexService.js rename to src/backend/services/MutexService.js index a31c9f66..a7e1cbad 100644 --- a/lib/MutexService.js +++ b/src/backend/services/MutexService.js @@ -2,13 +2,13 @@ class AcquireTimeoutError extends Error { } class MutexService { - constructor() { - this.queue = []; - this.locked = false; + constructor () { + this.queue = [] + this.locked = false } - async acquire(callback, timeLimitMS = 500) { - let timeoutHandle; + async acquire (callback, timeLimitMS = 500) { + let timeoutHandle const lockHandler = {} const timeoutPromise = new Promise((resolve, reject) => { @@ -18,23 +18,23 @@ class MutexService { timeoutHandle = setTimeout( () => reject(new AcquireTimeoutError('MutexService timeout reached')), timeLimitMS - ); - }); + ) + }) const asyncPromise = new Promise((resolve, reject) => { if (this.locked) { lockHandler.resolve = resolve lockHandler.reject = reject - this.queue.push(lockHandler); + this.queue.push(lockHandler) } else { - this.locked = true; - resolve(); + this.locked = true + resolve() } - }); + }) await Promise.race([asyncPromise, timeoutPromise]).then(async () => { - clearTimeout(timeoutHandle); + clearTimeout(timeoutHandle) try { if (callback[Symbol.toStringTag] === 'AsyncFunction') { await callback() @@ -46,22 +46,22 @@ class MutexService { this.release() } }).catch(e => { - let index = this.queue.indexOf(lockHandler); + const index = this.queue.indexOf(lockHandler) if (index !== -1) { - this.queue.splice(index, 1); + this.queue.splice(index, 1) } throw e }) } - release() { + release () { if (this.queue.length > 0) { - const queueItem = this.queue.shift(); - queueItem.resolve(); + const queueItem = this.queue.shift() + queueItem.resolve() } else { - this.locked = false; + this.locked = false } } } diff --git a/lib/Scheduler.js b/src/backend/services/Scheduler.js similarity index 64% rename from lib/Scheduler.js rename to src/backend/services/Scheduler.js index bf11aee6..67a7dd59 100644 --- a/lib/Scheduler.js +++ b/src/backend/services/Scheduler.js @@ -1,18 +1,18 @@ -const {EventEmitter} = require("events") +const { EventEmitter } = require('events') module.exports = class Scheduler extends EventEmitter { - constructor(eventName, action, ms) { + constructor (eventName, action, ms) { super() this.eventName = eventName this.action = action this.handle = undefined this.interval = ms - this.addListener(this.eventName, this.action); + this.addListener(this.eventName, this.action) } - start() { + start () { if (!this.handle) { - this.handle = setInterval(() => this.emit(this.eventName), this.interval); + this.handle = setInterval(() => this.emit(this.eventName), this.interval) } } } diff --git a/lib/UserRepository.js b/src/backend/services/UserRepository.js similarity index 94% rename from lib/UserRepository.js rename to src/backend/services/UserRepository.js index 88c4a355..356ff551 100644 --- a/lib/UserRepository.js +++ b/src/backend/services/UserRepository.js @@ -29,4 +29,4 @@ class UserRepository { } } -module.exports = UserRepository +module.exports.UserRepository = UserRepository diff --git a/src/backend/services/UserService.js b/src/backend/services/UserService.js new file mode 100644 index 00000000..8637cd42 --- /dev/null +++ b/src/backend/services/UserService.js @@ -0,0 +1,26 @@ +class UserService { + constructor () { + this.user = null + this.session = null + } + + setUserFromRequest (request) { + this.user = request.user + this.session = request.session.passport + } + + isAuthenticated () { + return !!this.user + } + + getUser () { + return this.user + } + + updatePassport (oAuthPassport) { + this.user.oAuthPassport = oAuthPassport + this.session.user.oAuthPassport = oAuthPassport + } +} + +module.exports.UserService = UserService diff --git a/lib/WordpressRepository.js b/src/backend/services/WordpressRepository.js similarity index 70% rename from lib/WordpressRepository.js rename to src/backend/services/WordpressRepository.js index 41ce7701..4fcb6014 100644 --- a/lib/WordpressRepository.js +++ b/src/backend/services/WordpressRepository.js @@ -1,86 +1,87 @@ -const {convert} = require("url-slug"); +const { convert } = require('url-slug') class WordpressRepository { - constructor(wordpressClient) { + constructor (wordpressClient) { this.wordpressClient = wordpressClient } - async fetchNews() { - let response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,content.rendered,date,categories&categories=587') + + async fetchNews () { + const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,content.rendered,date,categories&categories=587') if (response.status !== 200) { throw new Error('WordpressRepository::fetchNews failed with response status "' + response.status + '"') } const rawNewsData = JSON.parse(response.data) - + if (typeof rawNewsData !== 'object' || rawNewsData === null) { throw new Error('WordpressRepository::mapNewsResponse malformed response, not an object') } - return rawNewsData.map(item => ({ + return rawNewsData.map(item => ({ slug: convert(item.title.rendered), bcSlug: item.title.rendered.replace(/ /g, '-'), date: item.date, title: item.title.rendered, content: item.content.rendered, author: item._embedded.author[0].name, - media: item._embedded['wp:featuredmedia'][0].source_url, + media: item._embedded['wp:featuredmedia'][0].source_url })) } - async fetchTournamentNews() { - let response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=content.rendered,categories&categories=638') + async fetchTournamentNews () { + const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=content.rendered,categories&categories=638') if (response.status !== 200) { throw new Error('WordpressRepository::fetchTournamentNews failed with response status "' + response.status + '"') } - - let dataObjectToArray = JSON.parse(response.data); - let sortedData = dataObjectToArray.map(item => ({ + const dataObjectToArray = JSON.parse(response.data) + + const sortedData = dataObjectToArray.map(item => ({ content: item.content.rendered, category: item.categories - })); - + })) + return sortedData.filter(article => article.category[1] !== 284) } - async fetchContentCreators() { + async fetchContentCreators () { const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=content.rendered,categories&categories=639') if (response.status !== 200) { throw new Error('WordpressRepository::fetchContentCreators failed with response status "' + response.status + '"') } - const items = JSON.parse(response.data); - + const items = JSON.parse(response.data) + return items.map(item => ({ - content: item.content.rendered, + content: item.content.rendered })) } - async fetchFafTeams() { + async fetchFafTeams () { const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=100&_embed&_fields=content.rendered,categories&categories=636') if (response.status !== 200) { throw new Error('WordpressRepository::fetchFafTeams failed with response status "' + response.status + '"') } - - const items = JSON.parse(response.data); - + + const items = JSON.parse(response.data) + return items.map(item => ({ - content: item.content.rendered, + content: item.content.rendered })) } - async fetchNewshub() { + async fetchNewshub () { const response = await this.wordpressClient.get('/wp-json/wp/v2/posts/?per_page=10&_embed&_fields=_links.author,_links.wp:featuredmedia,_embedded,title,newshub_externalLinkUrl,newshub_sortIndex,content.rendered,date,categories&categories=283') if (response.status !== 200) { throw new Error('WordpressRepository::fetchNewshub failed with response status "' + response.status + '"') } - - const items = JSON.parse(response.data); + + const items = JSON.parse(response.data) const sortedData = items.map(item => ({ category: item.categories, sortIndex: item.newshub_sortIndex, @@ -89,15 +90,15 @@ class WordpressRepository { title: item.title.rendered, content: item.content.rendered, author: item._embedded.author[0].name, - media: item._embedded['wp:featuredmedia'][0].source_url, - })); - - sortedData.sort((articleA, articleB) => articleB.sortIndex - articleA.sortIndex); + media: item._embedded['wp:featuredmedia'][0].source_url + })) + + sortedData.sort((articleA, articleB) => articleB.sortIndex - articleA.sortIndex) return sortedData.filter((article) => { - return article.category[1] !== 284; + return article.category[1] !== 284 }) } } -module.exports = WordpressRepository +module.exports.WordpressRepository = WordpressRepository diff --git a/lib/WordpressService.js b/src/backend/services/WordpressService.js similarity index 84% rename from lib/WordpressService.js rename to src/backend/services/WordpressService.js index 648e6921..4fd73f6a 100644 --- a/lib/WordpressService.js +++ b/src/backend/services/WordpressService.js @@ -1,25 +1,25 @@ -const {MutexService} = require("./MutexService"); +const { MutexService } = require('./MutexService') const wordpressTTL = 60 * 60 class WordpressService { - constructor(cacheService, wordpressRepository, lockTimeout = 3000) { + constructor (cacheService, wordpressRepository, lockTimeout = 3000) { this.lockTimeout = lockTimeout this.cacheService = cacheService this.mutexServices = { - news: new MutexService(), - tournament: new MutexService(), - creators: new MutexService(), - teams: new MutexService(), - newshub: new MutexService(), + news: new MutexService(), + tournament: new MutexService(), + creators: new MutexService(), + teams: new MutexService(), + newshub: new MutexService() } this.wordpressRepository = wordpressRepository } - - getCacheKey(name) { + + getCacheKey (name) { return 'WordpressService_' + name } - async getNews(ignoreCache = false) { + async getNews (ignoreCache = false) { const cacheKey = this.getCacheKey('news') if (this.cacheService.has(cacheKey) && ignoreCache === false) { @@ -31,16 +31,16 @@ class WordpressService { }, this.lockTimeout) return this.getNews() } - + await this.mutexServices.news.acquire(async () => { const result = await this.wordpressRepository.fetchNews() - this.cacheService.set(cacheKey, result, wordpressTTL); + this.cacheService.set(cacheKey, result, wordpressTTL) }) return this.getNews() } - async getTournamentNews(ignoreCache = false) { + async getTournamentNews (ignoreCache = false) { const cacheKey = this.getCacheKey('tournament-news') if (this.cacheService.has(cacheKey) && ignoreCache === false) { @@ -55,13 +55,13 @@ class WordpressService { await this.mutexServices.tournament.acquire(async () => { const result = await this.wordpressRepository.fetchTournamentNews() - this.cacheService.set(cacheKey, result, wordpressTTL); + this.cacheService.set(cacheKey, result, wordpressTTL) }) return this.getTournamentNews() } - async getContentCreators(ignoreCache = false) { + async getContentCreators (ignoreCache = false) { const cacheKey = this.getCacheKey('content-creators') if (this.cacheService.has(cacheKey) && ignoreCache === false) { @@ -76,13 +76,13 @@ class WordpressService { await this.mutexServices.creators.acquire(async () => { const result = await this.wordpressRepository.fetchContentCreators() - this.cacheService.set(cacheKey, result, wordpressTTL); + this.cacheService.set(cacheKey, result, wordpressTTL) }) return this.getContentCreators() } - async getFafTeams(ignoreCache = false) { + async getFafTeams (ignoreCache = false) { const cacheKey = this.getCacheKey('faf-teams') if (this.cacheService.has(cacheKey) && ignoreCache === false) { @@ -97,13 +97,13 @@ class WordpressService { await this.mutexServices.teams.acquire(async () => { const result = await this.wordpressRepository.fetchFafTeams() - this.cacheService.set(cacheKey, result, wordpressTTL); + this.cacheService.set(cacheKey, result, wordpressTTL) }) return this.getFafTeams() } - async getNewshub(ignoreCache = false) { + async getNewshub (ignoreCache = false) { const cacheKey = this.getCacheKey('newshub') if (this.cacheService.has(cacheKey) && ignoreCache === false) { @@ -118,11 +118,11 @@ class WordpressService { await this.mutexServices.newshub.acquire(async () => { const result = await this.wordpressRepository.fetchNewshub() - this.cacheService.set(cacheKey, result, wordpressTTL); + this.cacheService.set(cacheKey, result, wordpressTTL) }) return this.getNewshub() } } -module.exports = WordpressService +module.exports.WordpressService = WordpressService diff --git a/lib/WordpressServiceFactory.js b/src/backend/services/WordpressServiceFactory.js similarity index 64% rename from lib/WordpressServiceFactory.js rename to src/backend/services/WordpressServiceFactory.js index 4d580cc8..8352931b 100644 --- a/lib/WordpressServiceFactory.js +++ b/src/backend/services/WordpressServiceFactory.js @@ -1,12 +1,12 @@ -const WordpressService = require("./WordpressService") -const WordpressRepository = require("./WordpressRepository") -const {Axios} = require("axios") +const WordpressService = require('./WordpressService') +const WordpressRepository = require('./WordpressRepository') +const { Axios } = require('axios') const cacheService = require('./CacheService') module.exports = (wordpressBaseURL) => { const config = { baseURL: wordpressBaseURL - }; + } const wordpressClient = new Axios(config) return new WordpressService(cacheService, new WordpressRepository(wordpressClient)) diff --git a/templates/layouts/default.pug b/src/backend/templates/layouts/default.pug similarity index 100% rename from templates/layouts/default.pug rename to src/backend/templates/layouts/default.pug diff --git a/src/backend/templates/mixins/flash-error.pug b/src/backend/templates/mixins/flash-error.pug new file mode 100644 index 00000000..12115e81 --- /dev/null +++ b/src/backend/templates/mixins/flash-error.pug @@ -0,0 +1,7 @@ +mixin flash-error(validationErrors) + if validationErrors + div.alert(class=validationErrors.class) + ul.validationErrors-errors + each error in validationErrors.messages.errors + if error.msg + li #{validationErrors.type} !{error.msg} diff --git a/templates/mixins/flash-messages.pug b/src/backend/templates/mixins/flash-messages.pug similarity index 85% rename from templates/mixins/flash-messages.pug rename to src/backend/templates/mixins/flash-messages.pug index d515504a..43827b47 100644 --- a/templates/mixins/flash-messages.pug +++ b/src/backend/templates/mixins/flash-messages.pug @@ -4,5 +4,5 @@ mixin flash-messages(messages) ul.flash-errors if flash.messages if flash.messages[0] - if flash.messages[0].msg + if flash.messages[0].msg li #{flash.type} !{flash.messages[0].msg} diff --git a/templates/mixins/form/account.pug b/src/backend/templates/mixins/form/account.pug similarity index 100% rename from templates/mixins/form/account.pug rename to src/backend/templates/mixins/form/account.pug diff --git a/templates/views/account/activate.pug b/src/backend/templates/views/account/activate.pug similarity index 100% rename from templates/views/account/activate.pug rename to src/backend/templates/views/account/activate.pug diff --git a/templates/views/account/changeEmail.pug b/src/backend/templates/views/account/changeEmail.pug similarity index 100% rename from templates/views/account/changeEmail.pug rename to src/backend/templates/views/account/changeEmail.pug diff --git a/templates/views/account/changePassword.pug b/src/backend/templates/views/account/changePassword.pug similarity index 100% rename from templates/views/account/changePassword.pug rename to src/backend/templates/views/account/changePassword.pug diff --git a/templates/views/account/changeUsername.pug b/src/backend/templates/views/account/changeUsername.pug similarity index 100% rename from templates/views/account/changeUsername.pug rename to src/backend/templates/views/account/changeUsername.pug diff --git a/templates/views/account/confirmPasswordReset.pug b/src/backend/templates/views/account/confirmPasswordReset.pug similarity index 100% rename from templates/views/account/confirmPasswordReset.pug rename to src/backend/templates/views/account/confirmPasswordReset.pug diff --git a/templates/views/account/confirmResyncAccount.pug b/src/backend/templates/views/account/confirmResyncAccount.pug similarity index 100% rename from templates/views/account/confirmResyncAccount.pug rename to src/backend/templates/views/account/confirmResyncAccount.pug diff --git a/templates/views/account/createAccount.pug b/src/backend/templates/views/account/createAccount.pug similarity index 100% rename from templates/views/account/createAccount.pug rename to src/backend/templates/views/account/createAccount.pug diff --git a/templates/views/account/linkGog.pug b/src/backend/templates/views/account/linkGog.pug similarity index 100% rename from templates/views/account/linkGog.pug rename to src/backend/templates/views/account/linkGog.pug diff --git a/templates/views/account/linkSteam.pug b/src/backend/templates/views/account/linkSteam.pug similarity index 100% rename from templates/views/account/linkSteam.pug rename to src/backend/templates/views/account/linkSteam.pug diff --git a/templates/views/account/register.pug b/src/backend/templates/views/account/register.pug similarity index 100% rename from templates/views/account/register.pug rename to src/backend/templates/views/account/register.pug diff --git a/templates/views/account/report.pug b/src/backend/templates/views/account/report.pug similarity index 86% rename from templates/views/account/report.pug rename to src/backend/templates/views/account/report.pug index d4660477..b94814db 100755 --- a/templates/views/account/report.pug +++ b/src/backend/templates/views/account/report.pug @@ -1,6 +1,8 @@ extends ../../layouts/default -include ../../mixins/flash-messages +include ../../mixins/flash-error include ../../mixins/form/account +block css + link(href="/styles/awesomplete.css?version=" + Date.now(), rel="stylesheet") block bannerMixin block content .containerCenter.text-center @@ -11,25 +13,25 @@ block content p Here you can report players who have broken the community rules in some way. We encourage users to report misconducting players to keep Forged Alliance Forever a healthy community. All reports will be processed by our moderation team. p Examples of reportable behaviour: ul - li + li a(href='/rules') Breaking any of the rules li Teamkilling li Griefing (e.g. reclaiming friendly units or structures) li Insulting and bad behaviour li Exploits - - p Bugs in the game should be - a(href='https://forum.faforever.com/category/8/game-issues-and-gameplay-questions') submitted to our tech support forum - | or preferably as an + + p Bugs in the game should be + a(href='https://forum.faforever.com/category/8/game-issues-and-gameplay-questions') submitted to our tech support forum + | or preferably as an a(href='https://github.com/FAForever/fa') issue on our github page. hr br br .row .col-md-offset-3.col-md-6 - +flash-messages(flash) - - form(method='post',action="/account/report",data-toggle="validator").accountForm + +flash-error(flash) + + form(method='post', action="/account/report", data-toggle="validator").accountForm .column6 div.form-group p Reporter: @@ -38,12 +40,12 @@ block content div.form-group p Offender username: - input(type='text', required='required', class='offender_name', id='offender_0', name='offender_0', placeholder='FAF username(s)').form-control + input(type='text', required='required', class='offender_name', id='offender', name='offender', placeholder='FAF username(s)').form-control span(aria-hidden='true').glyphicon.form-control-feedback .help-block Make sure to spell the name correctly and to respect the casing. - - + + .column6 div.form-group @@ -52,7 +54,7 @@ block content span(aria-hidden='true').glyphicon.form-control-feedback .help-block p Please enter the replay ID of the game where the incident happened - + div.form-group p Timestamp: @@ -66,7 +68,7 @@ block content textarea(rows='8', name='report_description', required='required', placeholder='Please provide a short but thorough description of the incident you are reporting. If there are no records available of the incident (e.g. not something that happened in #aeolus or in-game), please provide us a screenshot of it. You can use any image hosting site, e.g. http://imgur.com/.').form-control span(aria-hidden='true').glyphicon.form-control-feedback br - .column12 + .column12 .form-actions button(type='submit') Submit Report @@ -74,8 +76,8 @@ block content br br p Current reports - - + + .centerFormPlease table thead @@ -99,14 +101,15 @@ block content td #{report.lastModerator} td #{report.notice} td(style=report.statusStyle) #{report.status} - - + + block js script(type='text/javascript'). - window.reportable_members = '!{JSON.stringify(reportable_members)}'; - window.offenders_names = '!{JSON.stringify(offenders_names)}'; - + if (window.history.replaceState) { + window.history.replaceState(null, null, window.location.href); + } + script(src=webpackAssetJS('report')) diff --git a/templates/views/account/requestPasswordReset.pug b/src/backend/templates/views/account/requestPasswordReset.pug similarity index 100% rename from templates/views/account/requestPasswordReset.pug rename to src/backend/templates/views/account/requestPasswordReset.pug diff --git a/templates/views/ai.pug b/src/backend/templates/views/ai.pug similarity index 100% rename from templates/views/ai.pug rename to src/backend/templates/views/ai.pug diff --git a/templates/views/campaign-missions.pug b/src/backend/templates/views/campaign-missions.pug similarity index 100% rename from templates/views/campaign-missions.pug rename to src/backend/templates/views/campaign-missions.pug diff --git a/templates/views/clans.pug b/src/backend/templates/views/clans.pug similarity index 100% rename from templates/views/clans.pug rename to src/backend/templates/views/clans.pug diff --git a/templates/views/clans/accept_invite.pug b/src/backend/templates/views/clans/accept_invite.pug similarity index 100% rename from templates/views/clans/accept_invite.pug rename to src/backend/templates/views/clans/accept_invite.pug diff --git a/templates/views/clans/create.pug b/src/backend/templates/views/clans/create.pug similarity index 100% rename from templates/views/clans/create.pug rename to src/backend/templates/views/clans/create.pug diff --git a/templates/views/clans/manage.pug b/src/backend/templates/views/clans/manage.pug similarity index 100% rename from templates/views/clans/manage.pug rename to src/backend/templates/views/clans/manage.pug diff --git a/templates/views/clans/seeClan.pug b/src/backend/templates/views/clans/seeClan.pug similarity index 100% rename from templates/views/clans/seeClan.pug rename to src/backend/templates/views/clans/seeClan.pug diff --git a/templates/views/competitive_nav.pug b/src/backend/templates/views/competitive_nav.pug similarity index 100% rename from templates/views/competitive_nav.pug rename to src/backend/templates/views/competitive_nav.pug diff --git a/templates/views/content-creators.pug b/src/backend/templates/views/content-creators.pug similarity index 100% rename from templates/views/content-creators.pug rename to src/backend/templates/views/content-creators.pug diff --git a/templates/views/contribution.pug b/src/backend/templates/views/contribution.pug similarity index 100% rename from templates/views/contribution.pug rename to src/backend/templates/views/contribution.pug diff --git a/templates/views/donation.pug b/src/backend/templates/views/donation.pug similarity index 100% rename from templates/views/donation.pug rename to src/backend/templates/views/donation.pug diff --git a/templates/views/errors/404.pug b/src/backend/templates/views/errors/404.pug similarity index 100% rename from templates/views/errors/404.pug rename to src/backend/templates/views/errors/404.pug diff --git a/templates/views/errors/500.pug b/src/backend/templates/views/errors/500.pug similarity index 100% rename from templates/views/errors/500.pug rename to src/backend/templates/views/errors/500.pug diff --git a/templates/views/errors/503-known-issue.pug b/src/backend/templates/views/errors/503-known-issue.pug similarity index 100% rename from templates/views/errors/503-known-issue.pug rename to src/backend/templates/views/errors/503-known-issue.pug diff --git a/templates/views/faf-teams.pug b/src/backend/templates/views/faf-teams.pug similarity index 100% rename from templates/views/faf-teams.pug rename to src/backend/templates/views/faf-teams.pug diff --git a/templates/views/index.pug b/src/backend/templates/views/index.pug similarity index 100% rename from templates/views/index.pug rename to src/backend/templates/views/index.pug diff --git a/templates/views/leaderboards.pug b/src/backend/templates/views/leaderboards.pug similarity index 100% rename from templates/views/leaderboards.pug rename to src/backend/templates/views/leaderboards.pug diff --git a/templates/views/markdown.pug b/src/backend/templates/views/markdown.pug similarity index 100% rename from templates/views/markdown.pug rename to src/backend/templates/views/markdown.pug diff --git a/templates/views/markdown/cg.md b/src/backend/templates/views/markdown/cg.md similarity index 100% rename from templates/views/markdown/cg.md rename to src/backend/templates/views/markdown/cg.md diff --git a/templates/views/markdown/privacy-fr.md b/src/backend/templates/views/markdown/privacy-fr.md similarity index 100% rename from templates/views/markdown/privacy-fr.md rename to src/backend/templates/views/markdown/privacy-fr.md diff --git a/templates/views/markdown/privacy-ru.md b/src/backend/templates/views/markdown/privacy-ru.md similarity index 100% rename from templates/views/markdown/privacy-ru.md rename to src/backend/templates/views/markdown/privacy-ru.md diff --git a/templates/views/markdown/privacy.de.md b/src/backend/templates/views/markdown/privacy.de.md similarity index 100% rename from templates/views/markdown/privacy.de.md rename to src/backend/templates/views/markdown/privacy.de.md diff --git a/templates/views/markdown/privacy.md b/src/backend/templates/views/markdown/privacy.md similarity index 100% rename from templates/views/markdown/privacy.md rename to src/backend/templates/views/markdown/privacy.md diff --git a/templates/views/markdown/rules.md b/src/backend/templates/views/markdown/rules.md similarity index 100% rename from templates/views/markdown/rules.md rename to src/backend/templates/views/markdown/rules.md diff --git a/templates/views/markdown/tos-fr.md b/src/backend/templates/views/markdown/tos-fr.md similarity index 100% rename from templates/views/markdown/tos-fr.md rename to src/backend/templates/views/markdown/tos-fr.md diff --git a/templates/views/markdown/tos-ru.md b/src/backend/templates/views/markdown/tos-ru.md similarity index 100% rename from templates/views/markdown/tos-ru.md rename to src/backend/templates/views/markdown/tos-ru.md diff --git a/templates/views/markdown/tos.md b/src/backend/templates/views/markdown/tos.md similarity index 100% rename from templates/views/markdown/tos.md rename to src/backend/templates/views/markdown/tos.md diff --git a/templates/views/news.pug b/src/backend/templates/views/news.pug similarity index 100% rename from templates/views/news.pug rename to src/backend/templates/views/news.pug diff --git a/templates/views/newsArticle.pug b/src/backend/templates/views/newsArticle.pug similarity index 100% rename from templates/views/newsArticle.pug rename to src/backend/templates/views/newsArticle.pug diff --git a/templates/views/newshub.pug b/src/backend/templates/views/newshub.pug similarity index 100% rename from templates/views/newshub.pug rename to src/backend/templates/views/newshub.pug diff --git a/templates/views/play.pug b/src/backend/templates/views/play.pug similarity index 100% rename from templates/views/play.pug rename to src/backend/templates/views/play.pug diff --git a/templates/views/scfa-vs-faf.pug b/src/backend/templates/views/scfa-vs-faf.pug similarity index 100% rename from templates/views/scfa-vs-faf.pug rename to src/backend/templates/views/scfa-vs-faf.pug diff --git a/templates/views/tutorials-guides.pug b/src/backend/templates/views/tutorials-guides.pug similarity index 100% rename from templates/views/tutorials-guides.pug rename to src/backend/templates/views/tutorials-guides.pug diff --git a/src/frontend/js/entrypoint/leaderboards.js b/src/frontend/js/entrypoint/leaderboards.js index 17c630e6..1fabad3b 100644 --- a/src/frontend/js/entrypoint/leaderboards.js +++ b/src/frontend/js/entrypoint/leaderboards.js @@ -17,9 +17,10 @@ async function leaderboardOneJSON (leaderboardFile) { // Check which category is active const response = await fetch(`leaderboards/${leaderboardFile}.json`) - if (response.status === 400) { - window.location.href = '/leaderboards' + if (response.status !== 200) { + throw new Error('issues getting leaderboard') } + currentLeaderboard = leaderboardFile const data = await response.json() return await data diff --git a/src/frontend/js/entrypoint/report.js b/src/frontend/js/entrypoint/report.js index 6dd86f26..cab26fe2 100644 --- a/src/frontend/js/entrypoint/report.js +++ b/src/frontend/js/entrypoint/report.js @@ -1,48 +1,26 @@ -import $ from 'jquery' import Awesomplete from 'awesomplete' +import axios from 'axios' -const memberList = JSON.parse(window.reportable_members) -const searchBar = $('#offender_0') -addAwesompleteListener(searchBar) +async function getPlayers () { + const response = await axios.get('/data/recent-players.json') + if (response.status !== 200) { + throw new Error('issues getting data') + } + + return response.data +} -$('#add_offender').click(function () { - addOffender() +getPlayers().then((memberList) => { + addAwesompleteListener(document.getElementById('offender'), memberList) }) -function addAwesompleteListener (element) { - // Show label but insert value into the input: - /* eslint-disable no-new */ - new Awesomplete(element[0], { - list: memberList - }) - element[0].addEventListener('awesomplete-select', function (e) {}) - element[0].addEventListener('awesomplete-selectcomplete', function (e) { - const text = e.text - element.val(text) +function addAwesompleteListener (element, memberList) { + const list = memberList.map((player) => { + return player.name }) -} -function addOffender () { - const numberOfOffenders = $('.offender_name').length - for (let i = 0; i <= numberOfOffenders; i++) { - if (!$('#offender_' + i).length) { - const element = $('#offender_' + (i - 1)).clone(false) - element.insertAfter($('#offender_' + (i - 1))) - element.attr('id', 'offender_' + i) - element.attr('name', 'offender_' + i) - element.val('') - addAwesompleteListener(element) - return element - } - } -} - -const offenders = JSON.parse(window.offenders_names) -for (const k in offenders) { - const offender = offenders[k] - if (k === 0) { - searchBar.val(offender) - continue - } - addOffender().val(offender) + /* eslint-disable no-new */ + new Awesomplete(element, { + list + }) } diff --git a/tests/JavaApiClient.test.js b/tests/JavaApiClient.test.js index 255a6bbd..8f778322 100644 --- a/tests/JavaApiClient.test.js +++ b/tests/JavaApiClient.test.js @@ -1,9 +1,10 @@ -const { JavaApiClientFactory } = require('../lib/JavaApiClientFactory') -const appConfig = require('../config/app') +const { JavaApiClientFactory } = require('../src/backend/services/JavaApiClientFactory') +const appConfig = require('../src/backend/config/app') const refresh = require('passport-oauth2-refresh') const OidcStrategy = require('passport-openidconnect') const nock = require('nock') -const { AuthFailed } = require('../lib/ApiErrors') +const { AuthFailed } = require('../src/backend/services/ApiErrors') +const { UserService } = require('../src/backend/services/UserService') beforeEach(() => { refresh.use(appConfig.oauth.strategy, new OidcStrategy({ @@ -21,22 +22,24 @@ afterEach(() => { jest.restoreAllMocks() }) test('empty passport', () => { - expect(() => JavaApiClientFactory('http://api-localhost')).toThrowError('oAuthPassport not an object') + expect(() => JavaApiClientFactory.createInstance(new UserService(), 'http://api-localhost')).toThrowError('oAuthPassport not an object') }) test('empty token', () => { - expect(() => JavaApiClientFactory('http://api-localhost', { refreshToken: '123' })).toThrowError('oAuthPassport.token not a string') + expect(() => JavaApiClientFactory.createInstance(new UserService(), 'http://api-localhost', { refreshToken: '123' })).toThrowError('oAuthPassport.token not a string') }) test('empty refresh-token', () => { - expect(() => JavaApiClientFactory('http://api-localhost', { token: '123' })).toThrowError('oAuthPassport.refreshToken not a string') + expect(() => JavaApiClientFactory.createInstance(new UserService(), 'http://api-localhost', { token: '123' })).toThrowError('oAuthPassport.refreshToken not a string') }) test('multiple calls with stale token will trigger refresh only once', async () => { - const client = JavaApiClientFactory('http://api-localhost', { + const userService = new UserService() + userService.setUserFromRequest({ user: {}, session: { passport: { user: {} } } }) + const client = JavaApiClientFactory.createInstance(userService, 'http://api-localhost', { token: '123', refreshToken: '456' - }) + }, appConfig.oauth.strategy) const refreshSpy = jest.spyOn(refresh, 'requestNewAccessToken') const apiScope = nock('http://api-localhost') @@ -69,10 +72,12 @@ test('multiple calls with stale token will trigger refresh only once', async () }) test('refresh will throw on error', async () => { - const client = JavaApiClientFactory('http://api-localhost', { + const userService = new UserService() + userService.setUserFromRequest({ user: {}, session: { passport: { user: {} } } }) + const client = JavaApiClientFactory.createInstance(userService, 'http://api-localhost', { token: '123', refreshToken: '456' - }) + }, appConfig.oauth.strategy) const refreshSpy = jest.spyOn(refresh, 'requestNewAccessToken') const apiScope = nock('http://api-localhost') @@ -100,10 +105,12 @@ test('refresh will throw on error', async () => { }) test('refresh will not loop to death', async () => { - const client = JavaApiClientFactory('http://api-localhost', { + const userService = new UserService() + userService.setUserFromRequest({ user: {}, session: { passport: { user: {} } } }) + const client = JavaApiClientFactory.createInstance(userService, 'http://api-localhost', { token: '123', refreshToken: '456' - }) + }, appConfig.oauth.strategy) const apiScope = nock('http://api-localhost') .get('/example') diff --git a/tests/LeaderboardService.test.js b/tests/LeaderboardService.test.js index 465e63c5..8f00340e 100644 --- a/tests/LeaderboardService.test.js +++ b/tests/LeaderboardService.test.js @@ -1,5 +1,5 @@ -const LeaderboardService = require('../lib/LeaderboardService') -const LeaderboardRepository = require('../lib/LeaderboardRepository') +const { LeaderboardService } = require('../src/backend/services/LeaderboardService') +const { LeaderboardRepository } = require('../src/backend/services/LeaderboardRepository') const NodeCache = require('node-cache') const { Axios } = require('axios') diff --git a/tests/MutexService.test.js b/tests/MutexService.test.js index 62e8d0ca..fb532a32 100644 --- a/tests/MutexService.test.js +++ b/tests/MutexService.test.js @@ -1,4 +1,4 @@ -const { AcquireTimeoutError, MutexService } = require('../lib/MutexService') +const { AcquireTimeoutError, MutexService } = require('../src/backend/services/MutexService') test('release will unlock the queue', async () => { const mutexService = new MutexService() expect(mutexService.locked).toBe(false) diff --git a/tests/integration/IsAuthenticatedMiddleware.test.js b/tests/integration/IsAuthenticatedMiddleware.test.js index 60f45842..ae881a32 100644 --- a/tests/integration/IsAuthenticatedMiddleware.test.js +++ b/tests/integration/IsAuthenticatedMiddleware.test.js @@ -1,16 +1,14 @@ -const Express = require('../../ExpressApp') -const middlewares = require('../../routes/middleware') +const middlewares = require('../../src/backend/routes/middleware') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') +const { AppKernel } = require('../../src/backend/AppKernel') let testApp = null let testSession = null - -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - testSession = supertestSession(app) - testApp = app +beforeEach(async () => { + const kernel = new AppKernel() + await kernel.boot() + testSession = supertestSession(kernel.expressApp) + testApp = kernel.expressApp }) describe('Authenticate Middleware', function () { diff --git a/tests/integration/NewsRouter.test.js b/tests/integration/NewsRouter.test.js index a8f6c3f5..5200f578 100644 --- a/tests/integration/NewsRouter.test.js +++ b/tests/integration/NewsRouter.test.js @@ -1,16 +1,13 @@ -const Express = require('../../ExpressApp') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') +const { AppKernel } = require('../../src/backend/AppKernel') let testSession = null -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - fafApp.loadRouters(app) - - testSession = supertestSession(app) +beforeEach(async () => { + const kernel = new AppKernel() + await kernel.boot() + kernel.loadControllers() + testSession = supertestSession(kernel.expressApp) }) - describe('News Routes', function () { test('responds to /', async () => { const res = await testSession.get('/news') diff --git a/tests/integration/accountRouter.test.js b/tests/integration/accountRouter.test.js index f65ac552..2097a11d 100644 --- a/tests/integration/accountRouter.test.js +++ b/tests/integration/accountRouter.test.js @@ -1,13 +1,12 @@ -const Express = require('../../ExpressApp') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') +const { AppKernel } = require('../../src/backend/AppKernel') let testSession = null -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - fafApp.loadRouters(app) - testSession = supertestSession(app) +beforeEach(async () => { + const kernel = new AppKernel() + await kernel.boot() + kernel.loadControllers() + testSession = supertestSession(kernel.expressApp) }) describe('Account Routes', function () { diff --git a/tests/integration/asyncErrorHandler.test.js b/tests/integration/asyncErrorHandler.test.js index 68736f0e..c03c27ef 100644 --- a/tests/integration/asyncErrorHandler.test.js +++ b/tests/integration/asyncErrorHandler.test.js @@ -1,4 +1,4 @@ -const Express = require('../../ExpressApp') +const Express = require('../../src/backend/ExpressApp') const supertestSession = require('supertest-session') let testApp = null diff --git a/tests/integration/clanRouter.test.js b/tests/integration/clanRouter.test.js index 2fe81c56..9f906b0d 100644 --- a/tests/integration/clanRouter.test.js +++ b/tests/integration/clanRouter.test.js @@ -1,13 +1,12 @@ -const Express = require('../../ExpressApp') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') +const { AppKernel } = require('../../src/backend/AppKernel') let testSession = null beforeEach(async () => { - const app = new Express() - fafApp.setup(app) - fafApp.loadRouters(app) - testSession = supertestSession(app) + const kernel = new AppKernel() + await kernel.boot() + kernel.loadControllers() + testSession = supertestSession(kernel.expressApp) }) describe('Clan Routes', function () { diff --git a/tests/integration/defaultRouter.test.js b/tests/integration/defaultRouter.test.js index 09607455..e0e7a9fc 100644 --- a/tests/integration/defaultRouter.test.js +++ b/tests/integration/defaultRouter.test.js @@ -1,15 +1,13 @@ -const Express = require('../../ExpressApp') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') +const { AppKernel } = require('../../src/backend/AppKernel') let testSession = null -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - fafApp.loadRouters(app) - testSession = supertestSession(app) +beforeEach(async () => { + const kernel = new AppKernel() + await kernel.boot() + kernel.loadControllers() + testSession = supertestSession(kernel.expressApp) }) - describe('Default Routes', function () { const arr = [ '', diff --git a/tests/integration/leaderboardRouter.test.js b/tests/integration/leaderboardRouter.test.js index 192a44a5..97030930 100644 --- a/tests/integration/leaderboardRouter.test.js +++ b/tests/integration/leaderboardRouter.test.js @@ -1,21 +1,20 @@ -const Express = require('../../ExpressApp') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') const passportMock = require('../helpers/PassportMock') +const { AppKernel } = require('../../src/backend/AppKernel') let testSession = null -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - passportMock(app, { passAuthentication: true }) - fafApp.loadRouters(app) - testSession = supertestSession(app) +beforeEach(async () => { + const kernel = new AppKernel() + await kernel.boot() + passportMock(kernel.expressApp, { passAuthentication: true }) + kernel.loadControllers() + testSession = supertestSession(kernel.expressApp) }) describe('Leaderboard Routes', function () { - test('authentication required for main page', async () => { + test('no authentication required for main page', async () => { let response = await testSession.get('/leaderboards') - expect(response.status).toBe(302) + expect(response.status).toBe(200) await testSession.get('/mock-login') @@ -23,9 +22,9 @@ describe('Leaderboard Routes', function () { expect(response.status).toBe(200) }) - test('authentication required for datasets', async () => { + test('no authentication required for datasets', async () => { const response = await testSession.get('/leaderboards/1v1.json') - expect(response.status).toBe(401) + expect(response.status).toBe(200) }) test('fails with 404 on unknown leaderboard', async () => { diff --git a/tests/integration/markdownRouter.test.js b/tests/integration/markdownRouter.test.js index d61d5e6c..5c075552 100644 --- a/tests/integration/markdownRouter.test.js +++ b/tests/integration/markdownRouter.test.js @@ -1,15 +1,13 @@ -const Express = require('../../ExpressApp') const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') +const { AppKernel } = require('../../src/backend/AppKernel') let testSession = null -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - fafApp.loadRouters(app) - testSession = supertestSession(app) +beforeEach(async () => { + const kernel = new AppKernel() + await kernel.boot() + kernel.loadControllers() + testSession = supertestSession(kernel.expressApp) }) - describe('Privacy And TOS Routes', function () { const arr = [ '/privacy', diff --git a/tests/integration/servicesMiddleware.test.js b/tests/integration/servicesMiddleware.test.js deleted file mode 100644 index 3e333121..00000000 --- a/tests/integration/servicesMiddleware.test.js +++ /dev/null @@ -1,60 +0,0 @@ -const Express = require('../../ExpressApp') -const WordpressService = require('../../lib/WordpressService') -const UserService = require('../../lib/LoggedInUserService') -const LeaderboardService = require('../../lib/LeaderboardService') -const supertestSession = require('supertest-session') -const fafApp = require('../../fafApp') -const passportMock = require('../helpers/PassportMock') -const { Axios } = require('axios') - -let testApp = null -let testSession = null - -beforeEach(() => { - const app = new Express() - fafApp.setup(app) - passportMock(app, { passAuthentication: true }) - testSession = supertestSession(app) - testApp = app -}) - -describe('Services Middleware', function () { - test('public service are loaded without a user', (done) => { - expect.assertions(3) - testApp.get('/', (req, res) => { - try { - expect(req.services).not.toBeUndefined() - expect(Object.keys(req.services).length).toBe(1) - expect(req.services.wordpressService).toBeInstanceOf(WordpressService) - - return res.send('success') - } catch (e) { - done(e) - } - }) - - testSession.get('/').then(() => done()) - }) - - test('additional services are loaded with authenticated user', (done) => { - expect.assertions(6) - testApp.get('/', (req, res) => { - try { - expect(req.services).not.toBeUndefined() - expect(Object.keys(req.services).length).toBe(4) - expect(req.services.wordpressService).toBeInstanceOf(WordpressService) - expect(req.services.userService).toBeInstanceOf(UserService) - expect(req.services.javaApiClient).toBeInstanceOf(Axios) - expect(req.services.leaderboardService).toBeInstanceOf(LeaderboardService) - - return res.send('success') - } catch (e) { - done(e) - } - }) - - testSession.get('/mock-login').then(() => { - testSession.get('/').then(() => done()) - }) - }) -}) diff --git a/tests/setup.js b/tests/setup.js index 0a417bba..66dbe8e1 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,23 +1,26 @@ const fs = require('fs') -const wordpressService = require('../lib/WordpressService') -const leaderboardService = require('../lib/LeaderboardService') +const { WordpressService } = require('../src/backend/services/WordpressService') +const { LeaderboardService } = require('../src/backend/services/LeaderboardService') +const nock = require('nock') +nock.disableNetConnect() +nock.enableNetConnect('127.0.0.1') beforeEach(() => { const newsFile = JSON.parse(fs.readFileSync('tests/integration/testData/news.json', { encoding: 'utf8', flag: 'r' })) - jest.spyOn(wordpressService.prototype, 'getNews').mockResolvedValue(newsFile) + jest.spyOn(WordpressService.prototype, 'getNews').mockResolvedValue(newsFile) const tnFile = JSON.parse(fs.readFileSync('tests/integration/testData/tournament-news.json', { encoding: 'utf8', flag: 'r' })) - jest.spyOn(wordpressService.prototype, 'getTournamentNews').mockResolvedValue(tnFile) + jest.spyOn(WordpressService.prototype, 'getTournamentNews').mockResolvedValue(tnFile) const ccFile = JSON.parse(fs.readFileSync('tests/integration/testData/content-creators.json', { encoding: 'utf8', flag: 'r' })) - jest.spyOn(wordpressService.prototype, 'getContentCreators').mockResolvedValue(ccFile) + jest.spyOn(WordpressService.prototype, 'getContentCreators').mockResolvedValue(ccFile) const ftFile = JSON.parse(fs.readFileSync('tests/integration/testData/faf-teams.json', { encoding: 'utf8', flag: 'r' })) - jest.spyOn(wordpressService.prototype, 'getFafTeams').mockResolvedValue(ftFile) + jest.spyOn(WordpressService.prototype, 'getFafTeams').mockResolvedValue(ftFile) const nhFile = JSON.parse(fs.readFileSync('tests/integration/testData/newshub.json', { encoding: 'utf8', flag: 'r' })) - jest.spyOn(wordpressService.prototype, 'getNewshub').mockResolvedValue(nhFile) + jest.spyOn(WordpressService.prototype, 'getNewshub').mockResolvedValue(nhFile) - jest.spyOn(leaderboardService.prototype, 'getLeaderboard').mockImplementation((id) => { + jest.spyOn(LeaderboardService.prototype, 'getLeaderboard').mockImplementation((id) => { const mapping = { 1: 'global', 2: '1v1', diff --git a/webpack.config.js b/webpack.config.js index 15a8ea69..11252781 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,27 +1,27 @@ -const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); -const path = require("path"); +const { WebpackManifestPlugin } = require('webpack-manifest-plugin') +const path = require('path') module.exports = { - mode: "production", + mode: 'production', entry: { - "clans": ["./src/frontend/js/entrypoint/clans.js"], - "content-creators": ["./src/frontend/js/entrypoint/content-creators.js"], - "donation": ["./src/frontend/js/entrypoint/donation.js"], - "faf-teams": ["./src/frontend/js/entrypoint/faf-teams.js"], - "getClans": ["./src/frontend/js/entrypoint/getClans.js"], - "leaderboards": ["./src/frontend/js/entrypoint/leaderboards.js"], - "navigation": ["./src/frontend/js/entrypoint/navigation.js"], - "newshub": ["./src/frontend/js/entrypoint/newshub.js"], - "play": ["./src/frontend/js/entrypoint/play.js"], - "report": ["./src/frontend/js/entrypoint/report.js"], + clans: ['./src/frontend/js/entrypoint/clans.js'], + 'content-creators': ['./src/frontend/js/entrypoint/content-creators.js'], + donation: ['./src/frontend/js/entrypoint/donation.js'], + 'faf-teams': ['./src/frontend/js/entrypoint/faf-teams.js'], + getClans: ['./src/frontend/js/entrypoint/getClans.js'], + leaderboards: ['./src/frontend/js/entrypoint/leaderboards.js'], + navigation: ['./src/frontend/js/entrypoint/navigation.js'], + newshub: ['./src/frontend/js/entrypoint/newshub.js'], + play: ['./src/frontend/js/entrypoint/play.js'], + report: ['./src/frontend/js/entrypoint/report.js'] }, output: { - filename: "[name].[contenthash].js", - path: path.resolve(__dirname, "dist/js"), - publicPath: "/dist/js", - clean: true, + filename: '[name].[contenthash].js', + path: path.resolve(__dirname, 'dist/js'), + publicPath: '/dist/js', + clean: true }, plugins: [ - new WebpackManifestPlugin({ useEntryKeys: true }), - ], -}; + new WebpackManifestPlugin({ useEntryKeys: true }) + ] +} diff --git a/yarn.lock b/yarn.lock index cb6161f5..55ff360a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,11 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" +"@babel/compat-data@^7.22.6": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" + integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== + "@babel/compat-data@^7.22.9": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.3.tgz#3febd552541e62b5e883a25eb3effd7c7379db11" @@ -59,7 +64,7 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/helper-compilation-targets@^7.22.15": +"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== @@ -70,6 +75,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-define-polyfill-provider@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz#a71c10f7146d809f4a256c373f462d9bba8cf6ba" + integrity sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" @@ -263,6 +279,25 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-runtime@^7.17.10": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.4.tgz#5132b388580002fc5cb7c84eccfb968acdc231cb" + integrity sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" + semver "^6.3.1" + +"@babel/runtime@^7.17.9": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db" + integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -346,6 +381,49 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.54.0.tgz#4fab9a2ff7860082c304f750e94acd644cf984cf" integrity sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ== +"@hapi/boom@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.1.tgz#ebb14688275ae150aa6af788dbe482e6a6062685" + integrity sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA== + dependencies: + "@hapi/hoek" "^11.0.2" + +"@hapi/bourne@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-3.0.0.tgz#f11fdf7dda62fe8e336fa7c6642d9041f30356d7" + integrity sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w== + +"@hapi/hoek@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-10.0.1.tgz#ee9da297fabc557e1c040a0f44ee89c266ccc306" + integrity sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw== + +"@hapi/hoek@^11.0.2": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-11.0.2.tgz#cb3ea547daac7de5c9cf1d960c3f35c34f065427" + integrity sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw== + +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@hapi/wreck@^18.0.0": + version "18.0.1" + resolved "https://registry.yarnpkg.com/@hapi/wreck/-/wreck-18.0.1.tgz#6df04532be25fd128c5244e72ccc21438cf8bb65" + integrity sha512-OLHER70+rZxvDl75xq3xXOfd3e8XIvz8fWY0dqg92UvhZ29zo24vQgfqgHSYhB5ZiuFpSLeriOisAlxAo/1jWg== + dependencies: + "@hapi/boom" "^10.0.1" + "@hapi/bourne" "^3.0.0" + "@hapi/hoek" "^11.0.2" + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -862,6 +940,23 @@ resolved "https://registry.yarnpkg.com/@servie/events/-/events-1.0.0.tgz#8258684b52d418ab7b86533e861186638ecc5dc1" integrity sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw== +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -1054,6 +1149,32 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@^5.23.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -1647,6 +1768,30 @@ babel-plugin-jest-hoist@^29.6.3: "@types/babel__core" "^7.1.14" "@types/babel__traverse" "^7.0.6" +babel-plugin-polyfill-corejs2@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" + integrity sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.3" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.8.5: + version "0.8.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz#25c2d20002da91fe328ff89095c85a391d6856cf" + integrity sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.3" + core-js-compat "^3.33.1" + +babel-plugin-polyfill-regenerator@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz#d4c49e4b44614607c13fb769bcd85c72bb26a4a5" + integrity sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.3" + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -1867,7 +2012,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5, browserslist@^4.21.9: +browserslist@^4.14.5, browserslist@^4.21.9, browserslist@^4.22.1: version "4.22.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== @@ -1912,6 +2057,11 @@ builtin-modules@^3.3.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== + builtins@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" @@ -2294,6 +2444,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + commander@^9.0.0: version "9.5.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" @@ -2343,6 +2498,13 @@ connect-flash@^0.1.1: resolved "https://registry.yarnpkg.com/connect-flash/-/connect-flash-0.1.1.tgz#d8630f26d95a7f851f9956b1e8cc6732f3b6aa30" integrity sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA== +console.table@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/console.table/-/console.table-0.10.0.tgz#0917025588875befd70cf2eff4bef2c6e2d75d04" + integrity sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g== + dependencies: + easy-table "1.1.0" + constantinople@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151" @@ -2398,6 +2560,13 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== +core-js-compat@^3.33.1: + version "3.33.3" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.3.tgz#ec678b772c5a2d8a7c60a91c3a81869aa704ae01" + integrity sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow== + dependencies: + browserslist "^4.22.1" + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2770,6 +2939,13 @@ eachr@^4.5.0: dependencies: typechecker "^6.2.0" +easy-table@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.1.0.tgz#86f9ab4c102f0371b7297b92a651d5824bc8cb73" + integrity sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA== + optionalDependencies: + wcwidth ">=1.0.1" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -3871,7 +4047,7 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@^11.0.2: +globby@^11.0.2, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -5204,10 +5380,16 @@ jit-grunt@0.10.0: resolved "https://registry.yarnpkg.com/jit-grunt/-/jit-grunt-0.10.0.tgz#008c3a7fe1e96bd0d84e260ea1fa1783457f79c2" integrity sha512-eT/f4c9wgZ3buXB7X1JY1w6uNtAV0bhrbOGf/mFmBb0CDNLUETJ/VRoydayWOI54tOoam0cz9RooVCn3QY1WoA== -jquery@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" - integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== +joi@^17.6.4: + version "17.11.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" + integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" js-stringify@^1.0.2: version "1.0.2" @@ -5231,7 +5413,7 @@ js-yaml-js-types@1.0.0: dependencies: esprima "^4.0.1" -js-yaml@4.1.0, js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -5517,6 +5699,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -5943,6 +6130,21 @@ node-cache@^5.1.2: dependencies: clone "2.x" +node-dependency-injection@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/node-dependency-injection/-/node-dependency-injection-3.1.2.tgz#d15d984eaab9658aa2315eab9e5bdecdbd2d8581" + integrity sha512-Nl1pSjjKT+Sx7jTt0x3Nh+X4zOa5lieNcq5ynW+ro9KusXjzkAz2TSVuSl5rcQdSiXyt6UZNgslUGfoJx/Nkpw== + dependencies: + "@babel/plugin-transform-runtime" "^7.17.10" + "@babel/runtime" "^7.17.9" + "@typescript-eslint/typescript-estree" "^5.23.0" + chalk "^4.1.0" + commander "^8.3.0" + console.table "^0.10.0" + js-yaml "^4.0.0" + json5 "^2.2.2" + validate-npm-package-name "^3.0.0" + node-emoji@^1.10.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -7046,6 +7248,11 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -7200,7 +7407,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.10.0, resolve@^1.15.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.3, resolve@^1.22.4, resolve@^1.9.0: +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.2, resolve@^1.22.3, resolve@^1.22.4, resolve@^1.9.0: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -7356,7 +7563,7 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.3.4, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: +semver@^7.0.0, semver@^7.3.4, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -7506,6 +7713,16 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-oauth2@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/simple-oauth2/-/simple-oauth2-5.0.0.tgz#3b7d85700944b26f8f5451c017426292f330460c" + integrity sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ== + dependencies: + "@hapi/hoek" "^10.0.1" + "@hapi/wreck" "^18.0.0" + debug "^4.3.4" + joi "^17.6.4" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -8086,11 +8303,18 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.9.0: +tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -8199,6 +8423,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.2.tgz#00d1c7c1c46928c5845c1ee8d0cc2791031d4c43" + integrity sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ== + uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" @@ -8423,6 +8652,13 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw== + dependencies: + builtins "^1.0.3" + validator@^13.9.0: version "13.11.0" resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" @@ -8462,7 +8698,7 @@ watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -wcwidth@^1.0.1: +wcwidth@>=1.0.1, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==