[NEW] Personal access tokens for users to create API tokens (#11638)
parent
3487b2f8e9
commit
ed1d550d86
@ -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 @@ |
||||
import './Users'; |
@ -0,0 +1 @@ |
||||
import './personalAccessTokens'; |
@ -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'; |
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue