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

pull/10269/head^2
Marcos Spessatto Defendi 7 years ago committed by Diego Sampaio
parent 3487b2f8e9
commit ed1d550d86
  1. 1
      imports/personal-access-tokens/client/index.js
  2. 68
      imports/personal-access-tokens/client/personalAccessTokens.html
  3. 107
      imports/personal-access-tokens/client/personalAccessTokens.js
  4. 35
      imports/personal-access-tokens/server/api/methods/generateToken.js
  5. 3
      imports/personal-access-tokens/server/api/methods/index.js
  6. 23
      imports/personal-access-tokens/server/api/methods/regenerateToken.js
  7. 26
      imports/personal-access-tokens/server/api/methods/removeToken.js
  8. 6
      imports/personal-access-tokens/server/index.js
  9. 39
      imports/personal-access-tokens/server/models/Users.js
  10. 1
      imports/personal-access-tokens/server/models/index.js
  11. 1
      imports/personal-access-tokens/server/publications/index.js
  12. 35
      imports/personal-access-tokens/server/publications/personalAccessTokens.js
  13. 5
      imports/personal-access-tokens/server/settings.js
  14. 1
      imports/startup/client/index.js
  15. 1
      imports/startup/server/index.js
  16. 12
      packages/rocketchat-2fa/client/accountSecurity.html
  17. 58
      packages/rocketchat-api/server/v1/users.js
  18. 4
      packages/rocketchat-apps/client/admin/appInstall.html
  19. 4
      packages/rocketchat-apps/client/admin/appManage.html
  20. 2
      packages/rocketchat-apps/client/admin/apps.html
  21. 18
      packages/rocketchat-i18n/i18n/en.i18n.json
  22. 1338
      packages/rocketchat-livechat/.app/package-lock.json
  23. 19
      packages/rocketchat-theme/client/imports/components/header.css
  24. 15
      packages/rocketchat-theme/client/imports/components/modal/directory.css
  25. 1
      packages/rocketchat-theme/client/imports/forms/button.css
  26. 16
      packages/rocketchat-theme/client/imports/general/forms.css
  27. 4
      packages/rocketchat-ui-account/client/accountFlex.html
  28. 3
      packages/rocketchat-ui-account/client/accountFlex.js
  29. 4
      packages/rocketchat-ui-account/client/accountPreferences.html
  30. 4
      packages/rocketchat-ui-account/client/accountProfile.html
  31. 2
      packages/rocketchat-ui/client/components/header/header.html
  32. 4
      packages/rocketchat-ui/client/components/header/header.js
  33. 2
      packages/rocketchat-ui/client/views/app/directory.html
  34. 197
      tests/end-to-end/api/01-users.js

@ -0,0 +1 @@
import './personalAccessTokens';

@ -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>

@ -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();
});
});

@ -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;
},
});

@ -0,0 +1,3 @@
import './generateToken';
import './regenerateToken';
import './removeToken';

@ -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 });
},
});

@ -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,
},
});
},
});

@ -0,0 +1,6 @@
import './api/methods';
import './settings';
import './models';
import './publications';

@ -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);
};

@ -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();
});
});

@ -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 +1,2 @@
import '../../message-read-receipt/client';
import '../../personal-access-tokens/client';

@ -1 +1,2 @@
import '../../message-read-receipt/server';
import '../../personal-access-tokens/server';

@ -1,6 +1,6 @@
<template name="accountSecurity">
<section class="preferences-page">
{{> header sectionName="Security"}}
<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}}
@ -9,12 +9,12 @@
<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>
<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>
<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>
@ -23,7 +23,7 @@
<form class="inline">
<input type="text" class="rc-input__element" id="testCode" placeholder="{{_ "Enter_authentication_code"}}">
<button class="button primary verify-code">{{_ "Verify"}}</button>
<button class="rc-button rc-button--primary verify-code">{{_ "Verify"}}</button>
</form>
{{/unless}}
{{/if}}
@ -38,7 +38,7 @@
<h1>{{_ "Backup_codes"}}</h1>
<div class="section-content border-component-color">
<p>{{codesRemaining}}</p>
<button class="button regenerate-codes">{{_ "Regenerate_codes"}}</button>
<button class="rc-button rc-button--secondary regenerate-codes">{{_ "Regenerate_codes"}}</button>
</div>
</div>
</fieldset>

