From cd7c1efb13416763b2745407a3724b11cadbe406 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 23 Jun 2020 21:18:28 +0200 Subject: [PATCH] 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); }