diff --git a/example/bundle-hub2.css b/example/bundle-hub2.css index d7d5092cd..9ecc75c21 100644 --- a/example/bundle-hub2.css +++ b/example/bundle-hub2.css @@ -2965,7 +2965,7 @@ img.mfp-img { -webkit-user-select:none; -moz-user-select:none; user-select:none; - + /* Styles */ background: #fff; border-radius: 7px; @@ -10284,7 +10284,8 @@ body.page-password footer, text-overflow: ellipsis; overflow: hidden; } -#hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=text] { +#hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=text], +#hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=password] { border: 1px solid #ddd; border-radius: 2px; padding: 5px 7px; @@ -10294,6 +10295,8 @@ body.page-password footer, color: #666; } #hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=text]:focus, +#hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=password]:focus, +#hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=password]:active, #hub-reference .hub-api .api-definition .hub-auth-dropdown > div input[type=text]:active { outline: 0 none; border: 1px solid #888; @@ -11282,7 +11285,7 @@ div.simple-dropdown.open > div { font-variant: normal; text-transform: none; line-height: 1; - + /* Enable Ligatures ================ */ letter-spacing: 0; -webkit-font-feature-settings: "liga"; diff --git a/example/src/Demo.jsx b/example/src/Demo.jsx index 050541562..95442eafb 100644 --- a/example/src/Demo.jsx +++ b/example/src/Demo.jsx @@ -56,8 +56,8 @@ function Demo({ fetchSwagger, status, docs, oas, oauth }) { // user: { user: '123456', pass: 'abc', apiKey: '123456' }, user: { keys: [ - { name: 'project1', apiKey: '123', user: 'user1', pass: 'pass1' }, - { name: 'project2', apiKey: '456', user: 'user2', pass: 'pass2' }, + { id: 'asdfghjkl', name: 'project1', apiKey: '123', user: 'user1', pass: 'pass1' }, + { id: 'zxcvbnm', name: 'project2', apiKey: '456', user: 'user2', pass: 'pass2' }, ], }, defaults: [], diff --git a/example/swagger-files/multiple-securities.json b/example/swagger-files/multiple-securities.json index 696ba73d1..31f57144c 100644 --- a/example/swagger-files/multiple-securities.json +++ b/example/swagger-files/multiple-securities.json @@ -23,7 +23,6 @@ ], "summary": "or security", "description": "", - "operationId": "addThings", "parameters": [], "responses": { "405": { @@ -38,12 +37,12 @@ "security": [ { "oauth": ["write:things"], - "apiKey": [] + "apiKey": [], + "basicAuth": [] } ], "summary": "and security", "description": "", - "operationId": "addAndSecurity", "parameters": [], "responses": { "405": { @@ -66,7 +65,6 @@ ], "summary": "and or security", "description": "", - "operationId": "addAndOrSecurity", "parameters": [], "responses": { "405": { @@ -88,7 +86,6 @@ ], "summary": "or security", "description": "", - "operationId": "addOrSecurity", "parameters": [], "responses": { "405": { @@ -107,7 +104,6 @@ ], "summary": "one security for endpoint", "description": "", - "operationId": "addSingleAuth", "parameters": [], "responses": { "405": { @@ -121,7 +117,6 @@ "operationId": "noAuth", "summary": "no security needed", "description": "", - "operationId": "addNoSecurity", "parameters": [], "responses": { "405": { @@ -143,13 +138,12 @@ ], "summary": "or security", "description": "", - "operationId": "addMultipleOAuths", "parameters": [] } }, "/multiple-combo-auths": { "post": { - "operationId": "unsupported scheme in the and", + "operationId": "unsupportedSchemeInTheAnd", "security": [ { "oauth": ["write:things", "read:things"], @@ -161,7 +155,6 @@ ], "summary": "second and does not show security", "description": "", - "operationId": "addMultipleOAuths", "parameters": [], "responses": { "405": { @@ -172,7 +165,7 @@ }, "/multiple-combo-auths-schemes": { "post": { - "operationId": "nonexistent scheme in the or", + "operationId": "nonexistentSchemeInTheOr", "security": [ { "oauth": ["write:things", "read:things"], @@ -184,7 +177,6 @@ ], "summary": " one or security", "description": "", - "operationId": "addMultipleOAuths", "parameters": [], "responses": { "405": { @@ -203,7 +195,6 @@ ], "summary": "unknown auth type", "description": "", - "operationId": "unknownAuthType", "parameters": [] } }, @@ -217,7 +208,6 @@ ], "summary": "this scheme doesnt exist", "description": "", - "operationId": "unknownScheme", "parameters": [] } } @@ -258,6 +248,11 @@ "type": "demigorgon", "name": "eleven", "in": "header" + }, + "basicAuth": { + "type": "http", + "scheme": "basic", + "in": "header" } }, "responses": {}, diff --git a/packages/api-explorer/__tests__/AuthBox.test.jsx b/packages/api-explorer/__tests__/AuthBox.test.jsx index 0ff11d812..45623fb7a 100644 --- a/packages/api-explorer/__tests__/AuthBox.test.jsx +++ b/packages/api-explorer/__tests__/AuthBox.test.jsx @@ -14,6 +14,7 @@ const props = { open: false, operation: oas.operation('/or-security', 'post'), onChange: () => {}, + onGroupChange: () => {}, onSubmit: () => {}, toggle: () => {}, }; @@ -46,6 +47,12 @@ test.skip('should display a dropdown for when multiple oauths are present', () = ]); }); +test('should mask password inputs for basic http auth', () => { + const authBox = mount(); + expect(authBox.find('h3')).toHaveLength(3); + expect(authBox.find('input[type="password"]')).toHaveLength(1); +}); + test('should not display authentication warning if authData is passed', () => { const authBox = mount(); @@ -68,3 +75,29 @@ test('should display multiple securities', () => { expect(authBox.find('SecurityInput')).toHaveLength(2); }); + +describe('group selection', () => { + const groupProps = { + group: 'someid', + groups: [ + { id: '1230', name: 'someid' }, + { id: '7000', name: 'anotherId' }, + ], + }; + + it('should only display a single dropdown regardless of the number of securities', () => { + const comp = mount(); + + expect(comp.find('select')).toHaveLength(1); + }); + + it('should update auth on changes', () => { + const comp = mount(); + + const select = comp.find('select'); + select.instance().value = '7000'; + select.simulate('change'); + + expect(comp.props().onGroupChange).toHaveBeenCalledWith('7000'); + }); +}); diff --git a/packages/api-explorer/__tests__/fixtures/multiple-securities/oas.json b/packages/api-explorer/__tests__/fixtures/multiple-securities/oas.json index 39cf176fc..571b61070 100644 --- a/packages/api-explorer/__tests__/fixtures/multiple-securities/oas.json +++ b/packages/api-explorer/__tests__/fixtures/multiple-securities/oas.json @@ -23,7 +23,6 @@ ], "summary": "or security", "description": "", - "operationId": "addThings", "parameters": [], "responses": { "405": { @@ -38,12 +37,12 @@ "security": [ { "oauthScheme": ["write:things"], - "apiKeyScheme": [] + "apiKeyScheme": [], + "basicAuth": [] } ], "summary": "and security", "description": "", - "operationId": "addAndSecurity", "parameters": [], "responses": { "405": { @@ -66,7 +65,6 @@ ], "summary": "and or security", "description": "", - "operationId": "addAndOrSecurity", "parameters": [], "responses": { "405": { @@ -85,7 +83,6 @@ ], "summary": "one security for endpoint", "description": "", - "operationId": "addSingleAuth", "parameters": [], "responses": { "405": { @@ -99,7 +96,6 @@ "operationId": "noAuth", "summary": "no security needed", "description": "", - "operationId": "addNoSecurity", "parameters": [], "responses": { "405": { @@ -121,7 +117,6 @@ ], "summary": "or security", "description": "", - "operationId": "addMultipleOAuths", "parameters": [], "responses": { "405": { @@ -144,7 +139,6 @@ ], "summary": "second and does not show security", "description": "", - "operationId": "addMultipleOAuths", "parameters": [], "responses": { "405": { @@ -167,7 +161,6 @@ ], "summary": "one or security", "description": "", - "operationId": "addMultipleOAuths", "parameters": [], "responses": { "405": { @@ -186,7 +179,6 @@ ], "summary": "unknown auth type", "description": "", - "operationId": "unknownAuthType", "parameters": [] } }, @@ -200,7 +192,6 @@ ], "summary": "this scheme doesnt exist", "description": "", - "operationId": "unknownScheme", "parameters": [] } } @@ -241,6 +232,11 @@ "type": "demigorgon", "name": "eleven", "in": "header" + }, + "basicAuth": { + "type": "http", + "scheme": "basic", + "in": "header" } }, "responses": {}, diff --git a/packages/api-explorer/__tests__/lib/get-auth.test.js b/packages/api-explorer/__tests__/lib/get-auth.test.js index 5f1cd03f4..da4c8c91b 100644 --- a/packages/api-explorer/__tests__/lib/get-auth.test.js +++ b/packages/api-explorer/__tests__/lib/get-auth.test.js @@ -13,9 +13,24 @@ test('should fetch all auths from the OAS files', () => { oauthDiff: '', apiKeyScheme: 'apikey', unknownAuthType: '', + basicAuth: { + pass: '', + user: '', + }, }); }); +test('should fetch auths from selected app', () => { + const user = { + keys: [ + { oauthScheme: '111', name: 'app-1' }, + { oauthScheme: '222', name: 'app-2' }, + ], + }; + + expect(getAuth(user, { 'api-setting': oas }, 'app-2').oauthScheme).toBe('222'); +}); + test('should not error if oas.components is not set', () => { expect(() => { getAuth({ oauthScheme: 'oauth', apiKeyScheme: 'apikey' }, { 'api-setting': {} }); @@ -36,81 +51,85 @@ test('should not error if oas.components is not set', () => { }).not.toThrow(); }); -const { getSingle } = getAuth; - -const topLevelUser = { apiKey: '123456', user: 'user', pass: 'pass' }; -const keysUser = { - keys: [ - { apiKey: '123456', name: 'app-1' }, - { apiKey: '7890', name: 'app-2' }, - ], -}; -const topLevelSchemeUser = { schemeName: 'scheme-key' }; -const keysSchemeUser = { - keys: [ - { schemeName: 'scheme-key-1', name: 'app-1' }, - { schemeName: 'scheme-key-2', name: 'app-2' }, - { schemeName: { user: 'user', pass: 'pass' }, name: 'app-3' }, - ], -}; - -test('should return apiKey property for oauth', () => { - expect(getSingle(topLevelUser, { type: 'oauth2' })).toBe('123456'); -}); +describe('#getSingle', () => { + const { getSingle } = getAuth; + + const topLevelUser = { apiKey: '123456', user: 'user', pass: 'pass' }; + const keysUser = { + keys: [ + { apiKey: '123456', name: 'app-1' }, + { apiKey: '7890', name: 'app-2' }, + ], + }; + const topLevelSchemeUser = { schemeName: 'scheme-key' }; + const keysSchemeUser = { + keys: [ + { schemeName: 'scheme-key-1', name: 'app-1' }, + { schemeName: 'scheme-key-2', name: 'app-2' }, + { schemeName: { user: 'user', pass: 'pass' }, name: 'app-3' }, + ], + }; + + it('should return apiKey property for oauth', () => { + expect(getSingle(topLevelUser, { type: 'oauth2' })).toBe('123456'); + }); -test('should return apiKey property for apiKey', () => { - expect(getSingle(topLevelUser, { type: 'oauth2' })).toBe('123456'); -}); + it('should return apiKey property for apiKey', () => { + expect(getSingle(topLevelUser, { type: 'oauth2' })).toBe('123456'); + }); -test('should return a default value if scheme is sec0 and default auth provided', () => { - expect(getSingle({}, { type: 'apiKey', _key: 'sec0', 'x-default': 'default' })).toBe('default'); -}); + it('should return a default value if scheme is sec0 and default auth provided', () => { + expect(getSingle({}, { type: 'apiKey', _key: 'sec0', 'x-default': 'default' })).toBe('default'); + }); -test('should return apiKey property for bearer', () => { - expect(getSingle(topLevelUser, { type: 'http', scheme: 'bearer' })).toBe('123456'); -}); + it('should return apiKey property for bearer', () => { + expect(getSingle(topLevelUser, { type: 'http', scheme: 'bearer' })).toBe('123456'); + }); -test('should return user/pass properties for basic auth', () => { - expect(getSingle(topLevelUser, { type: 'http', scheme: 'basic' })).toStrictEqual({ - user: 'user', - pass: 'pass', + it('should return user/pass properties for basic auth', () => { + expect(getSingle(topLevelUser, { type: 'http', scheme: 'basic' })).toStrictEqual({ + user: 'user', + pass: 'pass', + }); }); -}); -test('should return first item from keys array if no app selected', () => { - expect(getSingle(keysUser, { type: 'oauth2' })).toBe('123456'); -}); + it('should return first item from keys array if no app selected', () => { + expect(getSingle(keysUser, { type: 'oauth2' })).toBe('123456'); + }); -test('should return selected app from keys array if app provided', () => { - expect(getSingle(keysUser, { type: 'oauth2' }, 'app-2')).toBe('7890'); -}); + it('should return selected app from keys array if app provided', () => { + expect(getSingle(keysUser, { type: 'oauth2' }, 'app-2')).toBe('7890'); + }); -test('should return item by scheme name if no apiKey/user/pass', () => { - expect(getSingle(topLevelSchemeUser, { type: 'oauth2', _key: 'schemeName' })).toBe('scheme-key'); - expect( - getSingle(topLevelSchemeUser, { type: 'http', scheme: 'bearer', _key: 'schemeName' }), - ).toBe('scheme-key'); - expect(getSingle(keysSchemeUser, { type: 'oauth2', _key: 'schemeName' })).toBe('scheme-key-1'); - expect(getSingle(keysSchemeUser, { type: 'oauth2', _key: 'schemeName' }, 'app-2')).toBe( - 'scheme-key-2', - ); - expect( - getSingle(keysSchemeUser, { type: 'http', scheme: 'basic', _key: 'schemeName' }, 'app-3'), - ).toStrictEqual({ - user: 'user', - pass: 'pass', + it('should return item by scheme name if no apiKey/user/pass', () => { + expect(getSingle(topLevelSchemeUser, { type: 'oauth2', _key: 'schemeName' })).toBe( + 'scheme-key', + ); + expect( + getSingle(topLevelSchemeUser, { type: 'http', scheme: 'bearer', _key: 'schemeName' }), + ).toBe('scheme-key'); + expect(getSingle(keysSchemeUser, { type: 'oauth2', _key: 'schemeName' })).toBe('scheme-key-1'); + expect(getSingle(keysSchemeUser, { type: 'oauth2', _key: 'schemeName' }, 'app-2')).toBe( + 'scheme-key-2', + ); + expect( + getSingle(keysSchemeUser, { type: 'http', scheme: 'basic', _key: 'schemeName' }, 'app-3'), + ).toStrictEqual({ + user: 'user', + pass: 'pass', + }); }); -}); -test('should return emptystring for anything else', () => { - expect(getSingle(topLevelUser, { type: 'unknown' })).toBe(''); - expect(getSingle({}, { type: 'http', scheme: 'basic' })).toStrictEqual({ user: '', pass: '' }); - expect(getSingle({}, { type: 'http', scheme: 'bearer' })).toBe(''); - expect(getSingle({}, { type: 'http', scheme: 'unknown' })).toBe(''); - expect(getSingle(keysUser, { type: 'unknown' })).toBe(''); - expect(getSingle(keysUser, { type: 'unknown' }, 'app-2')).toBe(''); -}); + it('should return emptystring for anything else', () => { + expect(getSingle(topLevelUser, { type: 'unknown' })).toBe(''); + expect(getSingle({}, { type: 'http', scheme: 'basic' })).toStrictEqual({ user: '', pass: '' }); + expect(getSingle({}, { type: 'http', scheme: 'bearer' })).toBe(''); + expect(getSingle({}, { type: 'http', scheme: 'unknown' })).toBe(''); + expect(getSingle(keysUser, { type: 'unknown' })).toBe(''); + expect(getSingle(keysUser, { type: 'unknown' }, 'app-2')).toBe(''); + }); -test('should allow scheme to be undefined', () => { - expect(getSingle(topLevelUser)).toBe(''); + it('should allow scheme to be undefined', () => { + expect(getSingle(topLevelUser)).toBe(''); + }); }); diff --git a/packages/api-explorer/__tests__/lib/is-auth-ready.test.js b/packages/api-explorer/__tests__/lib/is-auth-ready.test.js index d2320c70e..08aae70ce 100644 --- a/packages/api-explorer/__tests__/lib/is-auth-ready.test.js +++ b/packages/api-explorer/__tests__/lib/is-auth-ready.test.js @@ -15,6 +15,7 @@ describe('isAuthReady', () => { isAuthReady(operation, { oauthScheme: 'bearer', apiKeyScheme: 'bearer', + basicAuth: { user: 'test', password: 'pass' }, }), ).toBe(true); }); diff --git a/packages/api-explorer/package-lock.json b/packages/api-explorer/package-lock.json index a14669b60..8a66888f7 100644 --- a/packages/api-explorer/package-lock.json +++ b/packages/api-explorer/package-lock.json @@ -1402,6 +1402,200 @@ "react": "^16.4.2" } }, + "@readme/markdown": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@readme/markdown/-/markdown-4.12.1.tgz", + "integrity": "sha512-5QdaXBZF+b89lqqzaH4qrs0Yvr2Nc2CaIdT9bGTmtHl3S37Z0F5FgbPWUn6ONBfy/qxZtKgS4FxQR2eb2/RRCg==", + "requires": { + "@readme/syntax-highlighter": "^4.12.1", + "@readme/variable": "^4.12.1", + "hast-util-sanitize": "^1.2.0", + "prop-types": "^15.7.2", + "react": "^16.4.2", + "rehype-raw": "^4.0.1", + "rehype-react": "^3.1.0", + "rehype-sanitize": "^2.0.1", + "remark-breaks": "^1.0.0", + "remark-parse": "^7.0.2", + "remark-rehype": "^5.0.0", + "unified": "^8.4.0" + } + }, + "@readme/oas-extensions": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@readme/oas-extensions/-/oas-extensions-4.12.1.tgz", + "integrity": "sha512-f3PHVkei6ZlJKHQpruMGoHPTEh8GtzWAG9iC5H8ImZ255sW8j01i/ablDWLDymEsGZdZRZWDYHaSQbcY+6PPbg==" + }, + "@readme/oas-to-har": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@readme/oas-to-har/-/oas-to-har-4.12.1.tgz", + "integrity": "sha512-sN0zu9yZ5S7wH71H4fux97CKY6u8lp7x3xdvGIMYCCqOf72gAlJBohWjT/INlc2GDQcet7tyfaWoTsGUvKO4Hg==", + "requires": { + "@readme/oas-extensions": "^4.12.1", + "oas": "^1.0.1", + "querystring": "^0.2.0" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=" + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "external-editor": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", + "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", + "requires": { + "extend": "^3.0.0", + "spawn-sync": "^1.0.15", + "tmp": "^0.0.29" + } + }, + "inquirer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", + "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", + "requires": { + "ansi-escapes": "^1.1.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "external-editor": "^1.1.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "mute-stream": "0.0.6", + "pinkie-promise": "^2.0.0", + "run-async": "^2.2.0", + "rx": "^4.1.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + } + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mute-stream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", + "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=" + }, + "oas": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/oas/-/oas-1.0.1.tgz", + "integrity": "sha512-3cKnwLbILqe83pydC24V7dqr3343hsx0zdJbmHcg3tX90//b20B2dC5pQGOSH5BgzdH1S9817PzdUuEjzvIBfA==", + "requires": { + "cardinal": "^2.1.1", + "colors": "^1.1.2", + "figures": "^3.0.0", + "glob": "^7.1.2", + "inquirer": "^1.2.1", + "json2yaml": "^1.1.0", + "jsonfile": "^2.3.1", + "lodash": "^4.17.11", + "minimist": "^1.2.0", + "node-status": "^1.0.0", + "oas-normalize": "0.0.5", + "open": "^6.1.0", + "prompt-sync": "^4.1.4", + "request": "^2.88.0", + "swagger-inline": "1.0.5", + "uslug": "^1.0.4", + "yamljs": "^0.2.8" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "tmp": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", + "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "requires": { + "os-tmpdir": "~1.0.1" + } + }, + "yamljs": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.2.10.tgz", + "integrity": "sha1-SBzHwlynOvWfWR8MluPOVsdXpA8=", + "requires": { + "argparse": "^1.0.7", + "glob": "^7.0.5" + } + } + } + }, + "@readme/syntax-highlighter": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@readme/syntax-highlighter/-/syntax-highlighter-4.12.1.tgz", + "integrity": "sha512-l9DB8eKP8zr7AO6DTv2sntXyqSTX5o3yVq3YAj60etU0KnD2QqDUFkFmhwovHrE9pBWhB2tXySx2QlVZd9tHDg==", + "requires": { + "@readme/variable": "^4.12.1", + "codemirror": "^5.48.2", + "react": "^16.4.2" + } + }, + "@readme/variable": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@readme/variable/-/variable-4.12.1.tgz", + "integrity": "sha512-q8d/L8uQpmY2TtsIZyt8ZJK2BvN3bAy34n/A4eGB4EIYL1jG913OLuTdkgTRycrZpFojsubhZV5J0sTWPq6UTw==", + "requires": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "react": "^16.4.2" + } + }, "@types/babel__core": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz", diff --git a/packages/api-explorer/src/AuthBox.jsx b/packages/api-explorer/src/AuthBox.jsx deleted file mode 100644 index 1caa848e9..000000000 --- a/packages/api-explorer/src/AuthBox.jsx +++ /dev/null @@ -1,109 +0,0 @@ -const React = require('react'); -const PropTypes = require('prop-types'); -const classNames = require('classnames'); -const { Operation } = require('oas'); - -const SecurityInput = require('./SecurityInput'); - -function Securities({ authInputRef, operation, onChange, oauth, auth, onSubmit }) { - const securityTypes = operation.prepareSecurity(); - return Object.keys(securityTypes).map(type => { - const securities = securityTypes[type]; - return ( -
-

{`${type} Auth`}

-
-
- { - // https://github.com/readmeio/api-explorer/issues/20 - // (type === 'OAuth2' && securities.length > 1) && ( - // - // ) - } - {securities.map(security => ( - - ))} -
-
-
- ); - }); -} - -function AuthBox({ - authInputRef, - operation, - onSubmit, - onChange, - open, - needsAuth, - toggle, - oauth, - auth, -}) { - if (Object.keys(operation.prepareSecurity()).length === 0) return null; - - return ( -
- {/* eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid */} - - -
-
-
- { - e.preventDefault(); - onSubmit(); - }} - operation={operation} - /> -
-
-
- - Authentication is required for this endpoint -
-
-
-
- ); -} - -AuthBox.propTypes = { - auth: PropTypes.shape({}), - authInputRef: PropTypes.func, - needsAuth: PropTypes.bool, - oauth: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - open: PropTypes.bool, - operation: PropTypes.instanceOf(Operation).isRequired, - toggle: PropTypes.func.isRequired, -}; - -AuthBox.defaultProps = { - auth: {}, - authInputRef: () => {}, - needsAuth: false, - open: false, -}; - -module.exports = AuthBox; diff --git a/packages/api-explorer/src/AuthBox/index.jsx b/packages/api-explorer/src/AuthBox/index.jsx new file mode 100644 index 000000000..0ad4f3379 --- /dev/null +++ b/packages/api-explorer/src/AuthBox/index.jsx @@ -0,0 +1,177 @@ +require('./style.scss'); + +const React = require('react'); +const PropTypes = require('prop-types'); +const classNames = require('classnames'); +const { Operation } = require('oas'); + +const SecurityInput = require('../SecurityInput'); + +function GroupsList({ group, groups, onGroupChange }) { + function onSelect({ target }) { + onGroupChange(target.value); + } + + if (!groups || (groups && groups.length <= 1)) { + return false; + } + + return ( + + ); +} + +function Securities({ + auth, + authInputRef, + group, + groups, + oauth, + onChange, + onGroupChange, + onSubmit, + operation, +}) { + const securityTypes = operation.prepareSecurity(); + return ( +
+ {groups && groups.length > 1 && ( +
+ +
+ )} + {Object.keys(securityTypes).map(type => { + const securities = securityTypes[type]; + return ( +
+ +

{`${type} Auth`}

+
+
+
+ { + // https://github.com/readmeio/api-explorer/issues/20 + // (type === 'OAuth2' && securities.length > 1) && ( + // + // ) + } + {securities.map(security => ( + + ))} +
+
+
+ ); + })} +
+ ); +} + +function AuthBox({ + auth, + authInputRef, + group, + groups, + needsAuth, + oauth, + onChange, + onGroupChange, + onSubmit, + open, + operation, + toggle, +}) { + if (Object.keys(operation.prepareSecurity()).length === 0) return null; + + return ( +
+ {/* eslint-disable-next-line jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid */} + + +
+
+
+ { + e.preventDefault(); + onSubmit(); + }} + operation={operation} + /> +
+
+
+ + Authentication is required for this endpoint +
+
+
+
+ ); +} + +const commonProps = { + group: PropTypes.string, + groups: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + }), + ), + onGroupChange: PropTypes.func.isRequired, +}; + +GroupsList.propTypes = { + ...commonProps, +}; + +AuthBox.propTypes = { + ...commonProps, + auth: PropTypes.shape({}), + authInputRef: PropTypes.func, + needsAuth: PropTypes.bool, + oauth: PropTypes.bool.isRequired, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + open: PropTypes.bool, + operation: PropTypes.instanceOf(Operation).isRequired, + toggle: PropTypes.func.isRequired, +}; + +AuthBox.defaultProps = { + auth: {}, + authInputRef: () => {}, + needsAuth: false, + open: false, +}; + +Securities.propTypes = { ...AuthBox.propTypes }; +Securities.defaultProps = { ...AuthBox.defaultProps }; + +module.exports = AuthBox; diff --git a/packages/api-explorer/src/AuthBox/style.scss b/packages/api-explorer/src/AuthBox/style.scss new file mode 100644 index 000000000..c9ec38db7 --- /dev/null +++ b/packages/api-explorer/src/AuthBox/style.scss @@ -0,0 +1,92 @@ +@mixin AuthBox { + $bg: #fbfbfb; + $pop: #018ef5; + $text: #555; + $edge: lighten($text, 57%); + $fade: border .3s ease-out; + & { + display: flex; + flex-flow: nowrap column; + overflow: hidden; + border-radius: 3px; + } + .GroupsList { + display: flex; + justify-content: center; + padding: .75rem; + background: linear-gradient(to right, white 10%, darken($bg, 1.25%) 75%); + >select { + flex: 1; + font: inherit; + color: $text; + border-color: darken($edge, 5%); + outline-color: none; + background: #fff; + background-color: #fff; + &:focus { border-color: $pop } + } + } + details { + position: relative; + border: solid transparent; + margin: -1px 0; + border-width: 1px 0; + transition: $fade; + &:last-child { + margin-bottom: -2px; + } + >summary:first-child { + display: flex; + align-items: center; + color: $text; + font-weight: normal; + padding: 0 0 0 1rem; + border-radius: 0; + background: linear-gradient(to right, white 10%, $bg 75%); + cursor: pointer; + user-select: none; + > h3 { + flex: 1; + margin-bottom: 0; + margin-left: .25rem; + padding-left: .25rem; + padding-right: .25rem; + background: transparent; + border-top: none; + border-color: lighten($edge, 2%) !important; + transition: $fade; + } + &:focus { + outline: none; + box-shadow: + inset 0 0 0 1px $pop, + 0 0 0 1px solid $pop; + } + } + &[open] { + z-index: 2; + border-color: $edge; + } + &:first-of-type { + summary h3 { + margin-top: -1px; + border-top: 1px solid transparent !important; + } + &:not([open]) summary h3 { + border-top-color: lighten($edge, 2%) !important; + } + } + &:last-of-type:not([open]) summary h3 { + border-bottom-color: transparent !important; + } + } + form { + input { + margin-bottom: 0; + } + } +} + +.AuthBox { + #hub-reference .hub-auth-dropdown & { @include AuthBox } +} diff --git a/packages/api-explorer/src/Doc.jsx b/packages/api-explorer/src/Doc.jsx index 525d47f39..361131089 100644 --- a/packages/api-explorer/src/Doc.jsx +++ b/packages/api-explorer/src/Doc.jsx @@ -245,7 +245,7 @@ class Doc extends React.Component { return ( (this.authInput = el)} // eslint-disable-line no-return-assign dirty={this.state.dirty} + group={this.props.group} + groups={this.props.groups} loading={this.state.loading} needsAuth={this.state.needsAuth} oas={this.oas} oauth={this.props.oauth} onChange={this.props.onAuthChange} + onGroupChange={this.props.onGroupChange} onSubmit={this.onSubmit} operation={this.getOperation()} showAuthBox={this.state.showAuthBox} @@ -353,7 +356,6 @@ Doc.propTypes = { }), auth: PropTypes.shape({}).isRequired, baseUrl: PropTypes.string, - changeGroup: PropTypes.func.isRequired, doc: PropTypes.shape({ api: PropTypes.shape({ examples: PropTypes.shape({ @@ -394,6 +396,7 @@ Doc.propTypes = { oas: PropTypes.shape({}), oauth: PropTypes.bool.isRequired, onAuthChange: PropTypes.func.isRequired, + onGroupChange: PropTypes.func.isRequired, setLanguage: PropTypes.func.isRequired, suggestedEdits: PropTypes.bool.isRequired, tryItMetrics: PropTypes.func.isRequired, diff --git a/packages/api-explorer/src/PathUrl.jsx b/packages/api-explorer/src/PathUrl.jsx index 4d407021f..962ac6580 100644 --- a/packages/api-explorer/src/PathUrl.jsx +++ b/packages/api-explorer/src/PathUrl.jsx @@ -24,18 +24,21 @@ function splitPath(path) { } function PathUrl({ - oas, - operation, + auth, authInputRef, - loading, dirty, + loading, + group, + groups, + needsAuth, + oas, + oauth, onChange, + onGroupChange, + onSubmit, + operation, showAuthBox, - needsAuth, toggleAuth, - onSubmit, - oauth, - auth, }) { return (
@@ -46,9 +49,12 @@ function PathUrl({ ); } diff --git a/packages/api-explorer/src/index.jsx b/packages/api-explorer/src/index.jsx index d9334d333..30d21c7e4 100644 --- a/packages/api-explorer/src/index.jsx +++ b/packages/api-explorer/src/index.jsx @@ -23,16 +23,16 @@ class ApiExplorer extends React.Component { this.onAuthChange = this.onAuthChange.bind(this); this.state = { + auth: getAuth(this.props.variables.user, this.props.oasFiles), + group: this.getGroup(), language: Cookie.get('readme_language') || this.getDefaultLanguage(), selectedApp: { selected: '', changeSelected: this.changeSelected, }, - auth: getAuth(this.props.variables.user, this.props.oasFiles), - group: this.getGroup(), }; - this.changeGroup = this.changeGroup.bind(this); + this.onGroupChange = this.onGroupChange.bind(this); this.groups = this.props.variables.user.keys && this.props.variables.user.keys.map(key => ({ id: key.id, name: key.name })); @@ -88,8 +88,24 @@ class ApiExplorer extends React.Component { return this.props.oasFiles[apiSetting]; } - changeGroup(group) { - this.setState({ group }); + /** + * Change the current selected group and refresh the instance auth keys based on that selection. + * + * @param {string} group + */ + onGroupChange(group) { + const { user } = this.props.variables; + let groupName = false; + if (user.keys) { + // We need to remap the incoming group with the groups name so we can pick out the auth + // keys in `getAuth`. + groupName = user.keys.find(key => key.id === group).name; + } + + this.setState({ + group, + auth: getAuth(user, this.props.oasFiles, groupName), + }); } isLazy(index) { @@ -166,7 +182,6 @@ class ApiExplorer extends React.Component { appearance={this.props.appearance} auth={this.state.auth} baseUrl={this.props.baseUrl.replace(/\/$/, '')} - changeGroup={this.changeGroup} doc={doc} flags={this.props.flags} group={this.state.group} @@ -177,6 +192,7 @@ class ApiExplorer extends React.Component { oas={this.getOas(doc)} oauth={this.props.oauth} onAuthChange={this.onAuthChange} + onGroupChange={this.onGroupChange} setLanguage={this.setLanguage} suggestedEdits={this.props.suggestedEdits} tryItMetrics={this.props.tryItMetrics} diff --git a/packages/api-explorer/src/lib/get-auth.js b/packages/api-explorer/src/lib/get-auth.js index c4df2e622..6f4e322ba 100644 --- a/packages/api-explorer/src/lib/get-auth.js +++ b/packages/api-explorer/src/lib/get-auth.js @@ -32,7 +32,7 @@ function getSingle(user, scheme = {}, selectedApp = false) { return getKey(user, scheme); } -function getAuth(user, oasFiles) { +function getAuth(user, oasFiles, selectedApp = false) { return Object.keys(oasFiles) .map(id => { const oas = oasFiles[id]; @@ -40,16 +40,22 @@ function getAuth(user, oasFiles) { if ( Object.keys(oas.components || {}).length === 0 || Object.keys(oas.components.securitySchemes || {}).length === 0 - ) + ) { return {}; + } return Object.keys(oas.components.securitySchemes) .map(scheme => { return { - [scheme]: getSingle(user, { - ...oas.components.securitySchemes[scheme], - _key: scheme, - }), + [scheme]: getSingle( + user, + { + ...oas.components.securitySchemes[scheme], + _key: scheme, + selectedApp, + }, + selectedApp, + ), }; }) .reduce((prev, next) => Object.assign(prev, next), {}); diff --git a/packages/api-explorer/src/security-input-types/Basic.jsx b/packages/api-explorer/src/security-input-types/Basic.jsx index 27bb1a8de..e28843c5b 100644 --- a/packages/api-explorer/src/security-input-types/Basic.jsx +++ b/packages/api-explorer/src/security-input-types/Basic.jsx @@ -23,7 +23,7 @@ function Basic({ user, pass, change, authInputRef, Input }) { inputChange(e.target.name, e.target.value)} - type="text" + type="password" value={pass} />
diff --git a/packages/api-logs/package.json b/packages/api-logs/package.json index d4a015d5d..a5c041baa 100644 --- a/packages/api-logs/package.json +++ b/packages/api-logs/package.json @@ -15,7 +15,7 @@ "pretest": "npm run lint && npm run inspect && npm run prettier", "prettier": "prettier --list-different --write \"./**/**.{js,jsx}\"", "test": "jest --coverage --runInBand", - "watch": "webpack -w" + "watch": "webpack -w --progress" }, "publishConfig": { "registry": "http://registry.npmjs.org", diff --git a/scripts/update-example-swagger-files.js b/scripts/update-example-swagger-files.js index 8af14cbb7..7595ab136 100644 --- a/scripts/update-example-swagger-files.js +++ b/scripts/update-example-swagger-files.js @@ -4,20 +4,26 @@ const { readdirSync, writeFileSync } = require('fs'); const dir = join(__dirname, '/../example/swagger-files/'); const files = readdirSync(dir); -const directory = files.filter(file => file !== 'directory.json').map((file) => { - // eslint-disable-next-line import/no-dynamic-require, global-require - const swagger = require(join(dir, file)); +const directory = files + .filter(file => file !== 'directory.json' && !file.startsWith('.')) + .map(file => { + // eslint-disable-next-line import/no-dynamic-require, global-require + const swagger = require(join(dir, file)); - return { - [basename(file)]: { - preferred: swagger.info.version, - versions: { - [swagger.info.version]: { - swaggerUrl: join('swagger-files', file), + return { + [basename(file)]: { + preferred: swagger.info.version, + versions: { + [swagger.info.version]: { + swaggerUrl: join('swagger-files', file), + }, }, }, - }, - }; -}).reduce((prev, next) => Object.assign(prev, next)); + }; + }) + .reduce((prev, next) => Object.assign(prev, next)); -writeFileSync(join(__dirname, '../example/swagger-files/directory.json'), JSON.stringify(directory, null, 2)); +writeFileSync( + join(__dirname, '../example/swagger-files/directory.json'), + JSON.stringify(directory, null, 2), +);