diff --git a/lib/ApiErrors.js b/lib/ApiErrors.js new file mode 100644 index 00000000..449f01ca --- /dev/null +++ b/lib/ApiErrors.js @@ -0,0 +1,18 @@ +class JavaApiError extends Error +{ + constructor(status, url, errors) { + super('Failed request "' + url + '" with status "' + status + '"') + + this.status = status + this.url = url + this.errors = errors + } + +} + +class GenericJavaApiError extends Error +{ +} + +module.exports.JavaApiError = JavaApiError +module.exports.GenericJavaApiError = GenericJavaApiError diff --git a/lib/clan/ClanRepository.js b/lib/clan/ClanRepository.js new file mode 100644 index 00000000..d229cc4f --- /dev/null +++ b/lib/clan/ClanRepository.js @@ -0,0 +1,116 @@ +const {JavaApiError, GenericJavaApiError} = require("../ApiErrors"); + +class ClanRepository { + constructor(javaApiClient, monthsInThePast = 12) { + this.javaApiClient = javaApiClient + this.monthsInThePast = monthsInThePast + } + + getUpdateTimeForApiEntries() { + const date = new Date(); + date.setMonth(date.getMonth() - this.monthsInThePast); + + return date.toISOString() + } + + async updateClan(id, name, description, tag) { + const newClanObject = { + "data": { + "type": "clan", + "id": id, + "attributes": { + "description": description, + "name": name, + "tag": tag + } + } + }; + + try { + const response = await this.javaApiClient.patch(`/data/clan/${id}`, newClanObject) + + if (response.status !== 200) { + throw new Error('ClanRepository::fetchClanMembership failed with response status "' + response.status + '"') + } + + return this.mapClanMembership(JSON.parse(response.data)) + } catch (e) { + if (e.response && e.response.data?.errors) { + throw new JavaApiError(e.response.status, `patch /data/clan/${id}`, e.response.data.errors) + } + + throw GenericJavaApiError('ClanRepository::fetchClanMembership failed') + } + + + + } + + async fetchClanMembership(clanMembershipId) { + let response = await this.javaApiClient.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 Error('ClanRepository::fetchClanMembership failed with response status "' + response.status + '"') + } + + 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 (!data.hasOwnProperty('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, + } + + 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; + + 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 = ClanRepository diff --git a/public/js/app/clans.js b/public/js/app/clans.js index 9ddd1faa..3859ffcb 100644 --- a/public/js/app/clans.js +++ b/public/js/app/clans.js @@ -47,7 +47,6 @@ function clanUpdate() { 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 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/js/app/members/test.json~ b/public/js/app/members/test.json~ new file mode 100644 index 00000000..58ff2157 --- /dev/null +++ b/public/js/app/members/test.json~ @@ -0,0 +1 @@ +{"data":{"type":"clan","id":"2354","attributes":{"createTime":"2023-11-10T23:16:32Z","description":"asdasd","name":"asd","tag":"asd","updateTime":"2023-11-10T23:16:32Z","websiteUrl":"http://localhost:8096/clan/2354"},"relationships":{"founder":{"data":{"type":"player","id":"7"}},"leader":{"data":{"type":"player","id":"7"}}}},"included":[{"type":"clanMembership","id":"15594","attributes":{"createTime":"2023-11-10T23:16:32Z"},"relationships":{"player":{"data":{"type":"player","id":"7"}}}},{"type":"player","id":"7","attributes":{"login":"steambie","updateTime":"2023-10-27T10:38:46Z"}}]} \ No newline at end of file diff --git a/public/styles/site/clans.sass b/public/styles/site/clans.sass index d3f94d76..3c38a387 100644 --- a/public/styles/site/clans.sass +++ b/public/styles/site/clans.sass @@ -178,15 +178,6 @@ input 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 diff --git a/routes/views/account/post/error.js b/routes/views/account/post/error.js index 5d2baa9b..d68db080 100644 --- a/routes/views/account/post/error.js +++ b/routes/views/account/post/error.js @@ -1,3 +1,5 @@ +const {validationResult} = require('express-validator'); + module.exports = { parseApiErrors: function (body, flash) { let errorMessages = []; @@ -17,5 +19,32 @@ module.exports = { flash.class = 'alert-danger'; flash.messages = errorMessages; flash.type = 'Error!'; - } + }, + errorChecking: (req, res, path) => { + let flash = {} + let errorArray = []; + //We are putting a space in our forEach so that the errors comma don't stick to the next error. + validationResult(req).errors.forEach(error => errorArray.push(` ${error.msg}`)); + flash.class = 'alert-danger'; + flash.messages = errorArray; + flash.type = 'Error!'; + res.render(path, {flash: flash}); + }, + userUpdate: (req, res, path) => { + axios.get(`${process.env.API_URL}/me`, { + headers: { + 'Authorization': `Bearer ${req.user.token}`, + } + }).then(response => { + let user = response.data; + user.token = req.user.token; + user.data.id = user.data.attributes.userId; + req.logIn(user, function (err) { + if (err) console.error(err); + res.redirect(path); + }); + }).catch(e => { + console.log('error updating user') + }); + } } diff --git a/routes/views/clanRouter.js b/routes/views/clanRouter.js index 5000277f..c823249d 100644 --- a/routes/views/clanRouter.js +++ b/routes/views/clanRouter.js @@ -1,7 +1,24 @@ -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('*', (req, res) => res.status(503).render('errors/503-known-issue')); +router.get('/create', middlewares.isAuthenticated(), require('clans/get/create')) +router.get('/manage', middlewares.isAuthenticated(), require('clans/get/manage')) +router.get('/accept_invite', middlewares.isAuthenticated(), require('clans/get/accept_invite')) +router.post('/create', middlewares.isAuthenticated(), require('clans/post/create')) +router.post('/destroy', middlewares.isAuthenticated(), require('clans/post/destroy')) +router.post('/invite', middlewares.isAuthenticated(), require('clans/post/invite')) +router.post('/kick', middlewares.isAuthenticated(), require('clans/post/kick')) +router.post('/transfer', middlewares.isAuthenticated(), require('clans/post/transfer')) +router.post('/update', middlewares.isAuthenticated(), require('clans/post/update')) +router.post('/leave', middlewares.isAuthenticated(), require('clans/post/leave')) +router.post('/join', middlewares.isAuthenticated(), require('clans/post/join')) + +router.get('/', require('clans/get/clans')) +router.get('/getClan', require('clans/get/getClan')) +router.get('/*', (req, res) => { + let id = req.path.slice(-3) + res.redirect(`/clans/getClan?tag=${id}`) +}) module.exports = router diff --git a/routes/views/clans/get/clans.js b/routes/views/clans/get/clans.js new file mode 100644 index 00000000..e88c966c --- /dev/null +++ b/routes/views/clans/get/clans.js @@ -0,0 +1,29 @@ +exports = module.exports = function (req, res) { + let flash = {}; + if (req.query.flash) { + + flash.class = 'alert-success'; + flash.type = 'Success!'; + switch (req.query.flash) { + case 'leave': + flash.messages = 'You left your clan.'; + break; + + case 'destroy': + flash.messages = 'You deleted your clan.'; + break; + + case 'transfer': + flash.messages = `You have transferred your clan to ${req.query.newLeader}.`; + break; + + case 'error': + flash.class = 'alert-danger'; + flash.messages = 'There was an issue with your request.'; + flash.type = 'Error!'; + break; + } + } + res.render('clans', {flash: flash}); + +}; diff --git a/routes/views/clans/get/create.js b/routes/views/clans/get/create.js index 4b6a13eb..dde80e37 100644 --- a/routes/views/clans/get/create.js +++ b/routes/views/clans/get/create.js @@ -1,3 +1,5 @@ +const axios = require("axios"); + exports = module.exports = function(req, res) { let locals = res.locals; @@ -8,19 +10,17 @@ exports = module.exports = function(req, res) { const request = require('request'); - request.get( + axios.get(process.env.API_URL + '/clans/me', { - url: process.env.API_URL + '/clans/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, - function (err, childRes, body) { + headers: { + 'Authorization': 'Bearer ' + req.user.data.attributes.token + } + } + ).then(function (clanInfo) { - const clanInfo = JSON.parse(body); if (clanInfo.clan != null){ - res.redirect('/clans/manage'); - return; + res.redirect('/clans/manage'); + return; } locals.formData = req.body || {}; @@ -32,21 +32,21 @@ exports = module.exports = function(req, res) { var flash = null; if (req.query.flash){ - let buff = Buffer.from(req.query.flash, 'base64'); - let text = buff.toString('ascii'); - - try{ - flash = JSON.parse(text); - } - catch(e){ - console.error("Parsing error while trying to decode a flash error: " + text); - console.error(e); - flasg = [{msg: "Unknown error"}]; - } + let buff = Buffer.from(req.query.flash, 'base64'); + let text = buff.toString('ascii'); + + try{ + flash = JSON.parse(text); + } + catch(e){ + console.error("Parsing error while trying to decode a flash error: " + text); + console.error(e); + flasg = [{msg: "Unknown error"}]; + } } // Render the view res.render('clans/create', {flash: flash}); - } - ); + }) + .catch((e) => console.log(e.toString())) }; diff --git a/routes/views/clans/get/getClan.js b/routes/views/clans/get/getClan.js new file mode 100644 index 00000000..371454b0 --- /dev/null +++ b/routes/views/clans/get/getClan.js @@ -0,0 +1,64 @@ +const axios = require('axios'); +require('dotenv').config(); +exports = module.exports = function(req, res) { + + if (!req.query.tag) res.redirect('../clans?flash=error'); + else { + + + //We call the API and get the info needed + axios.get(`${process.env.API_URL}/data/clan?include=memberships.player&filter=tag==${req.query.tag.toLowerCase()}` + ).then(response => { + const {attributes} = response.data.data[0]; + const {name, description, createTime} = attributes; + + // first lets check user is logged in and has a clan + if (req.user && req.user.data.attributes.clan !== undefined) { + // lets check if the user belongs to the clan + if (req.user.data.attributes.clan.tag.toLowerCase() === req.query.tag.toLowerCase()) { + res.locals.leaveButton = true; + } + } + + //We set the values as local variables in our response + res.locals.clanName = name; + res.locals.clanDescription = description; + res.locals.clanCreation = createTime.slice(0, 10); + res.locals.clanTag = req.query.tag.toUpperCase(); + + + //We add in the clan members + let clanMembers = []; + response.data.included.forEach((member, index) => { + // We only allow odd numbers because the API brings extra information on even numbers that don't include a members login/username + if (index % 2 !== 0) { + clanMembers.push(member.attributes.login); + } + }); + res.locals.clanMembers = clanMembers; + + //We find the clan leader + const leaderID = response.data.data[0].relationships.leader.data.id; + response.data.included.forEach((element, index) => { + if (index % 2 !== 0) { + if (element.id === leaderID) { + res.locals.clanLeaderName = element.attributes.login; + } + } + }); + + + + + }).catch((e) => { + + res.redirect('../clans?flash=error'); + + }).finally(() => { + + + + res.render('clans/getClan'); + }); + } +}; diff --git a/routes/views/clans/get/manage.js b/routes/views/clans/get/manage.js index 5bcd4ab6..015398d3 100755 --- a/routes/views/clans/get/manage.js +++ b/routes/views/clans/get/manage.js @@ -1,134 +1,128 @@ -const request = require('request'); - -exports = module.exports = function(req, res) { - - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'clan'; - - let flash = null; - - let clanMembershipId = null; - try{ - clanMembershipId = req.user.data.attributes.clan.membershipId; - } - catch{ - // The user doesnt belong to a clan - res.redirect('/clans'); - return; - } - - // In case the user has just generated an invite link - if (req.query.invitation_id) { - flash = {}; - flash.class = 'alert-invite'; - - flash.messages = [ - {msg: - `

    Right click on me and copy the invitation link

    Note: It only works for the user you typed.`} - ]; - flash.type = ''; - - } - - - - - request.get( - { - url: - process.env.API_URL - + '/data/clanMembership/'+clanMembershipId+'/clan' - + '?include=memberships.player' - + '&fields[clan]=createTime,description,name,tag,updateTime,websiteUrl,founder,leader' - + '&fields[player]=login,updateTime' - + '&fields[clanMembership]=createTime,player', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, - function (err, childRes, body) { - - const clan = JSON.parse(body); - - if (err || !clan.data){ - flash = {}; - flash.class = 'alert-danger'; - flash.messages = [{msg: "Unknown error while retrieving your clan information"}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/clans?flash='+data); - } - - if (clan.data.relationships.leader.data.id != req.user.data.id){ - // Not the leader! Shouldn't be able to manage stuff - res.redirect(`/clans/${req.user.data.attributes.clan.tag}?member=true`); +const error = require("../../account/post/error"); +const appConfig = require('../../../../config/app') +const ClanRepository = require('../../../../lib/clan/ClanRepository') +const {Axios} = require("axios"); + +exports = module.exports = async function (req, res) { + + let flash = {}; + let clanMembershipId = null; + try { + clanMembershipId = req.user.data.attributes.clan.membershipId; + } catch (e) { + // The user doesnt belong to a clan + res.redirect('../clans'); return; - } - - locals.clan_name = clan.data.attributes.name; - locals.clan_tag = clan.data.attributes.tag; - locals.clan_description = clan.data.attributes.description; - locals.clan_create_time = clan.data.attributes.createTime; - locals.me = req.user.data.id; - locals.clan_id = clan.data.id; - locals.clan_link = process.env.HOST + "/clans/see?id="+clan.data.id; - - let members = {}; - - for (k in clan.included){ - switch(clan.included[k].type){ - case "player": - const player = clan.included[k]; - if (!members[player.id]) members[player.id] = {}; - members[player.id].id = player.id; - members[player.id].name = player.attributes.login; - - if (clan.data.relationships.founder.data.id == player.id){ - locals.founder_name = player.attributes.login - } - break; - - case "clanMembership": - const membership = clan.included[k]; - const member = membership.relationships.player.data; - if (!members[member.id]) members[member.id] = {}; - members[member.id].id = member.id; - members[member.id].membershipId = membership.id; - members[member.id].joinedAt = membership.attributes.createTime; - break; - - } - } - - locals.clan_members = members; - - if (req.originalUrl == '/clan_created') { - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'You have successfully created your clan'}]; - flash.type = 'Success!'; - } - else if (req.query.flash){ - let buff = Buffer.from(req.query.flash, 'base64'); - let text = buff.toString('ascii'); - try{ - flash = JSON.parse(text); - } - catch(e){ - console.error("Parsing error while trying to decode a flash error: " + text); - console.error(e); - flash = [{msg: "Unknown error"}]; - } - } - - // Render the view - res.render('clans/manage', {flash: flash}); } - ); + const config = { + baseURL: appConfig.apiUrl, + headers: {Authorization: `Bearer ${req.user.token}`} + }; + const javaApiClient = new Axios(config) + const clanRepository = new ClanRepository(javaApiClient) + + try { + const clan = await clanRepository.fetchClanMembership(clanMembershipId) + + return res.render('clans/manage', {flash: flash, clan: clan}); + } catch (e) { + // error.parseApiErrors(e.response, flash); + console.log(e.toString()) + return res.redirect('/'); + } + + // axios.get(`${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.token}`}, + // + // }).then(response => { + // const clan = response.data + // // Not the leader! Shouldn't be able to manage stuff + // if (clan.data.relationships.leader.data.id != req.user.data.attributes.userId) { + // res.redirect(`/clans/getClan?tag=${req.user.data.attributes.clan.tag}`); + // } else { + // // Lets create the schema for all the members and clan descriptions + // res.locals.clan_name = clan.data.attributes.name; + // res.locals.clan_tag = clan.data.attributes.tag; + // res.locals.clan_description = clan.data.attributes.description; + // res.locals.clan_create_time = clan.data.attributes.createTime; + // res.locals.me = req.user.data.attributes.userId; + // res.locals.clan_id = clan.data.id; + // + // let members = {}; + // + // for (k in clan.included) { + // switch (clan.included[k].type) { + // case "player": + // const player = clan.included[k]; + // if (!members[player.id]) members[player.id] = {}; + // members[player.id].id = player.id; + // members[player.id].name = player.attributes.login; + // + // if (clan.data.relationships.founder.data.id == player.id) { + // res.locals.founder_name = player.attributes.login + // } + // break; + // + // case "clanMembership": + // const membership = clan.included[k]; + // const member = membership.relationships.player.data; + // if (!members[member.id]) members[member.id] = {}; + // members[member.id].id = member.id; + // members[member.id].membershipId = membership.id; + // members[member.id].joinedAt = membership.attributes.createTime; + // break; + // } + // } + // + // // Lets check the different flash types + // if (req.query.flash) { + // flash.class = 'alert-success'; + // flash.type = 'Success!'; + // switch (req.query.flash) { + // + // case 'created': + // flash.messages = 'You have created your clan.'; + // break; + // + // case 'kick': + // flash.messages = `You have kicked ${req.query.kickPlayer}.`; + // break; + // + // case 'update': + // flash.messages = `You have updated your clan information.`; + // break; + // + // + // case 'error': + // flash.class = 'alert-danger'; + // flash.messages = 'There was an error with your request.'; + // flash.type = 'Error!'; + // break; + // case 'alreadyTaken': + // flash.class = 'alert-danger'; + // flash.messages = 'The clan name/tag is already taken. Choose a different one.'; + // flash.type = 'Error!'; + // break; + // } + // } + // + // + // //Lets check if they tried inviting an user + // if (req.query.invitation_id && req.query.invitation_id !== 'error') { + // + // flash.class = 'alert-invite'; + // flash.hasHTML = `${process.env.HOST}/clans/accept_invite?i=${req.query.invitation_id}`; + // flash.type = 'invite'; + // } else if (req.query.invitation_id === 'error') { + // flash.class = 'alert-danger'; + // flash.messages = `User isn't a valid username (check your spelling). If error continues contact support`; + // flash.type = 'Error!'; + // } + // res.render('clans/manage', {flash: flash, clan_members: members}); + // } + // }).catch((e) => { + // error.parseApiErrors(e.response, flash); + // res.render('clans/manage', {flash: flash}); + // }); }; diff --git a/routes/views/clans/post/create.js b/routes/views/clans/post/create.js index 8c43d917..65a0d907 100755 --- a/routes/views/clans/post/create.js +++ b/routes/views/clans/post/create.js @@ -1,130 +1,71 @@ let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({max: 3}); - check('clan_description', 'Please add a description for your clan').notEmpty().isLength({max: 1000}); - check('clan_name', "Please indicate your clan's name").notEmpty().isLength({max: 40}); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('create?flash=' + data); - } else { - - const clanName = req.body.clan_name; - const clanTag = req.body.clan_tag; - const clanDescription = req.body.clan_description; - const userId = req.body.user_id; - - // Let's check first that the name or tag are not taken - const clanFetchRoute = process.env.API_URL+'/data/clan?filter=name=="'+clanName+'",tag=="'+clanTag+'"'; - let exists = true; - try { - const httpData = await promiseRequest(clanFetchRoute); - exists = JSON.parse(httpData).data.length > 0; - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Error while creating the clan '+e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('create?flash='+data+'&clan_name='+clanName+'&clan_tag='+clanTag+'&clan_description='+clanDescription+''); - } - - const queryUrl = - process.env.API_URL - + '/clans/create' - + '?name=' + encodeURIComponent(clanName) - + '&tag='+encodeURIComponent(clanTag) - + '&description='+encodeURIComponent(clanDescription) - ; - - //Run post to endpoint - request.post({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode !== 200) { - let msg = 'Error while creating the clan'; - try { - - msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail); - } catch { +const axios = require('axios'); +const {body, validationResult} = require('express-validator'); +const error = require("../../account/post/error"); + +exports = module.exports = [ + + // validate the input + body('clan_tag', 'Your clan tag is too long (max 3 characters)').notEmpty().isLength({max: 3}), + body('clan_description', 'Your clan description is too long (max 1000 characters)').notEmpty().isLength({max: 1000}), + body('clan_name', 'Your clan name is too long (max 40 characters)').isLength({max: 40}), + async (req, res) => { + // check the validation object for errors + if (!validationResult(req).isEmpty()) { + error.errorChecking(req, res, 'clans/create'); } - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('create?flash='+data+'&clan_name='+clanName+'&clan_tag='+clanTag+'&clan_description='+clanDescription+''); - } + else { + + // Take variables from form + const clanName = req.body.clan_name; + const clanTag = req.body.clan_tag; + const clanDescription = req.body.clan_description; + + // We check if the clan already exists + let clanExists = ''; + await axios.get(`${process.env.API_URL}/data/clan?filter=name=="${clanName}",tag=="${clanTag}"`, { + headers: {'Authorization': 'Bearer ' + req.user.token} + }) + .then(response => { + clanExists = !response.data.data[0]; + }).catch(e => { + console.log(e) + error.parseApiErrors(e.response, flash); + }); + if (!clanExists) { + flash.class = 'alert-danger'; + flash.messages = 'The clan tag/name are already taken. Choose a different one.'; + flash.type = 'Error!'; + res.render('clans/create', {flash: flash}); + + } + // Clan doesn't exist, lets create it! + else { + axios.post(`${process.env.API_URL}/clans/create?name=${clanName}&tag=${clanTag}&description=${clanDescription}`, null, + { + headers: {'Authorization': 'Bearer ' + req.user.token} + }).then( () => { + + // Refreshing user + axios.get(`${process.env.API_URL}/me`, { + headers: { + 'Authorization': `Bearer ${req.user.token}`, + } + }).then( () => { + + //Lets update our user + error.userUpdate(req, res, '/clans/create' ); + }).catch(e => { + error.parseApiErrors(e.response, flash); + }); + + }).catch((e) => { + console.log(e.toString()) + error.parseApiErrors(e.response, flash); + res.render('clans/create', {flash: flash}); + }); + } - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.attributes.token = req.user.data.attributes.token; - user.data.id = user.data.attributes.userId; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('/clans/manage'); - }); - } - catch{ - console.error("There was an error updating a session after a clan creation"); - } - }); - - }); - } -} + } + } +]; diff --git a/routes/views/clans/post/destroy.js b/routes/views/clans/post/destroy.js index 0caba265..01a42236 100755 --- a/routes/views/clans/post/destroy.js +++ b/routes/views/clans/post/destroy.js @@ -1,111 +1,30 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - // Building update query - const queryUrl = - process.env.API_URL - + '/data/clan/' + req.body.clan_id - ; - - //Run post to endpoint - request.delete({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while destroying the clan'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{} - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'The clan was successfully destroyed'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('/clans?flash='+data); +let axios = require('axios'); +const {body, validationResult} = require('express-validator'); +const error = require("../../account/post/error"); + + +exports = module.exports = [ + // validate the input + body('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(), + (req, res) => { + // check the validation object for errors + if (!validationResult(req).isEmpty()) error.errorChecking(req, res, 'clans'); + // No errors in form, continue ahead + + else { + //Run post to endpoint + axios.delete(`${process.env.API_URL}/data/clan/${req.body.clan_id}`, + { + headers: {'Authorization': `Bearer ${req.user.token}`} + }).then( ()=> { + + // Refreshing user + error.userUpdate(req, res, '/clans?flash=destroy'); + + }).catch((e) => { + res.redirect('manage?flash=error'); }); - } - catch{ - console.error("There was an error updating a session after a clan destruction"); - } - }); - }); - } -} + } + } +] +; diff --git a/routes/views/clans/post/invite.js b/routes/views/clans/post/invite.js index 918dcb21..2c25d82b 100644 --- a/routes/views/clans/post/invite.js +++ b/routes/views/clans/post/invite.js @@ -1,156 +1,45 @@ -let flash = {}; -const request = require('request'); -const {check, validationResult} = require('express-validator'); +const axios = require('axios'); +const error = require("../../account/post/error"); -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - console.error("Call to " + url + " failed: " + error); - reject(error); - } - }); - }); -} -function setLongTimeout(func, delayMs) { - const maxDelay = 214748364-1; // JS Limit for 32 bit integers - - if (delayMs > maxDelay) { - const remainingDelay = delayMs - maxDelay; - - // we cut it in smaller, edible chunks - setTimeout(() => { - setLongTimeout(func, remainingDelay); - }, maxDelay); - } - else{ - setTimeout(func, delayMs); - } -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - - // validate the input - check('invited_player', 'Please indicate the player name').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - const clanId = req.body.clan_id; +exports = module.exports = async (req, res) => { + // Let's get the local variables + const clanId = req.body.clan_id; const userName = req.body.invited_player; - - // Let's check first that the player exists - const fetchRoute = process.env.API_URL + '/data/player?filter=login=="' + userName + '"&fields[player]='; - - let exists = true; - let playerData = null; let playerId = null; - try { - const httpData = await promiseRequest(fetchRoute); - playerData = JSON.parse(httpData).data; - exists = playerData.length > 0; - playerId = playerData[0].id; - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'The player ' + userName + " doesn't seem to exist" + e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - const queryUrl = - process.env.API_URL - + '/clans/generateInvitationLink' - + '?clanId=' + encodeURIComponent(clanId) - + '&playerId=' + encodeURIComponent(playerId) - ; - - //Run post to endpoint - request.get({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - if (res.statusCode !== 200) { - - let errorMessages = []; - let msg = 'Error while generating the invite link'; - try { - - msg += ': ' + JSON.stringify(JSON.parse(res.body).errors[0].detail); - } catch { - } - - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash='+data); - } - else{ - try{ - const token = JSON.parse(res.body).jwtToken; - - const id = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5).toUpperCase(); - + // Let's check first that the player exists + await axios.get(`${process.env.API_URL}/data/player?filter=login==${userName}&fields[player]=`) + .then(response => { + + // Player exists + if (response.data.data[0] ? response.data.data[0] : false) { + playerId = response.data.data[0].id; + + } else { // Player doesn't exist + + res.redirect(`manage?invitation_id=error`); + } + }).catch(e => { + console.log(e); + res.redirect(`manage?invitation_id=error`); + }); + //Player does exist, lets create the invite link + if (playerId !== null) { + await axios.get(`${process.env.API_URL}/clans/generateInvitationLink?clanId=${clanId}&playerId=${playerId}`, + { + headers: {'Authorization': `Bearer ${req.user.token}`}, + }).then(response => { + let data = response.data; + const token = data.jwtToken; + let id = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 5).toUpperCase(); req.app.locals.clanInvitations[id] = { - token:token, - clan:clanId + token: token, + clan: clanId }; - - // We use timeout here because if we delete the invite link whenver the page is GET, - // then discord and other messaging applications will destroy the link accidentally - // when pre-fetching the page. So we will delete it later. Regardless if the website is restarted all the links will be - // killed instantly, which is fine. They are short lived by design. - const lifespan = process.env.CLAN_INVITES_LIFESPAN_DAYS * 24 * 3600 * 1000; - setLongTimeout(()=>{ - delete req.app.locals.clanInvitations[id]; - console.log(`Killed invitation with id ${id} after having waited ${lifespan} seconds (${process.env.CLAN_INVITES_LIFESPAN_DAYS} days)`); - }, lifespan); - - return overallRes.redirect('manage?invitation_id='+id); - } - catch (e){ - flash.class = 'alert-danger'; - flash.messages = [{msg:"Unkown error while generating the invite link: "+e}]; - flash.type = 'Error!'; - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash='+data); - } - } - }); - } -} + res.redirect(`manage?invitation_id=${id}`); + }).catch(e => { + console.log(e); + res.redirect(`manage?invitation_id=error`); + }); + } +}; diff --git a/routes/views/clans/post/join.js b/routes/views/clans/post/join.js index b4e51d6b..d4d69c47 100644 --- a/routes/views/clans/post/join.js +++ b/routes/views/clans/post/join.js @@ -1,87 +1,28 @@ -const request = require('request'); +const axios = require("axios"); +const error = require("../../account/post/error"); -exports = module.exports = function(req, res) { +exports = module.exports = function (req, res) { - let locals = res.locals; - - // locals.section is used to set the currently selected - // item in the header navigation. - locals.section = 'clan'; - + // item in the header navigation. let flash = {}; - const overallRes = res; - - if (!req.query.token || !req.query.clan_id){ + + if (!req.query.token) { flash.type = 'Error!'; flash.class = 'alert-danger'; flash.messages = [{msg: 'The invitation link is invalid!'}]; + res.render('/clans', {flash: flash}); + } else { + const token = req.query.token; - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return res.redirect('/clans?flash='+data+''); + axios.post(`${process.env.API_URL}/clans/joinClan?token=${token}`, null, + { + headers: {'Authorization': `Bearer null`} + }).then(() => { + // Refreshing user, by going to clan/manage, user is redirected to their own clan. + error.userUpdate(req, res, '/clans/manage'); + }).catch( e => { + console.log(e); + res.redirect('../clans?flash=error'); + }); } - - const token = req.query.token; - const clanId = req.query.clan_id; - - request.post( - { - url: process.env.API_URL + '/clans/joinClan?token='+token, - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, - function (err, childRes, body) { - let flashData; - if (childRes.statusCode == 200 || childRes.statusCode == 201){ - flash.class = 'alert-success'; - flash.messages = [ - {msg: "Welcome to your new clan!"} - ]; - flash.type = 'Success!'; - let buff = Buffer.from(JSON.stringify(flash)); - flashData = buff.toString('base64'); - - // Refreshing user - return request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect(`${user.data.attributes.clan.tag}?member=true&flash=${flashData}`); - }); - } - catch{ - console.error("There was an error updating a session after an user left a clan"); - } - }); - } - else{ - flash.type = 'Error!'; - flash.class = 'alert-danger'; - let msg = 'The invitation is invalid or has expired, or you are already part of a clan'; - try{ - msg += ': '+JSON.stringify(JSON.parse(childRes.body).errors[0].detail); - } catch{} - - flash.messages = [{msg: msg}]; - - let buff = Buffer.from(JSON.stringify(flash)); - flashData = buff.toString('base64'); - - return overallRes.redirect('/clans?flash='+flashData+''); - } - - } - ); }; diff --git a/routes/views/clans/post/kick.js b/routes/views/clans/post/kick.js index 13914bf1..57a525eb 100755 --- a/routes/views/clans/post/kick.js +++ b/routes/views/clans/post/kick.js @@ -1,96 +1,25 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); +let axios = require('axios'); -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 = function (req, res) { -exports = module.exports = async function (req, res) { + // Check if we are missing the member to kick or if someone is trying to kick themselves. Should not happen normally, but you never know + if (req.body.membership_id === req.user.data.attributes.clan.membershipId || !req.body.membership_id) { + res.redirect('manage?flash=error&error=missingData'); + } else { + const membershipId = req.body.membership_id; + const kickedPlayer = req.body.membership_name; + //Run post to endpoint + axios.delete(`${process.env.API_URL}/data/clanMembership/${membershipId}`, { + headers: {'Authorization': `Bearer ${req.user.token}`} + }).then(() => { + res.redirect(`manage?flash=kick&kickPlayer=${kickedPlayer}`); - let locals = res.locals; + }).catch((e) => { + console.log(e); + res.redirect(`manage?flash=error`); - locals.formData = req.body || {}; + }); - let overallRes = res; + } - // validate the input - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - check('membership_id', 'Internal error while processing your query: invalid member ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - // Should not happen normally, but you never know - if (req.body.membership_id == req.user.data.attributes.clan.membershipId) errors = [{msg: "You cannot kick yourself"}]; - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - // Building update query - const membershipId = req.body.membership_id; - const queryUrl = - process.env.API_URL - + '/data/clanMembership/' + membershipId - - ; - - //Run post to endpoint - request.delete({ - url: queryUrl, - body: "", - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while removing the member'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{} - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'The member was kicked'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - }); - } -} +}; diff --git a/routes/views/clans/post/leave.js b/routes/views/clans/post/leave.js index 2028675f..94ff8bd4 100755 --- a/routes/views/clans/post/leave.js +++ b/routes/views/clans/post/leave.js @@ -1,111 +1,25 @@ -let flash = {}; -let request = require('request'); -const {check, validationResult} = require('express-validator'); +const error = require("../../account/post/error"); +let axios = require('axios'); + +exports = module.exports = function (req, res) { + if (!req.user.data.attributes.clan) { + res.redirect('../clans?flash=error&error=missingData'); + } else { + const membershipId = req.user.data.attributes.clan.membershipId; + //Run post to endpoint + axios.delete(`${process.env.API_URL}/data/clanMembership/${membershipId}`, { + headers: { + 'Authorization': `Bearer ${req.user.token}` + } + }).then(() => { + + // Refreshing user + error.userUpdate(req, res, '/clans?flash=leave'); + + }).catch(e => { + console.log(e.response); + res.redirect(`../clans?flash=error`); -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - check('membership_id', 'Internal error while processing your query: invalid member ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('/clans?flash=' + data); - } else { - - // Building update query - const membershipId = req.body.membership_id; - const queryUrl = `${process.env.API_URL}/data/clanMembership/${membershipId}`; - - //Run post to endpoint - request.delete({ - url: `${process.env.API_URL}/data/clanMembership/${req.user.data.attributes.clan.membershipId}`, - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while leaving the clan'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); - } - catch{ - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - } - - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('/clans?flash='+data); - } - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'You left the clan'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('/clans?flash='+data); - }); - } - catch{ - console.error("There was an error updating a session after an user left a clan"); - } }); - }); - } + } }; diff --git a/routes/views/clans/post/transfer.js b/routes/views/clans/post/transfer.js index 82cf178a..8a8b141e 100755 --- a/routes/views/clans/post/transfer.js +++ b/routes/views/clans/post/transfer.js @@ -1,159 +1,85 @@ -let flash = {}; -const request = require('request'); -const {check, validationResult} = require('express-validator'); +const axios = require('axios'); +const error = require("../../account/post/error"); -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error || `Unexpected status code ${res.statusCode}`); - } - }); - }); -} +exports = module.exports = function (req, res) { -exports = module.exports = async function (req, res) { + const clanId = req.body.clan_id; + const transferUsername = req.body.transfer_to; + let playerId = null; - let locals = res.locals; - locals.formData = req.body || {}; - let overallRes = res; + // If clan id or the transfer username are missing, then we can't transfer an unknown clan to an unknown clan member. + if (!transferUsername || !clanId) res.redirect('manage?flash=error&error=missingData'); + else { - // validate the input - check('transfer_to', 'Please indicate the recipient name').notEmpty(); - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - // check the validation object for errors - let errors = validationResult(req); + // Let's check first that the player exists AND is part of this clan + axios.get(`${process.env.API_URL}/data/clan/${clanId}?include=memberships.player&fields[player]=login`) + .then(response => { - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; + // can't transfer clan to yourself + if (transferUsername === req.user.data.attributes.userName) res.redirect('manage?flash=error&error=transferToSelf'); - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash=' + data); - } else { - - const clanId = req.body.clan_id; - const userName = req.body.transfer_to; + // Lets make an array of all members - // Let's check first that the player exists AND is part of this clan - const fetchRoute = process.env.API_URL+'/data/clan/'+clanId+'?include=memberships.player&fields[player]=login'; - - let playerId = null; - - try { - if (userName === req.user.data.attributes.userName) throw "You cannot transfer your own clan to yourself"; - - const httpData = await promiseRequest(fetchRoute); - clanData = JSON.parse(httpData); - - let members = {}; - - for (k in clanData.included){ - const record = clanData.included[k]; - if (record.type !== "player") continue; - members[record.attributes.login] = record.id; - } - - if (!members[userName]) throw "User does not exist or is not part of the clan"; - playerId = members[userName]; - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'There was an error during the transfer to ' + userName + ": "+e}]; - flash.type = 'Error!'; - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); + response.data.included.forEach(player => { + if (player.type === "player") { + if (player.attributes.login === transferUsername) playerId = player.id; - return overallRes.redirect('manage?flash='+data); - } - - - // Building update query - const queryUrl = - process.env.API_URL - + '/data/clan/' + clanId - ; - - const newClanObject = - { - "data": { - "type": "clan", - "id": clanId, - "relationships": { - "leader": { - "data":{ - "id": playerId, - "type": "player" } - } - } - } - }; - - //Run post to endpoint - request.patch({ - url: queryUrl, - body: JSON.stringify(newClanObject), - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - 'Content-Type': 'application/vnd.api+json' - } - }, function (err, res, body) { - - if (res.statusCode != 204) { - - let errorMessages = []; - let msg = 'Error during the ownership transfer'; - try{ - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); + }); + + }).then(() => { + +//Lets check our array for our transfer player + if (playerId === null) res.redirect('manage?flash=error&error=notClanMember'); + else { + + const newClanObject = + { + "data": { + "type": "clan", + "id": clanId, + "relationships": { + "leader": { + "data": { + "id": playerId, + "type": "player" + } + } + } + } + }; + + //Run post to endpoint / Transfer clan + axios.patch(`${process.env.API_URL}/data/clan/${clanId}`, newClanObject, + { + + headers: { + 'Authorization': `Bearer ${req.user.token}`, + 'Content-Type': 'application/vnd.api+json' + } + }).then(() => { + // Refreshing user + error.userUpdate(req, res, `../clans?flash=transfer&newLeader=${transferUsername}`); + + }).catch(e => { + + res.redirect('manage?flash=error'); + }); } - catch{} - - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - else{ - // Refreshing user - request.get({ - url: process.env.API_URL + '/me', - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - } - }, - - function (err, res, body) { - try{ - let user = JSON.parse(body); - user.data.id = user.data.attributes.userId; - user.data.attributes.token = req.user.data.attributes.token; - req.logIn(user, function(err){ - if (err) console.error(err); - return overallRes.redirect('see?id='+clanId); - }); - } - catch{ - console.error("There was an error updating a session after a clan transfer"); - } - }); - } - }); - } -} + + + }).catch(e => { + + res.redirect('manage?flash=error'); + }); + + } + + +}; diff --git a/routes/views/clans/post/update.js b/routes/views/clans/post/update.js index 4822433f..0159124e 100644 --- a/routes/views/clans/post/update.js +++ b/routes/views/clans/post/update.js @@ -1,156 +1,48 @@ -let flash = {}; -let request = require('request'); const {check, validationResult} = require('express-validator'); - -function promiseRequest(url) { - return new Promise(function (resolve, reject) { - request(url, function (error, res, body) { - if (!error && res.statusCode < 300) { - resolve(body); - } else { - reject(error); - } - }); - }); -} - -exports = module.exports = async function (req, res) { - - let locals = res.locals; - - locals.formData = req.body || {}; - - let overallRes = res; - - // validate the input - check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').notEmpty().isLength({max: 3}); - check('clan_description', 'Please add a description for your clan').notEmpty().isLength({max: 1000}); - check('clan_name', "Please indicate your clan's name").notEmpty().isLength({max: 64}); - check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(); - - // check the validation object for errors - let errors = validationResult(req); - - //Must have client side errors to fix - if (!errors.isEmpty()) { - flash.class = 'alert-danger'; - flash.messages = errors; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash=' + data); - } else { - - const newName = req.body.clan_name; - const newTag = req.body.clan_tag; - const oldName = req.body.original_clan_name; - const oldTag = req.body.original_clan_tag; - const clanDescription = req.body.clan_description; - const userId = req.body.user_id; - - // Is the name taken ? - try { - let msg = null; - - flash.class = 'alert-danger'; - flash.type = 'Error!'; - - if (oldName != newName){ - const fetchRoute = process.env.API_URL+'/data/clan?filter=name=="'+encodeURIComponent(newName)+'"'; - const data = await promiseRequest(fetchRoute); - const exists = JSON.parse(data).data.length > 0; - - if (exists) msg = "This name is already taken: "+encodeURIComponent(newName); - } - if (oldTag != newTag){ - const fetchRoute = process.env.API_URL+'/data/clan?filter=tag=="'+encodeURIComponent(newTag)+'"'; - const data = await promiseRequest(fetchRoute); - const exists = JSON.parse(data).data.length > 0; - - if (exists) msg = "This tag is already taken: "+encodeURIComponent(newTag); - } - - if (msg){ - flash.messages = [{msg: msg}]; - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - return overallRes.redirect('manage?flash='+data); - } - } - catch(e){ - flash.class = 'alert-danger'; - flash.messages = [{msg: 'Error while updating the clan '+e}]; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - // Building update query - const queryUrl = - process.env.API_URL - + '/data/clan/' + req.body.clan_id - ; - - const newClanObject ={ - "data": { - "type": "clan", - "id": req.body.clan_id, - "attributes": { - "description": clanDescription, - "name": newName, - "tag": newTag - } - } - }; - - - //Run post to endpoint - request.patch({ - url: queryUrl, - body: JSON.stringify(newClanObject), - headers: { - 'Authorization': 'Bearer ' + req.user.data.attributes.token, - 'Content-Type': 'application/vnd.api+json', - 'Accept': 'application/vnd.api+json' - } - }, function (err, res, body) { - - let resp; - let errorMessages = []; - - if (res.statusCode != 204) { - let msg = 'Error while updating the clan'; - try{ - - msg += ': '+JSON.stringify(JSON.parse(res.body).errors[0].detail); +const error = require("../../account/post/error"); +const appConfig = require("../../../../config/app"); +const axios = require('axios') +const ClanRepository = require('../../../../lib/clan/ClanRepository') +const {JavaApiError} = require("../../../../lib/ApiErrors"); + +exports = module.exports = [ + + // validate the input + check('clan_tag', 'Please indicate the clan tag - No special characters and 3 characters maximum').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").isLength({max: 64}), + check('clan_id', 'Internal error while processing your query: invalid clan ID').notEmpty(), + + async (req, res) => { + // check the validation object for errors + if (!validationResult(req).isEmpty()) error.errorChecking(req, res, 'clans'); + // No errors in form, continue ahead + else { + + const newName = req.body.clan_name; + const newTag = req.body.clan_tag; + const clanDescription = req.body.clan_description; + + + const config = { + baseURL: appConfig.apiUrl, + headers: { + 'Authorization': `Bearer ${req.user.token}` + } + }; + const javaApiClient = axios.create(config) + const clanRepository = new ClanRepository(javaApiClient) + + try { + const clan = await clanRepository.updateClan(req.body.clan_id, newName, clanDescription, newTag) + + return res.redirect('/clans/manage?flash=update'); + } catch (e) { + if (e instanceof JavaApiError) { + req.flash('info', 'Flash is back!') + } + return res.redirect('/clans/manage') + } } - catch{} - errorMessages.push({msg: msg}); - flash.class = 'alert-danger'; - flash.messages = errorMessages; - flash.type = 'Error!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - } - - - flash = {}; - flash.class = 'alert-success'; - flash.messages = [{msg: 'You have successfully updated your clan'}]; - flash.type = 'Success!'; - - let buff = Buffer.from(JSON.stringify(flash)); - let data = buff.toString('base64'); - - return overallRes.redirect('manage?flash='+data); - }); - } -}; + } +]; diff --git a/sessions/.gitkeep b/sessions/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/templates/mixins/flash-messages.pug b/templates/mixins/flash-messages.pug index d515504a..31fbc401 100644 --- a/templates/mixins/flash-messages.pug +++ b/templates/mixins/flash-messages.pug @@ -1,8 +1,5 @@ mixin flash-messages(messages) - if flash - div.alert(class=flash['class']) - ul.flash-errors - if flash.messages - if flash.messages[0] - if flash.messages[0].msg - li #{flash.type} !{flash.messages[0].msg} + div.alert + for message, type in messages + for message, type in messages + p #{type} #{message} diff --git a/templates/views/clans/accept_invite.pug b/templates/views/clans/accept_invite.pug index f5a7b6db..7806262f 100644 --- a/templates/views/clans/accept_invite.pug +++ b/templates/views/clans/accept_invite.pug @@ -2,18 +2,11 @@ extends ../../layouts/default include ../../mixins/flash-messages block bannerMixin block content - .containerCenter.text-center - .row - .col-md-12 - h1.account-title Accept invitation - h4.account-subtitle.text-center Click the button below to accept the invitation from #{clanLeaderName} to join #{clanName} - - .row - .col-md-offset-3.col-md-6 + .containerCenter.text-center + h2 Clan Invitation + h4 Click the button below to accept the invitation to join the clan #{clanName}. + +flash-messages(flash) - .row - .col-md-offset-3.col-md-6 form(method='post', action=acceptURL, data-toggle="validator") - button(type='submit').btn.btn-default.btn-lg.btn-outro.btn-danger Join #{clanName} - + button(type='submit') Join #{clanName} diff --git a/templates/views/clans/create.pug b/templates/views/clans/create.pug index 78d8abee..0f1cfd60 100644 --- a/templates/views/clans/create.pug +++ b/templates/views/clans/create.pug @@ -4,45 +4,39 @@ include ../../mixins/form/account block bannerMixin block content - .containerCenter.text-center - .row - .col-md-12 + .containerCenter.text-center + h1.account-title 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 - - .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) + 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 + +flash-messages(flash) + form(method='post', action="/clans/create", data-toggle="validator") + input(type='hidden', name="user_id", value=userId) - .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 + .clanManagement + .column12 + .clanManagementItem + label Name + input(type='text', required='required', 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: + .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) + 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 + span(aria-hidden='true') + br + .clanManagementItem + label Clan description + br - .form-actions - button(type='submit').bigButton Create your Clan + 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') + button(type='submit') Create your Clan diff --git a/templates/views/clans/getClan.pug b/templates/views/clans/getClan.pug new file mode 100644 index 00000000..8b8e516c --- /dev/null +++ b/templates/views/clans/getClan.pug @@ -0,0 +1,24 @@ +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 Founded on #{clanCreation} + h1 Led by #{clanLeaderName} 👑 + + if leaveButton + 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.');") + .formStart + input(type='hidden', name='clan_id', value=clan_id) + input(type='hidden', name='membership_id', value=my_membership) + button(type='submit') Leave my clan + h2 Clan Members + + ul.renderClanSubGrid + each member in clanMembers + li #{member} diff --git a/templates/views/clans/manage.pug b/templates/views/clans/manage.pug index b277f998..8d69c62d 100644 --- a/templates/views/clans/manage.pug +++ b/templates/views/clans/manage.pug @@ -3,129 +3,116 @@ include ../../mixins/flash-messages 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 - - - + + .clanManagementMain + .clanManagementContainer.column12 + // Display non-html flash message + if message + +flash-messages(message) + + + + + + .clanManagementContainer.column12 + h1 Clan Management + 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! + input(type='hidden', name='clan_id', value=clan_id) + input(type='text', name='invited_player', placeholder='Player name', required='required') + br + button(type='submit' onclick="copyTextButton()") Invite Player + // We use HTML for the invite button + if flash.hasHTML + br + h2 Invitation link + .displayNone + #{flash.hasHTML} + + + button(onclick="copyInviteLink()") Copy invite link to Clipboard + p Note: It only works for the user you typed. + + + .clanManagementContainer.column12 + h2 Clan Settings + form(method='post',action="/clans/update",data-toggle="validator") + input(type='hidden', name='clan_id', value=clan.clan_id) + label Clan Name + input(type='text', name='clan_name', value=clan.clan_name, placeholder='Clan name', required='required') + input(type='hidden', name='original_clan_name', value=clan.clan_name) + span(aria-hidden='true') + label Clan Tag: + input(type='text', required='required', name='clan_tag', value=clan.clan_tag, placeholder='TAG') + input(type='hidden', name='original_clan_tag', value=clan.clan_tag) + span(aria-hidden='true') + br + label Clan description + br + + textarea(rows='12', name='clan_description', required='required', placeholder='The description players will see when they look your clan') #{clan.clan_description} + span(aria-hidden='true') + br + + button(type='submit') Update Clan Settings + + .clanManagementContainer.column12 + .clanMembers + each member in clan.members + .column2 + p #{member.name} + .column1 + if member.id != me + form(method='post',action="/clans/kick") + input(type='hidden', name="membership_name", value=member.name) + input(type='hidden', name="membership_id", value=member.membershipId) + input(type='hidden', name='clan_id', value=clan_id) + button(type='submit') Kick + else + span + .clanManagementMain + .clanManagementDanger.clanManagementContainer.column12 + h1 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.clan_id) + h2 Transfer ownership + p This operation will transfer the leadership of your clan to a new member. + br + h2 After the Leadership transfer: + li You can no longer update the clan + + li You can no longer delete the clan + + li You can no longer invite new players + + li You can no longer kick a player + br + br + p By clicking the "transfer" button, you + b FORFEIT + | all your rights over this clan in favor of the new owner. + + + input(type='text', required='required', id="ownership_transfer_textbox", name='transfer_to', placeholder='New clan owner') + br + button(type='submit') 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.clan_id) + br + br + h2 Delete the clan + p All memberships will be terminated and the clan will be removed. The name and tag of the clan will become free. + p This operation + b CANNOT BE CANCELED + br + button(type='submit') Delete my clan + br + br + // This script makes it so people can copy the invite link with just a click + script. + function copyInviteLink() { + navigator.clipboard.writeText(document.getElementById("inviteLink").getAttribute("href"))} 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")