Skip to content
Snippets Groups Projects
Commit ed1d550d authored by Marcos Spessatto Defendi's avatar Marcos Spessatto Defendi Committed by Diego Sampaio
Browse files

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

parent 3487b2f8
No related branches found
No related tags found
No related merge requests found
Showing
with 452 additions and 42 deletions
import './personalAccessTokens';
<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>
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();
});
});
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;
},
});
import './generateToken';
import './regenerateToken';
import './removeToken';
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 });
},
});
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,
},
});
},
});
import './api/methods';
import './settings';
import './models';
import './publications';
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);
};
import './Users';
import './personalAccessTokens';
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();
});
});
RocketChat.settings.addGroup('General', function() {
this.section('REST API', function() {
this.add('API_Enable_Personal_Access_Tokens', false, { type: 'boolean', public: true });
});
});
import '../../message-read-receipt/client';
import '../../personal-access-tokens/client';
import '../../message-read-receipt/server';
import '../../personal-access-tokens/server';
<template name="accountSecurity">
<section class="preferences-page">
{{> header sectionName="Security"}}
<div class="preferences-page__content">
<form id="security" autocomplete="off" class="container">
<section class="preferences-page preferences-page--new">
{{> header sectionName="Security" fullpage=true}}
<div class="preferences-page__content">
<form id="security" autocomplete="off" class="container">
{{# if isAllowed}}
<fieldset>
<div class="section">
<h1>{{_ "Two-factor_authentication"}}</h1>
<div class="section-content border-component-color">
{{#if isEnabled}}
<button class="button danger disable-2fa">{{_ "Disable_two-factor_authentication"}}</button>
{{else}}
{{#unless isRegistering}}
<p>{{_ "Two-factor_authentication_is_currently_disabled"}}</p>
<fieldset>
<div class="section">
<h1>{{_ "Two-factor_authentication"}}</h1>
<div class="section-content border-component-color">
{{#if isEnabled}}
<button class="rc-button rc-button--cancel disable-2fa">{{_ "Disable_two-factor_authentication"}}</button>
{{else}}
{{#unless isRegistering}}
<p>{{_ "Two-factor_authentication_is_currently_disabled"}}</p>
<button class="button primary enable-2fa">{{_ "Enable_two-factor_authentication"}}</button>
{{else}}
<p>{{_ "Scan_QR_code"}}</p>
<p>{{_ "Scan_QR_code_alternative_s" code=imageSecret}}</p>
<button class="rc-button rc-button--primary enable-2fa">{{_ "Enable_two-factor_authentication"}}</button>
{{else}}
<p>{{_ "Scan_QR_code"}}</p>
<p>{{_ "Scan_QR_code_alternative_s" code=imageSecret}}</p>
<img src="{{imageData}}">
<img src="{{imageData}}">
<form class="inline">
<input type="text" class="rc-input__element" id="testCode" placeholder="{{_ "Enter_authentication_code"}}">
<button class="button primary verify-code">{{_ "Verify"}}</button>
</form>
{{/unless}}
{{/if}}
</div>
<form class="inline">
<input type="text" class="rc-input__element" id="testCode" placeholder="{{_ "Enter_authentication_code"}}">
<button class="rc-button rc-button--primary verify-code">{{_ "Verify"}}</button>
</form>
{{/unless}}
{{/if}}
</div>
</fieldset>
</div>
</fieldset>
{{#if isEnabled}}
<fieldset>
<div class="section">
<h1>{{_ "Backup_codes"}}</h1>
<div class="section-content border-component-color">
<p>{{codesRemaining}}</p>
<button class="button regenerate-codes">{{_ "Regenerate_codes"}}</button>
</div>
</div>
</fieldset>
{{/if}}
{{#if isEnabled}}
<fieldset>
<div class="section">
<h1>{{_ "Backup_codes"}}</h1>
<div class="section-content border-component-color">
<p>{{codesRemaining}}</p>
<button class="rc-button rc-button--secondary regenerate-codes">{{_ "Regenerate_codes"}}</button>
</div>
</div>
</fieldset>
{{/if}}
{{/if}}
</form>
</div>
......
......@@ -424,3 +424,61 @@ RocketChat.API.v1.addRoute('users.getUsernameSuggestion', { authRequired: true }
return RocketChat.API.v1.success({ result });
},
});
RocketChat.API.v1.addRoute('users.generatePersonalAccessToken', { authRequired: true }, {
post() {
const { tokenName } = this.bodyParams;
if (!tokenName) {
return RocketChat.API.v1.failure('The \'tokenName\' param is required');
}
const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName }));
return RocketChat.API.v1.success({ token });
},
});
RocketChat.API.v1.addRoute('users.regeneratePersonalAccessToken', { authRequired: true }, {
post() {
const { tokenName } = this.bodyParams;
if (!tokenName) {
return RocketChat.API.v1.failure('The \'tokenName\' param is required');
}
const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:regenerateToken', { tokenName }));
return RocketChat.API.v1.success({ token });
},
});
RocketChat.API.v1.addRoute('users.getPersonalAccessTokens', { authRequired: true }, {
get() {
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');
}
const loginTokens = RocketChat.models.Users.getLoginTokensByUserId(this.userId).fetch()[0];
const getPersonalAccessTokens = () => loginTokens.services.resume.loginTokens
.filter((loginToken) => loginToken.type && loginToken.type === 'personalAccessToken')
.map((loginToken) => ({
name: loginToken.name,
createdAt: loginToken.createdAt,
lastTokenPart: loginToken.lastTokenPart,
}));
return RocketChat.API.v1.success({
tokens: getPersonalAccessTokens(),
});
},
});
RocketChat.API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true }, {
post() {
const { tokenName } = this.bodyParams;
if (!tokenName) {
return RocketChat.API.v1.failure('The \'tokenName\' param is required');
}
Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:removeToken', {
tokenName,
}));
return RocketChat.API.v1.success();
},
});
<template name="appInstall">
<section class="preferences-page">
{{> header sectionName="App_Installation" hideHelp=true fixedHeight=true}}
<section class="preferences-page preferences-page--new">
{{> header sectionName="App_Installation" hideHelp=true fullpage=true}}
<div class="preferences-page__content">
{{#if isInstalling}}
{{> loading}}
......
......@@ -2,7 +2,7 @@
<template name="appManage">
{{#with app}}
{{# header sectionName='Manage_the_App' fixedHeight=true hideHelp=true}}
{{# header sectionName='Manage_the_App' fixedHeight=true hideHelp=true fullpage=true}}
<div class="rc-header__block rc-header__block-action">
<div class="rc-switch rc-switch--blue">
<label class="rc-switch__label">
......@@ -18,7 +18,7 @@
</div>
{{/header}}
<section class="page-settings flex-tab-main-content">
<section class="page-settings page-settings--new flex-tab-main-content">
{{#requiresPermission 'manage-apps'}}
{{#if isReady}}
<div class="rc-apps-details">
......
<template name="apps">
<section class="rc-directory rc-apps-marketplace">
{{#header sectionName="Apps" hideHelp=true fixedHeight=true}}
{{#header sectionName="Apps" hideHelp=true fixedHeight=true fullpage=true}}
<button class="rc-button rc-button--small rc-button--primary rc-directory-plus" data-button="install">{{> icon icon="plus"}}</button>
{{/header}}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment