├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ ├── Question.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example ├── hidden.js └── simple.js ├── index.js ├── intl ├── cs │ └── messages.json ├── de │ └── messages.json ├── en │ └── messages.json ├── es │ └── messages.json ├── fr │ └── messages.json ├── it │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── nl │ └── messages.json ├── pl │ └── messages.json ├── pt │ └── messages.json ├── ru │ └── messages.json ├── tr │ └── messages.json ├── zh-Hans │ └── messages.json └── zh-Hant │ └── messages.json ├── lib └── url-join.js ├── package.json ├── public ├── css │ └── loopbackStyles.css ├── images │ └── logo_small.png ├── index.html └── lib │ └── loadSwaggerUI.js └── test ├── .jshintrc ├── explorer.test.js └── fixtures └── dummy-swagger-ui ├── index.html └── swagger-ui.js /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | coverage 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback", 3 | "rules": { 4 | "max-len": ["error", 90, 4, { 5 | "ignoreComments": true, 6 | "ignoreUrls": true, 7 | "ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)" 8 | }] 9 | } 10 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | 18 | 19 | ## Steps to reproduce 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Expected Behavior 28 | 29 | 30 | 31 | ## Link to reproduction sandbox 32 | 33 | 37 | 38 | ## Additional information 39 | 40 | 45 | 46 | ## Related Issues 47 | 48 | 49 | 50 | _See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: feature 5 | 6 | --- 7 | 8 | ## Suggestion 9 | 10 | 11 | 12 | ## Use Cases 13 | 14 | 18 | 19 | ## Examples 20 | 21 | 22 | 23 | ## Acceptance criteria 24 | 25 | TBD - will be filled by the team. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. 4 | labels: question 5 | 6 | --- 7 | 8 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a security vulnerability 4 | url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues 5 | about: Do not report security vulnerabilities using GitHub issues. Please send an email to `reachsl@us.ibm.com` instead. 6 | - name: Get help on StackOverflow 7 | url: https://stackoverflow.com/tags/loopbackjs 8 | about: Please ask and answer questions on StackOverflow. 9 | - name: Join our mailing list 10 | url: https://groups.google.com/forum/#!forum/loopbackjs 11 | about: You can also post your question to our mailing list. 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Checklist 12 | 13 | 👉 [Read and sign the CLA (Contributor License Agreement)](https://cla.strongloop.com/agreements/strongloop/loopback-component-explorer) 👈 14 | 15 | - [ ] `npm test` passes on your machine 16 | - [ ] New tests added or existing tests modified to cover all changes 17 | - [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) 18 | - [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) 19 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | .idea 10 | *.iml 11 | *.tgz 12 | 13 | .idea 14 | pids 15 | logs 16 | results 17 | 18 | npm-debug.log 19 | node_modules 20 | 21 | LoopBackExplorer.iml 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "12" 7 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2020-03-06, Version 6.5.1 2 | ========================= 3 | 4 | * Update LTS status in README (Miroslav Bajtoš) 5 | 6 | * Cursor is made pointer of the add token button (Siraj Alam) 7 | 8 | 9 | 2019-11-28, Version 6.5.0 10 | ========================= 11 | 12 | * docs: describe GitHub advisory CVE-2019-17495 (Miroslav Bajtoš) 13 | 14 | * chore: improve README formatting (Miroslav Bajtoš) 15 | 16 | * Update README on swagger-ui (Diana Lau) 17 | 18 | * chore: improve issue and PR templates (Nora) 19 | 20 | * chore: add Node.js 12 to travis ci (Nora) 21 | 22 | * chore: drop support for Node.js 6 (Nora) 23 | 24 | * update LTS (Diana Lau) 25 | 26 | 27 | 2019-05-09, Version 6.4.0 28 | ========================= 29 | 30 | * chore: update copyrights years (Agnes Lin) 31 | 32 | * update README for LTS status (Diana Lau) 33 | 34 | * Updated README with small code fix. (Pradeep Kumar Tippa) 35 | 36 | * fix: update lodash (jannyHou) 37 | 38 | * Prevent accidental upgrades to swagger-ui v3+ (Miroslav Bajtoš) 39 | 40 | * Update eslint & config to latest major versions (Miroslav Bajtoš) 41 | 42 | * Update eslint & config to latest (Miroslav Bajtoš) 43 | 44 | 45 | 2018-10-18, Version 6.3.1 46 | ========================= 47 | 48 | * README: update LTS status (Miroslav Bajtoš) 49 | 50 | 51 | 2018-09-14, Version 6.3.0 52 | ========================= 53 | 54 | * Add config option for custom auth header (Jonathan Prince) 55 | 56 | * Fix config.json URL when running from /index.html (Jonathan Prince) 57 | 58 | 59 | 2018-07-24, Version 6.2.0 60 | ========================= 61 | 62 | * chore: update dependencies (Diana Lau) 63 | 64 | 65 | 2018-07-09, Version 6.1.0 66 | ========================= 67 | 68 | * [WebFM] cs/pl/ru translation (candytangnb) 69 | 70 | * remove package-lock.json (Diana Lau) 71 | 72 | * update dependencies (Diana Lau) 73 | 74 | 75 | 2018-05-10, Version 6.0.1 76 | ========================= 77 | 78 | * Update LTS information in README (Miroslav Bajtoš) 79 | 80 | * Enable Node.js 10 on Travis CI (Miroslav Bajtoš) 81 | 82 | 83 | 2018-04-18, Version 6.0.0 84 | ========================= 85 | 86 | * [SEMVER-MAJOR] Remove deprecated CORS support (Hiran del Castillo) 87 | 88 | * Add Travis CI configuration (Miroslav Bajtoš) 89 | 90 | 91 | 2018-04-17, Version 5.4.0 92 | ========================= 93 | 94 | * feat: rebuild swagger after a model was deleted (Miroslav Bajtoš) 95 | 96 | * update link for docs.strongloop.com (Diana Lau) 97 | 98 | * Revert "docs: update link from docs.strongloop.com" (Diana Lau) 99 | 100 | * docs: update link from docs.strongloop.com (Diana Lau) 101 | 102 | 103 | 2018-02-19, Version 5.3.0 104 | ========================= 105 | 106 | * Upgrade lodash from 3.x to 4.x (Miroslav Bajtoš) 107 | 108 | * chore: update license (Diana Lau) 109 | 110 | 111 | 2017-10-13, Version 5.2.0 112 | ========================= 113 | 114 | * update strong-globalize to 3.1.0 (shimks) 115 | 116 | 117 | 2017-09-15, Version 5.1.0 118 | ========================= 119 | 120 | * Refresh swagger on remoteMethodAdded (Raymond Feng) 121 | 122 | 123 | 2017-09-05, Version 5.0.0 124 | ========================= 125 | 126 | * Increment loopback-swagger ver (rashmihunt) 127 | 128 | * CODEOWNERS: turn off PR notifications for STRML (Miroslav Bajtoš) 129 | 130 | * version no to 5.0.0 (rashmihunt) 131 | 132 | * bump swagger version to 4.x (rashmihunt) 133 | 134 | * Add stalebot configuration (Kevin Delisle) 135 | 136 | * Update Issue and PR Templates (#216) (Sakib Hasan) 137 | 138 | * Update translated strings Q3 2017 (Allen Boone) 139 | 140 | * update translation file (Diana Lau) 141 | 142 | * Add CODEOWNER file (Diana Lau) 143 | 144 | * Replicate new issue_template from loopback (Siddhi Pai) 145 | 146 | * Replicate issue_template from loopback repo (Siddhi Pai) 147 | 148 | 149 | 2017-03-24, Version 4.2.0 150 | ========================= 151 | 152 | * Change Explorer header to use LoopBack (emckean) 153 | 154 | 155 | 2017-02-17, Version 4.1.1 156 | ========================= 157 | 158 | * Set z-index to 1 on header to fix styling bug (Imam Assidiqqi) 159 | 160 | * replace slc with lb (ivy ho) 161 | 162 | 163 | 2017-01-31, Version 4.1.0 164 | ========================= 165 | 166 | * bump loopback-swagger version to 3.0.1 (Jurien Hamaker) 167 | 168 | 169 | 2016-12-21, Version 4.0.0 170 | ========================= 171 | 172 | * Updates for LB3 release (Simon Ho) 173 | 174 | * Update paid support URL (Siddhi Pai) 175 | 176 | * Added another example for explorer Advanced Usage (Pratheek Hegde) 177 | 178 | * Start 3.x + drop support for Node v0.10/v0.12 (siddhipai) 179 | 180 | * Drop support for Node v0.10 and v0.12 (Siddhi Pai) 181 | 182 | * Start the development of the next major version (Siddhi Pai) 183 | 184 | * CSS line terminations (Wisu Suntoyo) 185 | 186 | * Fix jsoneditor not defined error (David Cheung) 187 | 188 | 189 | 2016-10-14, Version 3.0.0 190 | ========================= 191 | 192 | * Update to mainline swagger-ui (Samuel Reed) 193 | 194 | * Start 3.0 development (Miroslav Bajtoš) 195 | 196 | 197 | 2016-10-12, Version 2.7.0 198 | ========================= 199 | 200 | * Update translation files - round#2 (Candy) 201 | 202 | * Add translated files (gunjpan) 203 | 204 | * Use new api for disabling a remote method. (Richard Pringle) 205 | 206 | * Fix tests to not depend on exact EOL chars (Miroslav Bajtoš) 207 | 208 | * Update deps to loopback 3.0.0 RC (Miroslav Bajtoš) 209 | 210 | * Deprecate built-in CORS middleware (Miroslav Bajtoš) 211 | 212 | * Use loopback@3.0.0-alpha for running the tests. (Miroslav Bajtoš) 213 | 214 | 215 | 2016-09-05, Version 2.6.0 216 | ========================= 217 | 218 | * Add globalization (Simon Ho) 219 | 220 | * Update URLs in CONTRIBUTING.md (#169) (Ryan Graham) 221 | 222 | * Redirect get http 301 instead of 303 (jannyHou) 223 | 224 | * Add blank lines to separate error-checking and done logic from other logic (Supasate Choochaisri) 225 | 226 | * Ignore copyright in dummy swagger-ui test (Supasate Choochaisri) 227 | 228 | * Upgrade loopback devDependency (Supasate Choochaisri) 229 | 230 | * update copyright notices and license (Ryan Graham) 231 | 232 | * examples launch fix (Alexander Ryzhikov) 233 | 234 | 235 | 2016-05-02, Version 2.5.0 236 | ========================= 237 | 238 | * Add feature to hide disabled remote methods after explorer is initialized (Supasate Choochaisri) 239 | 240 | * More fixes of indentation in index.js (Miroslav Bajtoš) 241 | 242 | * Fix broken indentation (Miroslav Bajtoš) 243 | 244 | * Fix linting errors (Amir Jafarian) 245 | 246 | * Auto-update by eslint --fix (Amir Jafarian) 247 | 248 | * Add eslint infrastructure (Amir Jafarian) 249 | 250 | 251 | 2016-03-08, Version 2.4.0 252 | ========================= 253 | 254 | * Add `swaggerUI` option to enable/disable UI serving (Raymond Feng) 255 | 256 | 257 | 2016-02-02, Version 2.3.0 258 | ========================= 259 | 260 | 261 | 262 | 2016-01-13, Version 2.2.0 263 | ========================= 264 | 265 | * remove references to ubuntu font (Anthony Ettinger) 266 | 267 | * Update swaggerObject when a new model was added (Pradeep Kumar Tippa) 268 | 269 | * Refer to licenses with a link (Sam Roberts) 270 | 271 | 272 | 2015-10-01, Version 2.1.1 273 | ========================= 274 | 275 | * disable Swagger validation badge (Hage Yaapa) 276 | 277 | * Updated "resourcePath: 'swaggerResources'" to "resourcePath: 'swagger.json'" (Dennis Ashby) 278 | 279 | * Use strongloop conventions for licensing (Sam Roberts) 280 | 281 | 282 | 2015-09-17, Version 2.1.0 283 | ========================= 284 | 285 | * Rename the module to loopback-component-explorer (Rand McKinney) 286 | 287 | 288 | 2015-09-08, Version 2.0.1 289 | ========================= 290 | 291 | * Sort APIs and operations. (Samuel Reed) 292 | 293 | 294 | 2015-09-04, Version 2.0.0 295 | ========================= 296 | 297 | * Use loopback-swagger to generate swagger.json (Miroslav Bajtoš) 298 | 299 | * Bump up strong-swagger-ui version to ^21.0.0 (Miroslav Bajtoš) 300 | 301 | * Register loopback-explorer to app (Hage Yaapa) 302 | 303 | * Generate Swagger Spec 2.0 documentation (Miroslav Bajtoš) 304 | 305 | * Upgrade to strong-swagger-ui@21.0 (swagger-ui@2.1) (Miroslav Bajtoš) 306 | 307 | * bump major version (Ryan Graham) 308 | 309 | * Rework the module to a loopback component (Miroslav Bajtoš) 310 | 311 | * Add `opts.host` to customize host of resource URLs (cndreiter) 312 | 313 | * Removed branch-lock, and bumped version (Shelby Sanders) 314 | 315 | * Corrected to propagate properties from existing items object (Shelby Sanders) 316 | 317 | * Use strong-swagger-ui instead of swagger-ui (Miroslav Bajtoš) 318 | 319 | * Remove public/images/throbber.gif (Miroslav Bajtoš) 320 | 321 | * Move CSS customizations to loopbackStyles.css (Miroslav Bajtoš) 322 | 323 | * Added Swagger fields for items and max/min(Items|Length) (Shelby Sanders) 324 | 325 | * Corrected accidental duplication of responseMessages from merge (Shelby Sanders) 326 | 327 | * review comments (Ying Tang) 328 | 329 | * add more tests (Ying Tang) 330 | 331 | * float additionalProperties and description to top (Ying Tang) 332 | 333 | * Convert array to string for summary, note, and description. Fix additionalProperties (Ying Tang) 334 | 335 | * back out changes of id to URI (Ying Tang) 336 | 337 | * Uri id and $ref, join description (Ying Tang) 338 | 339 | * propertyName, not property (Ying Tang) 340 | 341 | * fix resource listing and remove id from each property (Ying Tang) 342 | 343 | * remove id fields from required array (Ying Tang) 344 | 345 | * remove required from sub-schema (Ying Tang) 346 | 347 | * bump version (Ying Tang) 348 | 349 | * add $ref and remove type for models (Ying Tang) 350 | 351 | * Changed Swagger() to omit resources with no content (Shelby Sanders) 352 | 353 | * Added event emission for swaggerResources to support customization (Shelby Sanders) 354 | 355 | * Corrected handling of type for operation, including containers (Shelby Sanders) 356 | 357 | * Corrected handling for absent settings.additionalProperties (Shelby Sanders) 358 | 359 | * Added support for public in order to hide operations from Swagger (Shelby Sanders) 360 | 361 | * added reference to settings for additional properties (Jake Ayala) 362 | 363 | * Protected against non-Model generation requests (Shelby Sanders) 364 | 365 | * Corrected merge issues (Shelby Sanders) 366 | 367 | * Added padding to content well in order to counteract changes in SwaggerUI (Shelby Sanders) 368 | 369 | * Added support for scanning accepts params for Model references (Shelby Sanders) 370 | 371 | * Changed addRoute() to honor X-Forwarded-Host (Shelby Sanders) 372 | 373 | * Removed branch-lock for loopback (Shelby Sanders) 374 | 375 | * Changed to possibly pull model description from ctor.settings (Shelby Sanders) 376 | 377 | * Corrected generateModelDefinition() to scan for model references nested in other models (Shelby Sanders) 378 | 379 | * Corrected prepareDataType() to handle collections and nesting, and changed to always and only use responseMessages (Shelby Sanders) 380 | 381 | * Corrected generateModelDefinition() to scan for model references in remote returns and errors (Shelby Sanders) 382 | 383 | * Corrected default for consumes+produces (Shelby Sanders) 384 | 385 | * Ported prepareDataType() from old strong-remoting:ext/swagger.js (Shelby Sanders) 386 | 387 | * Corrected issues with merge of 2.x changes (Shelby Sanders) 388 | 389 | * Load swagger ui from `swagger-ui` package instead. (Samuel Reed) 390 | 391 | * Ported extensions for more Swagger 1.2 metadata, returns+errors as responseMessages, consumes+produces, and X-Forwarded-Proto for reverse-proxying from HTTPS to HTTP (Shelby Sanders) 392 | 393 | * Upgraded to SwaggerUI 2.0.18 (Shelby Sanders) 394 | 395 | * Added support for toggling Model and Schema, and added support for primitives in StatusCodeView (Shelby Sanders) 396 | 397 | * Ensure Response Content Type is shown regardless of Response Class (Shelby Sanders) 398 | 399 | * Reverted to use special loading logic from loopback-explorer (Shelby Sanders) 400 | 401 | * Updated to latest Swagger-UI for better responseMessage signature handling (Shelby Sanders) 402 | 403 | * Upgraded to latest Swagger-UI for 1.2 support (Shelby Sanders) 404 | 405 | * Correct description of collections of object and nested (Shelby Sanders) 406 | 407 | * Add indication of response being a collection (Shelby Sanders) 408 | 409 | 410 | 2015-06-25, Version 1.8.0 411 | ========================= 412 | 413 | * Add opts.omitProtocolInBaseUrl (Miroslav Bajtoš) 414 | 415 | * Fix tests broken by fa3035c (#96) (Miroslav Bajtoš) 416 | 417 | * Fix model description getting lost (bkniffler) 418 | 419 | 420 | 2015-03-30, Version 1.7.2 421 | ========================= 422 | 423 | * Allow submitting token input with empty value to remove token. (Samuel Reed) 424 | 425 | * Fix duplicate stylesheet issue (Pradnya Baviskar) 426 | 427 | * Fix explorer tests for different line endings on Windows (Pradnya Baviskar) 428 | 429 | 430 | 2015-02-23, Version 1.7.1 431 | ========================= 432 | 433 | * Remove unused external font "Droid Sans". (Miroslav Bajtoš) 434 | 435 | 436 | 2015-02-17, Version 1.7.0 437 | ========================= 438 | 439 | * Made API doc of class use the http.path of the class if available, or the name of the class as a fallback (gandrianakis) 440 | 441 | 442 | 2015-01-09, Version 1.6.4 443 | ========================= 444 | 445 | * Prevent double slash in the resource URLs (Miroslav Bajtoš) 446 | 447 | * Allow `uiDirs` to be defined as a String (Simon Ho) 448 | 449 | * Save accessToken in localStorage. Fixes #47 (Samuel Reed) 450 | 451 | 452 | 2015-01-06, Version 1.6.3 453 | ========================= 454 | 455 | * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) 456 | 457 | * Add X-UA-Compatible tag (Nick Van Dyck) 458 | 459 | 460 | 2014-12-12, Version 1.6.2 461 | ========================= 462 | 463 | * Move 200 response to `type` on the operation object. See #75. (Samuel Reed) 464 | 465 | 466 | 2014-12-08, Version 1.6.1 467 | ========================= 468 | 469 | * Use full lodash instead of lodash components (Ryan Graham) 470 | 471 | 472 | 2014-12-02, Version 1.6.0 473 | ========================= 474 | 475 | * Remove model name from nickname, swagger spec understands op context. (Samuel Reed) 476 | 477 | 478 | 2014-11-29, Version 1.5.2 479 | ========================= 480 | 481 | * model-helper: ignore unknown property types (Miroslav Bajtoš) 482 | 483 | 484 | 2014-10-24, Version 1.5.1 485 | ========================= 486 | 487 | 488 | 489 | 2014-10-24, Version 1.5.0 490 | ========================= 491 | 492 | * Add an option `uiDirs` (Miroslav Bajtoš) 493 | 494 | * swagger: honour X-Forwarded-Proto header (Miroslav Bajtoš) 495 | 496 | 497 | 2014-10-21, Version 1.4.0 498 | ========================= 499 | 500 | * Bump version (Raymond Feng) 501 | 502 | * Add integration tests for included models (Miroslav Bajtoš) 503 | 504 | * route-helper: add `responseMessages` (Miroslav Bajtoš) 505 | 506 | * model-helper: support anonymous object types (Miroslav Bajtoš) 507 | 508 | * swagger: include models from accepts/returns args (Miroslav Bajtoš) 509 | 510 | * loopbackStyles: improve spacing in small window (Miroslav Bajtoš) 511 | 512 | * swagger: Deprecate `opts.swaggerVersion` (Miroslav Bajtoš) 513 | 514 | * swagger: use X-Forwarded-Host for basePath (Miroslav Bajtoš) 515 | 516 | * example: use PersistedModel instead of Model (Miroslav Bajtoš) 517 | 518 | * models: include model's `description` (Miroslav Bajtoš) 519 | 520 | * Refactor conversion of data types (Miroslav Bajtoš) 521 | 522 | * Move `convertText` to `typeConverter` (Miroslav Bajtoš) 523 | 524 | * Add support for `context` and `res` param types (Krishna Raman) 525 | 526 | * package: update devDependencies (Miroslav Bajtoš) 527 | 528 | * gitignore: add .idea, *.tgz, *.iml (Miroslav Bajtoš) 529 | 530 | * Support multi-line array `description` and `notes` (Miroslav Bajtoš) 531 | 532 | * Use `1.0.0` as the default app version. (Miroslav Bajtoš) 533 | 534 | * Extend `consumes` and `produces` metadata (Miroslav Bajtoš) 535 | 536 | * route-helper: include `notes` and `deprecated` (Miroslav Bajtoš) 537 | 538 | * Pull model description from ctor.settings first (Shelby Sanders) 539 | 540 | 541 | 2014-10-08, Version 1.3.0 542 | ========================= 543 | 544 | * swagger: allow cross-origin requests (Miroslav Bajtoš) 545 | 546 | * Sort endpoints by letter. (Samuel Reed) 547 | 548 | * Add syntax highlighting styles & highlight threshold. (Samuel Reed) 549 | 550 | * Add contribution guidelines (Ryan Graham) 551 | 552 | 553 | 2014-09-22, Version 1.2.11 554 | ========================== 555 | 556 | * Bump version (Raymond Feng) 557 | 558 | * Fix how the array of models is iterated (Raymond Feng) 559 | 560 | 561 | 2014-09-05, Version 1.2.10 562 | ========================== 563 | 564 | * Bump version (Raymond Feng) 565 | 566 | * Make sure nested/referenced models in array are mapped to swagger (Clark Wang) 567 | 568 | * Make sure nested/referenced models are mapped to swagger (Raymond Feng) 569 | 570 | 571 | 2014-08-15, Version 1.2.9 572 | ========================= 573 | 574 | * Bump version (Raymond Feng) 575 | 576 | * Newest Swagger UI requires application/x-www-form-urlencoded. (Samuel Reed) 577 | 578 | * Use `dist` property from swagger-ui package. (Samuel Reed) 579 | 580 | * Fixed undefined modelClass when using polymorphic relations (Navid Nikpour) 581 | 582 | 583 | 2014-08-08, Version 1.2.8 584 | ========================= 585 | 586 | * Bump version (Raymond Feng) 587 | 588 | * Fix the type name for a property if model class is used (Raymond Feng) 589 | 590 | 591 | 2014-08-04, Version 1.2.7 592 | ========================= 593 | 594 | * Bump version (Raymond Feng) 595 | 596 | * Set up default consumes/produces media types (Raymond Feng) 597 | 598 | * Fix the default opts (Raymond Feng) 599 | 600 | * Add required swagger 1.2 items property for property type array (Ritchie Martori) 601 | 602 | * Allow passing a custom protocol. (Samuel Reed) 603 | 604 | 605 | 2014-07-29, Version 1.2.6 606 | ========================= 607 | 608 | * Bump version (Raymond Feng) 609 | 610 | * res.send deprecated - updated to res.status (Geoffroy) 611 | 612 | * Remove hidden properties from definition. (Samuel Reed) 613 | 614 | 615 | 2014-07-25, Version 1.2.5 616 | ========================= 617 | 618 | * Bump version (Raymond Feng) 619 | 620 | * Ensure models from relations are included (Raymond Feng) 621 | 622 | 623 | 2014-07-22, Version 1.2.4 624 | ========================= 625 | 626 | * model-helper: handle arrays with undefined items (Miroslav Bajtoš) 627 | 628 | 629 | 2014-07-22, Version 1.2.3 630 | ========================= 631 | 632 | * model-helper: handle array types with no item type (Miroslav Bajtoš) 633 | 634 | 635 | 2014-07-20, Version 1.2.2 636 | ========================= 637 | 638 | * Bump version (Raymond Feng) 639 | 640 | * Properly convert complex return types. (Samuel Reed) 641 | 642 | 643 | 2014-07-18, Version 1.2.1 644 | ========================= 645 | 646 | * Bump version (Raymond Feng) 647 | 648 | * Fix up loopback.rest() model definition hack. (Samuel Reed) 649 | 650 | 651 | 2014-07-14, Version 1.2.0 652 | ========================= 653 | 654 | * Bump version and update deps (Raymond Feng) 655 | 656 | * s/accessToken/access_token in authorization key name (Samuel Reed) 657 | 658 | * Fix resources if the explorer is at a deep path. (Samuel Reed) 659 | 660 | * Fix debug namespace, express version. (Samuel Reed) 661 | 662 | * Remove forgotten TODO. (Samuel Reed) 663 | 664 | * Simplify `accepts` and `returns` hacks. (Samuel Reed) 665 | 666 | * More consise type tests (Samuel Reed) 667 | 668 | * Remove preMiddleware. (Samuel Reed) 669 | 670 | * Remove swagger.test.js license (Samuel Reed) 671 | 672 | * Remove peerDependencies, use express directly. (Samuel Reed) 673 | 674 | * Add url-join so path.join() doesn't break windows (Samuel Reed) 675 | 676 | * Rename translateKeys to translateDataTypeKeys. (Samuel Reed) 677 | 678 | * Refactor route-helper & add tests. (Samuel Reed) 679 | 680 | * LDL to Swagger fixes & extensions. (Samuel Reed) 681 | 682 | * Use express routes instead of modifying remoting. (Samuel Reed) 683 | 684 | * Fix missing strong-remoting devDependency. (Samuel Reed) 685 | 686 | * Restore existing styles. (Samuel Reed) 687 | 688 | * Allow easy setting of accessToken in explorer UI. (Samuel Reed) 689 | 690 | * Refactor key translations between LDL & Swagger. (Samuel Reed) 691 | 692 | * Refactoring swagger 1.2 rework. (Samuel Reed) 693 | 694 | * Make sure body parameter is shown. (Raymond Feng) 695 | 696 | * Some swagger 1.2 migration cleanup. (Samuel Reed) 697 | 698 | * Fix api resource path and type ref to models. (Raymond Feng) 699 | 700 | * Swagger 1.2 compatability. Moved strong-remoting/ext/swagger to this module. (Samuel Reed) 701 | 702 | * Load swagger ui from `swagger-ui` package instead. (Samuel Reed) 703 | 704 | 705 | 2014-05-28, Version 1.1.1 706 | ========================= 707 | 708 | * package.json: add support for loopback 2.x (Miroslav Bajtoš) 709 | 710 | * Make sure X-Powered-By header is disabled (Alex Pica) 711 | 712 | * Fix license url (Raymond Feng) 713 | 714 | * Update to dual MIT/StrongLoop license (Raymond Feng) 715 | 716 | 717 | 2014-01-14, Version 1.1.0 718 | ========================= 719 | 720 | * Bump up loopback min version to 1.5 (Miroslav Bajtoš) 721 | 722 | * Use `app.get('restApiRoot')` as default basePath (Miroslav Bajtoš) 723 | 724 | * Replace strong-remoting ext/swagger with app.docs (Miroslav Bajtoš) 725 | 726 | 727 | 2014-01-13, Version 1.0.2 728 | ========================= 729 | 730 | * Bump version (Raymond Feng) 731 | 732 | * README: mount REST at /api in the sample code (Miroslav Bajtos) 733 | 734 | * Reorder middleware to fix unit-test failures. (Miroslav Bajtos) 735 | 736 | * Fix loading of loopback dependencies. (Miroslav Bajtos) 737 | 738 | 739 | 2013-12-04, Version 1.0.1 740 | ========================= 741 | 742 | * First release! 743 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precendence. 4 | 5 | # People watching and approving all pull requests 6 | * @bajtos 7 | 8 | # Maintainers that do not wish to be notified about new pull requests 9 | _ @STRML 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-component-explorer`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-component-explorer` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-component-explorer) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2013,2017. All Rights Reserved. 2 | Node module: loopback-component-explorer 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-component-explorer 2 | 3 | **⚠️ LoopBack 3 is in Maintenance LTS mode, only critical bugs and critical 4 | security fixes will be provided. (See 5 | [Module Long Term Support Policy](#module-long-term-support-policy) below.)** 6 | 7 | We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as 8 | soon as possible. Refer to our 9 | [Migration Guide](https://loopback.io/doc/en/lb4/migration-overview.html) 10 | for more information on how to upgrade. 11 | 12 | 13 | ## Overview 14 | 15 | Browse and test your LoopBack app's APIs. 16 | 17 | ## Basic Usage 18 | 19 | Below is a simple LoopBack application. The explorer is mounted at `/explorer`. 20 | 21 | ```js 22 | var loopback = require('loopback'); 23 | var app = loopback(); 24 | var explorer = require('../'); 25 | var port = 3000; 26 | 27 | var Product = loopback.Model.extend('product'); 28 | Product.attachTo(loopback.memory()); 29 | app.model(Product); 30 | 31 | app.use('/api', loopback.rest()); 32 | 33 | // Register explorer using component-centric API: 34 | explorer(app, { basePath: '/api', mountPath: '/explorer' }); 35 | // Alternatively, register as a middleware: 36 | app.use('/explorer', explorer.routes(app, { basePath: '/api' })); 37 | 38 | console.log("Explorer mounted at localhost:" + port + "/explorer"); 39 | 40 | app.listen(port); 41 | ``` 42 | 43 | ## A note on swagger-ui vulnerabilities 44 | 45 | API Explorer for LoopBack 3 is built on top of `swagger-ui` version 2.x which 46 | is no longer maintained. While there are known security vulnerabilities in 47 | `swagger-ui`, we believe they don't affect LoopBack users. 48 | 49 | We would love to upgrade our (LB3) API Explorer to v3 of swagger-ui, but 50 | unfortunately such upgrade requires too much effort and more importantly 51 | addition of new features to LB3 runtime, which would break our LTS guarantees. 52 | For more details, see discussion in 53 | [loopback-component-explorer#263](https://github.com/strongloop/loopback-component-explorer/issues/263). 54 | 55 | ### npm advisory 985 56 | 57 | Link: https://www.npmjs.com/advisories/985 58 | 59 | > Versions of swagger-ui prior to 3.0.13 are vulnerable to Cross-Site Scripting 60 | > (XSS). The package fails to sanitize YAML files imported from URLs or 61 | > copied-pasted. This may allow attackers to execute arbitrary JavaScript. 62 | 63 | LoopBack's API Explorer does not allow clients to import swagger spec from YAML 64 | URL/pasted-content. That means loopback-component-explorer **IS NOT AFFECTED** 65 | by this vulnerability. 66 | 67 | ### npm advisory 975 68 | 69 | Link: https://www.npmjs.com/advisories/975 70 | 71 | > Versions of swagger-ui prior to 3.18.0 are vulnerable to Reverse Tabnapping. 72 | > The package uses `target='_blank'` in anchor tags, allowing attackers to 73 | > access `window.opener` for the original page. This is commonly used for 74 | > phishing attacks. 75 | 76 | This vulnerability affects anchor tags created from metadata provided by the 77 | Swagger spec, for example `info.termsOfServiceUrl`. LoopBack's API Explorer 78 | does not allow clients to provide custom swagger spec, URLs like 79 | `info.termsOfServiceUrl` are fully in control of the LoopBack application 80 | developer. That means loopback-component-explorer **IS NOT AFFECTED** by this 81 | vulnerability. 82 | 83 | ### npm advisory 976 84 | 85 | Link: https://www.npmjs.com/advisories/976 86 | 87 | > Versions of swagger-ui prior to 3.20.9 are vulnerable to Cross-Site Scripting 88 | > (XSS). The package fails to sanitize URLs used in the OAuth auth flow, which 89 | > may allow attackers to execute arbitrary JavaScript. 90 | 91 | LoopBack 3 API Explorer does not support OAuth auth flow, that means 92 | loopback-component-explorer **IS NOT AFFECTED** by this vulnerability. 93 | 94 | ### GitHub advisory CVE-2019-17495 95 | 96 | Link: https://github.com/advisories/GHSA-c427-hjc3-wrfw 97 | > A Cascading Style Sheets (CSS) injection vulnerability in Swagger UI before 98 | > 3.23.11 allows attackers to use the Relative Path Overwrite (RPO) technique 99 | > to perform CSS-based input field value exfiltration, such as exfiltration of 100 | > a CSRF token value. 101 | 102 | Quoting from the 103 | [disclosure](https://github.com/tarantula-team/CSS-injection-in-Swagger-UI/tree/15edeaaa5806aa8e83ee55d883f956a3c3573ac9): 104 | 105 | > We’ve observed that the `?url=` parameter in SwaggerUI allows an attacker to 106 | > override an otherwise hard-coded schema file. We realize that Swagger UI 107 | > allows users to embed untrusted Json format from remote servers This means we 108 | > can inject json content via the GET parameter to victim Swagger UI. etc. 109 | 110 | LoopBack 3 API Explorer does not suport `?url=` parameter, it always loads the 111 | Swagger spec file from the LoopBack server serving the Explorer UI. That means 112 | loopback-component-explorer **IS NOT AFFECTED** by this vulnerability. 113 | 114 | ## Upgrading from v1.x 115 | 116 | To upgrade your application using loopback-explorer version 1.x, just replace 117 | `explorer()` with `explorer.routes()` in your server script: 118 | 119 | ```js 120 | var explorer = require('loopback-component-explorer'); // Module was loopback-explorer in v. 2.0.1 and earlier 121 | 122 | // v1.x - does not work anymore 123 | app.use('/explorer', explorer(app, options)); 124 | // v2.x 125 | app.use('/explorer', explorer.routes(app, options)); 126 | ``` 127 | 128 | In applications scaffolded by `lb app`, the idiomatic way is to register 129 | loopback-component-explorer in `server/component-config.json`: 130 | 131 | ```json 132 | { 133 | "loopback-component-explorer": { 134 | "mountPath": "/explorer" 135 | } 136 | } 137 | ``` 138 | 139 | ## Advanced Usage 140 | 141 | Many aspects of the explorer are configurable. 142 | 143 | See [options](#options) for a description of these options: 144 | 145 | ```js 146 | // Mount middleware before calling `explorer()` to add custom headers, auth, etc. 147 | app.use('/explorer', loopback.basicAuth('user', 'password')); 148 | explorer(app, { 149 | basePath: '/custom-api-root', 150 | uiDirs: [ 151 | path.resolve(__dirname, 'public'), 152 | path.resolve(__dirname, 'node_modules', 'swagger-ui') 153 | ] 154 | apiInfo: { 155 | 'title': 'My API', 156 | 'description': 'Explorer example app.' 157 | }, 158 | resourcePath: 'swagger.json', 159 | version: '0.1-unreleasable' 160 | })); 161 | app.use('/custom-api-root', loopback.rest()); 162 | ``` 163 | In applications scaffolded by `lb app`, you can edit the `server/component-config.json`: 164 | 165 | ```json 166 | { 167 | "loopback-component-explorer": { 168 | "mountPath": "/explorer", 169 | "apiInfo": { 170 | "title": "My App", 171 | "description": "Description of my app APIs.", 172 | "termsOfServiceUrl": "http://api.mycompany.io/terms/", 173 | "contact": "apiteam@mycompany.io", 174 | "license": "Apache 2.0", 175 | "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html" 176 | } 177 | } 178 | } 179 | ``` 180 | 181 | ## Options 182 | 183 | Options are passed to `explorer(app, options)`. 184 | 185 | `basePath`: **String** 186 | 187 | > Default: `app.get('restAPIRoot')` or `'/api'`. 188 | 189 | > Sets the API's base path. This must be set if you are mounting your api 190 | > to a path different than '/api', e.g. with 191 | > `loopback.use('/custom-api-root', loopback.rest()); 192 | 193 | `mountPath`: **String** 194 | 195 | > Default: `/explorer` 196 | 197 | > Set the path where to mount the explorer component. 198 | 199 | `protocol`: **String** 200 | 201 | > Default: `null` 202 | 203 | > A hard override for the outgoing protocol (`http` or `https`) that is designated in Swagger 204 | > resource documents. By default, `loopback-component-explorer` will write the protocol that was used to retrieve 205 | > the doc. This option is useful if, for instance, your API sits behind an SSL terminator 206 | > and thus needs to report its endpoints as `https`, even though incoming traffic is auto-detected 207 | > as `http`. 208 | 209 | `uiDirs`: **Array of Strings** 210 | 211 | > Sets a list of paths within your application for overriding Swagger UI files. 212 | 213 | > If present, will search `uiDirs` first when attempting to load Swagger UI, 214 | > allowing you to pick and choose overrides to the interface. Use this to 215 | > style your explorer or add additional functionality. 216 | 217 | > See [index.html](public/index.html), where you may want to begin your overrides. 218 | > The rest of the UI is provided by [Swagger UI](https://github.com/wordnik/swagger-ui). 219 | 220 | `apiInfo`: **Object** 221 | 222 | > Additional information about your API. See the 223 | > [spec](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#513-info-object). 224 | 225 | `resourcePath`: **String** 226 | 227 | > Default: `'resources'` 228 | 229 | > Sets a different path for the 230 | > [resource listing](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#51-resource-listing). 231 | > You generally shouldn't have to change this. 232 | 233 | `version`: **String** 234 | 235 | > Default: Read from package.json 236 | 237 | > Sets your API version. If not present, will read from your app's package.json. 238 | 239 | `auth`: **Object** 240 | 241 | > Optional config for setting api access token, can be used to rename the query parameter or set an auth header. 242 | 243 | > The object has 2 keys: 244 | > - `in`: either `header` or `query` 245 | > - `name`: the name of the query parameter or header 246 | > 247 | > The default sets the token as a query parameter with the name `access_token` 248 | 249 | > Example for setting the api key in a header named `x-api-key`: 250 | > ``` 251 | > { 252 | > "loopback-component-explorer": { 253 | > "mountPath": "/explorer", 254 | > "auth": { 255 | > "in": "header", 256 | > "name": "x-api-key" 257 | > } 258 | > } 259 | > } 260 | > ``` 261 | 262 | ## Module Long Term Support Policy 263 | 264 | This module adopts the [ 265 | Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, 266 | with the following End Of Life (EOL) dates: 267 | 268 | | Version | Status | Published | EOL | 269 | | ------- | --------------- | --------- | -------- | 270 | | 6.x | Maintenance LTS | Apr 2018 | Dec 2020 | 271 | | 5.x | End-of-Life | Sep 2017 | Dec 2019 | 272 | 273 | Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). 274 | -------------------------------------------------------------------------------- /example/hidden.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const g = require('strong-globalize')(); 9 | 10 | const loopback = require('loopback'); 11 | const app = loopback(); 12 | const explorer = require('../'); 13 | const port = 3000; 14 | 15 | const User = loopback.Model.extend('user', { 16 | username: 'string', 17 | email: 'string', 18 | sensitiveInternalProperty: 'string', 19 | }, {hidden: ['sensitiveInternalProperty']}); 20 | 21 | User.attachTo(loopback.memory()); 22 | app.model(User); 23 | 24 | const apiPath = '/api'; 25 | explorer(app, {basePath: apiPath}); 26 | app.use(apiPath, loopback.rest()); 27 | g.log('{{Explorer}} mounted at {{localhost:%s/explorer}}', port); 28 | 29 | app.listen(port); 30 | -------------------------------------------------------------------------------- /example/simple.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const g = require('strong-globalize')(); 9 | 10 | const loopback = require('loopback'); 11 | const app = loopback(); 12 | const explorer = require('../'); 13 | const port = 3000; 14 | 15 | const Product = loopback.PersistedModel.extend('product', { 16 | foo: {type: 'string', required: true}, 17 | bar: 'string', 18 | aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}, 19 | }); 20 | Product.attachTo(loopback.memory()); 21 | app.model(Product); 22 | 23 | const apiPath = '/api'; 24 | explorer(app, {basePath: apiPath}); 25 | app.use(apiPath, loopback.rest()); 26 | g.log('{{Explorer}} mounted at {{http://localhost:%s/explorer}}', port); 27 | 28 | app.listen(port); 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const SG = require('strong-globalize'); 9 | SG.SetRootDir(__dirname); 10 | const g = SG(); 11 | 12 | /*! 13 | * Adds dynamically-updated docs as /explorer 14 | */ 15 | const url = require('url'); 16 | const path = require('path'); 17 | const urlJoin = require('./lib/url-join'); 18 | const _defaults = require('lodash').defaults; 19 | const createSwaggerObject = require('loopback-swagger').generateSwaggerSpec; 20 | const SWAGGER_UI_ROOT = require('swagger-ui/index').dist; 21 | const STATIC_ROOT = path.join(__dirname, 'public'); 22 | 23 | module.exports = explorer; 24 | explorer.routes = routes; 25 | 26 | /** 27 | * Example usage: 28 | * 29 | * var explorer = require('loopback-component-explorer'); 30 | * explorer(app, options); 31 | */ 32 | 33 | function explorer(loopbackApplication, options) { 34 | options = _defaults({}, options, {mountPath: '/explorer'}); 35 | loopbackApplication.use( 36 | options.mountPath, 37 | routes(loopbackApplication, options) 38 | ); 39 | loopbackApplication.set('loopback-component-explorer', options); 40 | } 41 | 42 | function routes(loopbackApplication, options) { 43 | const loopback = loopbackApplication.loopback; 44 | const loopbackMajor = 45 | (loopback && loopback.version && loopback.version.split('.')[0]) || 1; 46 | 47 | if (loopbackMajor < 2) { 48 | throw new Error( 49 | g.f( 50 | '{{loopback-component-explorer}} requires ' + 51 | '{{loopback}} 2.0 or newer' 52 | ) 53 | ); 54 | } 55 | 56 | options = _defaults({}, options, { 57 | resourcePath: 'swagger.json', 58 | apiInfo: loopbackApplication.get('apiInfo') || {}, 59 | swaggerUI: true, 60 | }); 61 | 62 | const router = new loopback.Router(); 63 | 64 | mountSwagger(loopbackApplication, router, options); 65 | 66 | // config.json is loaded by swagger-ui. The server should respond 67 | // with the relative URI of the resource doc. 68 | router.get('/config.json', function(req, res) { 69 | // Get the path we're mounted at. It's best to get this from the referer 70 | // in case we're proxied at a deep path. 71 | let source = url.parse(req.headers.referer || '').pathname; 72 | // strip index.html if present in referer 73 | if (source && /\/index\.html$/.test(source)) { 74 | source = source.replace(/\/index\.html$/, ''); 75 | } 76 | // If no referer is available, use the incoming url. 77 | if (!source) { 78 | source = req.originalUrl.replace(/\/config.json(\?.*)?$/, ''); 79 | } 80 | res.send({ 81 | url: urlJoin(source, '/' + options.resourcePath), 82 | auth: options.auth, 83 | }); 84 | }); 85 | 86 | if (options.swaggerUI) { 87 | // Allow specifying a static file roots for swagger files. Any files in 88 | // these folders will override those in the swagger-ui distribution. 89 | // In this way one could e.g. make changes to index.html without having 90 | // to worry about constantly pulling in JS updates. 91 | if (options.uiDirs) { 92 | if (typeof options.uiDirs === 'string') { 93 | router.use(loopback.static(options.uiDirs)); 94 | } else if (Array.isArray(options.uiDirs)) { 95 | options.uiDirs.forEach(function(dir) { 96 | router.use(loopback.static(dir)); 97 | }); 98 | } 99 | } 100 | 101 | // File in node_modules are overridden by a few customizations 102 | router.use(loopback.static(STATIC_ROOT)); 103 | 104 | // Swagger UI distribution 105 | router.use(loopback.static(SWAGGER_UI_ROOT)); 106 | } 107 | 108 | return router; 109 | } 110 | 111 | /** 112 | * Setup Swagger documentation on the given express app. 113 | * 114 | * @param {Application} loopbackApplication The loopback application to 115 | * document. 116 | * @param {Application} swaggerApp Swagger application used for hosting 117 | * swagger documentation. 118 | * @param {Object} opts Options. 119 | */ 120 | function mountSwagger(loopbackApplication, swaggerApp, opts) { 121 | let swaggerObject = createSwaggerObject(loopbackApplication, opts); 122 | 123 | // listening to modelRemoted event for updating the swaggerObject 124 | // with the newly created model to appear in the Swagger UI. 125 | loopbackApplication.on('modelRemoted', rebuildSwaggerObject); 126 | 127 | loopbackApplication.on('modelDeleted', rebuildSwaggerObject); 128 | 129 | // listening to started event for updating the swaggerObject 130 | // when a call to app.models.[modelName].nestRemoting([modelName]) 131 | // to appear that method in the Swagger UI. 132 | loopbackApplication.on('remoteMethodAdded', rebuildSwaggerObject); 133 | 134 | // listening to remoteMethodDisabled event for updating the swaggerObject 135 | // when a remote method is disabled to hide that method in the Swagger UI. 136 | loopbackApplication.on('remoteMethodDisabled', rebuildSwaggerObject); 137 | 138 | let resourcePath = (opts && opts.resourcePath) || 'swagger.json'; 139 | if (resourcePath[0] !== '/') resourcePath = '/' + resourcePath; 140 | 141 | swaggerApp.get(resourcePath, function sendSwaggerObject(req, res) { 142 | res.status(200).send(swaggerObject); 143 | }); 144 | 145 | function rebuildSwaggerObject() { 146 | swaggerObject = createSwaggerObject(loopbackApplication, opts); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /intl/cs/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} připojen k {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "Vestavěný middleware CORS poskytnutý modulem loopback-component-explorer byl zamítnut. Další podrobnosti viz {0}.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} připojen k {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} vyžaduje {{loopback}} 2.0 nebo novější" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} angehängt unter {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "Die integrierte CORS-Middleware von loopback-component-explorer wird nicht weiter unterstützt. Siehe {0} für weitere Details.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} angehängt unter {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} erfordert {{loopback}} ab Version 2.0" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} mounted at {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "The built-in CORS middleware provided by loopback-component-explorer was deprecated. See {0} for more details.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} mounted at {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} requires {{loopback}} 2.0 or newer" 6 | } 7 | -------------------------------------------------------------------------------- /intl/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} montado en {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "El middleware de CORS incorporado suministrado por loopback-component-explorer ha quedado en desuso. Consulte {0} para obtener detalles.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} montado en {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} requiere {{loopback}} 2.0 o posterior" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} monté sur {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "Le middleware CORS intégré fourni par loopback-component-explorer est obsolète. Pour plus de détails, voir {0}.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} monté sur {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} requiert {{loopback}} 2.0 ou version ultérieure" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/it/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} montato in {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "Il middleware CORS integrato fornito da loopback-component-explorer è stato dichiarato obsoleto. Consultare {0} per ulteriori dettagli.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} montato in {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} richiede {{loopback}} 2.0 o successivo" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} が {{http://localhost:{0}/explorer}} でマウントされました", 3 | "af448751b9a970c539400cadd2681e93": "loopback-component-explorer で提供される組み込み CORS ミドルウェアは、非推奨になりました。 詳しくは、{0} を参照してください。", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} が {{localhost:{0}/explorer}} でマウントされました", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} では {{loopback}} 2.0 以降が必要です" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}}이(가) {{http://localhost:{0}/explorer}}에 마운트됨", 3 | "af448751b9a970c539400cadd2681e93": "loopback-component-explorer에서 제공한 기본 CORS 미들웨어를 더 이상 사용하지 않습니다. 자세한 사항은 {0}을(를) 참조하십시오.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}}이(가) {{localhost:{0}/explorer}}에 마운트됨", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}}에서는 {{loopback}} 2.0 이상이 필요함" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} gekoppeld op {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "De ingebouwde CORS-middleware, aangeleverd door de loopback-component-explorer, is gedeprecieerd. Zie {0} voor meer informatie.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} gekoppeld op {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} vereist {{loopback}} 2.0 of hoger" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} podłączony pod adresem {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "Wbudowana warstwa pośrednia CORS zapewniana przez moduł loopback-component-explorer jest nieaktualna. Więcej informacji na ten temat zawiera sekcja {0}.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} podłączony pod adresem {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} wymaga aplikacji {{loopback}} 2.0 lub nowszej" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/pt/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} montado em {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "O middleware CORS integrado fornecido por loopback-component-explorer foi descontinuado. Consulte a seção {0} para obter mais detalhes.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} montado em {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} requer {{loopback}} 2.0 ou mais recente" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} примонтирован в {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "Встроенное промежуточное ПО CORS, заданное параметром loopback-component-explorer, устарело. Для получения дополнительной информации см. {0}.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} примонтирован в {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "Для {{loopback-component-explorer}} требуется {{loopback}} 2.0 и выше" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/tr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}}, {{http://localhost:{0}/explorer}} noktasında sisteme bağlandı", 3 | "af448751b9a970c539400cadd2681e93": "loopback-component-explorer tarafından sağlanan yerleşik CORS ara katmanı kullanım dışı bırakıldı. Daha fazla ayrıntı için bkz. {0}.", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}}, {{localhost:{0}/explorer}} noktasında sisteme bağlandı", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} için {{loopback}} 2.0 ya da daha yeni bir sürüm gerekli" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/zh-Hans/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "在 {{http://localhost:{0}/explorer}} 安装了 {{Explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "不推荐使用 loopback-component-explorer 提供的内置 CORS 中间件。请参阅 {0} 以获取更多详细信息。", 4 | "b9e94c12da3208f46a969191874c425c": "在 {{localhost:{0}/explorer}} 安装了 {{Explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} 需要 {{loopback}} 2.0 或更新版本" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /intl/zh-Hant/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "88bef790f515ec7b7af0c6871e60c077": "{{Explorer}} 已裝載於 {{http://localhost:{0}/explorer}}", 3 | "af448751b9a970c539400cadd2681e93": "loopback-component-explorer 提供的內建 CORS 中介軟體已淘汰。如需其他詳細資訊,請參閱 {0}。", 4 | "b9e94c12da3208f46a969191874c425c": "{{Explorer}} 已裝載於 {{localhost:{0}/explorer}}", 5 | "f3f2b04c273e23780d76306f8c72a60f": "{{loopback-component-explorer}} 需要 {{loopback}} 2.0 或更新版本" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /lib/url-join.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Simple url joiner. Ensure we don't have to care about whether or not 9 | // we are fed paths with leading/trailing slashes. 10 | module.exports = function urlJoin() { 11 | const args = Array.prototype.slice.call(arguments); 12 | return args.join('/').replace(/\/+/g, '/'); 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-component-explorer", 3 | "version": "6.5.1", 4 | "description": "Browse and test your LoopBack app's APIs", 5 | "engines": { 6 | "node": ">=8.9" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint .", 11 | "test": "mocha", 12 | "posttest": "npm run lint" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/strongloop/loopback-component-explorer.git" 17 | }, 18 | "keywords": [ 19 | "loopback", 20 | "api", 21 | "explorer" 22 | ], 23 | "author": "IBM Corp.", 24 | "readmeFilename": "README.md", 25 | "bugs": { 26 | "url": "https://github.com/strongloop/loopback-component-explorer/issues" 27 | }, 28 | "devDependencies": { 29 | "chai": "^3.2.0", 30 | "eslint": "^5.13.0", 31 | "eslint-config-loopback": "^13.0.0", 32 | "loopback": "^3.19.0", 33 | "mocha": "^5.2.0", 34 | "supertest": "^3.1.0" 35 | }, 36 | "license": "MIT", 37 | "dependencies": { 38 | "debug": "^3.1.0", 39 | "lodash": "^4.17.11", 40 | "loopback-swagger": "^5.0.0", 41 | "strong-globalize": "^4.1.1", 42 | "swagger-ui": "^2.2.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/css/loopbackStyles.css: -------------------------------------------------------------------------------- 1 | /* Styles used for loopback explorer customizations */ 2 | 3 | .swagger-section .swagger-ui-wrap, 4 | .swagger-section .swagger-ui-wrap b, 5 | .swagger-section .swagger-ui-wrap strong, 6 | .swagger-section .swagger-ui-wrap .model-signature, 7 | .swagger-section .swagger-ui-wrap h1, 8 | .swagger-section .swagger-ui-wrap form.formtastic fieldset.inputs ol li.text textarea, 9 | .swagger-section .swagger-ui-wrap ul#resources, 10 | .swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2, 11 | .swagger-section .swagger-ui-wrap p#colophon, 12 | .swagger-section .swagger-ui-wrap .markdown ul, 13 | .model-signature { 14 | font-family: 'helvetica neue', helvetica, arial, sans-serif !important; 15 | } 16 | 17 | /* layout spacing and global colors */ 18 | body { 19 | padding-top: 60px; 20 | font-family: 'helvetica neue', helvetica, arial, sans-serif; 21 | } 22 | 23 | body #header { 24 | background-color: #08592b !important; 25 | position: fixed; 26 | width: 100%; 27 | top: 0; 28 | z-index: 1; 29 | } 30 | 31 | body #header a#logo { 32 | padding: 20px 0 20px 60px !important; 33 | } 34 | 35 | body #header form#api_selector .input a#explore { 36 | background-color: #7dbd33 !important; 37 | cursor: pointer; 38 | } 39 | 40 | 41 | body #header form#api_selector .input a#explore:hover { 42 | background-color: #808080 !important; 43 | } 44 | 45 | /* HTTP GET */ 46 | 47 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading { 48 | background-color: #e6f3f6 !important; 49 | border: 1px solid #bfe1e8 !important; 50 | } 51 | 52 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a { 53 | background-color: #0085a1 !important; 54 | } 55 | 56 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { 57 | color: #0085a1 !important; 58 | } 59 | 60 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content { 61 | background-color: #e9f5f7 !important; 62 | border: 1px solid #bfe1e8 !important; 63 | } 64 | 65 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4 { 66 | color: #0085a1 !important; 67 | } 68 | 69 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content form input[type='text'].error { 70 | outline: 2px solid #cc0000 !important; 71 | } 72 | 73 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a { 74 | color: #66b6c7 !important; 75 | } 76 | 77 | li.operation.get .content > .content-type > div > label { 78 | color: #0085a1 !important; 79 | } 80 | 81 | /* HTTP POST */ 82 | 83 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading { 84 | background-color: #f4effa !important; 85 | border: 1px solid #e3d8f3 !important; 86 | } 87 | 88 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a { 89 | background-color: #9063cd !important; 90 | } 91 | 92 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { 93 | border-right-color: #e3d8f3 !important; 94 | color: #9063cd !important; 95 | } 96 | 97 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a { 98 | color: #9063cd !important; 99 | } 100 | 101 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content { 102 | background-color: #f6f2fb !important; 103 | border: 1px solid #e3d8f3 !important; 104 | } 105 | 106 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4 { 107 | color: #9063cd !important; 108 | } 109 | 110 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content form input[type='text'].error { 111 | outline: 2px solid #cc0000 !important; 112 | } 113 | 114 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a { 115 | color: #bca1e1 !important; 116 | } 117 | 118 | li.operation.post .content > .content-type > div > label { 119 | color: #9063cd !important; 120 | } 121 | 122 | /* HTTP PUT */ 123 | 124 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading { 125 | background-color: #ede7ee !important; 126 | border: 1px solid #d1c2d6 !important; 127 | } 128 | 129 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a { 130 | background-color: #470a59; 131 | } 132 | 133 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { 134 | border-right-color: #d1c2d6 !important; 135 | color: #470a59 !important; 136 | } 137 | 138 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a { 139 | color: #470a59 !important; 140 | } 141 | 142 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content { 143 | background-color: #efeaf1 !important; 144 | border: 1px solid #d1c2d6 !important; 145 | } 146 | 147 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4 { 148 | color: #470a59 !important; 149 | } 150 | 151 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content form input[type='text'].error { 152 | outline: 2px solid #cc0000 !important; 153 | } 154 | 155 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a { 156 | color: #916c9b !important; 157 | } 158 | 159 | li.operation.put .content > .content-type > div > label { 160 | color: #470a59 !important; 161 | } 162 | 163 | /* HTTP PATCH */ 164 | 165 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading { 166 | background-color: #e6f9fb !important; 167 | border: 1px solid #bff0f5 !important; 168 | } 169 | 170 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.http_method a { 171 | background-color: #00c1d5 !important; 172 | } 173 | 174 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li { 175 | border-right-color: #bff0f5 !important; 176 | color: #00c1d5 !important; 177 | } 178 | 179 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a { 180 | color: #00c1d5 !important; 181 | } 182 | 183 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content { 184 | background-color: #e9fafb !important; 185 | border: 1px solid #bff0f5 !important; 186 | } 187 | 188 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content h4 { 189 | color: #00c1d5 !important; 190 | } 191 | 192 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content form input[type='text'].error { 193 | outline: 2px solid #cc0000 !important; 194 | } 195 | 196 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header a { 197 | color: #66dae6 !important; 198 | } 199 | 200 | li.operation.patch .content > .content-type > div > label { 201 | color: #00c1d5 !important; 202 | } 203 | 204 | /* HTTP HEAD */ 205 | 206 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading { 207 | background-color: #fff2ec !important; 208 | border: 1px solid #ffdfd0 !important; 209 | } 210 | 211 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading h3 span.http_method a { 212 | background-color: #ff7f41 !important; 213 | } 214 | 215 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li { 216 | border-right-color: #ffdfd0 !important; 217 | color: #ff7f41 !important; 218 | } 219 | 220 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.heading ul.options li a { 221 | color: #ff7f41 !important; 222 | } 223 | 224 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content { 225 | background-color: #fff4ef !important; 226 | border: 1px solid #ffdfd0 !important; 227 | } 228 | 229 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content h4 { 230 | color: #ff7f41 !important; 231 | } 232 | 233 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content form input[type='text'].error { 234 | outline: 2px solid #cc0000 !important; 235 | } 236 | 237 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.head div.content div.sandbox_header a { 238 | color: #ffb28d !important; 239 | } 240 | 241 | li.operation.head .content > .content-type > div > label { 242 | color: #ff7f41 !important; 243 | } 244 | 245 | /* HTTP DELETE */ 246 | 247 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading { 248 | background-color: #fbede7 !important; 249 | border: 1px solid #f4d1c3 !important; 250 | } 251 | 252 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a { 253 | background-color: #d4470f !important; 254 | } 255 | 256 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { 257 | border-right-color: #f4d1c3 !important; 258 | color: #d4470f !important; 259 | } 260 | 261 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a { 262 | color: #d4470f !important; 263 | } 264 | 265 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content { 266 | background-color: #fbefeb !important; 267 | border: 1px solid #f4d1c3 !important; 268 | } 269 | 270 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4 { 271 | color: #d4470f !important; 272 | } 273 | 274 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content form input[type='text'].error { 275 | outline: 2px solid #cc0000 !important; 276 | } 277 | 278 | body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a { 279 | color: #e5916f !important; 280 | } 281 | 282 | li.operation.delete .content > .content-type > div > label { 283 | color: #d4470f !important; 284 | } 285 | 286 | /* Access Token widgets */ 287 | 288 | .accessTokenDisplay { 289 | color: white; 290 | margin-right: 10px; 291 | } 292 | .accessTokenDisplay.set { 293 | border-bottom: 1px dotted #333; position: relative; cursor: pointer; 294 | } 295 | .accessTokenDisplay.set:hover:after { 296 | content: attr(data-tooltip); 297 | position: absolute; 298 | white-space: nowrap; 299 | font-size: 12px; 300 | background: rgba(0, 0, 0, 0.85); 301 | padding: 3px 7px; 302 | color: #FFF; 303 | border-radius: 3px; 304 | -moz-border-radius: 3px; 305 | -webkit-border-radius: 3px; 306 | right: 0; 307 | bottom: -30px; 308 | } 309 | 310 | /* JSON syntax highlighting */ 311 | .json, .json .attribute { 312 | color: black; 313 | } 314 | 315 | .json .value .string { 316 | color: #800; 317 | } 318 | 319 | .json .value .number, .json .value .literal { 320 | color: #080; 321 | } 322 | 323 | .contentWell { 324 | padding-left: 30px; 325 | padding-right: 30px; 326 | } 327 | 328 | /* 329 | FIXME: Separate the overrides from the rest of the styles, rather than override screen.css entirely. 330 | */ 331 | /* Improve spacing when the browser window is small */ 332 | #message-bar, #swagger-ui-container { 333 | padding-left: 30px; 334 | padding-right: 30px; 335 | } 336 | 337 | #api_selector { 338 | padding: 0px 20px; 339 | } 340 | -------------------------------------------------------------------------------- /public/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongloop/loopback-component-explorer/7bcdfa7350c881cb65b3eaa5650d0fbe665af4cf/public/images/logo_small.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LoopBack API Explorer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 46 | 47 |
48 |
 
