From 55dc0a9754e6595b3d3530733beeddd0d3d52ec6 Mon Sep 17 00:00:00 2001 From: fcaps Date: Thu, 23 Nov 2023 07:24:00 +0100 Subject: [PATCH 1/3] reactivate clan pages --- Gruntfile.js | 3 +- fafApp.js | 5 +- grunt/concurrent.js | 2 +- grunt/copy.js | 24 +++ lib/clan/ClanRepository.js | 135 +++++++++++++ lib/clan/ClanService.js | 43 ++++ lib/clan/ClanServiceFactory.js | 14 ++ package.json | 2 + public/js/app/clan.js | 5 + public/js/app/clans.js | 175 ++++------------ public/js/app/getClans.js | 72 ------- public/styles/site/clans.sass | 231 +++------------------- routes/middleware.js | 7 +- routes/views/clanRouter.js | 16 +- routes/views/dataRouter.js | 18 ++ routes/views/defaultRouter.js | 4 + templates/views/clans.pug | 80 -------- templates/views/clans/clan.pug | 48 +++++ templates/views/clans/clans.pug | 9 + templates/views/clans/seeClan.pug | 27 --- tests/integration/clanRouter.test.js | 52 ++++- tests/integration/testData/clan-http.json | 1 + tests/integration/testData/clan.json | 1 + tests/setup.js | 5 + yarn.lock | 33 +++- 25 files changed, 478 insertions(+), 534 deletions(-) create mode 100644 grunt/copy.js create mode 100644 lib/clan/ClanRepository.js create mode 100644 lib/clan/ClanService.js create mode 100644 lib/clan/ClanServiceFactory.js create mode 100644 public/js/app/clan.js delete mode 100644 public/js/app/getClans.js delete mode 100644 templates/views/clans.pug create mode 100644 templates/views/clans/clan.pug create mode 100644 templates/views/clans/clans.pug delete mode 100644 templates/views/clans/seeClan.pug create mode 100644 tests/integration/testData/clan-http.json create mode 100644 tests/integration/testData/clan.json diff --git a/Gruntfile.js b/Gruntfile.js index 747fb360..ef46fabc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -23,6 +23,7 @@ module.exports = function(grunt) { grunt.registerTask('prod', [ 'sass:dist', 'concat:js', - 'uglify:dist' + 'uglify:dist', + 'copy' ]); }; diff --git a/fafApp.js b/fafApp.js index c8be2e82..26e66d94 100644 --- a/fafApp.js +++ b/fafApp.js @@ -63,8 +63,7 @@ module.exports.setup = (app) => { app.set('views', 'templates/views') app.set('view engine', 'pug') app.set('port', appConfig.expressPort) - - app.use(middleware.injectServices) + app.use(middleware.initLocals) app.use(express.static('public', { @@ -88,6 +87,8 @@ module.exports.setup = (app) => { })) app.use(passport.initialize()) app.use(passport.session()) + app.use(middleware.injectServices) + app.use(flash()) app.use(middleware.username) app.use(copyFlashHandler) diff --git a/grunt/concurrent.js b/grunt/concurrent.js index 4f0b4a7d..c83f66da 100644 --- a/grunt/concurrent.js +++ b/grunt/concurrent.js @@ -1,6 +1,6 @@ module.exports = { dev: { - tasks: ['nodemon', 'concat', 'watch'], + tasks: ['nodemon', 'concat', 'watch', 'copy'], options: { logConcurrentOutput: true } diff --git a/grunt/copy.js b/grunt/copy.js new file mode 100644 index 00000000..c19015d4 --- /dev/null +++ b/grunt/copy.js @@ -0,0 +1,24 @@ +module.exports = { + js: { + files:[ + { + expand: true, + cwd: 'node_modules/simple-datatables/dist', + src: 'module.js', + dest: 'public/js/', + rename: function(dest, src) { + return dest + 'simple-datatables.js' + } + }, + { + expand: true, + cwd: 'node_modules/simple-datatables/dist', + src: 'style.css', + dest: 'public/styles/css/', + rename: function(dest, src) { + return dest + 'simple-datatables.css' + } + } + ] + } +} diff --git a/lib/clan/ClanRepository.js b/lib/clan/ClanRepository.js new file mode 100644 index 00000000..55fceb14 --- /dev/null +++ b/lib/clan/ClanRepository.js @@ -0,0 +1,135 @@ +class ClanRepository { + constructor(javaApiClient) { + this.javaApiClient = javaApiClient + } + + async fetchAll() { + const response = await this.javaApiClient.get('/data/clan?sort=createTime&include=leader&fields[clan]=name,tag,description,leader,memberships,createTime&fields[player]=login&page[number]=1&page[size]=3000') + + if (response.status !== 200) { + throw new Error('ClanRepository::fetchAll failed with response status "' + response.status + '"') + } + + const responseData = JSON.parse(response.data) + + if (typeof responseData !== 'object' || responseData === null) { + throw new Error('ClanRepository::fetchAll malformed response, not an object') + } + + if (!responseData.hasOwnProperty('data')) { + throw new Error('ClanRepository::fetchAll malformed response, expected "data"') + } + + if (responseData.data.length === 0) { + console.log('[info] clans empty') + + return [] + } + + if (!responseData.hasOwnProperty('included')) { + throw new Error('ClanRepository::fetchAll malformed response, expected "included"') + } + + const clans = responseData.data.map((item, index) => ({ + id: parseInt(item.id), + leaderName: responseData.included[index].attributes.login, + name: item.attributes.name, + tag: item.attributes.tag, + createTime: item.attributes.createTime, + description: item.attributes.description, + population: item.relationships.memberships.data.length + })) + + clans.sort((a, b) => { + if (a.population > b.population) { + return -1 + } + if (a.population < b.population) { + return 1 + } + + return 0 + }); + + return await clans + } + + async fetchClan(id) { + let response = await this.javaApiClient.get(`/data/clan/${id}?include=memberships.player`) + + if (response.status !== 200) { + throw new Error('ClanRepository::fetchClan failed with response status "' + response.status + '"') + } + + const data = JSON.parse(response.data) + + if (typeof data !== 'object' || data === null) { + throw new Error('ClanRepository::fetchClan malformed response, not an object') + } + + if (!data.hasOwnProperty('data')) { + throw new Error('ClanRepository::fetchClan malformed response, expected "data"') + } + + if (typeof data.data !== 'object' || data.data === null) { + return null + } + + if (typeof data.included !== 'object' || data.included === null) { + throw new Error('ClanRepository::fetchClan malformed response, expected "included"') + } + + const clanRaw = data.data.attributes + + const clan = { + id: data.data.id, + name: clanRaw.name, + tag: clanRaw.tag, + description: clanRaw.description, + createTime: clanRaw.createTime, + requiresInvitation: clanRaw.requiresInvitation, + tagColor: clanRaw.tagColor, + updateTime: clanRaw.updateTime, + founder: null, + leader: null, + memberships: {}, + } + + let members = {}; + + for (let k in data.included) { + switch (data.included[k].type) { + case "player": + const player = data.included[k]; + if (!members[player.id]) members[player.id] = {}; + members[player.id].id = player.id; + members[player.id].name = player.attributes.login; + + if (player.id === data.data.relationships.leader.data.id) { + clan.leader = members[player.id] + } + + if (player.id === data.data.relationships.founder.data.id) { + clan.founder = members[player.id] + } + + break; + + case "clanMembership": + const membership = data.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; + } + } + + clan.memberships = members + + return clan + } +} + +module.exports = ClanRepository diff --git a/lib/clan/ClanService.js b/lib/clan/ClanService.js new file mode 100644 index 00000000..d96ebcc3 --- /dev/null +++ b/lib/clan/ClanService.js @@ -0,0 +1,43 @@ +const {MutexService} = require("../MutexService"); +const clanTTL = 60 * 5 + +class ClanService { + constructor(cacheService, clanRepository, lockTimeout = 3000) { + this.lockTimeout = lockTimeout + this.cacheService = cacheService + this.mutexService = new MutexService() + this.clanRepository = clanRepository + } + + getCacheKey(name) { + return 'ClanService_' + name + } + + async getClan(id) { + return this.clanRepository.fetchClan(id) + } + + async getAll() { + + const cacheKey = this.getCacheKey('all') + + if (this.cacheService.has(cacheKey)) { + return this.cacheService.get(cacheKey) + } + + if (this.mutexService.locked) { + await this.mutexService.acquire(() => { + }, this.lockTimeout) + return this.getAll() + } + + await this.mutexService.acquire(async () => { + const result = await this.clanRepository.fetchAll() + this.cacheService.set(cacheKey, result, clanTTL); + }) + + return this.getAll() + } +} + +module.exports = ClanService diff --git a/lib/clan/ClanServiceFactory.js b/lib/clan/ClanServiceFactory.js new file mode 100644 index 00000000..c75f585d --- /dev/null +++ b/lib/clan/ClanServiceFactory.js @@ -0,0 +1,14 @@ +const ClanService = require("./ClanService") +const ClanRepository = require("./ClanRepository") +const {Axios} = require("axios") +const cacheService = require('../CacheService') + +module.exports = (javaApiBaseURL, token) => { + const config = { + baseURL: javaApiBaseURL, + headers: {Authorization: `Bearer ${token}`} + }; + const clanClient = new Axios(config) + + return new ClanService(cacheService, new ClanRepository(clanClient)) +} diff --git a/package.json b/package.json index 70591b1d..e2e611a1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "express": "^4.18.1", "express-session": "^1.17.3", "express-validator": "7.0.1", + "grunt-contrib-copy": "^1.0.0", "moment": "^2.29.4", "node-cache": "^5.1.2", "node-fetch": "^2.6.7", @@ -21,6 +22,7 @@ "request": "2.88.2", "session-file-store": "^1.5.0", "showdown": "^2.1.0", + "simple-datatables": "^8.0.1", "supertest-session": "^5.0.1", "url-slug": "^4.0.1" }, diff --git a/public/js/app/clan.js b/public/js/app/clan.js new file mode 100644 index 00000000..e6475474 --- /dev/null +++ b/public/js/app/clan.js @@ -0,0 +1,5 @@ +import('/js/simple-datatables.js').then(({DataTable}) => { + new DataTable("#clan-members", { + perPageSelect: null + }) +}) diff --git a/public/js/app/clans.js b/public/js/app/clans.js index 9ddd1faa..b26faa96 100644 --- a/public/js/app/clans.js +++ b/public/js/app/clans.js @@ -1,146 +1,37 @@ -//Variables used across the board -let pageNumber = 0; -let lastPage = 5; -let clanList = []; -let timedOutPlayers = []; -let currentClan = ''; +import('/js/simple-datatables.js').then(({DataTable}) => { + fetch('/data/clans.json') + .then(response => response.json()) + .then(data => { + if (!data || !data.length) { + return + } + + window.datatable = new DataTable("#clan-table", { + perPageSelect: null, + data: { + headings: [ + 'TAG', + 'NAME', + 'LEADER', + 'POPULATION', + ], + data: data.map(item => { + return [item.tag, item.name, item.leaderName, item.population] + }) + }}) + + window.datatable.on("datatable.selectrow", (rowIndex, event) => { + if (typeof rowIndex === "number") { + event.preventDefault() + window.location.href = '/clans/view/' + data[rowIndex].id + } + }) + }) + .catch((err) => { + console.log(err, 'loading clans failed') + }) +}) -let clanListDivided = clanList.length / 100; -// This decides the time filter -let d = new Date(); -let timeFilter = 6; // 6 Months is the default value -let minusTimeFilter = d.setMonth(d.getMonth() - timeFilter); -let currentDate = new Date(minusTimeFilter).toISOString(); - - -//Names of buttons -async function clanOneJSON() { - //Check which category is active - const response = await fetch(`js/app/members/getAllClans.json`); - - const data = await response.json(); - - clanList = data; - - return await data; -} - -clanOneJSON(); - - -//Updates the clan according to what is needed -function clanUpdate() { - // We convert clanList into a string to find out how many pages we have. We can find so by checking the first two digits. In other words, if we have 1349 players, then we have 13 pages. However, if we have 834, we have 8 pages (only take the first digit). - clanListDivided = clanList.length / 100; - lastPage = Math.floor(clanListDivided); - - //Deletes everything with the class clanDelete, we do it to delete all the previous clan and add the new one back in. - let clanDelete = document.querySelectorAll('.clanDelete'); - clanDelete.forEach(clanDelete => { - clanDelete.remove(); - }); - - //determines the current page, whether to add or substract the missing players in case we pressed next or previous then it will add or substract players - let clanIndex = (clanList.length - 100) - (pageNumber * 100); //- addNextPlayer; - let next100Players = clanList.length - (pageNumber * 100); - - for (clanIndex; clanIndex < next100Players; clanIndex++) { - if (clanIndex < 0) { - clanIndex = 0; - console.log('There are no more players left.'); - } - // Gets the player data and inserts it into the li element - - - document.getElementById('clanPlayer').insertAdjacentHTML('afterbegin', `
  • ${clanList[clanIndex][0].leaderName}
  • `); - document.getElementById('clanName').insertAdjacentHTML('afterbegin', `
  • ${clanList[clanIndex][1].name}
  • `); - document.getElementById('clanTAG').insertAdjacentHTML('afterbegin', `
  • ${clanList[clanIndex][1].tag}
  • `); - - document.getElementById('clanPopulation').insertAdjacentHTML('afterbegin', `
  • ${clanList[clanIndex][1].population}
  • `); - - } -} - -//This function triggers when the next, previous, first or last button are clicked -let pageButton = document.querySelectorAll('.pageButton'); - -function pageChange(newPageNumber) { - pageNumber = newPageNumber; - pageButton.forEach(element => element.classList.remove(`exhaustedButton`)); - if (pageNumber === 0) { - //You see 4-7 pageButton because there are a total of 8 buttons counting the ones at the bottom of the page - pageButton[0].classList.add('exhaustedButton'); - pageButton[2].classList.add('exhaustedButton'); - pageButton[4].classList.add('exhaustedButton'); - pageButton[6].classList.add('exhaustedButton'); - } - if (pageNumber === lastPage) { - pageButton[1].classList.add('exhaustedButton'); - pageButton[3].classList.add('exhaustedButton'); - pageButton[5].classList.add('exhaustedButton'); - pageButton[7].classList.add('exhaustedButton'); - } - clanUpdate(); -} - -// Don't know why but the code refuses to run correctly unless it is delayed by at least 1/10 of a second/100ms. Not really an issue but idk why its doing this -setTimeout(() => { - clanUpdate(); -}, 1000); - - -// SEARCH BAR - - -//Gets called from the HTML search input form -function pressEnter(event) { - let inputText = event.target.value; - // this regex grabs the current input and due to the ^, it selects whatever starts with the input, so if you type Te, Tex will show up. - if (inputText === '') { - document.querySelectorAll('.removeOldSearch').forEach(element => element.remove()); - } else { - let regex = `^${inputText.toLowerCase()}`; - let searchName = clanList.filter(element => element[1].tag.toLowerCase().match(regex)); - - document.querySelectorAll('.removeOldSearch').forEach(element => element.remove()); - for (let player of searchName.slice(0, 5)) { - document.querySelector('#placeMe').insertAdjacentHTML('afterend', `
  • ${player[1].tag}
  • `); - } - - if (event.key === 'Enter') { - - findPlayer(inputText); - } - } - document.querySelector('#errorLog').innerText = ''; - -} - -function findPlayer(userInput) { - clanOneJSON(currentClan) - .then(() => { - //input from the searchbar becomes clanName and then searchClan is their index number - let searchClan = clanList.findIndex(element => element[1].tag.toLowerCase() === userInput.toLowerCase()); - if (searchClan !== -1) { - window.location.href = `/clans/${userInput}`; - } else { - throw new Error('clan couldnt be found'); - } - - - document.querySelector('#errorLog').innerText = ``; - }).catch(() => { - document.querySelector('#errorLog').innerText = `Clan "${userInput}" couldn't be found`; - }); -} - -// SEACRH AND CLEAR BUTTONS -document.querySelector('#clearSearch').addEventListener('click', () => { - document.querySelector('#searchResults').classList.add('appearWhenSearching'); - document.querySelector('#clearSearch').classList.add('appearWhenSearching'); - let clanDelete = document.querySelectorAll('.clanDeleteSearch'); - clanDelete.forEach(element => element.remove()); -}); diff --git a/public/js/app/getClans.js b/public/js/app/getClans.js deleted file mode 100644 index f34fec5e..00000000 --- a/public/js/app/getClans.js +++ /dev/null @@ -1,72 +0,0 @@ -const clanName = document.getElementById('clanName'); -const clanTag = document.getElementById('clanTag'); -const clanDescription = document.getElementById('clanDescription'); -const clanCreation = document.getElementById('clanCreation'); -const clanLeader = document.getElementById('clanLeader'); -const clanMembers = document.getElementById('clanMembers'); - - -let leaderName = ''; - -async function getClan() { - - //So here we check the tag of the clan in the url - let url = window.location.href; - const sliceIndicator = url.indexOf('/clans'); -// The slice has + 7 because thats the amount of characters in "/clans/" yes with two /, not one! - let findClanTag = url.slice(sliceIndicator + 7, sliceIndicator + 10); - let clanTag = await findClanTag.replace(/\?m|\?/gm,''); - - // We compare the url TAG with the TAGS available in getAllClans and find the clan leader this way - - // TODO: Change this hardcoded url into something with env - const response = await fetch(`https://api.faforever.com/data/clan?include=memberships.player&filter=tag==${clanTag}`); - const fetchData = await response.json(); - - const leaderID = fetchData.data[0].relationships.leader.data.id; - - fetchData.included.forEach((element, index) => { - if (index % 2 !== 0) { - if (element.id === leaderID) { - leaderName = element.attributes.login; - } - } - }); - - - //verifies if user is a member, which allows them to leave the clan - const clanMember = document.getElementById('iAmMember'); - const isMember = url.indexOf('?member'); - let verifyMembership = url.slice(isMember + 8); - if (verifyMembership === 'true') { - clanMember.style.display = 'block'; - } - return fetchData; -} -setTimeout( ()=> { - getClan() - .then(fetchData => { - - const { attributes} = fetchData.data[0]; - clanName.insertAdjacentHTML('afterbegin', - `${attributes.name}`); - clanDescription.insertAdjacentHTML('afterbegin', - `${attributes.description}`); - clanTag.insertAdjacentHTML('afterbegin', - `Welcome to "${attributes.tag}"`); - //clanLeader.insertAdjacentHTML('afterbegin', - // `${fetchData.data[0].attributes.id}`); - clanCreation.insertAdjacentHTML('afterbegin', - `Created on ${attributes.createTime.slice(0, 10)}`); - clanLeader.insertAdjacentHTML('afterbegin', - `Led by ${leaderName}`); - - for (let i = 0; i < fetchData.included.length; i++) { - if (i % 2 !== 0) { - clanMembers.insertAdjacentHTML('afterbegin', - `
  • ${fetchData.included[i].attributes.login}
  • `); - } - } - }); - -},750); diff --git a/public/styles/site/clans.sass b/public/styles/site/clans.sass index d3f94d76..7cd0594f 100644 --- a/public/styles/site/clans.sass +++ b/public/styles/site/clans.sass @@ -1,204 +1,35 @@ @use '../abstracts/variables' @use '../abstracts/mixins' -// Background of the whole thing -.clanBackground - padding: 7.5vw 11vw - background-image: url("/../../images/fafbgpattern0.5.jpg") - ul - list-style: none - li - padding: 0.5em 0 - -// This is the searchbar input form -input - margin: 1em 1em 2px 1em - background-color: variables.$background-tertiary - color: variables.$color-active - font-size: var(--paragraph-font-size) - border: 0.5em solid variables.$background-secondary - border-radius: 10px - z-index: 2 - position: relative - - - -// The main box with all the names, tags, ranks, etc. -.mainClanContainer - @include mixins.gridMainContainer - padding: 0 - p - padding: 0.2em - -.clanContainer - font-family: variables.$family-paragraph - padding: 0em - .clanItem - - ul - font-size: var(--paragraph-font-size) - padding: 1em 0 - font-weight: 700 - - // ACTUAL LISTS - // There is a double li element here because the js creates a li parent element and then puts another li child element (which the child is all the names you see like ANZ and so on) - li - list-style: none - font-weight: normal - padding: 0em 0 - li - font-size: var(--paragraph-font-size) - padding: 1em 0 - -.clanBorder - border: 1px solid variables.$color-active - padding: 0px - background-color: variables.$background-primary - -// Change background colors on items in the leaderboard -.clanItem - li:nth-child(odd) - background-color: mix(variables.$background-tertiary, variables.$background-primary, 25%) - li:nth-child(even) - background-color: variables.$background-secondary - -// First page last page, etc -.clanButton - padding: 1em 0.25em - display: inline-block - place-items: start - font-family: variables.$family-paragraph - font-size: var(--paragraph-font-size) - letter-spacing: 0em - ul - padding: 0.5em - font-weight: 900 - li - - display: inline-block - padding: 0.6em 1em - border: 0.05em solid variables.$color-inactive - background-color: variables.$background-tertiary - cursor: pointer - transition: 0.4s - &:hover - border: 0.05em solid variables.$color-active - filter: brightness(2) - .exhaustedButton - border: 0.05em solid variables.$color-inactive - background-color: variables.$background-secondary - color: variables.$color-inactive-plus - pointer-events: none - cursor: default - -#searchResults - margin: 1em 0 0 0 -.appearWhenSearching - display: none -.centerYourself - display: grid - justify-items: center - position: relative - -.searchBar - ul - background-color: variables.$background-secondary - color: variables.$color-inactive - font-size: calc(var(--paragraph-font-size) * 0.9) - border-radius: 0 0 10px 10px +#clan-view text-align: left - li - padding: 0.5em 1em - -.clearButton - li - cursor: pointer - margin: 1em 1em 0 1em - border-radius: 10px - background-color: variables.$background-tertiary - color: variables.$color-inactive - transition: 0.4s - &:hover - background-color: variables.$color-inactive-plus - color: variables.$color-active - .fa-trash-alt - padding: 0.5em - -@media (max-width: 800px) - .clanFilter - font-size: 10px - .clanContainer - ul - font-size: 10px - padding: 1em 0 0 0 - - li - text-overflow: ellipsis - font-size: 10px - padding: 1em 0 - - .clearButton - left: 90% - top: 68% - .clanBackground - padding: 1vw - .clanButton - padding: 1vw - font-size: var(--paragraph-font-size) - ul - li - padding: 0.65em - .clanDelete - font-size: 10px - - -@media (max-width: 1600px) - .clanContainer - ul - font-size: 14px - li - font-size: 14px - .clanFilter - font-size: 14px - - - -// Below are classes that depend -.clanManagement - @include mixins.gridMainContainer - padding: 0vw 15vw - margin: 1em 0 - .clanManagementItem - padding: 1.5em 0 - display: inline-block - justify-self: center - align-self: center - - //clan Description - textarea - width: 40vw - - .clanManagementDanger - background-color: variables.$Cybran-dark - border-radius: 20px - ul - text-align: center - li - display: inline-block - list-style: none - text-align: left - .clanManagementTable - padding: 1.5em - display: inline-block - justify-self: center - align-self: center - th - padding: 4em - -@media (max-width: 800px) - .clanManagement - padding: 5px -.bigButton - font-size: 1.25em - - + margin: 20px 60px +#clan-info + text-align: left + td + padding: 10px +#clan-table + button + margin: 0 + padding: 0 + .datatable-sorter::before + border-top: 4px solid white + .datatable-sorter::after + border-bottom: 4px solid white + tbody + tr:hover + background-color: #1f2349 + cursor: pointer + td + text-align: left +#clan-members + button + margin: 0 + padding: 0 + .datatable-sorter::before + border-top: 4px solid white + .datatable-sorter::after + border-bottom: 4px solid white + tbody + td + text-align: left diff --git a/routes/middleware.js b/routes/middleware.js index 79391dbf..b98eb057 100755 --- a/routes/middleware.js +++ b/routes/middleware.js @@ -1,4 +1,5 @@ -const WordpressServiceFactory = require("../lib/WordpressServiceFactory"); +const WordpressServiceFactory = require("../lib/WordpressServiceFactory") +const ClanServiceFactory = require("../lib/clan/ClanServiceFactory") const appConfig = require("../config/app"); const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) @@ -43,6 +44,10 @@ exports.injectServices = function(req, res, next) { req.services = { wordpressService: wordpressService } + + if (req.isAuthenticated()) { + req.services.clanService = ClanServiceFactory(appConfig.apiUrl, req.user.data.attributes.token) + } next() } diff --git a/routes/views/clanRouter.js b/routes/views/clanRouter.js index 5000277f..7a742602 100644 --- a/routes/views/clanRouter.js +++ b/routes/views/clanRouter.js @@ -1,7 +1,17 @@ -const express = require('express'); -const router = express.Router(); +const express = require('express') +const router = express.Router() +const middlewares = require('../middleware') -// This will be replaced soon, therefor I did not spend time on it +router.get('/', middlewares.isAuthenticated(), (req, res) => res.render('clans/clans')) +router.get('/view/:id', middlewares.isAuthenticated(), async (req, res) => { + const clanId = parseInt(req.params.id || null) + + if (!clanId) { + return res.redirect('/clans') + } + + return res.render('clans/clan', {clan: await req.services.clanService.getClan(clanId)}) +}) router.get('*', (req, res) => res.status(503).render('errors/503-known-issue')); module.exports = router diff --git a/routes/views/dataRouter.js b/routes/views/dataRouter.js index 8b0faeec..9b1735b5 100644 --- a/routes/views/dataRouter.js +++ b/routes/views/dataRouter.js @@ -33,6 +33,24 @@ router.get('/content-creators.json', async (req, res) => { getData(req, res, 'content-creators', await req.services.wordpressService.getContentCreators()) }) +router.get('/clans.json', async (req, res) => { + try { + return res.json(await req.services.clanService.getAll()) + } catch (e) { + if (e instanceof AcquireTimeoutError) { + return res.status(503).json({error: 'timeout reached'}) + } + + console.error('[error] dataRouter::get:clans.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/defaultRouter.js b/routes/views/defaultRouter.js index 2ec30e28..b96bc455 100644 --- a/routes/views/defaultRouter.js +++ b/routes/views/defaultRouter.js @@ -13,6 +13,10 @@ router.get('/contribution', (reqd, res) => res.render('contribution')) router.get('/content-creators', (reqd, res) => res.render('content-creators')) router.get('/play', (reqd, res) => res.render('play')) +// redirect for the game-client https://github.com/FAForever/website/issues/459 +router.get('/clan/:id', (req, res) => { + res.redirect('/clans/view/' + req.params.id) +}) // https://github.com/search?q=org%3AFAForever+account_activated&type=code router.get('/account_activated', (req, res) => res.redirect('/account/register')) diff --git a/templates/views/clans.pug b/templates/views/clans.pug deleted file mode 100644 index 8e40ff6b..00000000 --- a/templates/views/clans.pug +++ /dev/null @@ -1,80 +0,0 @@ -extends ../layouts/default -include ../mixins/flash-messages -block bannerMixin - -block content - - .clanBackground - .mainClanContainer - .clanContainer.column12.centerYourself - +flash-messages(flash) - p Search FAF Clans - #errorLog - input#input(onkeyup=`pressEnter(event)` type='text' placeholder='Clan TAG') - #searchbar - .searchBar - ul - #placeMe - ul#clearSearch.clearButton.appearWhenSearching - li.fas.fa-trash-alt - .mainClanContainer.clanBorder#searchResults.appearWhenSearching - - .clanContainer.column6 - .clanItem - ul Clan Name - li#clanNameSearch - .clanContainer.column1 - .clanItem - ul TAG - li#clanTAGSearch - .clanContainer.column3 - .clanItem - ul Clan Leader - li#clanPlayerSearch - .clanContainer.column2 - .clanItem - ul Population - li#clanPopulationSearch - .mainClanContainer - - //DO NOT Change the order of these without changing the js. For them to work, they need to be in this specific order - .clanContainer.clanButton.column12 - ul - li(onclick= `pageChange(0)`).pageButton.exhaustedButton First - li(onclick= `pageChange(lastPage)`).pageButton Last - ul - li(onclick= `pageChange(pageNumber - 1)`).pageButton.exhaustedButton Previous - li(onclick= `pageChange(pageNumber + 1)`).pageButton Next - - .mainClanContainer.clanBorder - - - .clanContainer.column6 - .clanItem - ul Clan Name - li#clanName - .clanContainer.column1 - .clanItem - ul TAG - li#clanTAG - .clanContainer.column3 - .clanItem - ul#spawnPlayer Clan Leader - li#clanPlayer - .clanContainer.column2 - .clanItem - ul Population - li#clanPopulation - .mainClanContainer - .clanContainer.clanButton.column12 - ul - li(onclick= `pageChange(0)`).pageButton.exhaustedButton First - li(onclick= `pageChange(lastPage)`).pageButton Last - ul - li(onclick= `pageChange(pageNumber - 1)`).pageButton.exhaustedButton Previous - li(onclick= `pageChange(pageNumber + 1)`).pageButton Next - -block js - - script( src="../../js/app/clans.js") - diff --git a/templates/views/clans/clan.pug b/templates/views/clans/clan.pug new file mode 100644 index 00000000..e20c94b2 --- /dev/null +++ b/templates/views/clans/clan.pug @@ -0,0 +1,48 @@ +extends ../../layouts/default +block head + link(rel='stylesheet' href='/styles/css/simple-datatables.css') +block content + #clan-view + a(href="/clans") <- OVERVIEW +
    +
    + .gridMainContainer + .column4 + table#clan-info + tr + td NAME + td #{clan.name} + tr + td TAG + td #{clan.tag} + tr + td LEADER + td #{clan.leader.name} 👑 + tr + td FOUNDER + if clan.founder + td #{clan.founder.name} + else + td GONE + tr + td JOIN + if clan.requiresInvitation + td Invitation Only + else + td Free For All + .column8 + h2 Description + div #{clan.description} + .column12 + table#clan-members + thead + tr + th NAME + th JOINED AT + tbody + each member in clan.memberships + tr + td #{member.name} + td #{member.joinedAt} +block js + script( src="../../js/app/clan.js" type="module") diff --git a/templates/views/clans/clans.pug b/templates/views/clans/clans.pug new file mode 100644 index 00000000..dd5c6783 --- /dev/null +++ b/templates/views/clans/clans.pug @@ -0,0 +1,9 @@ +extends ../../layouts/default + +block head + link(rel='stylesheet' href='/styles/css/simple-datatables.css') +block content + h1 Clans + table#clan-table +block js + script( src="../../js/app/clans.js" type="module") diff --git a/templates/views/clans/seeClan.pug b/templates/views/clans/seeClan.pug deleted file mode 100644 index cd8c3d08..00000000 --- a/templates/views/clans/seeClan.pug +++ /dev/null @@ -1,27 +0,0 @@ -extends ../../layouts/default -block bannerMixin - -block content - // Most of this page is generated through its js file, this pug file is just used to put the ids in place - .renderClan - .renderClanContainer.column12 - h2#clanTag - h1#clanName - p#clanDescription - p#clanCreation - h1#clanLeader - - - #iAmMember - form(method='post', action="/clans/leave", onsubmit="return confirm('You will not be able to return in that clan unless invited again. Press OK to confim.');") - input(type='hidden', name='clan_id', value=clan_id) - input(type='hidden', name='membership_id', value=my_membership) - h2.row.centered-flex - button(type='submit').danger.btn.btn-lg Leave my clan - p Clan Members - - ul.renderClanSubGrid#clanMembers - - -block js - script( src="../../js/app/getClans.js") diff --git a/tests/integration/clanRouter.test.js b/tests/integration/clanRouter.test.js index 90ff00af..5079a7f4 100644 --- a/tests/integration/clanRouter.test.js +++ b/tests/integration/clanRouter.test.js @@ -1,21 +1,65 @@ const express = require('express') const supertestSession = require("supertest-session"); const fafApp = require('../../fafApp') +const passportMock = require("../helpers/PassportMock"); let testSession = null beforeEach(async () => { const app = new express() fafApp.setup(app) + passportMock(app, {passAuthentication: true}) fafApp.loadRouters(app) testSession = supertestSession(app) }) describe('Clan Routes', function () { - const arr = ['/clans', '/clans/everything'] - test.each(arr)("responds with 503 to %p", (async (route) => { - const res = await testSession.get(route) + test('clan list is protected', async () => { + let response = await testSession.get('/clans') + expect(response.statusCode).toBe(302) + expect(response.headers.location).toBe('/login') + + await testSession.get('/mock-login') + + response = await testSession.get('/clans') + expect(response.statusCode).toBe(200) + expect(response.text).toContain('Clans'); + }) + + test('clan list can be accessed with user', async () => { + const response = await testSession.get('/clans') + expect(response.statusCode).toBe(302) + expect(response.headers.location).toBe('/login') + }) + + test('clan view is protected', async () => { + let response = await testSession.get('/clans/view/1') + expect(response.statusCode).toBe(302) + expect(response.headers.location).toBe('/login') + + await testSession.get('/mock-login') + + response = await testSession.get('/clans/view/148') + expect(response.statusCode).toBe(200) + expect(response.text).toContain('STS-Clan'); + }) + + test('clan view redirects on shitty id', async () => { + await testSession.get('/mock-login') + let response = await testSession.get('/clans/view/not-a-number') + expect(response.statusCode).toBe(302) + expect(response.headers.location).toBe('/clans') + }) + + test('game-client clan view link is redirected', async () => { + const response = await testSession.get('/clan/1') + expect(response.statusCode).toBe(302) + expect(response.headers.location).toBe('/clans/view/1') + }) + + test("responds with 503 to other clan routes", async () => { + const res = await testSession.get('/clans/everything-else') expect(res.statusCode).toBe(503) expect(res.text).toContain('Sorry commanders, we failed to build enough pgens and are now in a tech upgrade'); - })) + }) }) diff --git a/tests/integration/testData/clan-http.json b/tests/integration/testData/clan-http.json new file mode 100644 index 00000000..35d18287 --- /dev/null +++ b/tests/integration/testData/clan-http.json @@ -0,0 +1 @@ +{"data":{"type":"clan","id":"148","attributes":{"createTime":"2014-04-22T14:52:03Z","description":"This is the old GPG-Net STS-Clan. all german people can join :) \n\n","name":"STS-Clan","requiresInvitation":true,"tag":"STS","tagColor":null,"updateTime":"2014-04-22T14:52:03Z","websiteUrl":"http://localhost:8096/clan/148"},"relationships":{"founder":{"data":{"type":"player","id":"33328"}},"leader":{"data":{"type":"player","id":"33328"}},"memberships":{"data":[{"type":"clanMembership","id":"6782"},{"type":"clanMembership","id":"6783"},{"type":"clanMembership","id":"6784"},{"type":"clanMembership","id":"6785"},{"type":"clanMembership","id":"6786"},{"type":"clanMembership","id":"6787"},{"type":"clanMembership","id":"6788"},{"type":"clanMembership","id":"6789"}]}}},"included":[{"type":"clanMembership","id":"6789","attributes":{"createTime":"2014-05-27T20:23:33Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"102475"}}}},{"type":"player","id":"102475","attributes":{"createTime":"2014-05-27T20:56:05Z","login":"DragoonTT","updateTime":"2023-10-21T21:18:37Z","userAgent":null},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6789"}},"names":{"data":[]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6782","attributes":{"createTime":"2014-04-22T14:52:03Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"33328"}}}},{"type":"player","id":"33328","attributes":{"createTime":"2012-10-06T18:03:04Z","login":"MVN050","updateTime":"2023-10-21T21:18:37Z","userAgent":"faf-client"},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6782"}},"names":{"data":[]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6783","attributes":{"createTime":"2014-04-23T20:25:28Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"2719"}}}},{"type":"player","id":"2719","attributes":{"createTime":"2012-07-05T12:22:02Z","login":"Bubble678","updateTime":"2023-10-21T21:18:37Z","userAgent":"downlords-faf-client"},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6783"}},"names":{"data":[{"type":"nameRecord","id":"19886"}]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6784","attributes":{"createTime":"2014-04-23T20:28:35Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"2722"}}}},{"type":"player","id":"2722","attributes":{"createTime":"2012-07-05T12:13:24Z","login":"GeneralMajor","updateTime":"2023-10-21T21:18:37Z","userAgent":null},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6784"}},"names":{"data":[{"type":"nameRecord","id":"4341"},{"type":"nameRecord","id":"4467"}]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6785","attributes":{"createTime":"2014-04-27T14:02:01Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"79436"}}}},{"type":"player","id":"79436","attributes":{"createTime":"2013-11-15T18:39:17Z","login":"PWD_Lux","updateTime":"2023-10-21T21:18:37Z","userAgent":null},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6785"}},"names":{"data":[]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6786","attributes":{"createTime":"2014-04-28T19:57:43Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"33228"}}}},{"type":"player","id":"33228","attributes":{"createTime":"2012-10-06T18:02:58Z","login":"Totalschaden","updateTime":"2023-10-21T21:18:37Z","userAgent":"downlords-faf-client"},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6786"}},"names":{"data":[{"type":"nameRecord","id":"1248"},{"type":"nameRecord","id":"1530"},{"type":"nameRecord","id":"1818"}]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6787","attributes":{"createTime":"2014-05-06T17:04:19Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"98228"}}}},{"type":"player","id":"98228","attributes":{"createTime":"2014-04-19T19:45:30Z","login":"Cmd_Matrix","updateTime":"2023-10-21T21:18:37Z","userAgent":"downlords-faf-client"},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6787"}},"names":{"data":[]},"userNotes":{"data":[]}}},{"type":"clanMembership","id":"6788","attributes":{"createTime":"2014-05-11T12:58:54Z","updateTime":"2017-05-15T17:48:35Z"},"relationships":{"clan":{"data":{"type":"clan","id":"148"}},"player":{"data":{"type":"player","id":"56082"}}}},{"type":"player","id":"56082","attributes":{"createTime":"2014-05-05T19:20:03Z","login":"Fleischie","updateTime":"2023-10-21T21:18:37Z","userAgent":null},"relationships":{"avatarAssignments":{"data":[]},"bans":{"data":[]},"clanMembership":{"data":{"type":"clanMembership","id":"6788"}},"names":{"data":[]},"userNotes":{"data":[]}}}]} \ No newline at end of file diff --git a/tests/integration/testData/clan.json b/tests/integration/testData/clan.json new file mode 100644 index 00000000..9bac2a91 --- /dev/null +++ b/tests/integration/testData/clan.json @@ -0,0 +1 @@ +{"id":"148","name":"STS-Clan","tag":"STS","description":"This is the old GPG-Net STS-Clan. all german people can join :) \n\n","createTime":"2014-04-22T14:52:03Z","requiresInvitation":true,"tagColor":null,"updateTime":"2014-04-22T14:52:03Z","founder":{"id":"33328","membershipId":"6782","joinedAt":"2014-04-22T14:52:03Z","name":"MVN050"},"leader":{"id":"33328","membershipId":"6782","joinedAt":"2014-04-22T14:52:03Z","name":"MVN050"},"memberships":{"2719":{"id":"2719","membershipId":"6783","joinedAt":"2014-04-23T20:25:28Z","name":"Bubble678"},"2722":{"id":"2722","membershipId":"6784","joinedAt":"2014-04-23T20:28:35Z","name":"GeneralMajor"},"33228":{"id":"33228","membershipId":"6786","joinedAt":"2014-04-28T19:57:43Z","name":"Totalschaden"},"33328":{"id":"33328","membershipId":"6782","joinedAt":"2014-04-22T14:52:03Z","name":"MVN050"},"56082":{"id":"56082","membershipId":"6788","joinedAt":"2014-05-11T12:58:54Z","name":"Fleischie"},"79436":{"id":"79436","membershipId":"6785","joinedAt":"2014-04-27T14:02:01Z","name":"PWD_Lux"},"98228":{"id":"98228","membershipId":"6787","joinedAt":"2014-05-06T17:04:19Z","name":"Cmd_Matrix"},"102475":{"id":"102475","membershipId":"6789","joinedAt":"2014-05-27T20:23:33Z","name":"DragoonTT"}}} \ No newline at end of file diff --git a/tests/setup.js b/tests/setup.js index 3790bde5..3f649710 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,6 +1,7 @@ const fs = require("fs"); const wordpressService = require("../lib/WordpressService"); const leaderboardService = require("../lib/LeaderboardService"); +const clanService = require("../lib/clan/ClanService"); beforeEach(() => { const newsFile = JSON.parse(fs.readFileSync('tests/integration/testData/news.json',{encoding:'utf8', flag:'r'})) jest.spyOn(wordpressService.prototype, 'getNews').mockResolvedValue(newsFile); @@ -31,6 +32,10 @@ beforeEach(() => { throw new Error('do we need to change the mock?') }) + + jest.spyOn(clanService.prototype, 'getClan').mockImplementation((id) => { + return JSON.parse(fs.readFileSync('tests/integration/testData/clan.json',{encoding:'utf8', flag:'r'})) + }) }) afterEach(() => { diff --git a/yarn.lock b/yarn.lock index b8fea215..b44ae9f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1449,7 +1449,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@^1.0.0: +chalk@^1.0.0, chalk@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== @@ -1938,6 +1938,11 @@ dateformat@~4.6.2: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== +dayjs@^1.11.10: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2109,6 +2114,11 @@ dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" +diff-dom@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/diff-dom/-/diff-dom-5.1.2.tgz#951627784bc45e32999f0c97cd42e4cf8c35791f" + integrity sha512-ayOX+pBYzyLdt7iXFd+8jvWzhrcWk+9gQqYk7Zz8/0hpIsqSbtk6MNbtds+Ox6B8ONsdtIcfPmk3NXPdgb3+xQ== + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -2560,6 +2570,11 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" +file-sync-cmp@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b" + integrity sha512-0k45oWBokCqh2MOexeYKpyqmGKG+8mQ2Wd8iawx+uWd/weWJQAZ6SoPybagdCI4xFisag8iAR77WPm4h3pTfxA== + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -3042,6 +3057,14 @@ grunt-contrib-concat@^2.1.0: chalk "^4.1.2" source-map "^0.5.3" +grunt-contrib-copy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz#7060c6581e904b8ab0d00f076e0a8f6e3e7c3573" + integrity sha512-gFRFUB0ZbLcjKb67Magz1yOHGBkyU6uL29hiEW1tdQ9gQt72NuMKIy/kS6dsCbV0cZ0maNCb0s6y+uT1FKU7jA== + dependencies: + chalk "^1.1.1" + file-sync-cmp "^0.1.0" + grunt-contrib-jshint@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/grunt-contrib-jshint/-/grunt-contrib-jshint-3.2.0.tgz#d97c125ce6dafef1b0cc766cd87201ae0fb7b408" @@ -6382,6 +6405,14 @@ 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-datatables@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/simple-datatables/-/simple-datatables-8.0.1.tgz#9e1dd25bcdf30782aee14f6f138c8e3f97bbbc2f" + integrity sha512-1l9N7yacy4pECoOw/khm/n7XtEcpgoh5znJ1diqern1fwRqgOCgdI7keW9LCeV5es2M4K+db3qWjaWHbcFUh0Q== + dependencies: + dayjs "^1.11.10" + diff-dom "^5.1.2" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" From a08a85f183a906fdfac58e840c1935bb9eb04664 Mon Sep 17 00:00:00 2001 From: fcaps Date: Fri, 24 Nov 2023 10:39:12 +0100 Subject: [PATCH 2/3] token refresh oauth clients --- config/app.js | 1 + fafApp.js | 48 ++++++++++++- lib/ApiErrors.js | 1 + lib/JavaApiClient.js | 61 ++++++++++++++++ lib/LeaderboardRepository.js | 19 +++-- lib/LeaderboardServiceFactory.js | 14 ---- package.json | 2 + public/js/app/leaderboards.js | 5 ++ routes/middleware.js | 9 +++ routes/views/auth.js | 45 ++---------- routes/views/leaderboardRouter.js | 18 +++-- tests/JavaApiClient.test.js | 113 ++++++++++++++++++++++++++++++ yarn.lock | 21 +++++- 13 files changed, 288 insertions(+), 69 deletions(-) create mode 100644 lib/ApiErrors.js create mode 100644 lib/JavaApiClient.js delete mode 100644 lib/LeaderboardServiceFactory.js create mode 100644 tests/JavaApiClient.test.js diff --git a/config/app.js b/config/app.js index 89f4b120..6c795552 100644 --- a/config/app.js +++ b/config/app.js @@ -11,6 +11,7 @@ const appConfig = { tokenLifespan: process.env.TOKEN_LIFESPAN || 43200 }, oauth: { + strategy: 'faforever', clientId: process.env.OAUTH_CLIENT_ID || '12345', clientSecret: process.env.OAUTH_CLIENT_SECRET || '12345', url: oauthUrl, diff --git a/fafApp.js b/fafApp.js index c8be2e82..ab2e3277 100644 --- a/fafApp.js +++ b/fafApp.js @@ -15,6 +15,9 @@ 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 axios = require("axios"); +const refresh = require("passport-oauth2-refresh"); const copyFlashHandler = (req, res, next) => { res.locals.message = req.flash(); @@ -33,6 +36,45 @@ const errorHandler = (err, req, res, next) => { res.status(500).render('errors/500'); } +const loadAuth = () => { + 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'] + }, function (iss, sub, profile, jwtClaims, accessToken, refreshToken, params, verified) { + + axios.get( + appConfig.apiUrl + '/me', + { + headers: {'Authorization': `Bearer ${accessToken}`} + }).then((res) => { + const user = res.data + user.token = accessToken + user.refreshToken = refreshToken + user.data.attributes.token = accessToken; + user.data.id = user.data.attributes.userId; + + return verified(null, user); + }).catch(e => { + console.error('[Error] views/auth.js::passport::verify failed with "' + e.toString() + '"'); + + return verified(null, null); + }); + } + ) + + passport.use(appConfig.oauth.strategy, authStrategy) + refresh.use(appConfig.oauth.strategy, authStrategy) +} + module.exports.setupCronJobs = () => { setupCronJobs() } @@ -64,7 +106,7 @@ module.exports.setup = (app) => { app.set('view engine', 'pug') app.set('port', appConfig.expressPort) - app.use(middleware.injectServices) + app.use(middleware.initLocals) app.use(express.static('public', { @@ -88,6 +130,10 @@ module.exports.setup = (app) => { })) app.use(passport.initialize()) app.use(passport.session()) + loadAuth() + + app.use(middleware.injectServices) + app.use(flash()) app.use(middleware.username) app.use(copyFlashHandler) diff --git a/lib/ApiErrors.js b/lib/ApiErrors.js new file mode 100644 index 00000000..05e64490 --- /dev/null +++ b/lib/ApiErrors.js @@ -0,0 +1 @@ +module.exports.AuthFailed = class AuthFailed extends Error {} diff --git a/lib/JavaApiClient.js b/lib/JavaApiClient.js new file mode 100644 index 00000000..fa77d328 --- /dev/null +++ b/lib/JavaApiClient.js @@ -0,0 +1,61 @@ +const {Axios} = require("axios"); +const refresh = require('passport-oauth2-refresh') +const {AuthFailed} = require('./ApiErrors') +const appConfig = require("../config/app") + +const getRefreshToken = (user) => { + return new Promise((resolve, reject) => { + refresh.requestNewAccessToken(appConfig.oauth.strategy, user.refreshToken, function(err, accessToken, refreshToken) { + if (err || !accessToken) { + return reject(new AuthFailed('Failed to refresh token')) + } + + return resolve([accessToken, refreshToken]) + }) + }) +} + +module.exports = (javaApiBaseURL, user) => { + let tokenRefreshRunning = null + const client = new Axios({ + baseURL: javaApiBaseURL + }) + + client.interceptors.request.use( + async config => { + config.headers = { + 'Authorization': `Bearer ${user.token}`, + } + + return config; + }) + + client.interceptors.response.use(async (res) => { + if (!res.config._refreshTokenRequest && res.config && res.status === 401) { + res.config._refreshTokenRequest = true; + + if (!tokenRefreshRunning) { + tokenRefreshRunning = getRefreshToken(user) + } + + const [token, refreshToken] = await tokenRefreshRunning + + user.token = token + user.refreshToken = refreshToken + + res.config.headers['Authorization'] = `Bearer ${token}` + + return client.request(res.config) + } + + if (res.status === 401) { + new AuthFailed('Token no longer valid and refresh did not help') + } + + return res + }) + + + + return client +} diff --git a/lib/LeaderboardRepository.js b/lib/LeaderboardRepository.js index d8f75f3b..1fb50f1f 100644 --- a/lib/LeaderboardRepository.js +++ b/lib/LeaderboardRepository.js @@ -45,13 +45,18 @@ class LeaderboardRepository { let leaderboardData = [] data.data.forEach((item, index) => { - leaderboardData.push({ - rating: item.attributes.rating, - totalgames: item.attributes.totalGames, - wonGames: item.attributes.wonGames, - date: item.attributes.updateTime, - label: data.included[index].attributes.login, - }) + try { + leaderboardData.push({ + rating: item.attributes.rating, + totalgames: item.attributes.totalGames, + wonGames: item.attributes.wonGames, + date: item.attributes.updateTime, + label: data.included[index]?.attributes.login || 'unknown user', + }) + } catch (e) { + console.error('LeaderboardRepository::mapResponse failed on item with "' + e.toString() + '"') + } + }) return leaderboardData diff --git a/lib/LeaderboardServiceFactory.js b/lib/LeaderboardServiceFactory.js deleted file mode 100644 index 1d9ad1a0..00000000 --- a/lib/LeaderboardServiceFactory.js +++ /dev/null @@ -1,14 +0,0 @@ -const LeaderboardService = require("./LeaderboardService"); -const LeaderboardRepository = require("./LeaderboardRepository"); -const {Axios} = require("axios"); -const cacheService = require('./CacheService') - -module.exports = (javaApiBaseURL, token) => { - const config = { - baseURL: javaApiBaseURL, - headers: {Authorization: `Bearer ${token}`} - }; - const javaApiClient = new Axios(config) - - return new LeaderboardService(cacheService, new LeaderboardRepository(javaApiClient)) -} diff --git a/package.json b/package.json index 70591b1d..fb394c3e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "node-fetch": "^2.6.7", "npm-check": "^6.0.1", "passport": "^0.6.0", + "passport-oauth2-refresh": "^2.2.0", "passport-openidconnect": "^0.1.1", "pug": "3.0.2", "request": "2.88.2", @@ -40,6 +41,7 @@ "jshint-stylish": "2.2.1", "load-grunt-config": "4.0.1", "load-grunt-tasks": "5.1.0", + "nock": "^13.3.8", "supertest": "^6.3.3" }, "engines": { diff --git a/public/js/app/leaderboards.js b/public/js/app/leaderboards.js index 180e9f0a..d98100f6 100644 --- a/public/js/app/leaderboards.js +++ b/public/js/app/leaderboards.js @@ -17,6 +17,11 @@ let currentDate = new Date(minusTimeFilter).toISOString(); async function leaderboardOneJSON(leaderboardFile) { //Check which category is active const response = await fetch(`leaderboards/${leaderboardFile}.json`); + + if (response.status === 400) { + window.location.href = '/leaderboards' + } + currentLeaderboard = leaderboardFile; const data = await response.json(); return await data; diff --git a/routes/middleware.js b/routes/middleware.js index 79391dbf..6b85a5f6 100755 --- a/routes/middleware.js +++ b/routes/middleware.js @@ -1,5 +1,9 @@ const WordpressServiceFactory = require("../lib/WordpressServiceFactory"); +const JavaApiClientFactory = require("../lib/JavaApiClient"); const appConfig = require("../config/app"); +const LeaderboardService = require("../lib/LeaderboardService"); +const cacheService = require("../lib/CacheService"); +const LeaderboardRepository = require("../lib/LeaderboardRepository"); const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) exports.initLocals = function(req, res, next) { @@ -43,6 +47,11 @@ exports.injectServices = function(req, res, next) { req.services = { wordpressService: wordpressService } + + if (req.isAuthenticated()) { + req.services.javaApiClient = JavaApiClientFactory(appConfig.apiUrl, req.user) + req.services.leaderboardService = new LeaderboardService(cacheService, new LeaderboardRepository(req.services.javaApiClient)) + } next() } diff --git a/routes/views/auth.js b/routes/views/auth.js index 75a7f1bf..7c5aff04 100644 --- a/routes/views/auth.js +++ b/routes/views/auth.js @@ -1,44 +1,9 @@ const appConfig = require('../../config/app') -const passport = require('passport'); -const OidcStrategy = require('passport-openidconnect'); -const express = require("express"); -const axios = require("axios"); -const router = express.Router(); +const passport = require('passport') +const express = require("express") +const router = express.Router() -passport.serializeUser((user, done) => done(null, user)) -passport.deserializeUser((user, done) => done(null, user)) - -passport.use('faforever', 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', 'public_profile', 'write_account_data'] - }, function (iss, sub, profile, jwtClaims, accessToken, refreshToken, params, verified) { - - axios.get( - appConfig.apiUrl + '/me', - { - headers: {'Authorization': `Bearer ${accessToken}`} - }).then((res) => { - const user = res.data - user.token = accessToken - user.data.attributes.token = accessToken; - user.data.id = user.data.attributes.userId; - - return verified(null, user); - }).catch(e => { - console.error('[Error] views/auth.js::passport::verify failed with "' + e.toString() + '"'); - - return verified(null, null); - }); - } -)); - -router.get('/login', passport.authenticate('faforever')); +router.get('/login', passport.authenticate(appConfig.oauth.strategy)); router.get( '/' + appConfig.oauth.callback, @@ -47,7 +12,7 @@ router.get( return next() }, - passport.authenticate('faforever', {failureRedirect: '/login', failureFlash: true}), + passport.authenticate(appConfig.oauth.strategy, {failureRedirect: '/login', failureFlash: true}), (req, res) => { res.redirect(res.locals.returnTo || '/') } diff --git a/routes/views/leaderboardRouter.js b/routes/views/leaderboardRouter.js index 0f67fa5a..0a667ff0 100644 --- a/routes/views/leaderboardRouter.js +++ b/routes/views/leaderboardRouter.js @@ -1,9 +1,8 @@ -const appConfig = require('../../config/app') const express = require('express'); const router = express.Router(); -const LeaderboardServiceFactory = require('../../lib/LeaderboardServiceFactory') const {AcquireTimeoutError} = require('../../lib/MutexService'); const middlewares = require('../middleware') +const {AuthFailed} = require('../../lib/ApiErrors') const getLeaderboardId = (leaderboardName) => { @@ -33,14 +32,21 @@ router.get('/:leaderboard.json', middlewares.isAuthenticated(null, true), async return res.status(404).json({error: 'Leaderboard "' + req.params.leaderboard + '" does not exist'}) } - const token = req.user.data.attributes.token - const leaderboardService = LeaderboardServiceFactory(appConfig.apiUrl, token) - - return res.json(await leaderboardService.getLeaderboard(leaderboardId)) + 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() + '"') diff --git a/tests/JavaApiClient.test.js b/tests/JavaApiClient.test.js new file mode 100644 index 00000000..cc8d5142 --- /dev/null +++ b/tests/JavaApiClient.test.js @@ -0,0 +1,113 @@ +const JavaApiClientFactory = require("../lib/JavaApiClient") +const appConfig = require("../config/app") +const refresh = require("passport-oauth2-refresh") +const OidcStrategy = require("passport-openidconnect") +const nock = require('nock') +const {AuthFailed} = require("../lib/ApiErrors") + +beforeEach(() => { + refresh.use(appConfig.oauth.strategy, new OidcStrategy({ + issuer: 'me', + tokenURL: 'http://auth-localhost/oauth2/token', + authorizationURL: 'http://auth-localhost/oauth2/auth', + clientID: 'test', + clientSecret: 'test', + scope: ['openid', 'offline'] + }, () => {})) +}) + +afterEach(() => { + refresh._strategies = {} + jest.restoreAllMocks() +}) + +test('multiple calls with stale token will trigger refresh only once', async () => { + const client = JavaApiClientFactory('http://api-localhost', { + token: '123', + refreshToken: '456' + }) + + const refreshSpy = jest.spyOn(refresh, 'requestNewAccessToken') + const apiScope = nock('http://api-localhost') + .get('/example') + .times(2) + .reply(401, 'nope') + .get('/example') + .times(2) + .reply(200, 'OK') + + const authScope = nock('http://auth-localhost') + .post('/oauth2/token') + .times(1) + .reply(200, {access_token: 'new_tok', refresh_token: 'new_ref'}) + + const response = client.get('/example').then((res) => { + expect(res.request.headers['authorization']).toBe('Bearer new_tok') + }) + + const response2 = client.get('/example').then((res) => { + expect(res.request.headers['authorization']).toBe('Bearer new_tok') + }) + + await Promise.all([response, response2]) + + expect(refreshSpy).toBeCalledTimes(1) + + apiScope.done() + authScope.done() +}) + +test('refresh will throw on error', async () => { + const client = JavaApiClientFactory('http://api-localhost', { + token: '123', + refreshToken: '456' + }) + + const refreshSpy = jest.spyOn(refresh, 'requestNewAccessToken') + const apiScope = nock('http://api-localhost') + .get('/example') + .reply(401, 'nope') + + const authScope = nock('http://auth-localhost') + .post('/oauth2/token') + .times(1) + .reply(400, null) + + let thrown = false + try { + await client.get('/example') + } catch (e) { + expect(e).toBeInstanceOf(AuthFailed) + thrown = true + } + + expect(thrown).toBe(true) + expect(refreshSpy).toBeCalledTimes(1) + + apiScope.done() + authScope.done() +}) + +test('refresh will not loop to death', async () => { + const client = JavaApiClientFactory('http://api-localhost', { + token: '123', + refreshToken: '456' + }) + + const apiScope = nock('http://api-localhost') + .get('/example') + .times(2) + .reply(401, 'nope') + + const authScope = nock('http://auth-localhost') + .post('/oauth2/token') + .times(1) + .reply(200, {access_token: 'new_tok', refresh_token: 'new_ref'}) + + const response = await client.get('/example') + + expect(response.status).toBe(401) + + apiScope.done() + authScope.done() +}) diff --git a/yarn.lock b/yarn.lock index b8fea215..624094b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4311,7 +4311,7 @@ json-schema@0.4.0: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== @@ -4894,6 +4894,15 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +nock@^13.3.8: + version "13.3.8" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.8.tgz#7adf3c66f678b02ef0a78d5697ae8bc2ebde0142" + integrity sha512-96yVFal0c/W1lG7mmfRe7eO+hovrhJYd2obzzOZ90f6fjpeU/XNvd9cYHZKZAQJumDfhXgoTpkpJ9pvMj+hqHw== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" + node-cache@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" @@ -5299,6 +5308,11 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== +passport-oauth2-refresh@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.2.0.tgz#e60dd4e84e8df3c6ead87b6aab0754dec7a89aca" + integrity sha512-yXwXHL7ZZH0s2oknnjugfvwzCB5mpJ5ZNpzkb+b/sTsHeZFbx2BXfzvwsoD4aq6gq/aWuCxBV89ef+L/cjjrjg== + passport-openidconnect@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/passport-openidconnect/-/passport-openidconnect-0.1.1.tgz#83921ff5f87f634079f65262dada834af1972244" @@ -5615,6 +5629,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" From bd83078e98a8b0eac3a872263e55b7ff752d1f0b Mon Sep 17 00:00:00 2001 From: fcaps Date: Fri, 24 Nov 2023 11:26:36 +0100 Subject: [PATCH 3/3] merge retry client --- lib/clan/ClanRepository.js | 4 ++-- lib/clan/ClanServiceFactory.js | 14 -------------- routes/middleware.js | 15 ++++++++------- templates/views/clans/clan.pug | 7 +++++-- 4 files changed, 15 insertions(+), 25 deletions(-) delete mode 100644 lib/clan/ClanServiceFactory.js diff --git a/lib/clan/ClanRepository.js b/lib/clan/ClanRepository.js index 55fceb14..f6266495 100644 --- a/lib/clan/ClanRepository.js +++ b/lib/clan/ClanRepository.js @@ -4,7 +4,7 @@ class ClanRepository { } async fetchAll() { - const response = await this.javaApiClient.get('/data/clan?sort=createTime&include=leader&fields[clan]=name,tag,description,leader,memberships,createTime&fields[player]=login&page[number]=1&page[size]=3000') + const response = await this.javaApiClient.get('/data/clan?include=leader&fields[clan]=name,tag,description,leader,memberships,createTime&fields[player]=login&page[number]=1&page[size]=3000') if (response.status !== 200) { throw new Error('ClanRepository::fetchAll failed with response status "' + response.status + '"') @@ -32,7 +32,7 @@ class ClanRepository { const clans = responseData.data.map((item, index) => ({ id: parseInt(item.id), - leaderName: responseData.included[index].attributes.login, + leaderName: responseData.included[index]?.attributes.login || '-', name: item.attributes.name, tag: item.attributes.tag, createTime: item.attributes.createTime, diff --git a/lib/clan/ClanServiceFactory.js b/lib/clan/ClanServiceFactory.js deleted file mode 100644 index c75f585d..00000000 --- a/lib/clan/ClanServiceFactory.js +++ /dev/null @@ -1,14 +0,0 @@ -const ClanService = require("./ClanService") -const ClanRepository = require("./ClanRepository") -const {Axios} = require("axios") -const cacheService = require('../CacheService') - -module.exports = (javaApiBaseURL, token) => { - const config = { - baseURL: javaApiBaseURL, - headers: {Authorization: `Bearer ${token}`} - }; - const clanClient = new Axios(config) - - return new ClanService(cacheService, new ClanRepository(clanClient)) -} diff --git a/routes/middleware.js b/routes/middleware.js index fcf45b1d..2a05ed71 100755 --- a/routes/middleware.js +++ b/routes/middleware.js @@ -1,10 +1,11 @@ const WordpressServiceFactory = require("../lib/WordpressServiceFactory") -const ClanServiceFactory = require("../lib/clan/ClanServiceFactory") -const JavaApiClientFactory = require("../lib/JavaApiClient"); -const appConfig = require("../config/app"); -const LeaderboardService = require("../lib/LeaderboardService"); -const cacheService = require("../lib/CacheService"); -const LeaderboardRepository = require("../lib/LeaderboardRepository"); +const JavaApiClientFactory = require("../lib/JavaApiClient") +const appConfig = require("../config/app") +const LeaderboardService = require("../lib/LeaderboardService") +const cacheService = require("../lib/CacheService") +const LeaderboardRepository = require("../lib/LeaderboardRepository") +const ClanService = require("../lib/clan/ClanService") +const ClanRepository = require("../lib/clan/ClanRepository") const wordpressService = WordpressServiceFactory(appConfig.wordpressUrl) exports.initLocals = function(req, res, next) { @@ -52,7 +53,7 @@ exports.injectServices = function(req, res, next) { if (req.isAuthenticated()) { req.services.javaApiClient = JavaApiClientFactory(appConfig.apiUrl, req.user) req.services.leaderboardService = new LeaderboardService(cacheService, new LeaderboardRepository(req.services.javaApiClient)) - req.services.clanService = ClanServiceFactory(appConfig.apiUrl, req.user.data.attributes.token) + req.services.clanService = new ClanService(cacheService, new ClanRepository(req.services.javaApiClient)) } next() diff --git a/templates/views/clans/clan.pug b/templates/views/clans/clan.pug index e20c94b2..b32a93f8 100644 --- a/templates/views/clans/clan.pug +++ b/templates/views/clans/clan.pug @@ -17,13 +17,16 @@ block content td #{clan.tag} tr td LEADER - td #{clan.leader.name} 👑 + if clan.leader + td #{clan.leader.name} 👑 + else + td - tr td FOUNDER if clan.founder td #{clan.founder.name} else - td GONE + td - tr td JOIN if clan.requiresInvitation