From 001c8f96012a061b01acfbd01d898c7acbecc63d Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Thu, 23 Nov 2017 13:49:47 +0100 Subject: [PATCH 1/3] Inline the entire resin-cli-auth module This is part of a general push to demodularize any code that isn't realistically reusable outside resin-cli, to make the codebase easier to manage and understand. Once this is done, we'll deprecate the original module itself. Change-Type: patch --- gulpfile.coffee | 26 +++++- lib/actions/auth.coffee | 2 +- lib/auth/index.coffee | 63 +++++++++++++ lib/auth/pages/error.ejs | 21 +++++ lib/auth/pages/static/images/happy.png | Bin 0 -> 2386 bytes lib/auth/pages/static/images/sad.png | Bin 0 -> 2708 bytes lib/auth/pages/static/style.css | 60 ++++++++++++ lib/auth/pages/success.ejs | 21 +++++ lib/auth/server.coffee | 101 ++++++++++++++++++++ lib/auth/utils.coffee | 75 +++++++++++++++ package.json | 18 +++- tests/auth/server.spec.coffee | 124 +++++++++++++++++++++++++ tests/auth/tokens.json | 18 ++++ tests/auth/utils.spec.coffee | 111 ++++++++++++++++++++++ 14 files changed, 631 insertions(+), 9 deletions(-) create mode 100644 lib/auth/index.coffee create mode 100644 lib/auth/pages/error.ejs create mode 100644 lib/auth/pages/static/images/happy.png create mode 100644 lib/auth/pages/static/images/sad.png create mode 100644 lib/auth/pages/static/style.css create mode 100644 lib/auth/pages/success.ejs create mode 100644 lib/auth/server.coffee create mode 100644 lib/auth/utils.coffee create mode 100644 tests/auth/server.spec.coffee create mode 100644 tests/auth/tokens.json create mode 100644 tests/auth/utils.spec.coffee diff --git a/gulpfile.coffee b/gulpfile.coffee index 3aad5a5a..1ce0feb0 100644 --- a/gulpfile.coffee +++ b/gulpfile.coffee @@ -2,6 +2,8 @@ path = require('path') gulp = require('gulp') coffee = require('gulp-coffee') coffeelint = require('gulp-coffeelint') +inlinesource = require('gulp-inline-source') +mocha = require('gulp-mocha') shell = require('gulp-shell') packageJSON = require('./package.json') @@ -10,10 +12,17 @@ OPTIONS = coffeelint: path.join(__dirname, 'coffeelint.json') files: coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ] - app: [ 'lib/**/*.coffee', '!lib/**/*.spec.coffee' ] + app: 'lib/**/*.coffee' + tests: 'tests/**/*.spec.coffee' + pages: 'lib/auth/pages/*.ejs' directories: build: 'build/' +gulp.task 'pages', -> + gulp.src(OPTIONS.files.pages) + .pipe(inlinesource()) + .pipe(gulp.dest('build/auth/pages')) + gulp.task 'coffee', [ 'lint' ], -> gulp.src(OPTIONS.files.app) .pipe(coffee(bare: true, header: true)) @@ -26,5 +35,16 @@ gulp.task 'lint', -> })) .pipe(coffeelint.reporter()) -gulp.task 'watch', [ 'coffee' ], -> - gulp.watch([ OPTIONS.files.coffee ], [ 'coffee' ]) +gulp.task 'test', -> + gulp.src(OPTIONS.files.tests, read: false) + .pipe(mocha({ + reporter: 'min' + })) + +gulp.task 'build', [ + 'coffee', + 'pages' +] + +gulp.task 'watch', [ 'build' ], -> + gulp.watch([ OPTIONS.files.coffee ], [ 'build' ]) diff --git a/lib/actions/auth.coffee b/lib/actions/auth.coffee index 40b645ae..adcc5aaa 100644 --- a/lib/actions/auth.coffee +++ b/lib/actions/auth.coffee @@ -74,7 +74,7 @@ exports.login = _ = require('lodash') Promise = require('bluebird') resin = require('resin-sdk-preconfigured') - auth = require('resin-cli-auth') + auth = require('../auth') form = require('resin-cli-form') patterns = require('../utils/patterns') messages = require('../utils/messages') diff --git a/lib/auth/index.coffee b/lib/auth/index.coffee new file mode 100644 index 00000000..73889884 --- /dev/null +++ b/lib/auth/index.coffee @@ -0,0 +1,63 @@ +### +Copyright 2016 Resin.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +### + +###* +# @module auth +### + +open = require('open') +resin = require('resin-sdk-preconfigured') +server = require('./server') +utils = require('./utils') + +###* +# @summary Login to the Resin CLI using the web dashboard +# @function +# @public +# +# @description +# This function opens the user's default browser and points it +# to the Resin.io dashboard where the session token exchange will +# take place. +# +# Once the the token is retrieved, it's automatically persisted. +# +# @fulfil {String} - session token +# @returns {Promise} +# +# @example +# auth.login().then (sessionToken) -> +# console.log('I\'m logged in!') +# console.log("My session token is: #{sessionToken}") +### +exports.login = -> + options = + port: 8989 + path: '/auth' + + # Needs to be 127.0.0.1 not localhost, because the ip only is whitelisted + # from mixed content warnings (as the target of a form in the result page) + callbackUrl = "http://127.0.0.1:#{options.port}#{options.path}" + return utils.getDashboardLoginURL(callbackUrl).then (loginUrl) -> + + # Leave a bit of time for the + # server to get up and runing + setTimeout -> + open(loginUrl) + , 1000 + + return server.awaitForToken(options) + .tap(resin.auth.loginWithToken) diff --git a/lib/auth/pages/error.ejs b/lib/auth/pages/error.ejs new file mode 100644 index 00000000..64915338 --- /dev/null +++ b/lib/auth/pages/error.ejs @@ -0,0 +1,21 @@ + + + + + + Resin CLI - Error + + + + + +
+ +

Something went wrong

+

You couldn't login to the Resin CLI for some reason

+
+
+ Get help in our forums +
+ + diff --git a/lib/auth/pages/static/images/happy.png b/lib/auth/pages/static/images/happy.png new file mode 100644 index 0000000000000000000000000000000000000000..62143f77d53f5524759d8615346b38a8a0488b52 GIT binary patch literal 2386 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 Eg8Zn4i2wiq literal 0 HcmV?d00001 diff --git a/lib/auth/pages/static/images/sad.png b/lib/auth/pages/static/images/sad.png new file mode 100644 index 0000000000000000000000000000000000000000..9fdf949bfd6a8206f573d1d4d9b335db2cb5b010 GIT binary patch literal 2708 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 O0000S` literal 0 HcmV?d00001 diff --git a/lib/auth/pages/static/style.css b/lib/auth/pages/static/style.css new file mode 100644 index 00000000..9b50d08a --- /dev/null +++ b/lib/auth/pages/static/style.css @@ -0,0 +1,60 @@ +html, +body { + height: 100%; +} + +body { + text-align: center; + background-color: #fff; + color: rgb(24, 24, 24); + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; + position: relative; +} + +.center { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + width: 50%; + height: 50%; +} + +.icon { + display: block; + width: 40px; + height: 45px; + margin: 0 auto; + margin-bottom: 15px; +} + +h1 { + font-size: 3rem; + margin: 0; + margin-bottom: 12px; +} + +p { + color: rgb(99, 99, 99); + font-size: 1.1rem; + margin: 0; + margin-bottom: 15px; +} + +a.button { + padding: 15px 25px; + border-radius: 5px; + text-decoration: none; +} + +a.button.danger { + background-color: rgb(235, 110, 111); + color: #fff; +} + +a.button.normal { + background-color: rgb(252, 191, 44); + color: #fff; +} diff --git a/lib/auth/pages/success.ejs b/lib/auth/pages/success.ejs new file mode 100644 index 00000000..7b51c2b5 --- /dev/null +++ b/lib/auth/pages/success.ejs @@ -0,0 +1,21 @@ + + + + + + Resin CLI - Success + + + + + +
+ +

Success!

+

You successfully logged in the Resin CLI

+
+
+ Go to the dashboard +
+ + diff --git a/lib/auth/server.coffee b/lib/auth/server.coffee new file mode 100644 index 00000000..ec4fefd1 --- /dev/null +++ b/lib/auth/server.coffee @@ -0,0 +1,101 @@ +### +Copyright 2016 Resin.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +### + +express = require('express') +path = require('path') +bodyParser = require('body-parser') +Promise = require('bluebird') +resin = require('resin-sdk-preconfigured') +utils = require('./utils') + +createServer = ({ port, isDev } = {}) -> + app = express() + app.use bodyParser.urlencoded + extended: true + + app.set('view engine', 'ejs') + app.set('views', path.join(__dirname, 'pages')) + + if isDev + app.use(express.static(path.join(__dirname, 'pages', 'static'))) + + server = app.listen(port) + + return { app, server } + +###* +# @summary Await for token +# @function +# @protected +# +# @param {Object} options - options +# @param {String} options.path - callback path +# @param {Number} options.port - http port +# +# @example +# server.awaitForToken +# path: '/auth' +# port: 9001 +# .then (token) -> +# console.log(token) +### +exports.awaitForToken = (options) -> + { app, server } = createServer(port: options.port) + + return new Promise (resolve, reject) -> + closeServer = (errorMessage, successPayload) -> + server.close -> + if errorMessage + reject(new Error(errorMessage)) + return + + resolve(successPayload) + + renderAndDone = ({ request, response, viewName, errorMessage, statusCode, token }) -> + return getContext(viewName) + .then (context) -> + response.status(statusCode || 200).render(viewName, context) + request.connection.destroy() + closeServer(errorMessage, token) + + app.post options.path, (request, response) -> + token = request.body.token?.trim() + + Promise.try -> + if not token + throw new Error('No token') + return utils.isTokenValid(token) + .tap (isValid) -> + if not isValid + throw new Error('Invalid token') + .then -> + renderAndDone({ request, response, viewName: 'success', token }) + .catch (error) -> + renderAndDone({ + request, response, viewName: 'error', + statusCode: 401, errorMessage: error.message + }) + + app.use (request, response) -> + response.status(404).send('Not found') + closeServer('Unknown path or verb') + +exports.getContext = getContext = (viewName) -> + if viewName is 'success' + return Promise.props + dashboardUrl: resin.settings.get('dashboardUrl') + + return Promise.resolve({}) diff --git a/lib/auth/utils.coffee b/lib/auth/utils.coffee new file mode 100644 index 00000000..6dbb69c6 --- /dev/null +++ b/lib/auth/utils.coffee @@ -0,0 +1,75 @@ +### +Copyright 2016 Resin.io + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +### + +resin = require('resin-sdk-preconfigured') +_ = require('lodash') +url = require('url') +Promise = require('bluebird') + +###* +# @summary Get dashboard CLI login URL +# @function +# @protected +# +# @param {String} callbackUrl - callback url +# @fulfil {String} - dashboard login url +# @returns {Promise} +# +# @example +# utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) -> +# console.log(url) +### +exports.getDashboardLoginURL = (callbackUrl) -> + + # Encode percentages signs from the escaped url + # characters to avoid angular getting confused. + callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25') + + resin.settings.get('dashboardUrl').then (dashboardUrl) -> + return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}") + +###* +# @summary Check if a token is valid +# @function +# @protected +# +# @description +# This function checks that the token is not only well-structured +# but that it also authenticates with the server successfully. +# +# @param {String} sessionToken - token +# @fulfil {Boolean} - whether is valid or not +# @returns {Promise} +# +# utils.isTokenValid('...').then (isValid) -> +# if isValid +# console.log('Token is valid!') +### +exports.isTokenValid = (sessionToken) -> + if not sessionToken? or _.isEmpty(sessionToken.trim()) + return Promise.resolve(false) + + return resin.token.get().then (currentToken) -> + resin.auth.loginWithToken(sessionToken) + .return(sessionToken) + .then(resin.auth.isLoggedIn) + .tap (isLoggedIn) -> + return if isLoggedIn + + if currentToken? + return resin.auth.loginWithToken(currentToken) + else + return resin.auth.logout() diff --git a/package.json b/package.json index fd326f68..7de46de4 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "resin": "./bin/resin" }, "scripts": { - "build": "gulp coffee && tsc && npm run doc", - "ci": "npm run build && catch-uncommitted", + "build": "gulp build && tsc && npm run doc", + "pretest": "npm run build", + "test": "gulp test", + "ci": "npm run test && catch-uncommitted", "doc": "mkdir -p doc/ && coffee extras/capitanodoc/index.coffee > doc/cli.markdown", "watch": "gulp watch", "lint": "gulp lint", @@ -42,7 +44,10 @@ "gulp": "^3.9.0", "gulp-coffee": "^2.2.0", "gulp-coffeelint": "^0.6.0", + "gulp-inline-source": "^2.1.0", + "gulp-mocha": "^2.0.0", "gulp-shell": "^0.5.2", + "mochainon": "^2.0.0", "require-npm4-to-publish": "^1.0.0", "typescript": "^2.6.1" }, @@ -52,6 +57,7 @@ "any-promise": "^1.3.0", "bash": "0.0.1", "bluebird": "^3.3.3", + "body-parser": "^1.14.1", "capitano": "^1.7.0", "chalk": "^1.1.3", "coffee-script": "^1.12.6", @@ -62,7 +68,9 @@ "dockerode": "^2.5.0", "dockerode-options": "^0.2.1", "drivelist": "^5.0.22", + "ejs": "^2.5.7", "etcher-image-write": "^9.0.3", + "express": "^4.13.3", "global-tunnel-ng": "github:zvin/global-tunnel#dont-proxy-connections-to-file-sockets", "hasbin": "^1.2.3", "inquirer": "^3.1.1", @@ -75,6 +83,7 @@ "mz": "^2.6.0", "node-cleanup": "^2.1.2", "nplugm": "^3.0.0", + "open": "0.0.5", "president": "^2.0.1", "prettyjson": "^1.1.3", "progress-stream": "^2.0.0", @@ -82,7 +91,6 @@ "reconfix": "^0.0.3", "request": "^2.81.0", "resin-bundle-resolve": "^0.0.2", - "resin-cli-auth": "^1.2.0", "resin-cli-errors": "^1.2.0", "resin-cli-form": "^1.4.1", "resin-cli-visuals": "^1.4.0", @@ -105,11 +113,11 @@ "stream-to-promise": "^2.2.0", "tmp": "0.0.31", "umount": "^1.1.6", - "underscore.string": "^3.1.1", + "underscore.string": "^3.2.2", "unzip2": "^0.2.5", "update-notifier": "^2.2.0" }, "optionalDependencies": { "removedrive": "^1.0.0" } -} \ No newline at end of file +} diff --git a/tests/auth/server.spec.coffee b/tests/auth/server.spec.coffee new file mode 100644 index 00000000..5f6fdc0a --- /dev/null +++ b/tests/auth/server.spec.coffee @@ -0,0 +1,124 @@ +m = require('mochainon') +request = require('request') +Promise = require('bluebird') +path = require('path') +fs = require('fs') +ejs = require('ejs') +server = require('../../build/auth/server') +utils = require('../../build/auth/utils') +tokens = require('./tokens.json') + +options = + port: 3000 + path: '/auth' + +getPage = (name) -> + pagePath = path.join(__dirname, '..', '..', 'build', 'auth', 'pages', "#{name}.ejs") + tpl = fs.readFileSync(pagePath, encoding: 'utf8') + compiledTpl = ejs.compile(tpl) + return server.getContext(name) + .then (context) -> + compiledTpl(context) + +describe 'Server:', -> + + it 'should get 404 if posting to an unknown path', (done) -> + promise = server.awaitForToken(options) + m.chai.expect(promise).to.be.rejectedWith('Unknown path or verb') + + request.post "http://localhost:#{options.port}/foobarbaz", + form: + token: tokens.johndoe.token + , (error, response, body) -> + m.chai.expect(error).to.not.exist + m.chai.expect(response.statusCode).to.equal(404) + m.chai.expect(body).to.equal('Not found') + done() + + it 'should get 404 if not using the correct verb', (done) -> + promise = server.awaitForToken(options) + m.chai.expect(promise).to.be.rejectedWith('Unknown path or verb') + + request.get "http://localhost:#{options.port}#{options.path}", + form: + token: tokens.johndoe.token + , (error, response, body) -> + m.chai.expect(error).to.not.exist + m.chai.expect(response.statusCode).to.equal(404) + m.chai.expect(body).to.equal('Not found') + done() + + describe 'given the token authenticates with the server', -> + + beforeEach -> + @utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid') + @utilsIsTokenValidStub.returns(Promise.resolve(true)) + + afterEach -> + @utilsIsTokenValidStub.restore() + + it 'should eventually be the token', (done) -> + promise = server.awaitForToken(options) + m.chai.expect(promise).to.eventually.equal(tokens.johndoe.token) + + request.post "http://localhost:#{options.port}#{options.path}", + form: + token: tokens.johndoe.token + , (error, response, body) -> + m.chai.expect(error).to.not.exist + m.chai.expect(response.statusCode).to.equal(200) + getPage('success').then (expectedBody) -> + m.chai.expect(body).to.equal(expectedBody) + done() + + describe 'given the token does not authenticate with the server', -> + + beforeEach -> + @utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid') + @utilsIsTokenValidStub.returns(Promise.resolve(false)) + + afterEach -> + @utilsIsTokenValidStub.restore() + + it 'should be rejected', (done) -> + promise = server.awaitForToken(options) + m.chai.expect(promise).to.be.rejectedWith('Invalid token') + + request.post "http://localhost:#{options.port}#{options.path}", + form: + token: tokens.johndoe.token + , (error, response, body) -> + m.chai.expect(error).to.not.exist + m.chai.expect(response.statusCode).to.equal(401) + getPage('error').then (expectedBody) -> + m.chai.expect(body).to.equal(expectedBody) + done() + + it 'should be rejected if no token', (done) -> + promise = server.awaitForToken(options) + m.chai.expect(promise).to.be.rejectedWith('No token') + + request.post "http://localhost:#{options.port}#{options.path}", + form: + token: '' + , (error, response, body) -> + m.chai.expect(error).to.not.exist + m.chai.expect(response.statusCode).to.equal(401) + getPage('error').then (expectedBody) -> + m.chai.expect(body).to.equal(expectedBody) + done() + + it 'should be rejected if token is malformed', (done) -> + promise = server.awaitForToken(options) + m.chai.expect(promise).to.be.rejectedWith('Invalid token') + + request.post "http://localhost:#{options.port}#{options.path}", + form: + token: 'asdf' + , (error, response, body) -> + m.chai.expect(error).to.not.exist + m.chai.expect(response.statusCode).to.equal(401) + getPage('error').then (expectedBody) -> + m.chai.expect(body).to.equal(expectedBody) + done() + diff --git a/tests/auth/tokens.json b/tests/auth/tokens.json new file mode 100644 index 00000000..6b9eb4ad --- /dev/null +++ b/tests/auth/tokens.json @@ -0,0 +1,18 @@ +{ + "johndoe": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImpvaG5kb2UxIiwiZW1haWwiOiJqb2huZG9lQGpvaG5kb2UuY29tIiwiZ2l0bGFiX2lkIjoxMzI1LCJzb2NpYWxfc2VydmljZV9hY2NvdW50IjpudWxsLCJoYXNQYXNzd29yZFNldCI6dHJ1ZSwibmVlZHNQYXNzd29yZFJlc2V0IjpmYWxzZSwicHVibGljX2tleSI6ZmFsc2UsImZlYXR1cmVzIjpbXSwiaWQiOjEzNDQsImludGVyY29tVXNlckhhc2giOiJlMDM3NzhkZDI5ZTE1NzQ0NWYyNzJhY2M5MjExNzBjZjI4MTBiNjJmNTAyNjQ1MjY1Y2MzNDlkNmRlZGEzNTI0IiwicGVybWlzc2lvbnMiOltdLCJpYXQiOjE0MjY3ODMzMTJ9.v5bmh9HwyUZu8zhh1rA79mTL-1jzDOO8eUr_lVaBwhg", + "data": { + "email": "johndoe@johndoe.com", + "username": "johndoe1", + "id": 1344 + } + }, + "janedoe": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTUyLCJ1c2VybmFtZSI6ImphbmVkb2UiLCJlbWFpbCI6ImphbmVkb2VAYXNkZi5jb20iLCJzb2NpYWxfc2VydmljZV9hY2NvdW50IjpudWxsLCJoYXNfZGlzYWJsZWRfbmV3c2xldHRlciI6dHJ1ZSwiaGFzUGFzc3dvcmRTZXQiOnRydWUsIm5lZWRzUGFzc3dvcmRSZXNldCI6ZmFsc2UsInB1YmxpY19rZXkiOmZhbHNlLCJmZWF0dXJlcyI6W10sImludGVyY29tVXNlckhhc2giOiIwYjRmOWViNDRiMzcxZjBlMzI4ZWY1ZmUwM2FkN2ViMmY1ZjcyZGQ0MThlZjIzMTQ5ZDUyODcwOTY1NThjZTAzIiwicGVybWlzc2lvbnMiOltdLCJpYXQiOjE0MzUzMjAyNjN9.jVzUFu58vzdJFctR8ulyjGL0Em1kjIZSbSxX2SeU03Y", + "data": { + "email": "janedoe@asdf.com", + "username": "janedoe", + "id": 152 + } + } +} diff --git a/tests/auth/utils.spec.coffee b/tests/auth/utils.spec.coffee new file mode 100644 index 00000000..152b32c2 --- /dev/null +++ b/tests/auth/utils.spec.coffee @@ -0,0 +1,111 @@ +m = require('mochainon') +url = require('url') +Promise = require('bluebird') +resin = require('resin-sdk-preconfigured') +utils = require('../../build/auth/utils') +tokens = require('./tokens.json') + +describe 'Utils:', -> + + describe '.getDashboardLoginURL()', -> + + it 'should eventually be a valid url', (done) -> + utils.getDashboardLoginURL('https://127.0.0.1:3000/callback').then (loginUrl) -> + m.chai.expect -> + url.parse(loginUrl) + .to.not.throw(Error) + .nodeify(done) + + it 'should eventually contain an https protocol', (done) -> + Promise.props + dashboardUrl: resin.settings.get('dashboardUrl') + loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback') + .then ({ dashboardUrl, loginUrl }) -> + protocol = url.parse(loginUrl).protocol + m.chai.expect(protocol).to.equal(url.parse(dashboardUrl).protocol) + .nodeify(done) + + it 'should correctly escape a callback url without a path', (done) -> + Promise.props + dashboardUrl: resin.settings.get('dashboardUrl') + loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000') + .then ({ dashboardUrl, loginUrl }) -> + expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000" + m.chai.expect(loginUrl).to.equal(expectedUrl) + .nodeify(done) + + it 'should correctly escape a callback url with a path', (done) -> + Promise.props + dashboardUrl: resin.settings.get('dashboardUrl') + loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback') + .then ({ dashboardUrl, loginUrl }) -> + expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback" + m.chai.expect(loginUrl).to.equal(expectedUrl) + .nodeify(done) + + describe '.isTokenValid()', -> + + it 'should eventually be false if token is undefined', -> + promise = utils.isTokenValid(undefined) + m.chai.expect(promise).to.eventually.be.false + + it 'should eventually be false if token is null', -> + promise = utils.isTokenValid(null) + m.chai.expect(promise).to.eventually.be.false + + it 'should eventually be false if token is an empty string', -> + promise = utils.isTokenValid('') + m.chai.expect(promise).to.eventually.be.false + + it 'should eventually be false if token is a string containing only spaces', -> + promise = utils.isTokenValid(' ') + m.chai.expect(promise).to.eventually.be.false + + describe 'given the token does not authenticate with the server', -> + + beforeEach -> + @resinAuthIsLoggedInStub = m.sinon.stub(resin.auth, 'isLoggedIn') + @resinAuthIsLoggedInStub.returns(Promise.resolve(false)) + + afterEach -> + @resinAuthIsLoggedInStub.restore() + + it 'should eventually be false', -> + promise = utils.isTokenValid(tokens.johndoe.token) + m.chai.expect(promise).to.eventually.be.false + + describe 'given there was a token already', -> + + beforeEach (done) -> + resin.auth.loginWithToken(tokens.janedoe.token).nodeify(done) + + it 'should preserve the old token', (done) -> + resin.auth.getToken().then (originalToken) -> + m.chai.expect(originalToken).to.equal(tokens.janedoe.token) + return utils.isTokenValid(tokens.johndoe.token) + .then(resin.auth.getToken).then (currentToken) -> + m.chai.expect(currentToken).to.equal(tokens.janedoe.token) + .nodeify(done) + + describe 'given there was no token', -> + + beforeEach (done) -> + resin.auth.logout().nodeify(done) + + it 'should stay without a token', (done) -> + utils.isTokenValid(tokens.johndoe.token).then -> + m.chai.expect(resin.token.get()).to.eventually.not.exist + .nodeify(done) + + describe 'given the token does authenticate with the server', -> + + beforeEach -> + @resinAuthIsLoggedInStub = m.sinon.stub(resin.auth, 'isLoggedIn') + @resinAuthIsLoggedInStub.returns(Promise.resolve(true)) + + afterEach -> + @resinAuthIsLoggedInStub.restore() + + it 'should eventually be true', -> + promise = utils.isTokenValid(tokens.johndoe.token) + m.chai.expect(promise).to.eventually.be.true From bd6cb04a2baee5448d039bab83fcfef3d760b056 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Thu, 23 Nov 2017 14:02:47 +0100 Subject: [PATCH 2/3] Replace underscore.string usage with lodash --- lib/actions/help.coffee | 5 ++--- package.json | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/actions/help.coffee b/lib/actions/help.coffee index 9693e273..45b69788 100644 --- a/lib/actions/help.coffee +++ b/lib/actions/help.coffee @@ -15,7 +15,6 @@ limitations under the License. ### _ = require('lodash') -_.str = require('underscore.string') capitano = require('capitano') columnify = require('columnify') messages = require('../utils/messages') @@ -36,7 +35,7 @@ parse = (object) -> ] indent = (text) -> - text = _.map _.str.lines(text), (line) -> + text = _.map text.split('\n'), (line) -> return ' ' + line return text.join('\n') @@ -92,7 +91,7 @@ command = (params, options, done) -> if command.help? console.log("\n#{command.help}") else if command.description? - console.log("\n#{_.str.humanize(command.description)}") + console.log("\n#{_.capitalize(command.description)}") if not _.isEmpty(command.options) console.log('\nOptions:\n') diff --git a/package.json b/package.json index 7de46de4..f2c031d2 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "stream-to-promise": "^2.2.0", "tmp": "0.0.31", "umount": "^1.1.6", - "underscore.string": "^3.2.2", "unzip2": "^0.2.5", "update-notifier": "^2.2.0" }, From e4432d1a90a9d9750a915d6dc40fe16ee50728c5 Mon Sep 17 00:00:00 2001 From: "resin-io-versionbot[bot]" Date: Mon, 27 Nov 2017 17:25:41 +0000 Subject: [PATCH 3/3] v6.10.2 --- CHANGELOG.md | 4 ++++ package.json | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 361733ba..a5c80e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! This project adheres to [Semantic Versioning](http://semver.org/). +## v6.10.2 - 2017-11-27 + +* Inline the entire resin-cli-auth module #721 [Tim Perry] + ## v6.10.1 - 2017-11-27 * Set up TypeScript compilation, and make a small start on converting the CLI #720 [Tim Perry] diff --git a/package.json b/package.json index f2c031d2..ff186fee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resin-cli", - "version": "6.10.1", + "version": "6.10.2", "description": "The official resin.io CLI tool", "main": "./build/actions/index.js", "homepage": "https://github.com/resin-io/resin-cli", @@ -119,4 +119,4 @@ "optionalDependencies": { "removedrive": "^1.0.0" } -} +} \ No newline at end of file