Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Personal access tokens for users to create API tokens #11638

Merged
merged 11 commits into from
Aug 21, 2018
Merged
1 change: 1 addition & 0 deletions imports/personal-access-tokens/client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './personalAccessTokens';
68 changes: 68 additions & 0 deletions imports/personal-access-tokens/client/personalAccessTokens.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template name="accountTokens">
<section class="preferences-page preferences-page--new">
{{> header sectionName="Personal_Access_Tokens" hideHelp=true fullpage=true}}
<div class="preferences-page__content">
{{# if isAllowed}}
<h2>{{_ "API_Personal_Access_Tokens_To_REST_API"}}</h2>
<br>
<form id="form-tokens" class="">

<div class="rc-form-group rc-form-group--inline">
<!-- <input id="input-token-name"
type="text"
name="tokenName"
placeholder={{_ "Enter_a_name"}}
value="{{tokenName}}"> -->
<div class="rc-input rc-input--small rc-directory-search rc-form-item-inline">
<label class="rc-input__label">
<div class="rc-input__wrapper">
<input type="text" class="rc-input__element rc-input__element--small js-search" name="tokenName" id="tokenName"
placeholder={{_ "API_Add_Personal_Access_Token"}} autocomplete="off">
</div>
</label>
</div>
<button name="add" class="rc-button rc-button--primary rc-form-item-inline save-token">{{_ "Add"}}</button>
</div>
</form>
<br>
<div class="rc-table-content">
{{#table}}
<thead>
<tr>
<th width="30%">
<div class="table-fake-th">{{_ "API_Personal_Access_Token_Name"}}</div>
</th>
<th>
<div class="table-fake-th">{{_ "Created_at"}}</div>
</th>
<th>
<div class="table-fake-th">{{_ "Last_token_part"}}</div>
</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each tokens}}
<tr data-id="{{name}}">
<td>
<div class="rc-table-title">
{{name}}
</div>
</td>
<td>{{dateFormated createdAt}}</td>
<td>...{{lastTokenPart}}</td>
<td><button class="regenerate-personal-access-token"><i class="icon-ccw"></i></button></td>
<td><button class="remove-personal-access-token"><i class="icon-block"></i></button></td>
</tr>
{{else}}
<tr>
<td colspan="4">{{_ "There_are_no_personal_access_tokens_created_yet"}}</td>
</tr>
{{/each}}
</tbody>
{{/table}}
</div>
{{/if}}
</div>
</section>
</template>
107 changes: 107 additions & 0 deletions imports/personal-access-tokens/client/personalAccessTokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ReactiveVar } from 'meteor/reactive-var';
import toastr from 'toastr';
import moment from 'moment';

import './personalAccessTokens.html';

const PersonalAccessTokens = new Mongo.Collection('personal_access_tokens');

Template.accountTokens.helpers({
isAllowed() {
return RocketChat.settings.get('API_Enable_Personal_Access_Tokens');
},
tokens() {
return (PersonalAccessTokens.find({}).fetch()[0] && PersonalAccessTokens.find({}).fetch()[0].tokens) || [];
},
dateFormated(date) {
return moment(date).format('L LT');
},
});

const showSuccessModal = (token) => {
modal.open({
title: t('API_Personal_Access_Token_Generated'),
text: t('API_Personal_Access_Token_Generated_Text_Token_s_UserId_s', { token, userId: Meteor.userId() }),
type: 'success',
confirmButtonColor: '#DD6B55',
confirmButtonText: 'Ok',
closeOnConfirm: true,
html: true,
}, () => {
});
};
Template.accountTokens.events({
'submit #form-tokens'(e, instance) {
e.preventDefault();
const tokenName = e.currentTarget.elements.tokenName.value.trim();
if (tokenName === '') {
return toastr.error(t('Please_fill_a_token_name'));
}
Meteor.call('personalAccessTokens:generateToken', { tokenName }, (error, token) => {
if (error) {
return toastr.error(t(error.error));
}
showSuccessModal(token);
instance.find('#input-token-name').value = '';
});
},
'click .remove-personal-access-token'() {
modal.open({
title: t('Are_you_sure'),
text: t('API_Personal_Access_Tokens_Remove_Modal'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes'),
cancelButtonText: t('Cancel'),
closeOnConfirm: true,
html: false,
}, () => {
Meteor.call('personalAccessTokens:removeToken', {
tokenName: this.name,
}, (error) => {
if (error) {
return toastr.error(t(error.error));
}
toastr.success(t('Removed'));
});
});
},
'click .regenerate-personal-access-token'() {
modal.open({
title: t('Are_you_sure'),
text: t('API_Personal_Access_Tokens_Regenerate_Modal'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('API_Personal_Access_Tokens_Regenerate_It'),
cancelButtonText: t('Cancel'),
closeOnConfirm: true,
html: false,
}, () => {
Meteor.call('personalAccessTokens:regenerateToken', {
tokenName: this.name,
}, (error, token) => {
if (error) {
return toastr.error(t(error.error));
}
showSuccessModal(token);
});
});
},
});

Template.accountTokens.onCreated(function() {
this.ready = new ReactiveVar(true);
const subscription = this.subscribe('personalAccessTokens');
this.autorun(() => {
this.ready.set(subscription.ready());
});
});

Template.accountTokens.onRendered(function() {
Tracker.afterFlush(function() {
SideNav.setFlex('accountFlex');
SideNav.openFlex();
});
});
35 changes: 35 additions & 0 deletions imports/personal-access-tokens/server/api/methods/generateToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { Accounts } from 'meteor/accounts-base';

Meteor.methods({
'personalAccessTokens:generateToken'({ tokenName }) {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'personalAccessTokens:generateToken' });
}
if (!RocketChat.settings.get('API_Enable_Personal_Access_Tokens')) {
throw new Meteor.Error('error-personal-access-tokens-are-current-disabled', 'Personal Access Tokens are currently disabled', { method: 'personalAccessTokens:generateToken' });
}

const token = Random.secret();
const tokenExist = RocketChat.models.Users.findPersonalAccessTokenByTokenNameAndUserId({
userId: Meteor.userId(),
tokenName,
});
if (tokenExist) {
throw new Meteor.Error('error-token-already-exists', 'A token with this name already exists', { method: 'personalAccessTokens:generateToken' });
}

RocketChat.models.Users.addPersonalAccessTokenToUser({
userId: Meteor.userId(),
loginTokenObject: {
hashedToken: Accounts._hashLoginToken(token),
type: 'personalAccessToken',
createdAt: new Date(),
lastTokenPart: token.slice(-6),
name: tokenName,
},
});
return token;
},
});
3 changes: 3 additions & 0 deletions imports/personal-access-tokens/server/api/methods/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './generateToken';
import './regenerateToken';
import './removeToken';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Meteor } from 'meteor/meteor';

Meteor.methods({
'personalAccessTokens:regenerateToken'({ tokenName }) {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'personalAccessTokens:regenerateToken' });
}
if (!RocketChat.settings.get('API_Enable_Personal_Access_Tokens')) {
throw new Meteor.Error('error-personal-access-tokens-are-current-disabled', 'Personal Access Tokens are currently disabled', { method: 'personalAccessTokens:regenerateToken' });
}

const tokenExist = RocketChat.models.Users.findPersonalAccessTokenByTokenNameAndUserId({
userId: Meteor.userId(),
tokenName,
});
if (!tokenExist) {
throw new Meteor.Error('error-token-does-not-exists', 'Token does not exist', { method: 'personalAccessTokens:regenerateToken' });
}

Meteor.call('personalAccessTokens:removeToken', { tokenName });
return Meteor.call('personalAccessTokens:generateToken', { tokenName });
},
});
26 changes: 26 additions & 0 deletions imports/personal-access-tokens/server/api/methods/removeToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Meteor } from 'meteor/meteor';

Meteor.methods({
'personalAccessTokens:removeToken'({ tokenName }) {
if (!Meteor.userId()) {
throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'personalAccessTokens:removeToken' });
}
if (!RocketChat.settings.get('API_Enable_Personal_Access_Tokens')) {
throw new Meteor.Error('error-personal-access-tokens-are-current-disabled', 'Personal Access Tokens are currently disabled', { method: 'personalAccessTokens:removeToken' });
}
const tokenExist = RocketChat.models.Users.findPersonalAccessTokenByTokenNameAndUserId({
userId: Meteor.userId(),
tokenName,
});
if (!tokenExist) {
throw new Meteor.Error('error-token-does-not-exists', 'Token does not exist', { method: 'personalAccessTokens:removeToken' });
}
RocketChat.models.Users.removePersonalAccessTokenOfUser({
userId: Meteor.userId(),
loginTokenObject: {
type: 'personalAccessToken',
name: tokenName,
},
});
},
});
6 changes: 6 additions & 0 deletions imports/personal-access-tokens/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import './api/methods';
import './settings';
import './models';
import './publications';


39 changes: 39 additions & 0 deletions imports/personal-access-tokens/server/models/Users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
RocketChat.models.Users.getLoginTokensByUserId = function(userId) {
const query = {
'services.resume.loginTokens.type': {
$exists: true,
$eq: 'personalAccessToken',
},
_id: userId,
};

return this.find(query, { fields: { 'services.resume.loginTokens': 1 } });
};

RocketChat.models.Users.addPersonalAccessTokenToUser = function({ userId, loginTokenObject }) {
return this.update(userId, {
$push: {
'services.resume.loginTokens': loginTokenObject,
},
});
};

RocketChat.models.Users.removePersonalAccessTokenOfUser = function({ userId, loginTokenObject }) {
return this.update(userId, {
$pull: {
'services.resume.loginTokens': loginTokenObject,
},
});
};

RocketChat.models.Users.findPersonalAccessTokenByTokenNameAndUserId = function({ userId, tokenName }) {
const query = {
'services.resume.loginTokens': {
$elemMatch: { name: tokenName, type: 'personalAccessToken' },
},
_id: userId,
};

return this.findOne(query);
};

1 change: 1 addition & 0 deletions imports/personal-access-tokens/server/models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './Users';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './personalAccessTokens';
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Meteor } from 'meteor/meteor';

Meteor.publish('personalAccessTokens', function() {
if (!this.userId) {
return this.ready();
}
if (!RocketChat.settings.get('API_Enable_Personal_Access_Tokens')) {
return this.ready();
}
const self = this;
const getFieldsToPublish = (fields) => fields.services.resume.loginTokens
.filter((loginToken) => loginToken.type && loginToken.type === 'personalAccessToken')
.map((loginToken) => ({
name: loginToken.name,
createdAt: loginToken.createdAt,
lastTokenPart: loginToken.lastTokenPart,
}));
const handle = RocketChat.models.Users.getLoginTokensByUserId(this.userId).observeChanges({
added(id, fields) {
self.added('personal_access_tokens', id, { tokens: getFieldsToPublish(fields) });
},
changed(id, fields) {
self.changed('personal_access_tokens', id, { tokens: getFieldsToPublish(fields) });
},
removed(id) {
self.removed('personal_access_tokens', id);
},
});

self.ready();

self.onStop(function() {
handle.stop();
});
});
5 changes: 5 additions & 0 deletions imports/personal-access-tokens/server/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RocketChat.settings.addGroup('General', function() {
this.section('REST API', function() {
this.add('API_Enable_Personal_Access_Tokens', false, { type: 'boolean', public: true });
});
});
1 change: 1 addition & 0 deletions imports/startup/client/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import '../../message-read-receipt/client';
import '../../personal-access-tokens/client';
1 change: 1 addition & 0 deletions imports/startup/server/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import '../../message-read-receipt/server';
import '../../personal-access-tokens/server';
Loading