mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-20 06:07:55 +00:00
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
This commit is contained in:
parent
f106b95be2
commit
001c8f9601
@ -2,6 +2,8 @@ path = require('path')
|
|||||||
gulp = require('gulp')
|
gulp = require('gulp')
|
||||||
coffee = require('gulp-coffee')
|
coffee = require('gulp-coffee')
|
||||||
coffeelint = require('gulp-coffeelint')
|
coffeelint = require('gulp-coffeelint')
|
||||||
|
inlinesource = require('gulp-inline-source')
|
||||||
|
mocha = require('gulp-mocha')
|
||||||
shell = require('gulp-shell')
|
shell = require('gulp-shell')
|
||||||
packageJSON = require('./package.json')
|
packageJSON = require('./package.json')
|
||||||
|
|
||||||
@ -10,10 +12,17 @@ OPTIONS =
|
|||||||
coffeelint: path.join(__dirname, 'coffeelint.json')
|
coffeelint: path.join(__dirname, 'coffeelint.json')
|
||||||
files:
|
files:
|
||||||
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
||||||
app: [ 'lib/**/*.coffee', '!lib/**/*.spec.coffee' ]
|
app: 'lib/**/*.coffee'
|
||||||
|
tests: 'tests/**/*.spec.coffee'
|
||||||
|
pages: 'lib/auth/pages/*.ejs'
|
||||||
directories:
|
directories:
|
||||||
build: 'build/'
|
build: 'build/'
|
||||||
|
|
||||||
|
gulp.task 'pages', ->
|
||||||
|
gulp.src(OPTIONS.files.pages)
|
||||||
|
.pipe(inlinesource())
|
||||||
|
.pipe(gulp.dest('build/auth/pages'))
|
||||||
|
|
||||||
gulp.task 'coffee', [ 'lint' ], ->
|
gulp.task 'coffee', [ 'lint' ], ->
|
||||||
gulp.src(OPTIONS.files.app)
|
gulp.src(OPTIONS.files.app)
|
||||||
.pipe(coffee(bare: true, header: true))
|
.pipe(coffee(bare: true, header: true))
|
||||||
@ -26,5 +35,16 @@ gulp.task 'lint', ->
|
|||||||
}))
|
}))
|
||||||
.pipe(coffeelint.reporter())
|
.pipe(coffeelint.reporter())
|
||||||
|
|
||||||
gulp.task 'watch', [ 'coffee' ], ->
|
gulp.task 'test', ->
|
||||||
gulp.watch([ OPTIONS.files.coffee ], [ 'coffee' ])
|
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' ])
|
||||||
|
@ -74,7 +74,7 @@ exports.login =
|
|||||||
_ = require('lodash')
|
_ = require('lodash')
|
||||||
Promise = require('bluebird')
|
Promise = require('bluebird')
|
||||||
resin = require('resin-sdk-preconfigured')
|
resin = require('resin-sdk-preconfigured')
|
||||||
auth = require('resin-cli-auth')
|
auth = require('../auth')
|
||||||
form = require('resin-cli-form')
|
form = require('resin-cli-form')
|
||||||
patterns = require('../utils/patterns')
|
patterns = require('../utils/patterns')
|
||||||
messages = require('../utils/messages')
|
messages = require('../utils/messages')
|
||||||
|
63
lib/auth/index.coffee
Normal file
63
lib/auth/index.coffee
Normal file
@ -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)
|
21
lib/auth/pages/error.ejs
Normal file
21
lib/auth/pages/error.ejs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Resin CLI - Error</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<img class="icon" src="./static/images/sad.png" inline>
|
||||||
|
<h1>Something went wrong</h1>
|
||||||
|
<p>You couldn't login to the Resin CLI for some reason</p>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="https://forums.resin.io/" class="button danger">Get help in our forums</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
lib/auth/pages/static/images/happy.png
Normal file
BIN
lib/auth/pages/static/images/happy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
lib/auth/pages/static/images/sad.png
Normal file
BIN
lib/auth/pages/static/images/sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
60
lib/auth/pages/static/style.css
Normal file
60
lib/auth/pages/static/style.css
Normal file
@ -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;
|
||||||
|
}
|
21
lib/auth/pages/success.ejs
Normal file
21
lib/auth/pages/success.ejs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>Resin CLI - Success</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<img class="icon" src="./static/images/happy.png" inline>
|
||||||
|
<h1>Success!</h1>
|
||||||
|
<p>You successfully logged in the Resin CLI</p>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<a href="<%= dashboardUrl %>" class="button normal">Go to the dashboard</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
101
lib/auth/server.coffee
Normal file
101
lib/auth/server.coffee
Normal file
@ -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({})
|
75
lib/auth/utils.coffee
Normal file
75
lib/auth/utils.coffee
Normal file
@ -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()
|
16
package.json
16
package.json
@ -19,8 +19,10 @@
|
|||||||
"resin": "./bin/resin"
|
"resin": "./bin/resin"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gulp coffee && tsc && npm run doc",
|
"build": "gulp build && tsc && npm run doc",
|
||||||
"ci": "npm run build && catch-uncommitted",
|
"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",
|
"doc": "mkdir -p doc/ && coffee extras/capitanodoc/index.coffee > doc/cli.markdown",
|
||||||
"watch": "gulp watch",
|
"watch": "gulp watch",
|
||||||
"lint": "gulp lint",
|
"lint": "gulp lint",
|
||||||
@ -42,7 +44,10 @@
|
|||||||
"gulp": "^3.9.0",
|
"gulp": "^3.9.0",
|
||||||
"gulp-coffee": "^2.2.0",
|
"gulp-coffee": "^2.2.0",
|
||||||
"gulp-coffeelint": "^0.6.0",
|
"gulp-coffeelint": "^0.6.0",
|
||||||
|
"gulp-inline-source": "^2.1.0",
|
||||||
|
"gulp-mocha": "^2.0.0",
|
||||||
"gulp-shell": "^0.5.2",
|
"gulp-shell": "^0.5.2",
|
||||||
|
"mochainon": "^2.0.0",
|
||||||
"require-npm4-to-publish": "^1.0.0",
|
"require-npm4-to-publish": "^1.0.0",
|
||||||
"typescript": "^2.6.1"
|
"typescript": "^2.6.1"
|
||||||
},
|
},
|
||||||
@ -52,6 +57,7 @@
|
|||||||
"any-promise": "^1.3.0",
|
"any-promise": "^1.3.0",
|
||||||
"bash": "0.0.1",
|
"bash": "0.0.1",
|
||||||
"bluebird": "^3.3.3",
|
"bluebird": "^3.3.3",
|
||||||
|
"body-parser": "^1.14.1",
|
||||||
"capitano": "^1.7.0",
|
"capitano": "^1.7.0",
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
"coffee-script": "^1.12.6",
|
"coffee-script": "^1.12.6",
|
||||||
@ -62,7 +68,9 @@
|
|||||||
"dockerode": "^2.5.0",
|
"dockerode": "^2.5.0",
|
||||||
"dockerode-options": "^0.2.1",
|
"dockerode-options": "^0.2.1",
|
||||||
"drivelist": "^5.0.22",
|
"drivelist": "^5.0.22",
|
||||||
|
"ejs": "^2.5.7",
|
||||||
"etcher-image-write": "^9.0.3",
|
"etcher-image-write": "^9.0.3",
|
||||||
|
"express": "^4.13.3",
|
||||||
"global-tunnel-ng": "github:zvin/global-tunnel#dont-proxy-connections-to-file-sockets",
|
"global-tunnel-ng": "github:zvin/global-tunnel#dont-proxy-connections-to-file-sockets",
|
||||||
"hasbin": "^1.2.3",
|
"hasbin": "^1.2.3",
|
||||||
"inquirer": "^3.1.1",
|
"inquirer": "^3.1.1",
|
||||||
@ -75,6 +83,7 @@
|
|||||||
"mz": "^2.6.0",
|
"mz": "^2.6.0",
|
||||||
"node-cleanup": "^2.1.2",
|
"node-cleanup": "^2.1.2",
|
||||||
"nplugm": "^3.0.0",
|
"nplugm": "^3.0.0",
|
||||||
|
"open": "0.0.5",
|
||||||
"president": "^2.0.1",
|
"president": "^2.0.1",
|
||||||
"prettyjson": "^1.1.3",
|
"prettyjson": "^1.1.3",
|
||||||
"progress-stream": "^2.0.0",
|
"progress-stream": "^2.0.0",
|
||||||
@ -82,7 +91,6 @@
|
|||||||
"reconfix": "^0.0.3",
|
"reconfix": "^0.0.3",
|
||||||
"request": "^2.81.0",
|
"request": "^2.81.0",
|
||||||
"resin-bundle-resolve": "^0.0.2",
|
"resin-bundle-resolve": "^0.0.2",
|
||||||
"resin-cli-auth": "^1.2.0",
|
|
||||||
"resin-cli-errors": "^1.2.0",
|
"resin-cli-errors": "^1.2.0",
|
||||||
"resin-cli-form": "^1.4.1",
|
"resin-cli-form": "^1.4.1",
|
||||||
"resin-cli-visuals": "^1.4.0",
|
"resin-cli-visuals": "^1.4.0",
|
||||||
@ -105,7 +113,7 @@
|
|||||||
"stream-to-promise": "^2.2.0",
|
"stream-to-promise": "^2.2.0",
|
||||||
"tmp": "0.0.31",
|
"tmp": "0.0.31",
|
||||||
"umount": "^1.1.6",
|
"umount": "^1.1.6",
|
||||||
"underscore.string": "^3.1.1",
|
"underscore.string": "^3.2.2",
|
||||||
"unzip2": "^0.2.5",
|
"unzip2": "^0.2.5",
|
||||||
"update-notifier": "^2.2.0"
|
"update-notifier": "^2.2.0"
|
||||||
},
|
},
|
||||||
|
124
tests/auth/server.spec.coffee
Normal file
124
tests/auth/server.spec.coffee
Normal file
@ -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()
|
||||||
|
|
18
tests/auth/tokens.json
Normal file
18
tests/auth/tokens.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
tests/auth/utils.spec.coffee
Normal file
111
tests/auth/utils.spec.coffee
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user