diff --git a/.eslintignore b/.eslintignore index e69de29..33586bf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -0,0 +1,2 @@ +public +coverage diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 795176c..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,37 +0,0 @@ - - -# Description/Steps to reproduce - - - -# Link to reproduction sandbox - - - -# Expected result - - - -# Additional information - - diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..6bc732a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve +labels: bug + +--- + + + +## Steps to reproduce + + + +## Current Behavior + + + +## Expected Behavior + + + +## Link to reproduction sandbox + + + +## Additional information + + + +## Related Issues + + + +_See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..1fd76ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +labels: feature + +--- + +## Suggestion + + + +## Use Cases + + + +## Examples + + + +## Acceptance criteria + +TBD - will be filled by the team. diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md new file mode 100644 index 0000000..eb25195 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.md @@ -0,0 +1,27 @@ +--- +name: Question +about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. +labels: question + +--- + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9204746 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues + about: Do not report security vulnerabilities using GitHub issues. Please send an email to `reachsl@us.ibm.com` instead. + - name: Get help on StackOverflow + url: https://stackoverflow.com/tags/loopbackjs + about: Please ask and answer questions on StackOverflow. + - name: Join our mailing list + url: https://groups.google.com/forum/#!forum/loopbackjs + about: You can also post your question to our mailing list. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 368cb4c..22204e7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,25 +1,18 @@ -### Description - - -#### Related issues - +Include references to all related GitHub issues and other pull requests, for example: -- connect to +Fixes #123 +Implements #254 +See also #23 +--> -### Checklist +## Checklist - +👉 [Read and sign the CLA (Contributor License Agreement)](https://cla.strongloop.com/agreements/strongloop/loopback-component-explorer) 👈 +- [ ] `npm test` passes on your machine - [ ] New tests added or existing tests modified to cover all changes -- [ ] Code conforms with the [style - guide](http://loopback.io/doc/en/contrib/style-guide.html) +- [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) +- [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) diff --git a/.travis.yml b/.travis.yml index 921e6c8..5cccead 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ sudo: false language: node_js node_js: - - "4" - - "6" - "8" + - "10" + - "12" diff --git a/CHANGES.md b/CHANGES.md index 8b50fb9..3b3f039 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,93 @@ +2020-03-06, Version 6.5.1 +========================= + + * Update LTS status in README (Miroslav Bajtoš) + + * Cursor is made pointer of the add token button (Siraj Alam) + + +2019-11-28, Version 6.5.0 +========================= + + * docs: describe GitHub advisory CVE-2019-17495 (Miroslav Bajtoš) + + * chore: improve README formatting (Miroslav Bajtoš) + + * Update README on swagger-ui (Diana Lau) + + * chore: improve issue and PR templates (Nora) + + * chore: add Node.js 12 to travis ci (Nora) + + * chore: drop support for Node.js 6 (Nora) + + * update LTS (Diana Lau) + + +2019-05-09, Version 6.4.0 +========================= + + * chore: update copyrights years (Agnes Lin) + + * update README for LTS status (Diana Lau) + + * Updated README with small code fix. (Pradeep Kumar Tippa) + + * fix: update lodash (jannyHou) + + * Prevent accidental upgrades to swagger-ui v3+ (Miroslav Bajtoš) + + * Update eslint & config to latest major versions (Miroslav Bajtoš) + + * Update eslint & config to latest (Miroslav Bajtoš) + + +2018-10-18, Version 6.3.1 +========================= + + * README: update LTS status (Miroslav Bajtoš) + + +2018-09-14, Version 6.3.0 +========================= + + * Add config option for custom auth header (Jonathan Prince) + + * Fix config.json URL when running from /index.html (Jonathan Prince) + + +2018-07-24, Version 6.2.0 +========================= + + * chore: update dependencies (Diana Lau) + + +2018-07-09, Version 6.1.0 +========================= + + * [WebFM] cs/pl/ru translation (candytangnb) + + * remove package-lock.json (Diana Lau) + + * update dependencies (Diana Lau) + + +2018-05-10, Version 6.0.1 +========================= + + * Update LTS information in README (Miroslav Bajtoš) + + * Enable Node.js 10 on Travis CI (Miroslav Bajtoš) + + +2018-04-18, Version 6.0.0 +========================= + + * [SEMVER-MAJOR] Remove deprecated CORS support (Hiran del Castillo) + + * Add Travis CI configuration (Miroslav Bajtoš) + + 2018-04-17, Version 5.4.0 ========================= diff --git a/README.md b/README.md index 441e807..ed9c025 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # loopback-component-explorer -Browse and test your LoopBack app's APIs. +**⚠️ LoopBack 3 is in Maintenance LTS mode, only critical bugs and critical +security fixes will be provided. (See +[Module Long Term Support Policy](#module-long-term-support-policy) below.)** -## Supported versions +We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as +soon as possible. Refer to our +[Migration Guide](https://loopback.io/doc/en/lb4/migration-overview.html) +for more information on how to upgrade. -Current|Long Term Support -:-:|:-: -4.x|2.x -Learn more about our LTS plan in [docs](http://loopback.io/doc/en/contrib/Long-term-support.html). +## Overview -*The only difference between 3.x and 4.x versions is which Node.js versions -are supported (loopback-component-explorer 4.x dropped support for pre-v4 -versions of Node.js). Therefore we decided to not maintain 3.x version line and -provide LTS support for the 2.x version line instead.* +Browse and test your LoopBack app's APIs. ## Basic Usage @@ -41,6 +40,77 @@ console.log("Explorer mounted at localhost:" + port + "/explorer"); app.listen(port); ``` +## A note on swagger-ui vulnerabilities + +API Explorer for LoopBack 3 is built on top of `swagger-ui` version 2.x which +is no longer maintained. While there are known security vulnerabilities in +`swagger-ui`, we believe they don't affect LoopBack users. + +We would love to upgrade our (LB3) API Explorer to v3 of swagger-ui, but +unfortunately such upgrade requires too much effort and more importantly +addition of new features to LB3 runtime, which would break our LTS guarantees. +For more details, see discussion in +[loopback-component-explorer#263](https://github.com/strongloop/loopback-component-explorer/issues/263). + +### npm advisory 985 + +Link: https://www.npmjs.com/advisories/985 + +> Versions of swagger-ui prior to 3.0.13 are vulnerable to Cross-Site Scripting +> (XSS). The package fails to sanitize YAML files imported from URLs or +> copied-pasted. This may allow attackers to execute arbitrary JavaScript. + +LoopBack's API Explorer does not allow clients to import swagger spec from YAML +URL/pasted-content. That means loopback-component-explorer **IS NOT AFFECTED** +by this vulnerability. + +### npm advisory 975 + +Link: https://www.npmjs.com/advisories/975 + +> Versions of swagger-ui prior to 3.18.0 are vulnerable to Reverse Tabnapping. +> The package uses `target='_blank'` in anchor tags, allowing attackers to +> access `window.opener` for the original page. This is commonly used for +> phishing attacks. + +This vulnerability affects anchor tags created from metadata provided by the +Swagger spec, for example `info.termsOfServiceUrl`. LoopBack's API Explorer +does not allow clients to provide custom swagger spec, URLs like +`info.termsOfServiceUrl` are fully in control of the LoopBack application +developer. That means loopback-component-explorer **IS NOT AFFECTED** by this +vulnerability. + +### npm advisory 976 + +Link: https://www.npmjs.com/advisories/976 + +> Versions of swagger-ui prior to 3.20.9 are vulnerable to Cross-Site Scripting +> (XSS). The package fails to sanitize URLs used in the OAuth auth flow, which +> may allow attackers to execute arbitrary JavaScript. + +LoopBack 3 API Explorer does not support OAuth auth flow, that means +loopback-component-explorer **IS NOT AFFECTED** by this vulnerability. + +### GitHub advisory CVE-2019-17495 + +Link: https://github.com/advisories/GHSA-c427-hjc3-wrfw +> A Cascading Style Sheets (CSS) injection vulnerability in Swagger UI before +> 3.23.11 allows attackers to use the Relative Path Overwrite (RPO) technique +> to perform CSS-based input field value exfiltration, such as exfiltration of +> a CSRF token value. + +Quoting from the +[disclosure](https://github.com/tarantula-team/CSS-injection-in-Swagger-UI/tree/15edeaaa5806aa8e83ee55d883f956a3c3573ac9): + +> We’ve observed that the `?url=` parameter in SwaggerUI allows an attacker to +> override an otherwise hard-coded schema file. We realize that Swagger UI +> allows users to embed untrusted Json format from remote servers This means we +> can inject json content via the GET parameter to victim Swagger UI. etc. + +LoopBack 3 API Explorer does not suport `?url=` parameter, it always loads the +Swagger spec file from the LoopBack server serving the Explorer UI. That means +loopback-component-explorer **IS NOT AFFECTED** by this vulnerability. + ## Upgrading from v1.x To upgrade your application using loopback-explorer version 1.x, just replace @@ -50,7 +120,7 @@ To upgrade your application using loopback-explorer version 1.x, just replace var explorer = require('loopback-component-explorer'); // Module was loopback-explorer in v. 2.0.1 and earlier // v1.x - does not work anymore -app.use('/explorer', explorer(app, options); +app.use('/explorer', explorer(app, options)); // v2.x app.use('/explorer', explorer.routes(app, options)); ``` @@ -165,3 +235,39 @@ Options are passed to `explorer(app, options)`. > Default: Read from package.json > Sets your API version. If not present, will read from your app's package.json. + +`auth`: **Object** + +> Optional config for setting api access token, can be used to rename the query parameter or set an auth header. + +> The object has 2 keys: +> - `in`: either `header` or `query` +> - `name`: the name of the query parameter or header +> +> The default sets the token as a query parameter with the name `access_token` + +> Example for setting the api key in a header named `x-api-key`: +> ``` +> { +> "loopback-component-explorer": { +> "mountPath": "/explorer", +> "auth": { +> "in": "header", +> "name": "x-api-key" +> } +> } +> } +> ``` + +## Module Long Term Support Policy + +This module adopts the [ +Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, + with the following End Of Life (EOL) dates: + +| Version | Status | Published | EOL | +| ------- | --------------- | --------- | -------- | +| 6.x | Maintenance LTS | Apr 2018 | Dec 2020 | +| 5.x | End-of-Life | Sep 2017 | Dec 2019 | + +Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). diff --git a/example/hidden.js b/example/hidden.js index 3ab579a..71d3ae3 100644 --- a/example/hidden.js +++ b/example/hidden.js @@ -1,26 +1,28 @@ -// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Copyright IBM Corp. 2014,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var g = require('strong-globalize')(); +'use strict'; -var loopback = require('loopback'); -var app = loopback(); -var explorer = require('../'); -var port = 3000; +const g = require('strong-globalize')(); -var User = loopback.Model.extend('user', { +const loopback = require('loopback'); +const app = loopback(); +const explorer = require('../'); +const port = 3000; + +const User = loopback.Model.extend('user', { username: 'string', email: 'string', sensitiveInternalProperty: 'string', -}, { hidden: ['sensitiveInternalProperty'] }); +}, {hidden: ['sensitiveInternalProperty']}); User.attachTo(loopback.memory()); app.model(User); -var apiPath = '/api'; -explorer(app, { basePath: apiPath }); +const apiPath = '/api'; +explorer(app, {basePath: apiPath}); app.use(apiPath, loopback.rest()); g.log('{{Explorer}} mounted at {{localhost:%s/explorer}}', port); diff --git a/example/simple.js b/example/simple.js index ef39372..dd81ff9 100644 --- a/example/simple.js +++ b/example/simple.js @@ -1,25 +1,27 @@ -// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var g = require('strong-globalize')(); +'use strict'; -var loopback = require('loopback'); -var app = loopback(); -var explorer = require('../'); -var port = 3000; +const g = require('strong-globalize')(); -var Product = loopback.PersistedModel.extend('product', { - foo: { type: 'string', required: true }, +const loopback = require('loopback'); +const app = loopback(); +const explorer = require('../'); +const port = 3000; + +const Product = loopback.PersistedModel.extend('product', { + foo: {type: 'string', required: true}, bar: 'string', - aNum: { type: 'number', min: 1, max: 10, required: true, default: 5 }, + aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}, }); Product.attachTo(loopback.memory()); app.model(Product); -var apiPath = '/api'; -explorer(app, { basePath: apiPath }); +const apiPath = '/api'; +explorer(app, {basePath: apiPath}); app.use(apiPath, loopback.rest()); g.log('{{Explorer}} mounted at {{http://localhost:%s/explorer}}', port); diff --git a/index.js b/index.js index 312593e..f84810d 100644 --- a/index.js +++ b/index.js @@ -1,26 +1,24 @@ -// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; -var SG = require('strong-globalize'); +const SG = require('strong-globalize'); SG.SetRootDir(__dirname); -var g = SG(); +const g = SG(); /*! * Adds dynamically-updated docs as /explorer */ -var deprecated = require('depd')('loopback-explorer'); -var url = require('url'); -var path = require('path'); -var urlJoin = require('./lib/url-join'); -var _defaults = require('lodash').defaults; -var cors = require('cors'); -var createSwaggerObject = require('loopback-swagger').generateSwaggerSpec; -var SWAGGER_UI_ROOT = require('swagger-ui/index').dist; -var STATIC_ROOT = path.join(__dirname, 'public'); +const url = require('url'); +const path = require('path'); +const urlJoin = require('./lib/url-join'); +const _defaults = require('lodash').defaults; +const createSwaggerObject = require('loopback-swagger').generateSwaggerSpec; +const SWAGGER_UI_ROOT = require('swagger-ui/index').dist; +const STATIC_ROOT = path.join(__dirname, 'public'); module.exports = explorer; explorer.routes = routes; @@ -33,7 +31,7 @@ explorer.routes = routes; */ function explorer(loopbackApplication, options) { - options = _defaults({}, options, { mountPath: '/explorer' }); + options = _defaults({}, options, {mountPath: '/explorer'}); loopbackApplication.use( options.mountPath, routes(loopbackApplication, options) @@ -42,8 +40,8 @@ function explorer(loopbackApplication, options) { } function routes(loopbackApplication, options) { - var loopback = loopbackApplication.loopback; - var loopbackMajor = + const loopback = loopbackApplication.loopback; + const loopbackMajor = (loopback && loopback.version && loopback.version.split('.')[0]) || 1; if (loopbackMajor < 2) { @@ -61,7 +59,7 @@ function routes(loopbackApplication, options) { swaggerUI: true, }); - var router = new loopback.Router(); + const router = new loopback.Router(); mountSwagger(loopbackApplication, router, options); @@ -70,13 +68,18 @@ function routes(loopbackApplication, options) { router.get('/config.json', function(req, res) { // Get the path we're mounted at. It's best to get this from the referer // in case we're proxied at a deep path. - var source = url.parse(req.headers.referer || '').pathname; + let source = url.parse(req.headers.referer || '').pathname; + // strip index.html if present in referer + if (source && /\/index\.html$/.test(source)) { + source = source.replace(/\/index\.html$/, ''); + } // If no referer is available, use the incoming url. if (!source) { source = req.originalUrl.replace(/\/config.json(\?.*)?$/, ''); } res.send({ url: urlJoin(source, '/' + options.resourcePath), + auth: options.auth, }); }); @@ -115,7 +118,7 @@ function routes(loopbackApplication, options) { * @param {Object} opts Options. */ function mountSwagger(loopbackApplication, swaggerApp, opts) { - var swaggerObject = createSwaggerObject(loopbackApplication, opts); + let swaggerObject = createSwaggerObject(loopbackApplication, opts); // listening to modelRemoted event for updating the swaggerObject // with the newly created model to appear in the Swagger UI. @@ -132,12 +135,9 @@ function mountSwagger(loopbackApplication, swaggerApp, opts) { // when a remote method is disabled to hide that method in the Swagger UI. loopbackApplication.on('remoteMethodDisabled', rebuildSwaggerObject); - var resourcePath = (opts && opts.resourcePath) || 'swagger.json'; + let resourcePath = (opts && opts.resourcePath) || 'swagger.json'; if (resourcePath[0] !== '/') resourcePath = '/' + resourcePath; - var remotes = loopbackApplication.remotes(); - setupCors(swaggerApp, remotes); - swaggerApp.get(resourcePath, function sendSwaggerObject(req, res) { res.status(200).send(swaggerObject); }); @@ -146,22 +146,3 @@ function mountSwagger(loopbackApplication, swaggerApp, opts) { swaggerObject = createSwaggerObject(loopbackApplication, opts); } } - -function setupCors(swaggerApp, remotes) { - var corsOptions = remotes.options && remotes.options.cors; - if (corsOptions === false) return; - - deprecated( - g.f( - 'The built-in CORS middleware provided by loopback-component-explorer ' + - 'was deprecated. See %s for more details.', - 'https://loopback.io/doc/en/lb3/Security-considerations.html' - ) - ); - - if (corsOptions === undefined) { - corsOptions = { origin: true, credentials: true }; - } - - swaggerApp.use(cors(corsOptions)); -} diff --git a/intl/cs/messages.json b/intl/cs/messages.json new file mode 100644 index 0000000..31d8952 --- /dev/null +++ b/intl/cs/messages.json @@ -0,0 +1,7 @@ +{ + "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} připojen k {{http://localhost:{0}/explorer}}", + "af448751b9a970c539400cadd2681e93": "Vestavěný middleware CORS poskytnutý modulem loopback-component-explorer byl zamítnut. Další podrobnosti viz {0}.", + "b9e94c12da3208f46a969191874c425c": "{{Explorer}} připojen k {{localhost:{0}/explorer}}", + "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} vyžaduje {{loopback}} 2.0 nebo novější" +} + diff --git a/intl/pl/messages.json b/intl/pl/messages.json new file mode 100644 index 0000000..86eaecd --- /dev/null +++ b/intl/pl/messages.json @@ -0,0 +1,7 @@ +{ + "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} podłączony pod adresem {{http://localhost:{0}/explorer}}", + "af448751b9a970c539400cadd2681e93": "Wbudowana warstwa pośrednia CORS zapewniana przez moduł loopback-component-explorer jest nieaktualna. Więcej informacji na ten temat zawiera sekcja {0}.", + "b9e94c12da3208f46a969191874c425c": "{{Explorer}} podłączony pod adresem {{localhost:{0}/explorer}}", + "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} wymaga aplikacji {{loopback}} 2.0 lub nowszej" +} + diff --git a/intl/ru/messages.json b/intl/ru/messages.json new file mode 100644 index 0000000..c162a43 --- /dev/null +++ b/intl/ru/messages.json @@ -0,0 +1,7 @@ +{ + "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} примонтирован в {{http://localhost:{0}/explorer}}", + "af448751b9a970c539400cadd2681e93": "Встроенное промежуточное ПО CORS, заданное параметром loopback-component-explorer, устарело. Для получения дополнительной информации см. {0}.", + "b9e94c12da3208f46a969191874c425c": "{{Explorer}} примонтирован в {{localhost:{0}/explorer}}", + "f3f2b04c273e23780d76306f8c72a60f": "Для {{loopback-component-explorer}} требуется {{loopback}} 2.0 и выше" +} + diff --git a/lib/url-join.js b/lib/url-join.js index 2d64731..8240bfc 100644 --- a/lib/url-join.js +++ b/lib/url-join.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2014. All Rights Reserved. +// Copyright IBM Corp. 2014,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT @@ -8,6 +8,6 @@ // Simple url joiner. Ensure we don't have to care about whether or not // we are fed paths with leading/trailing slashes. module.exports = function urlJoin() { - var args = Array.prototype.slice.call(arguments); + const args = Array.prototype.slice.call(arguments); return args.join('/').replace(/\/+/g, '/'); }; diff --git a/package.json b/package.json index 13126f9..6bcebd3 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "loopback-component-explorer", - "version": "5.4.0", + "version": "6.5.1", "description": "Browse and test your LoopBack app's APIs", "engines": { - "node": ">=4" + "node": ">=8.9" }, "main": "index.js", "scripts": { @@ -20,27 +20,25 @@ "api", "explorer" ], - "author": "Ritchie Martori", + "author": "IBM Corp.", "readmeFilename": "README.md", "bugs": { "url": "https://github.com/strongloop/loopback-component-explorer/issues" }, "devDependencies": { "chai": "^3.2.0", - "eslint": "^2.8.0", - "eslint-config-loopback": "^2.0.0", + "eslint": "^5.13.0", + "eslint-config-loopback": "^13.0.0", "loopback": "^3.19.0", - "mocha": "^2.2.5", - "supertest": "^1.0.1" + "mocha": "^5.2.0", + "supertest": "^3.1.0" }, "license": "MIT", "dependencies": { - "cors": "^2.7.1", - "debug": "^2.2.0", - "depd": "^1.1.0", - "lodash": "^4.17.5", + "debug": "^3.1.0", + "lodash": "^4.17.11", "loopback-swagger": "^5.0.0", - "strong-globalize": "^3.1.0", + "strong-globalize": "^4.1.1", "swagger-ui": "^2.2.5" } } diff --git a/public/css/loopbackStyles.css b/public/css/loopbackStyles.css index 56d9d11..bd3ba0f 100644 --- a/public/css/loopbackStyles.css +++ b/public/css/loopbackStyles.css @@ -34,6 +34,7 @@ body #header a#logo { body #header form#api_selector .input a#explore { background-color: #7dbd33 !important; + cursor: pointer; } diff --git a/public/lib/loadSwaggerUI.js b/public/lib/loadSwaggerUI.js index 595436c..a26beb4 100644 --- a/public/lib/loadSwaggerUI.js +++ b/public/lib/loadSwaggerUI.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Copyright IBM Corp. 2014,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT @@ -27,6 +27,7 @@ $(function() { validatorUrl: null, url: config.url || '/swagger/resources', apiKey: '', + auth: config.auth, dom_id: 'swagger-ui-container', supportHeaderParams: true, onComplete: function(swaggerApi, swaggerUi) { @@ -76,12 +77,15 @@ $(function() { function setAccessToken(e) { e.stopPropagation(); // Don't let the default #explore handler fire e.preventDefault(); + var authOptions = window.swaggerUi.options.auth || {}; + var keyLocation = authOptions.in || 'query'; + var keyName = authOptions.name || 'access_token'; var key = $('#input_accessToken')[0].value; log('key: ' + key); if (key && key.trim() !== '') { log('added accessToken ' + key); var apiKeyAuth = - new SwaggerClient.ApiKeyAuthorization('access_token', key, 'query'); + new SwaggerClient.ApiKeyAuthorization(keyName, key, keyLocation); window.swaggerUi.api.clientAuthorizations.add('key', apiKeyAuth); accessToken = key; $('.accessTokenDisplay').text('Token Set.').addClass('set'); diff --git a/test/explorer.test.js b/test/explorer.test.js index ac9bc66..425d68a 100644 --- a/test/explorer.test.js +++ b/test/explorer.test.js @@ -1,16 +1,33 @@ -// Copyright IBM Corp. 2013,2016. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var loopback = require('loopback'); -var explorer = require('../'); -var request = require('supertest'); -var assert = require('assert'); -var path = require('path'); -var expect = require('chai').expect; -var urlJoin = require('../lib/url-join'); -var os = require('os'); +'use strict'; + +// NOTE(bajtos) It's important to run this check before we load the Explorer +// because require() may fail (e.g. with MODULE_NOT_FOUND error) and make +// it difficult to identify the actual problem +const uiVersion = require('../package.json').dependencies['swagger-ui']; +if (!uiVersion.startsWith('^2')) { + console.error(` +Upgrading from swagger-ui@2 to a newer major version (${uiVersion}) is difficult, +see https://github.com/strongloop/loopback-component-explorer/issues/254 +If you are confident about this change and have manually verified API Explorer +functionality in the browser, including access-token based authentication, +then you can delete this check. +`); + process.exit(2); +} + +const loopback = require('loopback'); +const explorer = require('../'); +const request = require('supertest'); +const assert = require('assert'); +const path = require('path'); +const expect = require('chai').expect; +const urlJoin = require('../lib/url-join'); +const os = require('os'); describe('explorer', function() { describe('with default config', function() { @@ -18,7 +35,7 @@ describe('explorer', function() { it('should register "loopback-component-explorer" to the app', function() { expect(this.app.get('loopback-component-explorer')) - .to.have.property('mountPath', '/explorer'); + .to.have.property('mountPath', '/explorer'); }); it('should redirect to /explorer/', function(done) { @@ -33,6 +50,40 @@ describe('explorer', function() { .get('/explorer/') .expect('Content-Type', /html/) .expect(200) + .end(function(err, res) { + if (err) return done(err); + + assert(!!~res.text.indexOf('LoopBack API Explorer'), + 'text does not contain expected string'); + + done(); + }); + }); + + it('should serve correct swagger-ui config', function(done) { + request(this.app) + .get('/explorer/config.json') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + + expect(res.body).to + .have.property('url', '/explorer/swagger.json'); + + done(); + }); + }); + }); + + describe('when filename is included in url', function() { + beforeEach(givenLoopBackAppWithExplorer()); + + it('should serve the explorer at /explorer/index.html', function(done) { + request(this.app) + .get('/explorer/index.html') + .expect('Content-Type', /html/) + .expect(200) .end(function(err, res) { if (err) throw err; @@ -46,6 +97,7 @@ describe('explorer', function() { it('should serve correct swagger-ui config', function(done) { request(this.app) .get('/explorer/config.json') + .set('Referer', 'http://example.com/explorer/index.html') .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { @@ -64,7 +116,7 @@ describe('explorer', function() { it('should register "loopback-component-explorer" to the app', function() { expect(this.app.get('loopback-component-explorer')) - .to.have.property('mountPath', '/swagger'); + .to.have.property('mountPath', '/swagger'); }); it('should serve correct swagger-ui config', function(done) { @@ -85,9 +137,9 @@ describe('explorer', function() { describe('with custom app.restApiRoot', function() { it('should serve correct swagger-ui config', function(done) { - var app = loopback(); + const app = loopback(); app.set('restApiRoot', '/rest-api-root'); - app.set('remoting', { cors: false }); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app); request(app) @@ -107,9 +159,9 @@ describe('explorer', function() { // SwaggerUI builds resource URL by concatenating basePath + resourcePath // Since the resource paths are always startign with a slash, // if the basePath ends with a slash too, an incorrect URL is produced - var app = loopback(); + const app = loopback(); app.set('restApiRoot', '/apis/'); - app.set('remoting', { cors: false }); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app); request(app) @@ -118,8 +170,8 @@ describe('explorer', function() { .end(function(err, res) { if (err) return done(err); - var baseUrl = res.body.basePath; - var apiPath = Object.keys(res.body.paths)[0]; + const baseUrl = res.body.basePath; + const apiPath = Object.keys(res.body.paths)[0]; expect(baseUrl + apiPath).to.equal('/apis/products'); done(); @@ -128,10 +180,10 @@ describe('explorer', function() { }); describe('with custom front-end files', function() { - var app; + let app; beforeEach(function setupExplorerWithUiDirs() { app = loopback(); - app.set('remoting', { cors: false }); + app.set('remoting', {cors: false}); explorer(app, { uiDirs: [path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui')], }); @@ -154,7 +206,7 @@ describe('explorer', function() { request(app).get('/explorer/') .expect(200) .end(function(err, res) { - if (err) return done(er); + if (err) return done(err); // expect the content of `dummy-swagger-ui/index.html` expect(res.text).to.contain('custom index.html'); done(); @@ -163,10 +215,10 @@ describe('explorer', function() { }); describe('with swaggerUI option', function() { - var app; + let app; beforeEach(function setupExplorerWithoutUI() { app = loopback(); - app.set('remoting', { cors: false }); + app.set('remoting', {cors: false}); explorer(app, { swaggerUI: false, }); @@ -199,11 +251,11 @@ describe('explorer', function() { }); describe('explorer.routes API', function() { - var app; + let app; beforeEach(function() { app = loopback(); - app.set('remoting', { cors: false }); - var Product = loopback.PersistedModel.extend('product'); + app.set('remoting', {cors: false}); + const Product = loopback.PersistedModel.extend('product'); Product.attachTo(loopback.memory()); app.model(Product); }); @@ -221,10 +273,10 @@ describe('explorer', function() { }); describe('when specifying custom static file root directories', function() { - var app; + let app; beforeEach(function() { app = loopback(); - app.set('remoting', { cors: false }); + app.set('remoting', {cors: false}); }); it('should allow `uiDirs` to be defined as an Array', function(done) { @@ -258,42 +310,9 @@ describe('explorer', function() { }); }); - describe('Cross-origin resource sharing', function() { - it('allows cross-origin requests by default', function(done) { - var app = loopback(); - process.once('deprecation', function() { /* ignore */ }); - configureRestApiAndExplorer(app, '/explorer'); - - request(app) - .options('/explorer/swagger.json') - .set('Origin', 'http://example.com/') - .expect('Access-Control-Allow-Origin', /^http:\/\/example.com\/|\*/) - .expect('Access-Control-Allow-Methods', /\bGET\b/) - .end(done); - }); - - it('can be disabled by configuration', function(done) { - var app = loopback(); - app.set('remoting', { cors: false }); - configureRestApiAndExplorer(app, '/explorer'); - - request(app) - .options('/explorer/swagger.json') - .end(function(err, res) { - if (err) return done(err); - - var allowOrigin = res.get('Access-Control-Allow-Origin'); - expect(allowOrigin, 'Access-Control-Allow-Origin') - .to.equal(undefined); - - done(); - }); - }); - }); - it('updates swagger object when a new model is added', function(done) { - var app = loopback(); - app.set('remoting', { cors: false }); + const app = loopback(); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app, '/explorer'); // Ensure the swagger object was built @@ -304,7 +323,7 @@ describe('explorer', function() { if (err) return done(err); // Create a new model - var Model = loopback.PersistedModel.extend('Customer'); + const Model = loopback.PersistedModel.extend('Customer'); Model.attachTo(loopback.memory()); app.model(Model); @@ -315,9 +334,9 @@ describe('explorer', function() { .end(function(err, res) { if (err) return done(err); - var modelNames = Object.keys(res.body.definitions); + const modelNames = Object.keys(res.body.definitions); expect(modelNames).to.contain('Customer'); - var paths = Object.keys(res.body.paths); + const paths = Object.keys(res.body.paths); expect(paths).to.have.contain('/Customers'); done(); @@ -326,11 +345,11 @@ describe('explorer', function() { }); it('updates swagger object when a model is removed', function(done) { - var app = loopback(); - app.set('remoting', { cors: false }); + const app = loopback(); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app, '/explorer'); - var Model = loopback.PersistedModel.extend('Customer'); + const Model = loopback.PersistedModel.extend('Customer'); Model.attachTo(loopback.memory()); app.model(Model); @@ -350,9 +369,9 @@ describe('explorer', function() { .end(function(err, res) { if (err) return done(err); - var modelNames = Object.keys(res.body.definitions); + const modelNames = Object.keys(res.body.definitions); expect(modelNames).to.not.contain('Customer'); - var paths = Object.keys(res.body.paths); + const paths = Object.keys(res.body.paths); expect(paths).to.not.contain('/Customers'); done(); @@ -361,8 +380,8 @@ describe('explorer', function() { }); it('updates swagger object when a remote method is disabled', function(done) { - var app = loopback(); - app.set('remoting', { cors: false }); + const app = loopback(); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app, '/explorer'); // Ensure the swagger object was built @@ -373,10 +392,10 @@ describe('explorer', function() { if (err) return done(err); // Check the method that will be disabled - var paths = Object.keys(res.body.paths); + const paths = Object.keys(res.body.paths); expect(paths).to.contain('/products/findOne'); - var Product = app.models.Product; + const Product = app.models.Product; Product.disableRemoteMethodByName('findOne'); // Request swagger.json again @@ -386,7 +405,7 @@ describe('explorer', function() { .end(function(err, res) { if (err) return done(err); - var paths = Object.keys(res.body.paths); + const paths = Object.keys(res.body.paths); expect(paths).to.not.contain('/products/findOne'); done(); @@ -395,8 +414,8 @@ describe('explorer', function() { }); it('updates swagger object when a remote method is added', function(done) { - var app = loopback(); - app.set('remoting', { cors: false }); + const app = loopback(); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app, '/explorer'); // Ensure the swagger object was built @@ -407,10 +426,10 @@ describe('explorer', function() { if (err) return done(err); // Check the method that will be disabled - var paths = Object.keys(res.body.paths); + const paths = Object.keys(res.body.paths); expect(paths).to.contain('/products/findOne'); - var Product = app.models.Product; + const Product = app.models.Product; Product.findOne2 = function(cb) { cb(null, 1); }; Product.remoteMethod('findOne2', {}); @@ -421,7 +440,7 @@ describe('explorer', function() { .end(function(err, res) { if (err) return done(err); - var paths = Object.keys(res.body.paths); + const paths = Object.keys(res.body.paths); expect(paths).to.contain('/products/findOne2'); done(); @@ -431,8 +450,8 @@ describe('explorer', function() { function givenLoopBackAppWithExplorer(explorerBase) { return function(done) { - var app = this.app = loopback(); - app.set('remoting', { cors: false }); + const app = this.app = loopback(); + app.set('remoting', {cors: false}); configureRestApiAndExplorer(app, explorerBase); done(); @@ -440,11 +459,11 @@ describe('explorer', function() { } function configureRestApiAndExplorer(app, explorerBase) { - var Product = loopback.PersistedModel.extend('product'); + const Product = loopback.PersistedModel.extend('product'); Product.attachTo(loopback.memory()); app.model(Product); - explorer(app, { mountPath: explorerBase }); + explorer(app, {mountPath: explorerBase}); app.set('legacyExplorer', false); app.use(app.get('restApiRoot') || '/', loopback.rest()); } diff --git a/test/fixtures/dummy-swagger-ui/swagger-ui.js b/test/fixtures/dummy-swagger-ui/swagger-ui.js index 4ed36a7..8cd9b8d 100644 --- a/test/fixtures/dummy-swagger-ui/swagger-ui.js +++ b/test/fixtures/dummy-swagger-ui/swagger-ui.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2014. All Rights Reserved. +// Copyright IBM Corp. 2014,2019. All Rights Reserved. // Node module: loopback-component-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT