Skip to content

Commit fa56ae0

Browse files
Partial implementation of Invite Links
1 parent ca8e3aa commit fa56ae0

File tree

21 files changed

+543
-1
lines changed

21 files changed

+543
-1
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<template name="adminInvites">
2+
<div class="main-content-flex">
3+
<section class="page-container page-list flex-tab-main-content">
4+
{{> header sectionName="Invites"}}
5+
<div class="content">
6+
{{#unless hasPermission 'create-invite-links'}}
7+
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
8+
{{else}}
9+
<div class="results">
10+
{{{_ "Showing_results" invites.length}}}
11+
</div>
12+
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
13+
<thead>
14+
<tr class="admin-table-row">
15+
<th class="content-background-color border-component-color" width="30%">
16+
<div class="table-fake-th">{{_ "Token"}}</div>
17+
</th>
18+
<th class="content-background-color border-component-color" width="20%">
19+
<div class="table-fake-th">{{_ "Created_at"}}</div>
20+
</th>
21+
<th class="content-background-color border-component-color" width="20%">
22+
<div class="table-fake-th">{{_ "Expiration_(Days)"}}</div>
23+
</th>
24+
<th class="content-background-color border-component-color" width="10%">
25+
<div class="table-fake-th">{{_ "Uses"}}</div>
26+
</th>
27+
<th class="content-background-color border-component-color" width="20%">
28+
<div class="table-fake-th">{{_ "Uses_left"}}</div>
29+
</th>
30+
</tr>
31+
</thead>
32+
<tbody>
33+
{{#each invites}}
34+
<tr class="invites-info row-link admin-table-row">
35+
<td class="border-component-color">
36+
<div class="rc-table-wrapper">
37+
<div class="rc-table-info">
38+
<span class="rc-table-title">{{hash}}</span></div>
39+
</div>
40+
</td>
41+
<td class="border-component-color">
42+
<div class="rc-table-wrapper">
43+
<div class="rc-table-info">
44+
<span class="rc-table-title">{{creationDate}}</span></div>
45+
</div>
46+
</td>
47+
<td class="border-component-color">
48+
<div class="rc-table-wrapper">
49+
<div class="rc-table-info">
50+
<span class="rc-table-title">{{daysToExpire}}</span></div>
51+
</div>
52+
</td>
53+
<td class="border-component-color">
54+
<div class="rc-table-wrapper">
55+
<div class="rc-table-info">
56+
<span class="rc-table-title">{{uses}}</span></div>
57+
</div>
58+
</td>
59+
<td class="border-component-color">
60+
<div class="rc-table-wrapper">
61+
<div class="rc-table-info">
62+
<span class="rc-table-title">{{maxUsesLeft}}</span></div>
63+
</div>
64+
</td>
65+
</tr>
66+
{{/each}}
67+
</tbody>
68+
{{/table}}
69+
{{/unless}}
70+
</div>
71+
</section>
72+
</div>
73+
</template>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import s from 'underscore.string';
2+
import { Template } from 'meteor/templating';
3+
import { ReactiveVar } from 'meteor/reactive-var';
4+
import { Meteor } from 'meteor/meteor';
5+
6+
import { t } from '../../../utils';
7+
8+
Template.adminInvites.helpers({
9+
isReady() {
10+
if (Template.instance().ready != null) {
11+
return Template.instance().ready.get();
12+
}
13+
return undefined;
14+
},
15+
invites() {
16+
return Template.instance().invites.get();
17+
},
18+
isLoading() {
19+
if (Template.instance().ready != null) {
20+
if (!Template.instance().ready.get()) {
21+
return 'btn-loading';
22+
}
23+
}
24+
},
25+
daysToExpire() {
26+
const { expires, days, createdAt } = this;
27+
28+
if (days > 0) {
29+
if (expires < new Date()) {
30+
return t('Expired');
31+
}
32+
33+
return Math.ceil((expires - createdAt) / (1000 * 60 * 60 * 24));
34+
}
35+
36+
return t('Never');
37+
},
38+
maxUsesLeft() {
39+
const { maxUses, uses } = this;
40+
41+
if (maxUses > 0) {
42+
if (uses >= maxUses) {
43+
return t('None');
44+
}
45+
46+
return maxUses - uses;
47+
}
48+
49+
return t('Unlimited');
50+
},
51+
creationDate() {
52+
const { createdAt } = this;
53+
54+
return createdAt.toLocaleDateString();
55+
},
56+
});
57+
58+
Template.adminInvites.onCreated(function() {
59+
const instance = this;
60+
this.invites = new ReactiveVar([]);
61+
this.ready = new ReactiveVar(false);
62+
63+
this.autorun(function() {
64+
const invites = [];
65+
66+
Meteor.call('listInvites', (error, result) => {
67+
if (!result) {
68+
return;
69+
}
70+
71+
for (const invite of result) {
72+
const newInvite = {
73+
_id: invite._id,
74+
createdAt: invite.createdAt,
75+
expires: invite.expires,
76+
hash: invite.hash,
77+
days: invite.days,
78+
maxUses: invite.maxUses,
79+
rid: invite.rid,
80+
userId: invite.userId,
81+
uses: invite.uses,
82+
};
83+
invites.push(newInvite);
84+
}
85+
86+
instance.invites.set(invites);
87+
instance.ready.set(true);
88+
});
89+
});
90+
});

app/invites/client/admin/route.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { FlowRouter } from 'meteor/kadira:flow-router';
2+
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
3+
4+
FlowRouter.route('/admin/invites', {
5+
name: 'invites',
6+
async action(/* params */) {
7+
await import('./views');
8+
BlazeLayout.render('main', { center: 'adminInvites' });
9+
},
10+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { AdminBox } from '../../../ui-utils';
2+
import { hasAtLeastOnePermission } from '../../../authorization';
3+
4+
AdminBox.addOption({
5+
href: 'invites',
6+
i18nLabel: 'Invites',
7+
icon: 'user-plus',
8+
permissionGranted() {
9+
return hasAtLeastOnePermission(['create-invite-links']);
10+
},
11+
});

app/invites/client/admin/views.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import './adminInvites.html';
2+
import './adminInvites';

app/invites/client/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './admin/route';
2+
import './admin/startup';
3+

app/invites/server/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './startup/permissions';
2+
import './methods/findOrCreateInvite';
3+
import './methods/listInvites';
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Meteor } from 'meteor/meteor';
2+
import { Random } from 'meteor/random';
3+
4+
import { hasPermission } from '../../../authorization';
5+
import { Notifications } from '../../../notifications';
6+
import { Invites, Rooms } from '../../../models';
7+
8+
function getInviteUrl(invite, roomName) {
9+
const { rid, hash } = invite;
10+
11+
const host = 'open.rocket.chat';
12+
const url = `https://go.rocket.chat/${ roomName }?host=${ host }&rid=${ rid }&path=channel/${ roomName }&token=${ hash }`;
13+
14+
return url;
15+
}
16+
17+
Meteor.methods({
18+
findOrCreateInvite(invite) {
19+
if (!hasPermission(this.userId, 'create-invite-links')) {
20+
throw new Meteor.Error('not_authorized');
21+
}
22+
23+
if (!invite.rid) {
24+
throw new Meteor.Error('error-the-field-is-required', 'The field rid is required', { method: 'findOrCreateInvite', field: 'rid' });
25+
}
26+
27+
const room = Rooms.findOneById(invite.rid);
28+
if (!room) {
29+
throw new Meteor.Error('error-invalid-room', 'The rid field is invalid', { method: 'findOrCreateInvite', field: 'rid' });
30+
}
31+
32+
let { days, maxUses } = invite;
33+
const possibleDays = [0, 1, 7, 15, 30];
34+
const possibleUses = [0, 1, 5, 10, 25, 50, 100];
35+
36+
if (!possibleDays.includes(days)) {
37+
days = 1;
38+
}
39+
40+
if (!possibleUses.includes(maxUses)) {
41+
maxUses = 0;
42+
}
43+
44+
// Before anything, let's check if there's an existing invite with the same settings for the same channel and user and that has not yet expired.
45+
const query = {
46+
rid: invite.rid,
47+
userId: this.userId,
48+
days,
49+
maxUses,
50+
};
51+
52+
if (days > 0) {
53+
query.expires = {
54+
$gt: new Date(),
55+
};
56+
}
57+
58+
if (maxUses > 0) {
59+
query.uses = 0;
60+
}
61+
62+
// If an existing invite was found, return it's hash instead of creating a new one.
63+
const existing = Invites.find(query).fetch();
64+
if (existing && existing.length) {
65+
return {
66+
hash: existing[0].hash,
67+
url: getInviteUrl(existing[0], room.fname),
68+
days: existing[0].days,
69+
maxUses: existing[0].maxUses,
70+
uses: existing[0].uses,
71+
expires: existing[0].expires,
72+
};
73+
}
74+
75+
const hash = Random.id();
76+
77+
// insert invite
78+
const now = new Date();
79+
let expires = null;
80+
if (days > 0) {
81+
expires = new Date(now);
82+
expires.setDate(expires.getDate() + days);
83+
}
84+
85+
const createInvite = {
86+
hash,
87+
days,
88+
maxUses,
89+
rid: invite.rid,
90+
userId: this.userId,
91+
createdAt: now,
92+
expires,
93+
uses: 0,
94+
};
95+
96+
Invites.create(createInvite);
97+
98+
Notifications.notifyLogged('updateInvites', { invite: createInvite });
99+
return {
100+
hash,
101+
url: getInviteUrl(createInvite, room.fname),
102+
days,
103+
maxUses,
104+
uses: 0,
105+
expires,
106+
};
107+
},
108+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Meteor } from 'meteor/meteor';
2+
3+
import { Invites } from '../../../models';
4+
5+
Meteor.methods({
6+
listInvites() {
7+
const currentUserId = Meteor.userId();
8+
if (!currentUserId) {
9+
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listInvites' });
10+
}
11+
12+
return Invites.find({}).fetch();
13+
},
14+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Meteor } from 'meteor/meteor';
2+
3+
import { Permissions } from '../../../models';
4+
5+
Meteor.startup(() => {
6+
if (Permissions) {
7+
if (!Permissions.findOne({ _id: 'create-invite-links' })) {
8+
Permissions.insert({ _id: 'create-invite-links', roles: ['admin', 'moderator', 'owner'] });
9+
}
10+
}
11+
});

0 commit comments

Comments
 (0)