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