From 4e1b874aaa1c2c6c2eae024e30b1c86117fe952a Mon Sep 17 00:00:00 2001 From: fcaps <48691346+fcaps@users.noreply.github.com> Date: Tue, 5 Dec 2023 02:06:38 +0100 Subject: [PATCH] clan create, update and delete + flash changes (#522) --- public/styles/site/clans.sass | 11 ++ src/backend/AppKernel.js | 14 +- .../dependency-injection/RequestContainer.js | 19 ++ .../RequestContainerCompilerPass.js | 1 + src/backend/routes/views/clanRouter.js | 8 + src/backend/routes/views/clans/create.js | 64 +++++++ src/backend/routes/views/clans/get/create.js | 53 ------ src/backend/routes/views/clans/get/manage.js | 139 ++------------ src/backend/routes/views/clans/post/create.js | 96 ---------- .../routes/views/clans/post/destroy.js | 110 ++--------- src/backend/routes/views/clans/post/update.js | 176 ++++-------------- src/backend/services/ApiErrors.js | 11 ++ .../services/ClanManagementRepository.js | 69 +++++++ src/backend/services/ClanManagementService.js | 46 +++++ src/backend/services/ClanService.js | 10 +- src/backend/services/DataRepository.js | 68 +++++++ src/backend/services/UserService.js | 22 ++- src/backend/templates/layouts/default.pug | 59 +++--- .../templates/mixins/flash-connect.pug | 13 ++ src/backend/templates/mixins/flash-error.pug | 2 +- src/backend/templates/views/clans/create.pug | 56 ++---- src/backend/templates/views/clans/manage.pug | 150 +++------------ 22 files changed, 496 insertions(+), 701 deletions(-) create mode 100644 src/backend/routes/views/clans/create.js delete mode 100644 src/backend/routes/views/clans/get/create.js delete mode 100755 src/backend/routes/views/clans/post/create.js create mode 100644 src/backend/services/ClanManagementRepository.js create mode 100644 src/backend/services/ClanManagementService.js create mode 100644 src/backend/templates/mixins/flash-connect.pug diff --git a/public/styles/site/clans.sass b/public/styles/site/clans.sass index 294a1057..b6eaaeea 100644 --- a/public/styles/site/clans.sass +++ b/public/styles/site/clans.sass @@ -34,3 +34,14 @@ tbody td text-align: left + +.clanManagement textarea + margin: 1em 1em 2px 1em + background-color: #3f3f3f + color: #ffffff + font-size: var(--paragraph-font-size) + border: 0.5em solid #262626 + border-radius: 10px + z-index: 2 + position: relative + diff --git a/src/backend/AppKernel.js b/src/backend/AppKernel.js index 879f4cbf..b0c12564 100644 --- a/src/backend/AppKernel.js +++ b/src/backend/AppKernel.js @@ -96,8 +96,18 @@ class AppKernel { }) this.expressApp.use(flash()) - this.expressApp.use((req, res, next) => { - res.locals.message = req.flash() + this.expressApp.use(function (req, res, next) { + req.asyncFlash = async function () { + const result = req.flash(...arguments) + await new Promise(resolve => req.session.save(resolve)) + + return result + } + + return next() + }) + this.expressApp.use(async (req, res, next) => { + res.locals.connectFlash = () => req.flash() next() }) diff --git a/src/backend/dependency-injection/RequestContainer.js b/src/backend/dependency-injection/RequestContainer.js index cbcefb06..c0d52db4 100644 --- a/src/backend/dependency-injection/RequestContainer.js +++ b/src/backend/dependency-injection/RequestContainer.js @@ -1,6 +1,8 @@ const { ContainerBuilder, Reference } = require('node-dependency-injection') const { UserRepository } = require('../services/UserRepository') const { UserService } = require('../services/UserService') +const { ClanManagementService } = require('../services/ClanManagementService') +const { ClanManagementRepository } = require('../services/ClanManagementRepository') module.exports.RequestContainer = (appContainer, request) => { const container = new ContainerBuilder() @@ -11,9 +13,26 @@ module.exports.RequestContainer = (appContainer, request) => { .addArgument(new Reference('JavaApiClient')) .lazy = true + container.register('ClanManagementService', ClanManagementService) + .addArgument(new Reference('UserService')) + .addArgument(new Reference('ClanManagementRepository')) + .lazy = true + + container.register('ClanManagementRepository', ClanManagementRepository) + .addArgument(new Reference('JavaApiClient')) + .lazy = true + + container.register('ClanManagementService', ClanManagementService) + .addArgument(new Reference('UserService')) + .addArgument(new Reference('ClanManagementRepository')) + .addArgument(appContainer.get('ClanService')) + .lazy = true + container.register('JavaApiClient') .synthetic = true + container.register('UserService', UserService) + .lazy = true return container } diff --git a/src/backend/dependency-injection/RequestContainerCompilerPass.js b/src/backend/dependency-injection/RequestContainerCompilerPass.js index 66a62b59..78c000bd 100644 --- a/src/backend/dependency-injection/RequestContainerCompilerPass.js +++ b/src/backend/dependency-injection/RequestContainerCompilerPass.js @@ -11,6 +11,7 @@ class RequestContainerCompilerPass { 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)) + container.get('UserService').setUserRepository(container.get('UserRepository')) } return container diff --git a/src/backend/routes/views/clanRouter.js b/src/backend/routes/views/clanRouter.js index 02a062da..f7a2529f 100644 --- a/src/backend/routes/views/clanRouter.js +++ b/src/backend/routes/views/clanRouter.js @@ -1,5 +1,7 @@ +const create = require('../views/clans/create') const express = require('../../ExpressApp') const router = express.Router() +const middlewares = require('../middleware') // This will be replaced soon, therefor I did not spend time on it router.get('/', (req, res) => res.render('clans/clans')) @@ -12,6 +14,12 @@ router.get('/view/:id', async (req, res) => { return res.render('clans/clan', { clan: await req.appContainer.get('ClanService').getClan(clanId) }) }) +router.get('/create', create) +router.post('/create', create) +router.get('/manage', middlewares.isAuthenticated(), require('./clans/get/manage')) +router.post('/update', middlewares.isAuthenticated(), require('./clans/post/update')) +router.post('/destroy', middlewares.isAuthenticated(), require('./clans/post/destroy')) + router.get('*', (req, res) => res.status(503).render('errors/503-known-issue')) module.exports = router diff --git a/src/backend/routes/views/clans/create.js b/src/backend/routes/views/clans/create.js new file mode 100644 index 00000000..1d67926a --- /dev/null +++ b/src/backend/routes/views/clans/create.js @@ -0,0 +1,64 @@ +const { body, validationResult, matchedData } = require('express-validator') +const { JavaApiError } = require('../../../services/ApiErrors') + +module.exports = + [ + body('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({ max: 3 }), + body('clan_description', 'Please add a description for your clan').notEmpty().isLength({ max: 1000 }), + body('clan_name', "Please indicate your clan's name").notEmpty().isLength({ max: 40 }), + async (req, res) => { + if (req.requestContainer.get('UserService').getUser()?.clan) { + return res.redirect('/clans/manage') + } + + if (req.method === 'POST') { + const errors = validationResult(req) + + if (!errors.isEmpty()) { + return res.render('clans/create', { + errors: { + class: 'alert-danger', + messages: errors, + type: 'Error!' + }, + clan_tag: req.body.clan_tag, + clan_name: req.body.clan_name, + clan_description: req.body.clan_description + }) + } + + try { + const data = matchedData(req) + await req.requestContainer.get('ClanManagementService').create(data.clan_tag, data.clan_name, data.clan_description) + + await req.asyncFlash('info', 'Clan created') + + return res.redirect('/clans/view/' + req.requestContainer.get('UserService').getUser().clan.id) + } catch (e) { + let messages = { errors: [{ msg: 'Server-Error while creating the clan' }] } + + console.error(e.stack) + if (e instanceof JavaApiError && e.error.errors[0]) { + messages = { errors: [{ msg: e.error.errors[0].detail }] } + } + + return res.render('clans/create', { + clan_tag: req.body.clan_tag, + clan_name: req.body.clan_name, + clan_description: req.body.clan_description, + errors: { + class: 'alert-danger', + messages, + type: 'Error!' + } + }) + } + } + + res.render('clans/create', { + clan_tag: '', + clan_name: '', + clan_description: '' + }) + } + ] diff --git a/src/backend/routes/views/clans/get/create.js b/src/backend/routes/views/clans/get/create.js deleted file mode 100644 index 0b2367f1..00000000 --- a/src/backend/routes/views/clans/get/create.js +++ /dev/null @@ -1,53 +0,0 @@ -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 index 2af57b70..a0344982 100755 --- a/src/backend/routes/views/clans/get/manage.js +++ b/src/backend/routes/views/clans/get/manage.js @@ -1,129 +1,22 @@ -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 +const { JavaApiError } = require('../../../../services/ApiErrors') +exports = module.exports = async (req, res) => { + const clanMembershipId = req.requestContainer.get('UserService').getUser()?.clan?.membershipId || null + if (!clanMembershipId) { + return res.redirect('/clans') } - // 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 - } - } + try { + const clan = await req.appContainer.get('ClanService').getClanMembership(clanMembershipId) - locals.clan_members = members + return res.render('clans/manage', { clan_description: clan.clan_description, clan_name: clan.clan_name, clan_tag: clan.clan_tag }) + } catch (e) { + let message = e.toString() + if (e instanceof JavaApiError && e.error?.errors) { + message = e.error.errors[0].detail + } - 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' }] - } - } + await req.asyncFlash('error', message) - // Render the view - res.render('clans/manage', { flash }) - } - ) + return res.redirect('/') + } } diff --git a/src/backend/routes/views/clans/post/create.js b/src/backend/routes/views/clans/post/create.js deleted file mode 100755 index 7b352590..00000000 --- a/src/backend/routes/views/clans/post/create.js +++ /dev/null @@ -1,96 +0,0 @@ -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 index 786c878c..8652af8e 100755 --- a/src/backend/routes/views/clans/post/destroy.js +++ b/src/backend/routes/views/clans/post/destroy.js @@ -1,96 +1,18 @@ -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') - } - }) - }) +const { JavaApiError } = require('../../../../services/ApiErrors') + +module.exports = async function (req, res) { + try { + await req.requestContainer.get('ClanManagementService').deleteClan() + await req.asyncFlash('info', 'Clan deleted') + res.redirect('/clans/create') + } catch (e) { + let message = e.toString() + if (e instanceof JavaApiError && e.error?.errors) { + message = e.error.errors[0].detail + } + + await req.asyncFlash('error', message) + + return res.redirect('/clans/manage') } } diff --git a/src/backend/routes/views/clans/post/update.js b/src/backend/routes/views/clans/post/update.js index 7563b64f..e56c3a2e 100644 --- a/src/backend/routes/views/clans/post/update.js +++ b/src/backend/routes/views/clans/post/update.js @@ -1,145 +1,47 @@ -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 +const { validationResult, body, matchedData } = require('express-validator') +const { JavaApiError } = require('../../../../services/ApiErrors') + +exports = module.exports = [ + + body('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({ max: 3 }), + body('clan_description', 'Please add a description for your clan').notEmpty().isLength({ max: 1000 }), + body('clan_name', "Please indicate your clan's name").notEmpty().isLength({ max: 40 }), + + async (req, res) => { + const errors = validationResult(req) + + if (!errors.isEmpty()) { + return res.render('clans/manage', { + errors: { + class: 'alert-danger', + messages: errors, + type: 'Error!' + }, + clan_tag: req.body.clan_tag, + clan_name: req.body.clan_name, + clan_description: 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) - } + const data = matchedData(req) + await req.requestContainer.get('ClanManagementService').update(data.clan_tag, data.clan_name, data.clan_description) + await req.asyncFlash('info', 'Clan updated') - if (msg) { - flash.messages = [{ msg }] - const buff = Buffer.from(JSON.stringify(flash)) - const data = buff.toString('base64') - return overallRes.redirect('manage?flash=' + data) - } + return res.redirect('/clans/view/' + req.requestContainer.get('UserService').getUser().clan.id) } 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) + let message = e.toString() + if (e instanceof JavaApiError && e.error?.errors) { + message = e.error.errors[0].detail } - flash = {} - flash.class = 'alert-success' - flash.messages = [{ msg: 'You have successfully updated your clan' }] - flash.type = 'Success!' + await req.asyncFlash('error', message) - const buff = Buffer.from(JSON.stringify(flash)) - const data = buff.toString('base64') - - return overallRes.redirect('manage?flash=' + data) - }) + return res.render('clans/manage', { + clan_tag: req.body.clan_tag, + clan_name: req.body.clan_name, + clan_description: req.body.clan_description + }) + } } -} +] diff --git a/src/backend/services/ApiErrors.js b/src/backend/services/ApiErrors.js index 05e64490..bd73857c 100644 --- a/src/backend/services/ApiErrors.js +++ b/src/backend/services/ApiErrors.js @@ -1 +1,12 @@ +class JavaApiError extends Error { + constructor (status, url, error) { + super('Failed request "' + url + '" with status "' + status + '"') + + this.status = status + this.url = url + this.error = error + } +} +module.exports.JavaApiError = JavaApiError +module.exports.GenericJavaApiError = class GenericJavaApiError extends Error {} module.exports.AuthFailed = class AuthFailed extends Error {} diff --git a/src/backend/services/ClanManagementRepository.js b/src/backend/services/ClanManagementRepository.js new file mode 100644 index 00000000..948c6eba --- /dev/null +++ b/src/backend/services/ClanManagementRepository.js @@ -0,0 +1,69 @@ +const { JavaApiError, GenericJavaApiError } = require('./ApiErrors') + +class ClanManagementRepository { + constructor (javaApiClient) { + this.javaApiClient = javaApiClient + } + + async create (tag, name, description) { + try { + const response = await this.javaApiClient.post('/clans/create' + + '?name=' + encodeURIComponent(name) + + '&tag=' + encodeURIComponent(tag) + + '&description=' + encodeURIComponent(description)) + + if (response.status !== 200) { + throw new JavaApiError(response.status, response.config.url, JSON.parse(response.data) || []) + } + } catch (e) { + if (e instanceof JavaApiError) { + throw e + } + + throw new GenericJavaApiError(e.toString()) + } + } + + async deleteClan (clanId) { + const response = await this.javaApiClient.delete('/data/clan/' + clanId) + + if (response.status !== 204) { + throw new JavaApiError(response.status, response.config.url, JSON.parse(response.data) || []) + } + } + + async update (id, tag, name, description) { + const newClanObject = { + data: { + type: 'clan', + id, + attributes: { + description, + name, + tag + } + } + } + + try { + const response = await this.javaApiClient.patch(`/data/clan/${id}`, JSON.stringify(newClanObject), { + headers: { + 'Content-Type': 'application/vnd.api+json', + Accept: 'application/vnd.api+json' + } + }) + + if (response.status !== 204) { + throw new JavaApiError(response.status, response.config.url, JSON.parse(response.data) || []) + } + } catch (e) { + if (e instanceof JavaApiError) { + throw e + } + + throw new GenericJavaApiError(e.toString()) + } + } +} + +module.exports.ClanManagementRepository = ClanManagementRepository diff --git a/src/backend/services/ClanManagementService.js b/src/backend/services/ClanManagementService.js new file mode 100644 index 00000000..f20f2dab --- /dev/null +++ b/src/backend/services/ClanManagementService.js @@ -0,0 +1,46 @@ +class ClanManagementService { + constructor (userService, clanManagementRepository, clanService) { + this.userService = userService + this.clanManagementRepository = clanManagementRepository + this.clanService = clanService + } + + async create (tag, name, description) { + await this.clanManagementRepository.create(tag, name, description) + try { + this.clanService.getAll(true).then(() => {}).catch((e) => console.error(e.stack)) + await this.userService.refreshUser() + } catch (e) { + console.error(e.stack) + } + } + + async update (tag, name, description) { + await this.clanManagementRepository.update(this.userService.getUser().clan.id, tag, name, description) + try { + this.clanService.getAll(true).then(() => {}).catch((e) => console.error(e.stack)) + await this.userService.refreshUser() + } catch (e) { + console.error(e.stack) + } + } + + async deleteClan () { + const clanId = parseInt(this.userService.getUser()?.clan.id) + if (!clanId) { + throw new Error('User has no clan to destroy') + } + + await this.clanManagementRepository.deleteClan(clanId) + + // update user and caches, may this should be an event and not directly called here + try { + this.clanService.getAll(true).then(() => {}).catch((e) => console.error(e.stack)) + await this.userService.refreshUser() + } catch (e) { + console.error(e.stack) + } + } +} + +module.exports.ClanManagementService = ClanManagementService diff --git a/src/backend/services/ClanService.js b/src/backend/services/ClanService.js index 8e3f01b4..5a80b7fe 100644 --- a/src/backend/services/ClanService.js +++ b/src/backend/services/ClanService.js @@ -13,14 +13,14 @@ class ClanService { return 'ClanService_' + name } - async getClan (id, ignoreCache = false) { + async getClan (id) { return await this.dataRepository.fetchClan(id) } - async getAll () { + async getAll (ignoreCache = false) { const cacheKey = this.getCacheKey('all') - if (this.cacheService.has(cacheKey)) { + if (this.cacheService.has(cacheKey) && ignoreCache === false) { return this.cacheService.get(cacheKey) } @@ -37,6 +37,10 @@ class ClanService { return this.getAll() } + + async getClanMembership (clanMembershipId) { + return this.dataRepository.fetchClanMembership(clanMembershipId) + } } module.exports.ClanService = ClanService diff --git a/src/backend/services/DataRepository.js b/src/backend/services/DataRepository.js index d54d8b91..e0ee3075 100644 --- a/src/backend/services/DataRepository.js +++ b/src/backend/services/DataRepository.js @@ -1,3 +1,5 @@ +const { JavaApiError } = require('./ApiErrors') + class DataRepository { constructor (javaApiM2MClient) { this.javaApiM2MClient = javaApiM2MClient @@ -131,6 +133,72 @@ class DataRepository { return clan } + + async fetchClanMembership (clanMembershipId) { + const response = await this.javaApiM2MClient.get(`/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`) + + if (response.status !== 200) { + throw new JavaApiError(response.status, response.config.url, JSON.parse(response.data) || []) + } + + return this.mapClanMembership(JSON.parse(response.data)) + } + + mapClanMembership (data) { + if (typeof data !== 'object' || data === null) { + throw new Error('ClanRepository::mapClanMembership malformed response, not an object') + } + + if (!Object.prototype.hasOwnProperty.call(data, 'data')) { + throw new Error('ClanRepository::mapClanMembership 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::mapClanMembership malformed response, expected "included"') + } + + const clanMembershipRaw = data.data.attributes + + const clanMembership = { + clan_id: data.data.id, + clan_name: clanMembershipRaw.name, + clan_tag: clanMembershipRaw.tag, + clan_description: clanMembershipRaw.description, + clan_create_time: clanMembershipRaw.createTime + } + + const members = {} + + for (const 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 + + 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 + } + } + } + + clanMembership.members = members + + return clanMembership + } } module.exports.DataRepository = DataRepository diff --git a/src/backend/services/UserService.js b/src/backend/services/UserService.js index 8637cd42..f5dd7555 100644 --- a/src/backend/services/UserService.js +++ b/src/backend/services/UserService.js @@ -2,11 +2,16 @@ class UserService { constructor () { this.user = null this.session = null + this.userRepository = null } setUserFromRequest (request) { this.user = request.user - this.session = request.session.passport + this.session = request.session + } + + setUserRepository (userRepository) { + this.userRepository = userRepository } isAuthenticated () { @@ -14,12 +19,23 @@ class UserService { } getUser () { - return this.user + return this.session.passport.user } updatePassport (oAuthPassport) { this.user.oAuthPassport = oAuthPassport - this.session.user.oAuthPassport = oAuthPassport + this.session.passport.user.oAuthPassport = oAuthPassport + } + + async refreshUser () { + const oAuthPassport = this.user.oAuthPassport + + this.user = await this.userRepository.fetchUser(oAuthPassport) + this.session.passport.user = this.user + + await new Promise(resolve => this.session.save(resolve)) + + return this.user } } diff --git a/src/backend/templates/layouts/default.pug b/src/backend/templates/layouts/default.pug index 4cd0024e..3e2d58ba 100755 --- a/src/backend/templates/layouts/default.pug +++ b/src/backend/templates/layouts/default.pug @@ -1,3 +1,4 @@ +include ../mixins/flash-connect doctype html html(lang='en') //- HTML HEADER @@ -20,7 +21,7 @@ html(lang='en') //- Customise the stylesheet for your site by editing /public/styles/site.sass link(href="/styles/css/site.min.css?version=" + Date.now(), rel="stylesheet") - + //- Include template-specific stylesheets by extending the css block block css @@ -32,7 +33,7 @@ html(lang='en') //- HTML BODY body .mainTopNavContainer - // this is the "thin" navbar that has forums, discord, youtube icons, etc. + // this is the "thin" navbar that has forums, discord, youtube icons, etc. .topNavContainer a.topnav_item(href='/account/requestPasswordReset') Reset Password .topNavContainer @@ -59,7 +60,7 @@ html(lang='en') .topNavContainer a(href='https://www.reddit.com/r/FAF') img(src='/images/fontAwesomeIcons/reddit.svg') - // Main Navbar with FAF NEWS, LOGIN AND DOWNLOAD. + // Main Navbar with FAF NEWS, LOGIN AND DOWNLOAD. .mainNavContainer .navContainer .navItem @@ -75,7 +76,7 @@ html(lang='en') li.navList NEWS ul.navAbsolute li.navList GAME - + ul.navAbsolute a(href='/campaign-missions') li Campaign & Co-Op Missions @@ -87,7 +88,7 @@ html(lang='en') li SC:FA vs FAF li.navList COMMUNITY - + ul.navAbsolute a(href='/faf-teams') li Teams and Association @@ -116,7 +117,7 @@ html(lang='en') ul.navItem a(href="/account/register") li.navList REGISTER - + if appGlobals.loggedInUser .navContainer.navEnd#loginList ul.loginItem @@ -125,7 +126,7 @@ html(lang='en') h3 #{appGlobals.loggedInUser.name} ul#loginAbsolute li.loginDropdown - ul(role='menu') + ul(role='menu') if appGlobals.loggedInUser.clan a(href="/clans/manage"): li My Clan else @@ -158,13 +159,13 @@ html(lang='en') .movingBackground2 img(src='/images/faflogo.svg' alt='') - + a(href='/').mobileNavElement Home - - + + a(href='/news').mobileNavElement News - - + + .mobileNavElement.mobileNavMenu Game .mobileNavMenuContent a(href='/leaderboards') Leaderboards @@ -172,8 +173,8 @@ html(lang='en') a(href='/ai') AI/Co-op Play a(href='/scfa-vs-faf') SC:FA vs FAF a(href='/play') Play Now - - + + .mobileNavElement.mobileNavMenu Community .mobileNavMenuContent a(href='/faf-teams') Teams and Association @@ -183,8 +184,8 @@ html(lang='en') a(href='/clans') Clans if !appGlobals.loggedInUser a(href='/login').mobileNavElement Login - - + + if appGlobals.loggedInUser .mobileNavElement.mobileNavMenu My Account .mobileNavMenuContent @@ -197,8 +198,8 @@ html(lang='en') a(href="/account/changeUsername") Change Username a(href="/account/report") Report Player a(href="/logout") Log Out - - + + .mobileSocialMedia a(href='https://discord.gg/mXahVSKGVb') img(src='/images/fontAwesomeIcons/discord.svg') @@ -215,14 +216,14 @@ html(lang='en') .movingBackground1 .movingBackground2 - - - .splatForgedBorder + + + .splatForgedBorder .movingBackground1 .movingBackground2 - - block bannerData + + block bannerData block banner @@ -232,7 +233,7 @@ html(lang='en') .bannerContainer.bannerTitle #{bannerSecondTitle} .bannerContainer.bannerSubtitle #{bannerSubTitle} - block bannerButton + block bannerButton block bannerMixin if bannerImage +banner() @@ -240,7 +241,7 @@ html(lang='en') .movingBackground1 .movingBackground2 - + +flash-connect(connectFlash) //- The content block should contain the body of your template's content block content .splatForgedBorder.transformationReverse @@ -264,7 +265,7 @@ html(lang='en') li a(href='/rules') RULES - ul CONTRIBUTE + ul CONTRIBUTE li a(href='/contribution') CONTRIBUTIONS li @@ -272,7 +273,7 @@ html(lang='en') ul LEGAL li - a(href='/privacy') PRIVACY STATEMENT + a(href='/privacy') PRIVACY STATEMENT li a(href='/tos') TERMS OF SERVICE @@ -281,9 +282,9 @@ html(lang='en') a(href='https://discord.gg/mXahVSKGVb') DISCORD li a(href='https://forum.faforever.com/') FORUMS - + script(src=webpackAssetJS('navigation')) //- Include template-specific javascript files by extending the js block block js - + diff --git a/src/backend/templates/mixins/flash-connect.pug b/src/backend/templates/mixins/flash-connect.pug new file mode 100644 index 00000000..569b33b8 --- /dev/null +++ b/src/backend/templates/mixins/flash-connect.pug @@ -0,0 +1,13 @@ +mixin flash-connect(connectFlash) + - var flashes = connectFlash() + if flashes + if flashes.info + div.alert(class='alert-success') + ul + each info in flashes.info + li !{info} + if flashes.error + div.alert(class='alert-danger') + ul + each error in flashes.error + li !{error} diff --git a/src/backend/templates/mixins/flash-error.pug b/src/backend/templates/mixins/flash-error.pug index 12115e81..bf98fcf5 100644 --- a/src/backend/templates/mixins/flash-error.pug +++ b/src/backend/templates/mixins/flash-error.pug @@ -1,5 +1,5 @@ mixin flash-error(validationErrors) - if validationErrors + if validationErrors && validationErrors.messages div.alert(class=validationErrors.class) ul.validationErrors-errors each error in validationErrors.messages.errors diff --git a/src/backend/templates/views/clans/create.pug b/src/backend/templates/views/clans/create.pug index 78d8abee..f93bccec 100644 --- a/src/backend/templates/views/clans/create.pug +++ b/src/backend/templates/views/clans/create.pug @@ -1,48 +1,32 @@ extends ../../layouts/default -include ../../mixins/flash-messages +include ../../mixins/flash-error include ../../mixins/form/account block bannerMixin block content - .containerCenter.text-center .row .col-md-12 - h1.account-title Create a clan + h1 Create a clan div - p You can create your own clan, and then invite other players to join it. - p Be sure to - a(href='/rules') review the rules - | before naming your clan! - p Offensive clan names will result in an immediate sanction - + | You can create your own clan, and then invite other players to join it. +
+ | Be sure to  + a(href='/rules') review the rules + | before naming your clan! +
+ | Offensive clan names will result in an immediate sanction + .row .col-md-offset-3.col-md-6 - +flash-messages(flash) - form(method='post', action="/clans/create", data-toggle="validator") - input(type='hidden', name="user_id", value=userId) - + +flash-error(errors) .clanManagement - .column12 - div.clanManagementItem - label Name - input(type='text', name='clan_name', value=clan_name, placeholder='Clan name').form-control - input(type='hidden', name='original_clan_name', value=clan_name) - span(aria-hidden='true').glyphicon.form-control-feedback - - div.clanManagementItem - label Tag: - - input(type='text', required='required', name='clan_tag', value=clan_tag, placeholder='TAG', style="display:inline;margin-left:5px;margin-right:5px;width:5em;").form-control - input(type='hidden', name='original_clan_tag', value=clan_tag) - - span(aria-hidden='true').glyphicon.form-control-feedback - br - div.clanManagementItem - label Clan description - br - br - textarea(rows='12', name='clan_description', required='required', placeholder='The description players will see when they look your clan').form-control #{clan_description} - span(aria-hidden='true').glyphicon.form-control-feedback + .column12 + form(method='post', action="/clans/create", data-toggle="validator") + input(type='text' name='clan_name' value=clan_name placeholder='Clan name' title='Name' required='required') + br + input(type='text' required='required' name='clan_tag' title='Tag' value=clan_tag placeholder='TAG') + br + textarea(rows='12' name='clan_description' title='description' required='required' placeholder='The description players will see when they look your clan') #{clan_description} + br - .form-actions - button(type='submit').bigButton Create your Clan + button(type='submit').bigButton Create your Clan diff --git a/src/backend/templates/views/clans/manage.pug b/src/backend/templates/views/clans/manage.pug index b277f998..9378656f 100644 --- a/src/backend/templates/views/clans/manage.pug +++ b/src/backend/templates/views/clans/manage.pug @@ -1,131 +1,33 @@ extends ../../layouts/default -include ../../mixins/flash-messages +include ../../mixins/flash-error include ../../mixins/form/account block bannerMixin block content - - .containerCenter - .row - .col-md-12 - h1.account-title Clan Management - br - .row - .col-md-offset-3.col-md-6 - +flash-messages(flash) - - .row.important-form - .col-md-6 - h2 Invite players - form(method='post',action="/clans/invite") - p This will generate an invitation link for the player of your choice - p Be sure to type the player name correctly! - - .row.inline-panel - input(type='hidden', name='clan_id', value=clan_id) - input(type='text', name='invited_player', placeholder='Player name', style="margin-left:5px;margin-right:5px").form-control - button(type='submit' onclick="copyTextButton()") Invite - - - br - h2 Clan Settings - .col-md-6 - form(method='post',action="/clans/update",data-toggle="validator") - input(type='hidden', name='clan_id', value=clan_id) - .clanManagement - .column12 - div.clanManagementItem - label Name - input(type='text', name='clan_name', value=clan_name, placeholder='Clan name').form-control - input(type='hidden', name='original_clan_name', value=clan_name) - span(aria-hidden='true').glyphicon.form-control-feedback - - div.clanManagementItem - label Tag: - - input(type='text', required='required', name='clan_tag', value=clan_tag, placeholder='TAG', style="display:inline;margin-left:5px;margin-right:5px;width:5em;").form-control - input(type='hidden', name='original_clan_tag', value=clan_tag) - - span(aria-hidden='true').glyphicon.form-control-feedback - br - div.clanManagementItem - label Clan description - br - br - textarea(rows='12', name='clan_description', required='required', placeholder='The description players will see when they look your clan').form-control #{clan_description} - span(aria-hidden='true').glyphicon.form-control-feedback - - .form-actions - button(type='submit').bigButton Update Clan Settings .clanManagement - .column12 - .clanManagementTable(style="overflow:auto;") - table.table.table-striped.table-hover - thead - tr - th.text-center Player - th.text-center Joined - th.text-center Kick member - tbody - each member in clan_members - tr(class= member.id == me ? "leader me" : "") - td - if member.id == me - abbr(title="Leader") 👑 - | #{member.name} - td #{member.joinedAt} - td - if member.id != me - form(method='post',action="/clans/kick") - input(type='hidden', name="membership_id", value=member.membershipId) - input(type='hidden', name='clan_id', value=clan_id) - button(type='submit').btn Kick - else - span - - .clanManagement - .clanManagementDanger.column12 - h1.danger DANGER ZONE - p The settings below CANNOT be undone. Do not touch these settings unless you are sure about what you are doing. - - form(method='post',action="/clans/transfer",data-toggle="validator", onsubmit="return confirm('ALL YOUR RIGHTS OVER THE CLAN WILL BE LOST. Press OK to confirm the clan transfer');") - input(type='hidden', name='clan_id', value=clan_id) - h2 Transfer ownership - p.text-left This operation will transfer the leadership of your clan to a new member. - br - p.text-left After the Leadership transfer, - ul.text-left - li You can no longer update the clan - br - li You can no longer delete the clan - br - li You can no longer invite new players - br - li You can no longer kick a player - br - br - p.danger By clicking the "transfer" button, you - b FORFEIT - | all your rights over this clan in favor of the new owner. - - .row.centered-flex - input(type='text', id="ownership_transfer_textbox", name='transfer_to', placeholder='Member name', style="margin-left:5px;margin-right:5px").form-control - button(type='submit').btn-danger.btn Transfer clan ownership - - form(method='post',action="/clans/destroy", onsubmit="return confirm('THIS OPERATION IS DEFINITIVE. Press OK to confirm you want to delete your clan');") - input(type='hidden', name='clan_id', value=clan_id) - br - br - h2 Delete the clan - p.text-left All memberships will be terminated and the clan will be removed. The name and tag of the clan will become free. - p.danger This operation - b CANNOT BE CANCELED - - - .row.centered-flex - button(type='submit').btn-danger.btn Delete my clan - br - br + .column12 + +flash-error(errors) + + .column12 + h2 Clan Settings + form(method='post', action="/clans/update", data-toggle="validator") + input(type='text' name='clan_name' value=clan_name placeholder='Clan name' title='Name' required='required') + br + input(type='text' required='required' name='clan_tag' title='Tag' value=clan_tag placeholder='TAG') + br + textarea(rows='12' name='clan_description' title='description' required='required' placeholder='The description players will see when they look your clan') #{clan_description} + br + + button(type='submit') Update Clan Settings + br + br + hr + .clanManagement + .column12 + h3 DANGER ZONE + div The settings below CANNOT be undone. Do not touch these settings unless you are sure about what you are doing. + br - - + form(method='post',action="/clans/destroy", onsubmit="return confirm('THIS OPERATION IS DEFINITIVE. Press OK to confirm you want to delete your clan');") + button(type='submit') Delete my clan + br