Skip to content

Commit

Permalink
Fix clan invite links (#377)
Browse files Browse the repository at this point in the history
* Clan invite links die whenever seen

* Two step clan invitation

* Log invite kills

* Update Dockerfile

* Update Dockerfile

removed copy of env

Co-authored-by: Louvenarde <[email protected]>
Co-authored-by: Rowey <[email protected]>
  • Loading branch information
3 people authored Apr 6, 2022
1 parent 7947b89 commit 1ad6abc
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 23 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ RECENT_USERS_LIST_UPDATE_INTERVAL = 900 # 15 minutes
CLIENT_RELEASE_FETCHING_INTERVAL = 900 # 15 minutes
CLAN_LIST_UPDATE_INTERVAL = 900 # 15 minutes
PLAYER_COUNT_UPDATE_INTERVAL = 10 # 10 seconds
CLAN_INVITES_LIFESPAN_DAYS = 30 # 30 days before minified clan links are cleared from memory after being GET once. Reduce this value if website uses too much memory
3 changes: 2 additions & 1 deletion express.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ app.get('/clans/create', loggedIn, require(routes + 'clans/get/create'));
app.get('/clans/manage', loggedIn, require(routes + 'clans/get/manage'));
app.get('/clans/see', require(routes + 'clans/get/see'));
app.get('/clans/browse', require(routes + 'clans/get/browse'));
app.get('/clans/accept', loggedIn, require(routes + 'clans/get/accept'));
app.get('/clans/accept_invite', loggedIn, require(routes + 'clans/get/accept_invite'));

app.post('/clans/create', loggedIn, require(routes + 'clans/post/create'));
app.post('/clans/destroy', loggedIn, require(routes + 'clans/post/destroy'));
Expand All @@ -184,6 +184,7 @@ app.post('/clans/kick', loggedIn, require(routes + 'clans/post/kick'));
app.post('/clans/transfer', loggedIn, require(routes + 'clans/post/transfer'));
app.post('/clans/update', loggedIn, require(routes + 'clans/post/update'));
app.post('/clans/leave', loggedIn, require(routes + 'clans/post/leave'));
app.post('/clans/join', loggedIn, require(routes + 'clans/post/join'));

// Compatibility
app.get('/clan/*', function (req, res){
Expand Down
95 changes: 95 additions & 0 deletions routes/views/clans/get/accept_invite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const request = require('request');

exports = module.exports = function(req, res) {

var locals = res.locals;

// locals.section is used to set the currently selected
// item in the header navigation.
locals.section = 'clan';
let flash = {};

if (!req.query.i){
flash.type = 'Error!';
flash.class = 'alert-danger';
flash.messages = [{msg: 'The invitation link is wrong or truncated. Key informations are missing.'}];

let buff = Buffer.from(JSON.stringify(flash));
let data = buff.toString('base64');

return res.redirect('/clans?flash='+data);
}

const invitationId = req.query.i;

if (!req.app.locals.clanInvitations[invitationId]){
flash.type = 'Error!';
flash.class = 'alert-danger';
flash.messages = [{msg: 'The invitation link is wrong or truncated. Invite code missing from website clan map.'}];

let buff = Buffer.from(JSON.stringify(flash));
let data = buff.toString('base64');

return res.redirect('/clans?flash='+data);
}

const invite = req.app.locals.clanInvitations[invitationId];
const clanId = invite.clan;

if (req.user.data.attributes.clan != null){
// User is already in a clan!
return res.redirect('/clans/see?id='+req.user.data.attributes.clan.id);
}

const queryUrl = process.env.API_URL
+ '/data/clan/'+clanId
+ '?include=memberships.player'
+ '&fields[clan]=createTime,description,name,tag,updateTime,websiteUrl,founder,leader'
+ '&fields[player]=login,updateTime'
;

request.get(
{
url: queryUrl
},
function (err, childRes, body) {

const clan = JSON.parse(body);

if (err || !clan.data){
flash.type = 'Error!';
flash.class = 'alert-danger';
flash.messages = [{msg: 'The clan you want to join is invalid or does no longer exist'}];

let buff = Buffer.from(JSON.stringify(flash));
let data = buff.toString('base64');

return res.redirect('./?flash='+data);
}

locals.clanName = clan.data.attributes.name;
locals.clanLeaderName = "<unknown>";

for (k in clan.included){
switch(clan.included[k].type){
case "player":
const player = clan.included[k];

// Getting the leader name
if (player.id == clan.data.relationships.leader.data.id)
{
locals.clanLeaderName = player.attributes.login;
}

break;
}
}

const token = invite.token;
locals.acceptURL = `/clans/join?clan_id=${clanId}&token=${token}`;

// Render the view
res.render('clans/accept_invite');
}
);
};
4 changes: 2 additions & 2 deletions routes/views/clans/get/manage.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ exports = module.exports = function(req, res) {
flash.class = 'alert-success';
flash.messages = [
{msg:
"<a class='invite-link' href='"+process.env.HOST + "/clans/accept"
"<p><a class='invite-link' onclick='return false' href='"+process.env.HOST + "/clans/accept_invite"
+"?i=" + req.query.invitation_id
+"'><b>Right click on me and copy link</b>, then send it to the invited player</a>"}
+"'><b>Right click on me and copy link</b>, then send it to the invited player</a></p><p>Note: The link is <b>short-lived</b> and <b>one use only</b>. It will automatically expire after <b>"+process.env.CLAN_INVITES_LIFESPAN_DAYS+"</b> days regardless of use."}
];
flash.type = '';
}
Expand Down
26 changes: 26 additions & 0 deletions routes/views/clans/post/invite.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ function promiseRequest(url) {
});
}

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;
Expand Down Expand Up @@ -114,6 +130,16 @@ exports = module.exports = async function (req, res) {
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){
Expand Down
25 changes: 5 additions & 20 deletions routes/views/clans/get/accept.js → routes/views/clans/post/join.js
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,19 @@ exports = module.exports = function(req, res) {
let flash = {};
const overallRes = res;

if (!req.query.i){
if (!req.query.token || !req.query.clan_id){
flash.type = 'Error!';
flash.class = 'alert-danger';
flash.messages = [{msg: 'The invitation link is wrong or truncated. Key informations are missing.'}];
flash.messages = [{msg: 'The invitation link is invalid!'}];

let buff = Buffer.from(JSON.stringify(flash));
let data = buff.toString('base64');

return overallRes.redirect('/clans?flash='+data+'');
return res.redirect('/clans?flash='+data+'');
}

const invitationId = req.query.i;

if (!req.app.locals.clanInvitations[invitationId]){
flash.type = 'Error!';
flash.class = 'alert-danger';
flash.messages = [{msg: 'The invitation link is wrong or truncated. Key informations are missing.'}];

let buff = Buffer.from(JSON.stringify(flash));
let data = buff.toString('base64');

return overallRes.redirect('/clans?flash='+data+'');
}

const invite = req.app.locals.clanInvitations[invitationId];
const clanId = invite.clan;
const token = invite.token;
delete req.app.locals.clanInvitations[invitationId];
const token = req.query.token;
const clanId = req.query.clan_id;

request.post(
{
Expand Down
19 changes: 19 additions & 0 deletions templates/views/clans/accept_invite.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
extends ../../layouts/default
include ../../mixins/flash-messages

block content
.container.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}
hr
.row
.col-md-offset-3.col-md-6
+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}

0 comments on commit 1ad6abc

Please sign in to comment.