@ -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();
},
});

@ -1,6 +1,6 @@
<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">

@ -1,6 +1,6 @@
<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}}

@ -250,6 +250,7 @@
"Animals_and_Nature": "Animals & Nature",
"Announcement": "Announcement",
"API": "API",
"API_Add_Personal_Access_Token": "Add new Personal Access Token",
"API_Allow_Infinite_Count": "Allow Getting Everything",
"API_Allow_Infinite_Count_Description": "Should calls to the REST API be allowed to return everything in one call?",
"API_Analytics": "Analytics",
@ -271,11 +272,20 @@
"API_Enable_CORS": "Enable CORS",
"API_Enable_Direct_Message_History_EndPoint": "Enable Direct Message History Endpoint",
"API_Enable_Direct_Message_History_EndPoint_Description": "This enables the `/api/v1/im.history.others` which allows the viewing of direct messages sent by other users that the caller is not part of.",
"API_Enable_Personal_Access_Tokens": "Enable Personal Access Tokens to REST API",
"API_Enable_Personal_Access_Tokens_Description": "Enable personal access tokens for use with the REST API",
"API_Enable_Shields": "Enable Shields",
"API_Enable_Shields_Description": "Enable shields available at `/api/v1/shield.svg`",
"API_GitHub_Enterprise_URL": "Server URL",
"API_GitHub_Enterprise_URL_Description": "Example: http://domain.com (excluding trailing slash)",
"API_Gitlab_URL": "GitLab URL",
"API_Personal_Access_Token_Name": "Personal Access Token Name",
"API_Personal_Access_Tokens_To_REST_API": "Personal access tokens to REST API",
"API_Personal_Access_Tokens_Remove_Modal": "Are you sure you wish to remove this personal access token?",
"API_Personal_Access_Token_Generated": "Personal Access Token successfully generated",
"API_Personal_Access_Token_Generated_Text_Token_s_UserId_s": "Please save your token carefully as you will no longer be able to view it afterwards. <br/>Token: <strong>__token__</strong><br/>Your user Id: <strong>__userId__</strong>",
"API_Personal_Access_Tokens_Regenerate_Modal": "If you lost or forgot your token, you can regenerate it, but remember that all applications that use this token should be updated",
"API_Personal_Access_Tokens_Regenerate_It": "Regenerate token",
"API_Shield_Types": "Shield Types",
"API_Shield_Types_Description": "Types of shields to enable as a comma separated list, choose from `online`, `channel` or `*` for all",
"API_Token": "API Token",
@ -992,6 +1002,7 @@
"Enabled": "Enabled",
"Encrypted_message": "Encrypted message",
"End_OTR": "End OTR",
"Enter_a_name": "Enter a name",
"Enter_a_regex": "Enter a regex",
"Enter_a_room_name": "Enter a room name",
"Enter_a_username": "Enter a username",
@ -1084,6 +1095,9 @@
"error-room-is-not-closed": "Room is not closed",
"error-the-field-is-required": "The field __field__ is required.",
"error-this-is-not-a-livechat-room": "This is not a Livechat room",
"error-personal-access-tokens-are-current-disabled": "Personal Access Tokens are currently disabled",
"error-token-already-exists": "A token with this name already exists",
"error-token-does-not-exists": "Token does not exists",
"error-too-many-requests": "Error, too many requests. Please slow down. You must wait __seconds__ seconds before trying again.",
"error-user-has-no-roles": "User has no roles",
"error-user-is-not-activated": "User is not activated",
@ -1492,6 +1506,7 @@
"Last_login": "Last login",
"Last_Message_At": "Last Message At",
"Last_seen": "Last seen",
"Last_token_part": "Last token part",
"Last_Message": "Last Message",
"Launched_successfully": "Launched successfully",
"Layout": "Layout",
@ -1958,6 +1973,7 @@
"People": "People",
"Permalink": "Permalink",
"Permissions": "Permissions",
"Personal_Access_Tokens": "Personal Access Tokens",
"pin-message": "Pin Message",
"pin-message_description": "Permission to pin a message in a channel",
"Pin_Message": "Pin Message",
@ -1987,6 +2003,7 @@
"Please_fill_a_username": "Please fill a username",
"Please_fill_all_the_information": "Please fill all the information",
"Please_fill_name_and_email": "Please fill name and email",
"Please_fill_a_token_name": "Please fill a valid token name",
"Please_go_to_the_Administration_page_then_Livechat_Facebook": "Please go to the Administration page then Livechat > Facebook",
"Please_select_an_user": "Please select an user",
"Please_select_enabled_yes_or_no": "Please select an option for Enabled",
@ -2500,6 +2517,7 @@
"There_are_no_applications": "No oAuth Applications have been added yet.",
"There_are_no_applications_installed": "There are currently no Rocket.Chat Applications installed.",
"There_are_no_integrations": "There are no integrations",
"There_are_no_personal_access_tokens_created_yet": "There are no Personal Access Tokens created yet.",
"There_are_no_users_in_this_role": "There are no users in this role.",
"This_conversation_is_already_closed": "This conversation is already closed.",
"This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "This email has already been used and has not been verified. Please change your password.",

File diff suppressed because it is too large Load Diff

@ -1,3 +1,22 @@
.rc-header.rc-header--fullpage {
padding: 1.25rem 0.5rem 0.75rem;
& .rc-header__wrap {
padding: 0.75rem 0;
box-shadow: none;
}
& .rc-header__block {
font-size: 1.375rem;
}
& .rc-button {
margin: -10px 0;
}
}
.rc-header {
padding: 0 0.5rem;

@ -14,21 +14,6 @@
}
}
& .rc-header {
padding: 1.25rem 0.5rem 0.75rem;
&__wrap {
padding: 0.75rem 0;
box-shadow: none;
}
&__block {
font-size: 1.375rem;
}
}
&-topic {
color: var(--rc-color-primary-light);
}

@ -111,6 +111,7 @@
border-style: solid;
background: transparent;
}
&--stack {
width: 100%;
}

@ -252,17 +252,31 @@
height: 100%;
&--new {
padding: 0 1.5rem;
}
&__content {
overflow-y: auto;
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
padding: 25px;
padding: 25px 0;
}
&--apps .preferences-page__header {
justify-content: space-between;
}
& .container {
flex: 1;
width: 100%;
}
}
@media (width > 500px) {

@ -17,6 +17,10 @@
{{/if}}
{{> sidebarItem menuItem "Security" "lock" "account" "security" }}
{{#if accessTokensEnabled}}
{{> sidebarItem menuItem "Personal_Access_Tokens" "key" "account" "tokens" }}
{{/if}}
</ul>
</div>
</aside>

@ -12,6 +12,9 @@ Template.accountFlex.helpers({
allowUserProfileChange() {
return RocketChat.settings.get('Accounts_AllowUserProfileChange');
},
accessTokensEnabled() {
return RocketChat.settings.get('API_Enable_Personal_Access_Tokens');
},
menuItem(name, icon, section, group) {
return {
name: t(name),

@ -1,6 +1,6 @@
<template name="accountPreferences">
<section class="page-container page-home page-static">
{{#header sectionName="Preferences" buttons=true}}
<section class="preferences-page preferences-page--new page-container page-home page-static">
{{#header sectionName="Preferences" buttons=true fullpage=true}}
<div class="rc-header__section-button">
<button class="rc-button rc-button--primary save"><span>{{_ "Save_changes"}}</span></button>
</div>

@ -11,8 +11,8 @@
</div>
</template>
<template name="accountProfile">
<section class="preferences-page">
{{#header sectionName="Profile" buttons=true}}
<section class="preferences-page preferences-page--new">
{{#header sectionName="Profile" buttons=true fullpage=true}}
<div class="rc-header__section-button">
<button class="rc-button rc-button--primary" name="send" type="submit" data-button="create" form="profile" {{canSave 'disabled'}}>{{_ "Save_changes"}}</button>
</div>

@ -1,5 +1,5 @@
<template name="header">
<header class="rc-header{{#if fixedHeight}} rc-header--height-fixed{{/if}}" >
<header class="rc-header{{#if fixedHeight}} rc-header--height-fixed{{/if}} {{#if fullpage}} rc-header--fullpage{{/if}}" >
<div class="rc-header__wrap">
<div class="rc-header__block rc-header--burger">

@ -94,6 +94,10 @@ Template.header.helpers({
return Template.instance().data.fixedHeight;
},
fullpage() {
return Template.instance().data.fullpage;
},
isChannel() {
return Template.instance().currentChannel != null;
},

@ -1,6 +1,6 @@
<template name="directory">
<section class="rc-directory">
{{> header sectionName="Directory" hideHelp=true}}
{{> header sectionName="Directory" hideHelp=true fullpage=true}}
<div class="rc-table-content">
{{>tabs tabs=tabsData}}
<div class="rc-input rc-input--small rc-directory-search">

@ -918,6 +918,203 @@ describe('[Users]', function() {
.end(done);
});
});
});
describe('Personal Access Tokens', () => {
const tokenName = `${ Date.now() }token`;
describe('successful cases', () => {
it('Enable "API_Enable_Personal_Access_Tokens" setting...', (done) => {
request.post('/api/v1/settings/API_Enable_Personal_Access_Tokens')
.set(credentials)
.send({ value: true })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
describe('[/users.generatePersonalAccessToken]', () => {
it('should return a personal access token to user', (done) => {
request.post(api('users.generatePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('token');
})
.end(done);
});
it('should throw an error when user tries generate a token with the same name', (done) => {
request.post(api('users.generatePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
});
describe('[/users.regeneratePersonalAccessToken]', () => {
it('should return a personal access token to user when user regenerates the token', (done) => {
request.post(api('users.regeneratePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('token');
})
.end(done);
});
it('should throw an error when user tries regenerate a token that does not exist', (done) => {
request.post(api('users.regeneratePersonalAccessToken'))
.set(credentials)
.send({
tokenName: 'tokenthatdoesnotexist',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
});
describe('[/users.getPersonalAccessTokens]', () => {
it('should return my personal access tokens', (done) => {
request.get(api('users.getPersonalAccessTokens'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('tokens').and.to.be.an('array');
})
.end(done);
});
});
describe('[/users.removePersonalAccessToken]', () => {
it('should return success when user remove a personal access token', (done) => {
request.post(api('users.removePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
it('should throw an error when user tries remove a token that does not exist', (done) => {
request.post(api('users.removePersonalAccessToken'))
.set(credentials)
.send({
tokenName: 'tokenthatdoesnotexist',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
});
});
describe('unsuccessful cases', () => {
it('disable "API_Enable_Personal_Access_Tokens" setting...', (done) => {
request.post('/api/v1/settings/API_Enable_Personal_Access_Tokens')
.set(credentials)
.send({ value: false })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
describe('should return an error when setting "API_Enable_Personal_Access_Tokens" is disabled for the following routes', () => {
it('/users.generatePersonalAccessToken', (done) => {
request.post(api('users.generatePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('error-personal-access-tokens-are-current-disabled');
})
.end(done);
});
it('/users.regeneratePersonalAccessToken', (done) => {
request.post(api('users.regeneratePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('error-personal-access-tokens-are-current-disabled');
})
.end(done);
});
it('/users.getPersonalAccessTokens', (done) => {
request.get(api('users.getPersonalAccessTokens'))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('error-personal-access-tokens-are-current-disabled');
})
.end(done);
});
it('/users.removePersonalAccessToken', (done) => {
request.post(api('users.removePersonalAccessToken'))
.set(credentials)
.send({
tokenName,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('error-personal-access-tokens-are-current-disabled');
})
.end(done);
});
it('should throw an error when user tries remove a token that does not exist', (done) => {
request.post(api('users.removePersonalAccessToken'))
.set(credentials)
.send({
tokenName: 'tokenthatdoesnotexist',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.errorType).to.be.equal('error-personal-access-tokens-are-current-disabled');
})
.end(done);
});
});
});
});
});

Loading…
Cancel
Save