49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /public/lib/loadSwaggerUI.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // Refactoring of inline script from index.html. 9 | /*global SwaggerUi, log, ApiKeyAuthorization, hljs, window, $ */ 10 | $(function() { 11 | // Pre load translate... 12 | if (window.SwaggerTranslator) { 13 | window.SwaggerTranslator.translate(); 14 | } 15 | 16 | var lsKey = 'swagger_accessToken'; 17 | $.getJSON('config.json', function(config) { 18 | log(config); 19 | loadSwaggerUi(config); 20 | }); 21 | 22 | var accessToken; 23 | function loadSwaggerUi(config) { 24 | var methodOrder = ['get', 'head', 'options', 'put', 'post', 'delete']; 25 | /* eslint-disable camelcase */ 26 | window.swaggerUi = new SwaggerUi({ 27 | validatorUrl: null, 28 | url: config.url || '/swagger/resources', 29 | apiKey: '', 30 | auth: config.auth, 31 | dom_id: 'swagger-ui-container', 32 | supportHeaderParams: true, 33 | onComplete: function(swaggerApi, swaggerUi) { 34 | log('Loaded SwaggerUI'); 35 | log(swaggerApi); 36 | log(swaggerUi); 37 | 38 | if (window.SwaggerTranslator) { 39 | window.SwaggerTranslator.translate(); 40 | } 41 | 42 | $('pre code').each(function(i, e) { 43 | hljs.highlightBlock(e); 44 | }); 45 | 46 | // Recover accessToken from localStorage if present. 47 | if (window.localStorage) { 48 | var key = window.localStorage.getItem(lsKey); 49 | if (key) { 50 | $('#input_accessToken').val(key).submit(); 51 | } 52 | } 53 | }, 54 | onFailure: function(data) { 55 | log('Unable to Load SwaggerUI'); 56 | log(data); 57 | }, 58 | docExpansion: 'none', 59 | highlightSizeThreshold: 16384, 60 | apisSorter: 'alpha', 61 | operationsSorter: function(a, b) { 62 | var pathCompare = a.path.localeCompare(b.path); 63 | return pathCompare !== 0 ? 64 | pathCompare : 65 | methodOrder.indexOf(a.method) - methodOrder.indexOf(b.method); 66 | }, 67 | }); 68 | /* eslint-disable camelcase */ 69 | 70 | $('#explore').click(setAccessToken); 71 | $('#api_selector').submit(setAccessToken); 72 | $('#input_accessToken').keyup(onInputChange); 73 | 74 | window.swaggerUi.load(); 75 | } 76 | 77 | function setAccessToken(e) { 78 | e.stopPropagation(); // Don't let the default #explore handler fire 79 | e.preventDefault(); 80 | var authOptions = window.swaggerUi.options.auth || {}; 81 | var keyLocation = authOptions.in || 'query'; 82 | var keyName = authOptions.name || 'access_token'; 83 | var key = $('#input_accessToken')[0].value; 84 | log('key: ' + key); 85 | if (key && key.trim() !== '') { 86 | log('added accessToken ' + key); 87 | var apiKeyAuth = 88 | new SwaggerClient.ApiKeyAuthorization(keyName, key, keyLocation); 89 | window.swaggerUi.api.clientAuthorizations.add('key', apiKeyAuth); 90 | accessToken = key; 91 | $('.accessTokenDisplay').text('Token Set.').addClass('set'); 92 | $('.accessTokenDisplay').attr('data-tooltip', 'Current Token: ' + key); 93 | 94 | // Save this token to localStorage if we can to make it persist on refresh. 95 | if (window.localStorage) { 96 | window.localStorage.setItem(lsKey, key); 97 | } 98 | } else { 99 | // If submitted with an empty token, remove the current token. Can be 100 | // useful to intentionally remove authorization. 101 | log('removed accessToken.'); 102 | $('.accessTokenDisplay').text('Token Not Set.').removeClass('set'); 103 | $('.accessTokenDisplay').removeAttr('data-tooltip'); 104 | if (window.swaggerUi) { 105 | window.swaggerUi.api.clientAuthorizations.remove('key'); 106 | } 107 | if (window.localStorage) { 108 | window.localStorage.removeItem(lsKey); 109 | } 110 | } 111 | } 112 | 113 | function onInputChange(e) { 114 | var el = e.currentTarget; 115 | var key = $(e.currentTarget)[0].value; 116 | if (!key || key.trim === '') return; 117 | if (accessToken !== key) { 118 | $('.accessTokenDisplay').text('Token changed; submit to confirm.'); 119 | } else { 120 | $('.accessTokenDisplay').text('Token Set.'); 121 | } 122 | } 123 | 124 | function log() { 125 | if ('console' in window) { 126 | console.log.apply(console, arguments); 127 | } 128 | } 129 | }); 130 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "camelcase" : true, 4 | "eqnull" : true, 5 | "indent": 2, 6 | "undef": true, 7 | "quotmark": "single", 8 | "maxlen": 80, 9 | "trailing": true, 10 | "newcap": true, 11 | "nonew": true, 12 | "undef": false, 13 | "globals" : { 14 | /* MOCHA */ 15 | "describe" : false, 16 | "it" : false, 17 | "before" : false, 18 | "beforeEach" : false, 19 | "after" : false, 20 | "afterEach" : false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/explorer.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | // NOTE(bajtos) It's important to run this check before we load the Explorer 9 | // because require() may fail (e.g. with MODULE_NOT_FOUND error) and make 10 | // it difficult to identify the actual problem 11 | const uiVersion = require('../package.json').dependencies['swagger-ui']; 12 | if (!uiVersion.startsWith('^2')) { 13 | console.error(` 14 | Upgrading from swagger-ui@2 to a newer major version (${uiVersion}) is difficult, 15 | see https://github.com/strongloop/loopback-component-explorer/issues/254 16 | If you are confident about this change and have manually verified API Explorer 17 | functionality in the browser, including access-token based authentication, 18 | then you can delete this check. 19 | `); 20 | process.exit(2); 21 | } 22 | 23 | const loopback = require('loopback'); 24 | const explorer = require('../'); 25 | const request = require('supertest'); 26 | const assert = require('assert'); 27 | const path = require('path'); 28 | const expect = require('chai').expect; 29 | const urlJoin = require('../lib/url-join'); 30 | const os = require('os'); 31 | 32 | describe('explorer', function() { 33 | describe('with default config', function() { 34 | beforeEach(givenLoopBackAppWithExplorer()); 35 | 36 | it('should register "loopback-component-explorer" to the app', function() { 37 | expect(this.app.get('loopback-component-explorer')) 38 | .to.have.property('mountPath', '/explorer'); 39 | }); 40 | 41 | it('should redirect to /explorer/', function(done) { 42 | request(this.app) 43 | .get('/explorer') 44 | .expect(301) 45 | .end(done); 46 | }); 47 | 48 | it('should serve the explorer at /explorer/', function(done) { 49 | request(this.app) 50 | .get('/explorer/') 51 | .expect('Content-Type', /html/) 52 | .expect(200) 53 | .end(function(err, res) { 54 | if (err) return done(err); 55 | 56 | assert(!!~res.text.indexOf('LoopBack API Explorer'), 57 | 'text does not contain expected string'); 58 | 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should serve correct swagger-ui config', function(done) { 64 | request(this.app) 65 | .get('/explorer/config.json') 66 | .expect('Content-Type', /json/) 67 | .expect(200) 68 | .end(function(err, res) { 69 | if (err) return done(err); 70 | 71 | expect(res.body).to 72 | .have.property('url', '/explorer/swagger.json'); 73 | 74 | done(); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('when filename is included in url', function() { 80 | beforeEach(givenLoopBackAppWithExplorer()); 81 | 82 | it('should serve the explorer at /explorer/index.html', function(done) { 83 | request(this.app) 84 | .get('/explorer/index.html') 85 | .expect('Content-Type', /html/) 86 | .expect(200) 87 | .end(function(err, res) { 88 | if (err) throw err; 89 | 90 | assert(!!~res.text.indexOf('LoopBack API Explorer'), 91 | 'text does not contain expected string'); 92 | 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should serve correct swagger-ui config', function(done) { 98 | request(this.app) 99 | .get('/explorer/config.json') 100 | .set('Referer', 'http://example.com/explorer/index.html') 101 | .expect('Content-Type', /json/) 102 | .expect(200) 103 | .end(function(err, res) { 104 | if (err) return done(err); 105 | 106 | expect(res.body).to 107 | .have.property('url', '/explorer/swagger.json'); 108 | 109 | done(); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('with custom explorer base', function() { 115 | beforeEach(givenLoopBackAppWithExplorer('/swagger')); 116 | 117 | it('should register "loopback-component-explorer" to the app', function() { 118 | expect(this.app.get('loopback-component-explorer')) 119 | .to.have.property('mountPath', '/swagger'); 120 | }); 121 | 122 | it('should serve correct swagger-ui config', function(done) { 123 | request(this.app) 124 | .get('/swagger/config.json') 125 | .expect('Content-Type', /json/) 126 | .expect(200) 127 | .end(function(err, res) { 128 | if (err) return done(err); 129 | 130 | expect(res.body).to 131 | .have.property('url', '/swagger/swagger.json'); 132 | 133 | done(); 134 | }); 135 | }); 136 | }); 137 | 138 | describe('with custom app.restApiRoot', function() { 139 | it('should serve correct swagger-ui config', function(done) { 140 | const app = loopback(); 141 | app.set('restApiRoot', '/rest-api-root'); 142 | app.set('remoting', {cors: false}); 143 | configureRestApiAndExplorer(app); 144 | 145 | request(app) 146 | .get('/explorer/config.json') 147 | .expect(200) 148 | .end(function(err, res) { 149 | if (err) return done(err); 150 | 151 | expect(res.body).to 152 | .have.property('url', '/explorer/swagger.json'); 153 | 154 | done(); 155 | }); 156 | }); 157 | 158 | it('removes trailing slash from baseUrl', function(done) { 159 | // SwaggerUI builds resource URL by concatenating basePath + resourcePath 160 | // Since the resource paths are always startign with a slash, 161 | // if the basePath ends with a slash too, an incorrect URL is produced 162 | const app = loopback(); 163 | app.set('restApiRoot', '/apis/'); 164 | app.set('remoting', {cors: false}); 165 | configureRestApiAndExplorer(app); 166 | 167 | request(app) 168 | .get('/explorer/swagger.json') 169 | .expect(200) 170 | .end(function(err, res) { 171 | if (err) return done(err); 172 | 173 | const baseUrl = res.body.basePath; 174 | const apiPath = Object.keys(res.body.paths)[0]; 175 | expect(baseUrl + apiPath).to.equal('/apis/products'); 176 | 177 | done(); 178 | }); 179 | }); 180 | }); 181 | 182 | describe('with custom front-end files', function() { 183 | let app; 184 | beforeEach(function setupExplorerWithUiDirs() { 185 | app = loopback(); 186 | app.set('remoting', {cors: false}); 187 | explorer(app, { 188 | uiDirs: [path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui')], 189 | }); 190 | }); 191 | 192 | it('overrides swagger-ui files', function(done) { 193 | request(app).get('/explorer/swagger-ui.js') 194 | .expect(200) 195 | .end(function(err, res) { 196 | if (err) return done(err); 197 | 198 | // expect the content of `dummy-swagger-ui/swagger-ui.js` 199 | expect(res.text).to.contain('/* custom swagger-ui file */'); 200 | 201 | done(); 202 | }); 203 | }); 204 | 205 | it('overrides strongloop overrides', function(done) { 206 | request(app).get('/explorer/') 207 | .expect(200) 208 | .end(function(err, res) { 209 | if (err) return done(err); 210 | // expect the content of `dummy-swagger-ui/index.html` 211 | expect(res.text).to.contain('custom index.html'); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | 217 | describe('with swaggerUI option', function() { 218 | let app; 219 | beforeEach(function setupExplorerWithoutUI() { 220 | app = loopback(); 221 | app.set('remoting', {cors: false}); 222 | explorer(app, { 223 | swaggerUI: false, 224 | }); 225 | }); 226 | 227 | it('overrides swagger-ui files', function(done) { 228 | request(app).get('/explorer/swagger-ui.js') 229 | .expect(404, done); 230 | }); 231 | 232 | it('should serve config.json', function(done) { 233 | request(app) 234 | .get('/explorer/config.json') 235 | .expect(200) 236 | .end(function(err, res) { 237 | if (err) return done(err); 238 | 239 | expect(res.body).to 240 | .have.property('url', '/explorer/swagger.json'); 241 | 242 | done(); 243 | }); 244 | }); 245 | 246 | it('should serve swagger.json', function(done) { 247 | request(app) 248 | .get('/explorer/swagger.json') 249 | .expect(200, done); 250 | }); 251 | }); 252 | 253 | describe('explorer.routes API', function() { 254 | let app; 255 | beforeEach(function() { 256 | app = loopback(); 257 | app.set('remoting', {cors: false}); 258 | const Product = loopback.PersistedModel.extend('product'); 259 | Product.attachTo(loopback.memory()); 260 | app.model(Product); 261 | }); 262 | 263 | it('creates explorer routes', function(done) { 264 | app.use('/explorer', explorer.routes(app)); 265 | app.use(app.get('restApiRoot') || '/', loopback.rest()); 266 | 267 | request(app) 268 | .get('/explorer/config.json') 269 | .expect('Content-Type', /json/) 270 | .expect(200) 271 | .end(done); 272 | }); 273 | }); 274 | 275 | describe('when specifying custom static file root directories', function() { 276 | let app; 277 | beforeEach(function() { 278 | app = loopback(); 279 | app.set('remoting', {cors: false}); 280 | }); 281 | 282 | it('should allow `uiDirs` to be defined as an Array', function(done) { 283 | explorer(app, { 284 | uiDirs: [path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui')], 285 | }); 286 | 287 | request(app).get('/explorer/') 288 | .expect(200) 289 | .end(function(err, res) { 290 | if (err) return done(err); 291 | // expect the content of `dummy-swagger-ui/index.html` 292 | expect(res.text).to.contain('custom index.html'); 293 | done(); 294 | }); 295 | }); 296 | 297 | it('should allow `uiDirs` to be defined as an String', function(done) { 298 | explorer(app, { 299 | uiDirs: path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui'), 300 | }); 301 | 302 | request(app).get('/explorer/') 303 | .expect(200) 304 | .end(function(err, res) { 305 | if (err) return done(err); 306 | // expect the content of `dummy-swagger-ui/index.html` 307 | expect(res.text).to.contain('custom index.html'); 308 | done(); 309 | }); 310 | }); 311 | }); 312 | 313 | it('updates swagger object when a new model is added', function(done) { 314 | const app = loopback(); 315 | app.set('remoting', {cors: false}); 316 | configureRestApiAndExplorer(app, '/explorer'); 317 | 318 | // Ensure the swagger object was built 319 | request(app) 320 | .get('/explorer/swagger.json') 321 | .expect(200) 322 | .end(function(err) { 323 | if (err) return done(err); 324 | 325 | // Create a new model 326 | const Model = loopback.PersistedModel.extend('Customer'); 327 | Model.attachTo(loopback.memory()); 328 | app.model(Model); 329 | 330 | // Request swagger.json again 331 | request(app) 332 | .get('/explorer/swagger.json') 333 | .expect(200) 334 | .end(function(err, res) { 335 | if (err) return done(err); 336 | 337 | const modelNames = Object.keys(res.body.definitions); 338 | expect(modelNames).to.contain('Customer'); 339 | const paths = Object.keys(res.body.paths); 340 | expect(paths).to.have.contain('/Customers'); 341 | 342 | done(); 343 | }); 344 | }); 345 | }); 346 | 347 | it('updates swagger object when a model is removed', function(done) { 348 | const app = loopback(); 349 | app.set('remoting', {cors: false}); 350 | configureRestApiAndExplorer(app, '/explorer'); 351 | 352 | const Model = loopback.PersistedModel.extend('Customer'); 353 | Model.attachTo(loopback.memory()); 354 | app.model(Model); 355 | 356 | // Ensure the swagger object was built 357 | request(app) 358 | .get('/explorer/swagger.json') 359 | .expect(200) 360 | .end(function(err) { 361 | if (err) return done(err); 362 | 363 | app.deleteModelByName('Customer'); 364 | 365 | // Request swagger.json again 366 | request(app) 367 | .get('/explorer/swagger.json') 368 | .expect(200) 369 | .end(function(err, res) { 370 | if (err) return done(err); 371 | 372 | const modelNames = Object.keys(res.body.definitions); 373 | expect(modelNames).to.not.contain('Customer'); 374 | const paths = Object.keys(res.body.paths); 375 | expect(paths).to.not.contain('/Customers'); 376 | 377 | done(); 378 | }); 379 | }); 380 | }); 381 | 382 | it('updates swagger object when a remote method is disabled', function(done) { 383 | const app = loopback(); 384 | app.set('remoting', {cors: false}); 385 | configureRestApiAndExplorer(app, '/explorer'); 386 | 387 | // Ensure the swagger object was built 388 | request(app) 389 | .get('/explorer/swagger.json') 390 | .expect(200) 391 | .end(function(err, res) { 392 | if (err) return done(err); 393 | 394 | // Check the method that will be disabled 395 | const paths = Object.keys(res.body.paths); 396 | expect(paths).to.contain('/products/findOne'); 397 | 398 | const Product = app.models.Product; 399 | Product.disableRemoteMethodByName('findOne'); 400 | 401 | // Request swagger.json again 402 | request(app) 403 | .get('/explorer/swagger.json') 404 | .expect(200) 405 | .end(function(err, res) { 406 | if (err) return done(err); 407 | 408 | const paths = Object.keys(res.body.paths); 409 | expect(paths).to.not.contain('/products/findOne'); 410 | 411 | done(); 412 | }); 413 | }); 414 | }); 415 | 416 | it('updates swagger object when a remote method is added', function(done) { 417 | const app = loopback(); 418 | app.set('remoting', {cors: false}); 419 | configureRestApiAndExplorer(app, '/explorer'); 420 | 421 | // Ensure the swagger object was built 422 | request(app) 423 | .get('/explorer/swagger.json') 424 | .expect(200) 425 | .end(function(err, res) { 426 | if (err) return done(err); 427 | 428 | // Check the method that will be disabled 429 | const paths = Object.keys(res.body.paths); 430 | expect(paths).to.contain('/products/findOne'); 431 | 432 | const Product = app.models.Product; 433 | Product.findOne2 = function(cb) { cb(null, 1); }; 434 | Product.remoteMethod('findOne2', {}); 435 | 436 | // Request swagger.json again 437 | request(app) 438 | .get('/explorer/swagger.json') 439 | .expect(200) 440 | .end(function(err, res) { 441 | if (err) return done(err); 442 | 443 | const paths = Object.keys(res.body.paths); 444 | expect(paths).to.contain('/products/findOne2'); 445 | 446 | done(); 447 | }); 448 | }); 449 | }); 450 | 451 | function givenLoopBackAppWithExplorer(explorerBase) { 452 | return function(done) { 453 | const app = this.app = loopback(); 454 | app.set('remoting', {cors: false}); 455 | configureRestApiAndExplorer(app, explorerBase); 456 | 457 | done(); 458 | }; 459 | } 460 | 461 | function configureRestApiAndExplorer(app, explorerBase) { 462 | const Product = loopback.PersistedModel.extend('product'); 463 | Product.attachTo(loopback.memory()); 464 | app.model(Product); 465 | 466 | explorer(app, {mountPath: explorerBase}); 467 | app.set('legacyExplorer', false); 468 | app.use(app.get('restApiRoot') || '/', loopback.rest()); 469 | } 470 | }); 471 | -------------------------------------------------------------------------------- /test/fixtures/dummy-swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | custom index.html 2 | -------------------------------------------------------------------------------- /test/fixtures/dummy-swagger-ui/swagger-ui.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2014,2019. All Rights Reserved. 2 | // Node module: loopback-component-explorer 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /* custom swagger-ui file */ 7 | --------------------------------------------------------------------------------