From cd7c1efb13416763b2745407a3724b11cadbe406 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 23 Jun 2020 21:18:28 +0200 Subject: [PATCH 1/4] plugins/graphiql: add GitHub endpoint with automatic scope detection --- plugins/graphiql/dev/index.tsx | 14 ++- .../graphiql/src/lib/api/GraphQLEndpoints.ts | 96 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/plugins/graphiql/dev/index.tsx b/plugins/graphiql/dev/index.tsx index 32d18b4f0b..efcf19a89d 100644 --- a/plugins/graphiql/dev/index.tsx +++ b/plugins/graphiql/dev/index.tsx @@ -15,15 +15,25 @@ */ import { createDevApp } from '@backstage/dev-utils'; +import { githubAuthApiRef, errorApiRef } from '@backstage/core'; import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src'; createDevApp() .registerPlugin(plugin) .registerApiFactory({ implements: graphQlBrowseApiRef, - deps: {}, - factory() { + deps: { + errorApi: errorApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory({ errorApi, githubAuthApi }) { return GraphQLEndpoints.from([ + GraphQLEndpoints.github({ + id: 'github', + title: 'GitHub', + errorApi, + githubAuthApi, + }), GraphQLEndpoints.create({ id: 'gitlab', title: 'GitLab', diff --git a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts index c407d92289..ee0b4bfd4e 100644 --- a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts +++ b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts @@ -15,6 +15,7 @@ */ import { GraphQLBrowseApi, GraphQLEndpoint } from './types'; +import { ErrorApi, OAuthApi } from '@backstage/core'; // Helper for generic http endpoints export type EndpointConfig = { @@ -28,6 +29,23 @@ export type EndpointConfig = { headers?: { [name in string]: string }; }; +export type GithubEndpointConfig = { + id: string; + title: string; + /** + * Github GraphQL API url, defaults to https://api.github.com/graphql + */ + url?: string; + /** + * Errors will be posted to the ErrorApi if it is provided. + */ + errorApi?: ErrorApi; + /** + * GitHub Auth API used to authenticate requests. + */ + githubAuthApi: OAuthApi; +}; + export class GraphQLEndpoints implements GraphQLBrowseApi { // Create a support static create(config: EndpointConfig): GraphQLEndpoint { @@ -51,6 +69,84 @@ export class GraphQLEndpoints implements GraphQLBrowseApi { }; } + /** + * Creates a GitHub GraphQL endpoint that uses the GithubAuth API to authenticate requests. + * + * If a request requires more permissions than is granted by the existing session, + * the fetcher will automatically ask for the additional scopes that are required. + */ + static github(config: GithubEndpointConfig): GraphQLEndpoint { + const { + id, + title, + url = 'https://api.github.com/graphql', + errorApi, + githubAuthApi, + } = config; + type ResponseBody = { + errors?: Array<{ type: string; message: string }>; + }; + + return { + id, + title, + fetcher: async (params: any) => { + let retried = false; + + const doRequest = async (): Promise => { + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${await githubAuthApi.getAccessToken()}`, + }, + body: JSON.stringify(params), + }); + if (!res.ok) { + throw new Error( + `Request failed with status ${res.status} ${res.statusText}`, + ); + } + const data = (await res.json()) as ResponseBody; + + if (!data.errors || retried) { + return data; + } + retried = true; + + const missingScopes = data.errors + .filter(({ type }) => type === 'INSUFFICIENT_SCOPES') + .flatMap(({ message }) => { + const scopesMatch = message.match( + /one of the following scopes: (\[.*?\])/, + ); + if (!scopesMatch) { + return []; + } + try { + const scopes = JSON.parse(scopesMatch[1].replace(/'/g, '"')); + return scopes; + } catch (error) { + if (errorApi) { + errorApi.post( + new Error( + `Failed to parse scope string "${scopesMatch[1]}", ${error}`, + ), + ); + } + return []; + } + }); + + await githubAuthApi.getAccessToken(missingScopes); + return doRequest(); + }; + + return await doRequest(); + }, + }; + } + static from(endpoints: GraphQLEndpoint[]) { return new GraphQLEndpoints(endpoints); } From c6b9d27a71a7d5e4842d000a4dc726772e49aeb7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 23 Jun 2020 21:25:40 +0200 Subject: [PATCH 2/4] dev-utils: add google and github auth APIs to default api factories --- packages/dev-utils/src/devApp/apiFactories.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/dev-utils/src/devApp/apiFactories.ts b/packages/dev-utils/src/devApp/apiFactories.ts index 967918d87a..7ac6aff261 100644 --- a/packages/dev-utils/src/devApp/apiFactories.ts +++ b/packages/dev-utils/src/devApp/apiFactories.ts @@ -24,6 +24,10 @@ import { AlertApiForwarder, oauthRequestApiRef, OAuthRequestManager, + GoogleAuth, + googleAuthApiRef, + GithubAuth, + githubAuthApiRef, } from '@backstage/core'; // TODO(rugvip): We should likely figure out how to reuse all of these between apps @@ -49,3 +53,25 @@ export const oauthRequestApiFactory = createApiFactory({ deps: {}, factory: () => new OAuthRequestManager(), }); + +export const googleAuthApiFactory = createApiFactory({ + implements: googleAuthApiRef, + deps: { oauthRequestApi: oauthRequestApiRef }, + factory: ({ oauthRequestApi }) => + GoogleAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +}); + +export const githubAuthApiFactory = createApiFactory({ + implements: githubAuthApiRef, + deps: { oauthRequestApi: oauthRequestApiRef }, + factory: ({ oauthRequestApi }) => + GithubAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, + }), +}); From 2b3713f7b64226127606e3b07c9661828080e11a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 23 Jun 2020 21:48:20 +0200 Subject: [PATCH 3/4] app: add graphiql plugin with gitlab and github endpoints --- packages/app/package.json | 1 + packages/app/src/apis.ts | 23 ++++++++++++++++++++++- packages/app/src/components/Root/Root.tsx | 6 ++++++ packages/app/src/plugins.ts | 1 + 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/app/package.json b/packages/app/package.json index 06858b015b..6ccf48584f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,6 +9,7 @@ "@backstage/plugin-circleci": "^0.1.1-alpha.12", "@backstage/plugin-explore": "^0.1.1-alpha.12", "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.12", + "@backstage/plugin-graphiql": "^0.1.1-alpha.12", "@backstage/plugin-lighthouse": "^0.1.1-alpha.12", "@backstage/plugin-register-component": "^0.1.1-alpha.12", "@backstage/plugin-scaffolder": "^0.1.1-alpha.12", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 9bfc618216..5011b16eaf 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -45,6 +45,10 @@ import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; import { gitOpsApiRef, GitOpsRestApi } from '@backstage/plugin-gitops-profiles'; +import { + graphQlBrowseApiRef, + GraphQLEndpoints, +} from '@backstage/plugin-graphiql'; export const apis = (config: ConfigApi) => { // eslint-disable-next-line no-console @@ -78,7 +82,7 @@ export const apis = (config: ConfigApi) => { }), ); - builder.add( + const githubAuthApi = builder.add( githubAuthApiRef, GithubAuth.create({ apiOrigin: 'http://localhost:7000', @@ -105,5 +109,22 @@ export const apis = (config: ConfigApi) => { builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')); + builder.add( + graphQlBrowseApiRef, + GraphQLEndpoints.from([ + GraphQLEndpoints.create({ + id: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com/api/graphql', + }), + GraphQLEndpoints.github({ + id: 'github', + title: 'GitHub', + errorApi, + githubAuthApi, + }), + ]), + ); + return builder.build(); }; diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 453c98bc72..4b92f8281e 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -39,6 +39,7 @@ import { SidebarPinButton, } from '@backstage/core'; import { NavLink } from 'react-router-dom'; +import { graphiQLRouteRef } from '@backstage/plugin-graphiql'; const useSidebarLogoStyles = makeStyles({ root: { @@ -94,6 +95,11 @@ const Root: FC<{}> = ({ children }) => ( + diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 05373d7d96..09cd117fc0 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -24,3 +24,4 @@ export { plugin as RegisterComponent } from '@backstage/plugin-register-componen export { plugin as Sentry } from '@backstage/plugin-sentry'; export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles'; export { plugin as TechDocs } from '@backstage/plugin-techdocs'; +export { plugin as GraphiQL } from '@backstage/plugin-graphiql'; From 96c4b7ffa5676ecb051722116c236c24b3c794ac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 23 Jun 2020 21:48:40 +0200 Subject: [PATCH 4/4] plugins/graphiql: grayscale icon --- plugins/graphiql/src/assets/graphiql.icon.svg | 77 ++++--------------- 1 file changed, 16 insertions(+), 61 deletions(-) diff --git a/plugins/graphiql/src/assets/graphiql.icon.svg b/plugins/graphiql/src/assets/graphiql.icon.svg index 66aee7be5a..d847a573cf 100644 --- a/plugins/graphiql/src/assets/graphiql.icon.svg +++ b/plugins/graphiql/src/assets/graphiql.icon.svg @@ -1,64 +1,19 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +