From 62413b378016d0b12b09f83dba36fa5fe8a910c2 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 19 Nov 2015 19:50:33 +0900 Subject: [PATCH] allow user to create an access token to access stf api from user settings tab. --- lib/db/api.js | 18 ++++++++ lib/db/tables.js | 5 +++ lib/units/websocket/index.js | 35 +++++++++++++++ lib/util/jwtutil.js | 4 +- .../generate-access-token-directive.js | 26 +++++++++++ .../generate-access-token-spec.js | 0 .../generate-access-token.css | 3 ++ .../generate-access-token.jade | 20 +++++++++ .../stf/tokens/generate-access-token/index.js | 6 +++ res/app/components/stf/tokens/index.js | 3 ++ res/app/components/stf/user/user-service.js | 36 +++++++++++++++ .../access-tokens/access-tokens-controller.js | 30 ++++++++++++- .../keys/access-tokens/access-tokens.css | 12 ++++- .../keys/access-tokens/access-tokens.jade | 27 +++++++---- res/app/settings/keys/access-tokens/index.js | 5 ++- res/app/settings/keys/adb-keys/adb-keys.css | 45 ------------------- res/app/settings/keys/adb-keys/adb-keys.jade | 2 +- res/app/settings/keys/adb-keys/index.js | 2 +- res/app/settings/keys/index.js | 2 +- res/app/settings/keys/keys.css | 45 ++++++++++++++++++- res/app/settings/keys/keys.jade | 2 +- 21 files changed, 263 insertions(+), 65 deletions(-) create mode 100644 res/app/components/stf/tokens/generate-access-token/generate-access-token-directive.js create mode 100644 res/app/components/stf/tokens/generate-access-token/generate-access-token-spec.js create mode 100644 res/app/components/stf/tokens/generate-access-token/generate-access-token.css create mode 100644 res/app/components/stf/tokens/generate-access-token/generate-access-token.jade create mode 100644 res/app/components/stf/tokens/generate-access-token/index.js create mode 100644 res/app/components/stf/tokens/index.js diff --git a/lib/db/api.js b/lib/db/api.js index 9079fec8..020a0642 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -308,4 +308,22 @@ dbapi.loadDevice = function(serial) { return db.run(r.table('devices').get(serial)) } +dbapi.saveUserAccessToken = function(email, token) { + return db.run(r.table('users').get(email).update({ + accessTokens: r.row('accessTokens').default([]).append({ + title: token.title + , tokenId: token.tokenId + , jwt: token.jwt + }) + })) +} + +dbapi.removeUserAccessToken = function(email, title) { + return db.run(r.table('users').get(email).update({ + accessTokens: r.row('accessTokens').default([]).filter(function(token) { + return token('title').ne(title) + }) + })) +} + module.exports = dbapi diff --git a/lib/db/tables.js b/lib/db/tables.js index 61679896..cbdf5683 100644 --- a/lib/db/tables.js +++ b/lib/db/tables.js @@ -8,6 +8,11 @@ module.exports = { indexFunction: function(user) { return user('adbKeys')('fingerprint') } + , accessTokens: { + indexFunction: function(user) { + return user('accessTokens')('tokenId') + } + } , options: { multi: true } diff --git a/lib/units/websocket/index.js b/lib/units/websocket/index.js index b736bb7f..e62a99e3 100644 --- a/lib/units/websocket/index.js +++ b/lib/units/websocket/index.js @@ -7,6 +7,7 @@ var Promise = require('bluebird') var _ = require('lodash') var request = Promise.promisifyAll(require('request')) var adbkit = require('adbkit') +var uuid = require('node-uuid') var logger = require('../../util/logger') var wire = require('../../wire') @@ -20,6 +21,7 @@ var zmqutil = require('../../util/zmqutil') var cookieSession = require('./middleware/cookie-session') var ip = require('./middleware/remote-ip') var auth = require('./middleware/auth') +var jwtutil = require('../../util/jwtutil') module.exports = function(options) { var log = logger.createLogger('websocket') @@ -334,6 +336,39 @@ module.exports = function(options) { .on('user.settings.reset', function() { dbapi.resetUserSettings(user.email) }) + .on('user.keys.accessToken.generate', function(data) { + var expiry = Date.now() + 100 * 365 * 24 * 3600 + , jwt = jwtutil.encode({ + payload: { + email: user.email + , name: user.name + } + , secret: options.secret + , expiry: expiry + }) + + var tokenId = uuid.v4() + , title = data.title + + return dbapi.saveUserAccessToken(user.email, { + title: title + , tokenId: tokenId + , jwt: jwt + }) + .then(function() { + socket.emit('user.keys.accessToken.generated', { + title: title + , tokenId: tokenId + , jwt: jwt + }) + }) + }) + .on('user.keys.accessToken.remove', function(data) { + return dbapi.removeUserAccessToken(user.email, data.title) + .then(function() { + socket.emit('user.keys.accessToken.removed', data.title) + }) + }) .on('user.keys.adb.add', function(data) { return adbkit.util.parsePublicKey(data.key) .then(function(key) { diff --git a/lib/util/jwtutil.js b/lib/util/jwtutil.js index 1b7c50d9..b5079bd1 100644 --- a/lib/util/jwtutil.js +++ b/lib/util/jwtutil.js @@ -5,10 +5,12 @@ module.exports.encode = function(options) { assert.ok(options.payload, 'payload required') assert.ok(options.secret, 'secret required') + var expiry = options.expiry || Date.now() + 24 * 3600 + return jws.sign({ header: { alg: 'HS256' - , exp: Date.now() + 24 * 3600 + , exp: expiry } , payload: options.payload , secret: options.secret diff --git a/res/app/components/stf/tokens/generate-access-token/generate-access-token-directive.js b/res/app/components/stf/tokens/generate-access-token/generate-access-token-directive.js new file mode 100644 index 00000000..d674f9bb --- /dev/null +++ b/res/app/components/stf/tokens/generate-access-token/generate-access-token-directive.js @@ -0,0 +1,26 @@ +module.exports = function generateAccessTokenDirective() { + return { + restrict: 'EA', + replace: true, + scope: { + showGenerate: '=', + showClipboard: '=', + }, + template: require('./generate-access-token.jade'), + controller: function($scope, UserService) { + $scope.generateForm = { + title: '' + } + + $scope.generateToken = function () { + UserService.generateAccessToken($scope.generateForm.title) + $scope.closeGenerateToken() + } + + $scope.closeGenerateToken = function () { + $scope.title = '' + $scope.showGenerate = false + } + } + } +} diff --git a/res/app/components/stf/tokens/generate-access-token/generate-access-token-spec.js b/res/app/components/stf/tokens/generate-access-token/generate-access-token-spec.js new file mode 100644 index 00000000..e69de29b diff --git a/res/app/components/stf/tokens/generate-access-token/generate-access-token.css b/res/app/components/stf/tokens/generate-access-token/generate-access-token.css new file mode 100644 index 00000000..60eed1f1 --- /dev/null +++ b/res/app/components/stf/tokens/generate-access-token/generate-access-token.css @@ -0,0 +1,3 @@ +.stf-generate-access-token { + +} diff --git a/res/app/components/stf/tokens/generate-access-token/generate-access-token.jade b/res/app/components/stf/tokens/generate-access-token/generate-access-token.jade new file mode 100644 index 00000000..541b77e2 --- /dev/null +++ b/res/app/components/stf/tokens/generate-access-token/generate-access-token.jade @@ -0,0 +1,20 @@ +.panel.panel-default.stf-generate-access-token(ng-show='showGenerate') + .panel-heading + h3.panel-title(translate) Generate Access Token + .panel-body + + form.form-horizontal(name='generateAccessTokenForm', ng-submit='generateToken(title)') + + .form-group + label.control-label + i.fa.fa-key.fa-fw + span(translate) Title + + input(id='access-token-title', type='text', name='accessTokenTitle', ng-model='generateForm.title', ng-required='true', + text-focus-select).form-control + + button.btn.btn-primary-outline.btn-sm.pull-right(type='submit') + i.fa.fa-plus.fa-fw + span(translate) Generate New Token + + error-message(message='{{error}}') diff --git a/res/app/components/stf/tokens/generate-access-token/index.js b/res/app/components/stf/tokens/generate-access-token/index.js new file mode 100644 index 00000000..6c055e0a --- /dev/null +++ b/res/app/components/stf/tokens/generate-access-token/index.js @@ -0,0 +1,6 @@ +require('./generate-access-token.css') + +module.exports = angular.module('stf.tokens.generate-access-token', [ + +]) + .directive('generateAccessToken', require('./generate-access-token-directive')) diff --git a/res/app/components/stf/tokens/index.js b/res/app/components/stf/tokens/index.js new file mode 100644 index 00000000..a4ad4f75 --- /dev/null +++ b/res/app/components/stf/tokens/index.js @@ -0,0 +1,3 @@ +module.exports = angular.module('stf.tokens', [ + require('./generate-access-token').name, +]) diff --git a/res/app/components/stf/user/user-service.js b/res/app/components/stf/user/user-service.js index e1de7c07..766cafc7 100644 --- a/res/app/components/stf/user/user-service.js +++ b/res/app/components/stf/user/user-service.js @@ -8,6 +8,22 @@ module.exports = function UserServiceFactory( var user = UserService.currentUser = AppState.user + UserService.getAccessTokens = function() { + return (user.accessTokens || (user.accessTokens = [])) + } + + UserService.generateAccessToken = function(title) { + socket.emit('user.keys.accessToken.generate', { + title: title + }) + } + + UserService.removeAccessToken = function(title) { + socket.emit('user.keys.accessToken.remove', { + title: title + }) + } + UserService.getAdbKeys = function() { return (user.adbKeys || (user.adbKeys = [])) } @@ -24,6 +40,26 @@ module.exports = function UserServiceFactory( socket.emit('user.keys.adb.remove', key) } + // socket.on('user.keys.accessToken.generated', function(token) { + // UserService.getAccessTokens().push(token) + // $rootScope.$broadcast('user.keys.accessTokens.updated', user.accessTokens) + // $rootScope.$apply() + // }) + + socket.on('user.keys.accessToken.generated', function(token) { + $rootScope.$broadcast('user.keys.accessTokens.generated', token) + $rootScope.$apply() + }) + + socket.on('user.keys.accessToken.removed', function(title) { + user.accessTokens = UserService.getAccessTokens().filter(function(token) { + return token.title !== title + }) + $rootScope.$broadcast('user.keys.accessTokens.updated', user.accessTokens) + $rootScope.$apply() + }) + + socket.on('user.keys.adb.added', function(key) { UserService.getAdbKeys().push(key) $rootScope.$broadcast('user.keys.adb.updated', user.adbKeys) diff --git a/res/app/settings/keys/access-tokens/access-tokens-controller.js b/res/app/settings/keys/access-tokens/access-tokens-controller.js index b10320b4..f34b88a6 100644 --- a/res/app/settings/keys/access-tokens/access-tokens-controller.js +++ b/res/app/settings/keys/access-tokens/access-tokens-controller.js @@ -1,3 +1,31 @@ -module.exports = function AccessTokensCtrl() { +module.exports = function AccessTokensCtrl($scope, $http, UserService) { + $scope.accessTokens = [] + $scope.newToken = null + + function updateTokens() { + $scope.accessTokens = UserService.getAccessTokens() + } + + $scope.removeToken = function (title) { + UserService.removeAccessToken(title) + } + + $scope.tokenGenerated = function() { + $scope.accessToken = '' + $scope.showGenerated = false + UserService.getAccessTokens().push($scope.newToken) + $scope.newToken = null + updateTokens() + } + + $scope.$on('user.keys.accessTokens.generated', function(event, token) { + $scope.showGenerated = true + $scope.accessTokenId = token.tokenId + $scope.newToken = token + }) + + $scope.$on('user.keys.accessTokens.updated', updateTokens) + + updateTokens() } diff --git a/res/app/settings/keys/access-tokens/access-tokens.css b/res/app/settings/keys/access-tokens/access-tokens.css index faeea397..da579719 100644 --- a/res/app/settings/keys/access-tokens/access-tokens.css +++ b/res/app/settings/keys/access-tokens/access-tokens.css @@ -1,3 +1,11 @@ -.stf-access-tokens { +.stf-access-tokens .access-token-generated-okay { + display: inline-block; +} -} \ No newline at end of file +.stf-access-tokens .token-id-textarea { + resize: none; + cursor: text; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + width: 85%; +} diff --git a/res/app/settings/keys/access-tokens/access-tokens.jade b/res/app/settings/keys/access-tokens/access-tokens.jade index 098177e4..e9d0d218 100644 --- a/res/app/settings/keys/access-tokens/access-tokens.jade +++ b/res/app/settings/keys/access-tokens/access-tokens.jade @@ -1,11 +1,11 @@ -.widget-container.fluid-height.stf-access-tokens(ng-controller='AccessTokensCtrl') +.widget-container.fluid-height.stf-keys.stf-access-tokens(ng-controller='AccessTokensCtrl') .heading i.fa.fa-key span(translate) Access Tokens button.btn.pull-right.btn-sm( - ng-click='showAdd = !showAdd', - ng-class='{ "btn-primary-outline": !showAdd, "btn-primary": showAdd }') + ng-click='showGenerate = !showGenerate', + ng-class='{ "btn-primary-outline": !showGenerate, "btn-primary": showGenerate }') i.fa.fa-plus.fa-fw a(ng-href='/#!/docs/Access-Tokens').pull-right.btn.btn-sm @@ -14,16 +14,27 @@ .widget-content.padded nothing-to-show(icon='fa-key', message='{{"No access tokens" | translate}}', - ng-if='!adbKeys.length && !showAdd') + ng-if='!accessTokens.length && !showGenerate && !showGenerated') + + generate-access-token(show-clipboard='true', show-generate='showGenerate') + + div(ng-show='showGenerated') + .alert.alert-info.selectable + strong(translate) Warning: + span   + span(translate) Make sure to copy your access token now. You won't be able to see it again! + br + button.btn.pull-right.btn-primary.btn-sm(ng-click='tokenGenerated()') + i.fa.fa-check.fa-fw + textarea(readonly, rows='1', text-focus-select, ng-model='accessTokenId').form-control.token-id-textarea ul.list-group.key-list - li.list-group-item(ng-repeat='key in adbKeys').animate-repeat + li.list-group-item(ng-repeat='token in accessTokens').animate-repeat a i.fa.fa-key.fa-2x.fa-fw.key-list-icon .key-list-details.selectable - .key-list-title(ng-bind='key.title') - .key-list-fingerprint(ng-bind='key.fingerprint') + .key-list-title(ng-bind='token.title') - button.btn.btn-xs.btn-danger-outline.pull-right.key-list-remove(ng-click='removeKey(key)') + button.btn.btn-xs.btn-danger-outline.pull-right.key-list-remove(ng-click='removeToken(token.title)') i.fa.fa-trash-o span(translate) Remove diff --git a/res/app/settings/keys/access-tokens/index.js b/res/app/settings/keys/access-tokens/index.js index d6ebb992..e92ade13 100644 --- a/res/app/settings/keys/access-tokens/index.js +++ b/res/app/settings/keys/access-tokens/index.js @@ -1,7 +1,8 @@ require('./access-tokens.css') -module.exports = angular.module('stf.access-tokens', [ - require('stf/common-ui').name +module.exports = angular.module('stf.settings.keys.access-tokens', [ + require('stf/common-ui').name, + require('stf/tokens/generate-access-token').name ]) .run(["$templateCache", function ($templateCache) { $templateCache.put( diff --git a/res/app/settings/keys/adb-keys/adb-keys.css b/res/app/settings/keys/adb-keys/adb-keys.css index 0b4e456d..196e5689 100644 --- a/res/app/settings/keys/adb-keys/adb-keys.css +++ b/res/app/settings/keys/adb-keys/adb-keys.css @@ -1,23 +1,3 @@ -.stf-adb-keys .key-list-icon { - -} - -.stf-adb-keys .key-list a { - padding: 10px; - border-bottom: 1px solid #dddddd; -} - -.stf-adb-keys .key-list-details { - display: inline-block; -} - -.stf-adb-keys .key-list-title { - color: #007aff; - font-size: 14px; - font-weight: 300; - margin: 2px 0 6px; -} - .stf-adb-keys .key-list-fingerprint { font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 10px; @@ -25,28 +5,3 @@ color: #999999; font-weight: 300; } - -.stf-adb-keys .key-list-remove { - display: inline-block; -} - -.animate-repeat.ng-move, -.animate-repeat.ng-enter, -.animate-repeat.ng-leave { - -webkit-transition: all ease-out 150ms; - transition: all ease-out 150ms; -} - -.animate-repeat.ng-leave.ng-leave-active, -.animate-repeat.ng-move, -.animate-repeat.ng-enter { - opacity: 0; - max-height: 0; -} - -.animate-repeat.ng-leave, -.animate-repeat.ng-move.ng-move-active, -.animate-repeat.ng-enter.ng-enter-active { - opacity: 1; - max-height: 100px; -} diff --git a/res/app/settings/keys/adb-keys/adb-keys.jade b/res/app/settings/keys/adb-keys/adb-keys.jade index c335cd76..f0132735 100644 --- a/res/app/settings/keys/adb-keys/adb-keys.jade +++ b/res/app/settings/keys/adb-keys/adb-keys.jade @@ -1,4 +1,4 @@ -.widget-container.fluid-height.stf-adb-keys(ng-controller='AdbKeysCtrl') +.widget-container.fluid-height.stf-keys.stf-adb-keys(ng-controller='AdbKeysCtrl') .heading i.fa.fa-android span(translate) ADB Keys diff --git a/res/app/settings/keys/adb-keys/index.js b/res/app/settings/keys/adb-keys/index.js index 0ee60357..2688d1fe 100644 --- a/res/app/settings/keys/adb-keys/index.js +++ b/res/app/settings/keys/adb-keys/index.js @@ -1,6 +1,6 @@ require('./adb-keys.css') -module.exports = angular.module('stf.settings.adb-keys', [ +module.exports = angular.module('stf.settings.keys.adb-keys', [ require('stf/common-ui').name, require('stf/keys/add-adb-key').name ]) diff --git a/res/app/settings/keys/index.js b/res/app/settings/keys/index.js index 6acdea3a..f9e263dd 100644 --- a/res/app/settings/keys/index.js +++ b/res/app/settings/keys/index.js @@ -1,6 +1,6 @@ require('./keys.css') -module.exports = angular.module('stf.keys', [ +module.exports = angular.module('stf.settings.keys', [ require('./adb-keys').name, require('./access-tokens').name ]) diff --git a/res/app/settings/keys/keys.css b/res/app/settings/keys/keys.css index 186571c6..f643e607 100644 --- a/res/app/settings/keys/keys.css +++ b/res/app/settings/keys/keys.css @@ -1,3 +1,44 @@ -.stf-keys { +.stf-keys .key-list-icon { -} \ No newline at end of file +} + +.stf-keys .key-list a { + padding: 10px; + border-bottom: 1px solid #dddddd; +} + +.stf-keys .key-list-details { + display: inline-block; +} + +.stf-keys .key-list-title { + color: #007aff; + font-size: 14px; + font-weight: 300; + margin: 2px 0 6px; +} + +.stf-keys .key-list-remove { + display: inline-block; +} + +.animate-repeat.ng-move, +.animate-repeat.ng-enter, +.animate-repeat.ng-leave { + -webkit-transition: all ease-out 150ms; + transition: all ease-out 150ms; +} + +.animate-repeat.ng-leave.ng-leave-active, +.animate-repeat.ng-move, +.animate-repeat.ng-enter { + opacity: 0; + max-height: 0; +} + +.animate-repeat.ng-leave, +.animate-repeat.ng-move.ng-move-active, +.animate-repeat.ng-enter.ng-enter-active { + opacity: 1; + max-height: 100px; +} diff --git a/res/app/settings/keys/keys.jade b/res/app/settings/keys/keys.jade index 85022616..a5e4ac85 100644 --- a/res/app/settings/keys/keys.jade +++ b/res/app/settings/keys/keys.jade @@ -1,5 +1,5 @@ .row - //.col-md-6 + .col-md-6 div(ng-include='"settings/keys/access-tokens/access-tokens.jade"') .col-md-6 div(ng-include='"settings/keys/adb-keys/adb-keys.jade"')