diff --git a/lerna.json b/lerna.json index 71775b7af0..9e00bb25a6 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.10" + "version": "0.1.1-alpha.12" } diff --git a/packages/app/package.json b/packages/app/package.json index 740ba4a4c3..6ccf48584f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,23 +1,24 @@ { "name": "example-app", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/plugin-catalog": "^0.1.1-alpha.10", - "@backstage/plugin-circleci": "^0.1.1-alpha.10", - "@backstage/plugin-explore": "^0.1.1-alpha.10", - "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.10", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.10", - "@backstage/plugin-register-component": "^0.1.1-alpha.10", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.10", - "@backstage/plugin-sentry": "^0.1.1-alpha.10", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.10", - "@backstage/plugin-techdocs": "^0.1.1-alpha.10", - "@backstage/plugin-welcome": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/plugin-catalog": "^0.1.1-alpha.12", + "@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", + "@backstage/plugin-sentry": "^0.1.1-alpha.12", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.12", + "@backstage/plugin-techdocs": "^0.1.1-alpha.12", + "@backstage/plugin-welcome": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "prop-types": "^15.7.2", 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'; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 49265cae33..b0aef88fea 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.10", + "@backstage/config": "^0.1.1-alpha.12", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -41,7 +41,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "@types/compression": "^1.7.0", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 2ae211fd8e..3be1f5ee3e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -17,15 +17,15 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.10", - "@backstage/catalog-model": "^0.1.1-alpha.10", - "@backstage/config": "^0.1.1-alpha.10", - "@backstage/config-loader": "^0.1.1-alpha.10", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.10", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.10", - "@backstage/plugin-identity-backend": "^0.1.1-alpha.10", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.10", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.10", + "@backstage/backend-common": "^0.1.1-alpha.12", + "@backstage/catalog-model": "^0.1.1-alpha.12", + "@backstage/config": "^0.1.1-alpha.12", + "@backstage/config-loader": "^0.1.1-alpha.12", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.12", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.12", + "@backstage/plugin-identity-backend": "^0.1.1-alpha.12", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.12", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.12", "dockerode": "^3.2.0", "express": "^4.17.1", "knex": "^0.21.1", @@ -33,10 +33,10 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@types/dockerode": "^2.5.32", + "@backstage/cli": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", - "@types/helmet": "^0.0.47" + "@types/helmet": "^0.0.47", + "@types/dockerode": "^2.5.32" } } diff --git a/plugins/catalog-backend/examples/artist-lookup-component.yaml b/packages/catalog-model/examples/artist-lookup-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/artist-lookup-component.yaml rename to packages/catalog-model/examples/artist-lookup-component.yaml diff --git a/plugins/catalog-backend/examples/playback-lib-component.yaml b/packages/catalog-model/examples/playback-lib-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/playback-lib-component.yaml rename to packages/catalog-model/examples/playback-lib-component.yaml diff --git a/plugins/catalog-backend/examples/playback-order-component.yaml b/packages/catalog-model/examples/playback-order-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/playback-order-component.yaml rename to packages/catalog-model/examples/playback-order-component.yaml diff --git a/plugins/catalog-backend/examples/podcast-api-component.yaml b/packages/catalog-model/examples/podcast-api-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/podcast-api-component.yaml rename to packages/catalog-model/examples/podcast-api-component.yaml diff --git a/plugins/catalog-backend/examples/queue-proxy-component.yaml b/packages/catalog-model/examples/queue-proxy-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/queue-proxy-component.yaml rename to packages/catalog-model/examples/queue-proxy-component.yaml diff --git a/plugins/catalog-backend/examples/searcher-component.yaml b/packages/catalog-model/examples/searcher-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/searcher-component.yaml rename to packages/catalog-model/examples/searcher-component.yaml diff --git a/plugins/catalog-backend/examples/shuffle-api-component.yaml b/packages/catalog-model/examples/shuffle-api-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/shuffle-api-component.yaml rename to packages/catalog-model/examples/shuffle-api-component.yaml diff --git a/plugins/catalog-backend/examples/www-artist-component.yaml b/packages/catalog-model/examples/www-artist-component.yaml similarity index 100% rename from plugins/catalog-backend/examples/www-artist-component.yaml rename to packages/catalog-model/examples/www-artist-component.yaml diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index eb59012829..889d9d2e02 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,20 +20,20 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.10", + "@backstage/config": "^0.1.1-alpha.12", "@types/yup": "^0.28.2", "lodash": "^4.17.15", "uuid": "^8.0.0", - "yup": "^0.28.5" + "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "@types/jest": "^25.2.2", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/cli/package.json b/packages/cli/package.json index e80016e2e1..f74c1b1fb0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public" @@ -29,14 +29,14 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.10", - "@backstage/config-loader": "^0.1.1-alpha.10", + "@backstage/config": "^0.1.1-alpha.12", + "@backstage/config-loader": "^0.1.1-alpha.12", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", - "@rollup/plugin-commonjs": "^12.0.0", + "@rollup/plugin-commonjs": "^13.0.0", "@rollup/plugin-json": "^4.0.2", - "@rollup/plugin-node-resolve": "^7.1.1", + "@rollup/plugin-node-resolve": "^8.1.0", "@spotify/eslint-config": "^7.0.1", "@sucrase/webpack-loader": "^2.0.0", "@svgr/plugin-jsx": "4.3.x", @@ -116,6 +116,7 @@ "zombie": "^6.1.4" }, "files": [ + "asset-types", "templates", "config", "bin", diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs index d916677d33..904186ac60 100644 --- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs +++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs @@ -37,6 +37,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index b04da6b793..d5728213e9 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -42,6 +42,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 3fc7667466..c652351c0e 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.10", + "@backstage/config": "^0.1.1-alpha.12", "fs-extra": "^9.0.0", "yaml": "^1.9.2", - "yup": "^0.28.5" + "yup": "^0.29.1" }, "devDependencies": { "@types/jest": "^25.2.2", @@ -41,6 +41,6 @@ "@types/yup": "^0.28.2" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/config/package.json b/packages/config/package.json index 5b67e9b974..ad31611fac 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -37,6 +37,6 @@ "@types/node": "^12.0.0" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 0f538d31d0..c6b25fc0f7 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/config": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -41,8 +41,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/test-utils-core": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/test-utils-core": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -52,6 +52,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/core-api/src/apis/definitions/IdentityApi.ts b/packages/core-api/src/apis/definitions/IdentityApi.ts index dbc36ad967..13f4cd24bc 100644 --- a/packages/core-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-api/src/apis/definitions/IdentityApi.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { createApiRef } from '../ApiRef'; +import { ProfileInfo } from './auth'; /** * The Identity API used to identify and get information about the signed in user. @@ -29,6 +30,11 @@ export type IdentityApi = { */ getUserId(): string; + /** + * The profile of the signed in user. + */ + getProfile(): ProfileInfo; + /** * An OpenID Connect ID Token which proves the identity of the signed in user. * diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index c884bf975b..ccd40e66ce 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -37,13 +37,13 @@ import { Observable } from '../..'; */ export type OAuthScope = string | string[]; -export type AccessTokenOptions = { +export type AuthRequestOptions = { /** * If this is set to true, the user will not be prompted to log in, - * and an empty access token will be returned if there is no existing session. + * and an empty response will be returned if there is no existing session. * - * This can be used to perform a check whether the user is logged in with a set of scopes, - * or if you don't want to force a user to be logged in, but provide functionality if they already are. + * This can be used to perform a check whether the user is logged in, or if you don't + * want to force a user to be logged in, but provide functionality if they already are. * * @default false */ @@ -88,7 +88,7 @@ export type OAuthApi = { */ getAccessToken( scope?: OAuthScope, - options?: AccessTokenOptions, + options?: AuthRequestOptions, ): Promise; /** @@ -97,29 +97,6 @@ export type OAuthApi = { logout(): Promise; }; -export type IdTokenOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty id token will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @default false - */ - optional?: boolean; - - /** - * If this is set to true, the request will bypass the regular oauth login modal - * and open the login popup directly. - * - * The method must be called synchronously from a user action for this to work in all browsers. - * - * @default false - */ - instantPopup?: boolean; -}; - /** * This API provides access to OpenID Connect credentials. It lets you request ID tokens, * which can be passed to backend services to prove the user's identity. @@ -136,7 +113,7 @@ export type OpenIdConnectApi = { * to log in. The returned promise will not resolve until the user has successfully logged in. * The returned promise can be rejected, but only if the user rejects the login request. */ - getIdToken(options?: IdTokenOptions): Promise; + getIdToken(options?: AuthRequestOptions): Promise; /** * Log out the user's session. This will reload the page. @@ -144,38 +121,65 @@ export type OpenIdConnectApi = { logout(): Promise; }; -export type ProfileInfoOptions = { - /** - * If this is set to true, the user will not be prompted to log in, - * and an empty profile will be returned if there is no existing session. - * - * This can be used to perform a check whether the user is logged in, or if you don't - * want to force a user to be logged in, but provide functionality if they already are. - * - * @default false - */ - optional?: boolean; -}; - /** * This API provides access to profile information of the user from an auth provider. */ export type ProfileInfoApi = { - getProfile(options?: ProfileInfoOptions): Promise; + /** + * Get profile information for the user as supplied by this auth provider. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. + */ + getProfile(options?: AuthRequestOptions): Promise; }; /** - * Profile information of the user from an auth provider. + * This API provides access to the user's identity within Backstage. + * + * An auth provider that implements this interface can be used to sign-in to backstage. It is + * not intended to be used directly from a plugin, but instead serves as a connection between + * this authentication method and the app's @IdentityApi + */ +export type BackstageIdentityApi = { + /** + * Get the user's identity within Backstage. This should normally not be called directly, + * use the @IdentityApi instead. + * + * If the optional flag is not set, a session is guaranteed to be returned, while if + * the optional flag is set, the session may be undefined. See @AuthRequestOptions for more details. + */ + getBackstageIdentity( + options?: AuthRequestOptions, + ): Promise; +}; + +export type BackstageIdentity = { + /** + * The backstage user ID. + */ + id: string; + + /** + * An ID token that can be used to authenticate the user within Backstage. + */ + idToken: string; +}; + +/** + * Profile information of the user. */ export type ProfileInfo = { /** * Email ID. */ email: string; + /** * Display name that can be presented to the user. */ - name?: string; + displayName?: string; + /** * URL to an avatar image of the user. */ @@ -207,7 +211,11 @@ export type SessionStateApi = { * email and expiration information. Do not rely on any other fields, as they might not be present. */ export const googleAuthApiRef = createApiRef< - OAuthApi & OpenIdConnectApi & ProfileInfoApi & SessionStateApi + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionStateApi >({ id: 'core.auth.google', description: 'Provides authentication towards Google APIs and identities', @@ -219,7 +227,9 @@ export const googleAuthApiRef = createApiRef< * See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ * for a full list of supported scopes. */ -export const githubAuthApiRef = createApiRef({ +export const githubAuthApiRef = createApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionStateApi +>({ id: 'core.auth.github', description: 'Provides authentication towards Github APIs', }); diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts index 7c8e6ce9f0..3ff29cc6ef 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -20,7 +20,7 @@ describe('GithubAuth', () => { it('should get access token', async () => { const getSession = jest .fn() - .mockResolvedValue({ accessToken: 'access-token' }); + .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); const githubAuth = new GithubAuth({ getSession } as any); expect(await githubAuth.getAccessToken()).toBe('access-token'); diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index f4a7092f79..4641dd7b50 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -19,9 +19,11 @@ import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GithubSession } from './types'; import { OAuthApi, - AccessTokenOptions, SessionStateApi, SessionState, + ProfileInfo, + BackstageIdentity, + AuthRequestOptions, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -41,10 +43,13 @@ type CreateOptions = { }; export type GithubAuthResponse = { - accessToken: string; - idToken: string; - scope: string; - expiresInSeconds: number; + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; }; const DEFAULT_PROVIDER = { @@ -69,9 +74,14 @@ class GithubAuth implements OAuthApi, SessionStateApi { oauthRequestApi: oauthRequestApi, sessionTransform(res: GithubAuthResponse): GithubSession { return { - accessToken: res.accessToken, - scopes: GithubAuth.normalizeScope(res.scope), - expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), + ...res, + providerInfo: { + accessToken: res.providerInfo.accessToken, + scopes: GithubAuth.normalizeScope(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, }; }, }); @@ -79,7 +89,7 @@ class GithubAuth implements OAuthApi, SessionStateApi { const sessionManager = new StaticAuthSessionManager({ connector, defaultScopes: new Set(['user']), - sessionScopes: session => session.scopes, + sessionScopes: (session: GithubSession) => session.providerInfo.scopes, }); return new GithubAuth(sessionManager); @@ -93,7 +103,7 @@ class GithubAuth implements OAuthApi, SessionStateApi { constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken(scope?: string, options?: AccessTokenOptions) { + async getAccessToken(scope?: string, options?: AuthRequestOptions) { const normalizedScopes = GithubAuth.normalizeScope(scope); const session = await this.sessionManager.getSession({ ...options, @@ -101,11 +111,25 @@ class GithubAuth implements OAuthApi, SessionStateApi { }); this.sessionStateTracker.setIsSignedId(!!session); if (session) { - return session.accessToken; + return session.providerInfo.accessToken; } return ''; } + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { + const session = await this.sessionManager.getSession(options); + this.sessionStateTracker.setIsSignedId(!!session); + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + this.sessionStateTracker.setIsSignedId(!!session); + return session?.profile; + } + async logout() { await this.sessionManager.removeSession(); this.sessionStateTracker.setIsSignedId(false); diff --git a/packages/core-api/src/apis/implementations/auth/github/types.ts b/packages/core-api/src/apis/implementations/auth/github/types.ts index 282017b80d..54c427423c 100644 --- a/packages/core-api/src/apis/implementations/auth/github/types.ts +++ b/packages/core-api/src/apis/implementations/auth/github/types.ts @@ -14,8 +14,15 @@ * limitations under the License. */ +import { ProfileInfo } from '../../..'; +import { BackstageIdentity } from '../../../definitions'; + export type GithubSession = { - accessToken: string; - scopes: Set; - expiresAt: Date; + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; }; diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts index 1ee3575b89..5290ee798b 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts @@ -23,9 +23,9 @@ const PREFIX = 'https://www.googleapis.com/auth/'; describe('GoogleAuth', () => { it('should get refreshed access token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ accessToken: 'access-token', expiresAt: theFuture }); + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + }); const googleAuth = new GoogleAuth({ getSession } as any); expect(await googleAuth.getAccessToken()).toBe('access-token'); @@ -33,9 +33,9 @@ describe('GoogleAuth', () => { }); it('should get refreshed id token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ idToken: 'id-token', expiresAt: theFuture }); + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); const googleAuth = new GoogleAuth({ getSession } as any); expect(await googleAuth.getIdToken()).toBe('id-token'); @@ -43,9 +43,9 @@ describe('GoogleAuth', () => { }); it('should get optional id token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ idToken: 'id-token', expiresAt: theFuture }); + const getSession = jest.fn().mockResolvedValue({ + providerInfo: { idToken: 'id-token', expiresAt: theFuture }, + }); const googleAuth = new GoogleAuth({ getSession } as any); expect(await googleAuth.getIdToken({ optional: true })).toBe('id-token'); @@ -58,9 +58,11 @@ describe('GoogleAuth', () => { const getSession = jest .fn() .mockResolvedValueOnce({ - accessToken: 'access-token', - expiresAt: theFuture, - scopes: new Set([`${PREFIX}not-enough`]), + providerInfo: { + accessToken: 'access-token', + expiresAt: theFuture, + scopes: new Set([`${PREFIX}not-enough`]), + }, }) .mockRejectedValue(error); const googleAuth = new GoogleAuth({ getSession } as any); @@ -77,17 +79,21 @@ describe('GoogleAuth', () => { it('should wait for all session refreshes', async () => { const initialSession = { - idToken: 'token1', - expiresAt: theFuture, - scopes: new Set(), + providerInfo: { + idToken: 'token1', + expiresAt: theFuture, + scopes: new Set(), + }, }; const getSession = jest .fn() .mockResolvedValueOnce(initialSession) .mockResolvedValue({ - idToken: 'token2', - expiresAt: theFuture, - scopes: new Set(), + providerInfo: { + idToken: 'token2', + expiresAt: theFuture, + scopes: new Set(), + }, }); const googleAuth = new GoogleAuth({ getSession } as any); @@ -95,7 +101,7 @@ describe('GoogleAuth', () => { await expect(googleAuth.getIdToken()).resolves.toBe('token1'); expect(getSession).toBeCalledTimes(1); - initialSession.expiresAt = thePast; + initialSession.providerInfo.expiresAt = thePast; const promise1 = googleAuth.getIdToken(); const promise2 = googleAuth.getIdToken(); diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index 183ea592b4..db26d2d9a4 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -20,13 +20,13 @@ import { GoogleSession } from './types'; import { OAuthApi, OpenIdConnectApi, - IdTokenOptions, - AccessTokenOptions, ProfileInfoApi, - ProfileInfoOptions, ProfileInfo, SessionStateApi, SessionState, + BackstageIdentityApi, + AuthRequestOptions, + BackstageIdentity, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -46,11 +46,14 @@ type CreateOptions = { }; export type GoogleAuthResponse = { + providerInfo: { + accessToken: string; + idToken: string; + scope: string; + expiresInSeconds: number; + }; profile: ProfileInfo; - accessToken: string; - idToken: string; - scope: string; - expiresInSeconds: number; + backstageIdentity: BackstageIdentity; }; const DEFAULT_PROVIDER = { @@ -62,7 +65,12 @@ const DEFAULT_PROVIDER = { const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; class GoogleAuth - implements OAuthApi, OpenIdConnectApi, ProfileInfoApi, SessionStateApi { + implements + OAuthApi, + OpenIdConnectApi, + ProfileInfoApi, + BackstageIdentityApi, + SessionStateApi { static create({ apiOrigin, basePath, @@ -78,11 +86,15 @@ class GoogleAuth oauthRequestApi: oauthRequestApi, sessionTransform(res: GoogleAuthResponse): GoogleSession { return { - profile: res.profile, - idToken: res.idToken, - accessToken: res.accessToken, - scopes: GoogleAuth.normalizeScopes(res.scope), - expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), + ...res, + providerInfo: { + idToken: res.providerInfo.idToken, + accessToken: res.providerInfo.accessToken, + scopes: GoogleAuth.normalizeScopes(res.providerInfo.scope), + expiresAt: new Date( + Date.now() + res.providerInfo.expiresInSeconds * 1000, + ), + }, }; }, }); @@ -94,9 +106,10 @@ class GoogleAuth `${SCOPE_PREFIX}userinfo.email`, `${SCOPE_PREFIX}userinfo.profile`, ]), - sessionScopes: session => session.scopes, - sessionShouldRefresh: session => { - const expiresInSec = (session.expiresAt.getTime() - Date.now()) / 1000; + sessionScopes: (session: GoogleSession) => session.providerInfo.scopes, + sessionShouldRefresh: (session: GoogleSession) => { + const expiresInSec = + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; return expiresInSec < 60 * 5; }, }); @@ -114,7 +127,7 @@ class GoogleAuth async getAccessToken( scope?: string | string[], - options?: AccessTokenOptions, + options?: AuthRequestOptions, ) { const normalizedScopes = GoogleAuth.normalizeScopes(scope); const session = await this.sessionManager.getSession({ @@ -123,16 +136,16 @@ class GoogleAuth }); this.sessionStateTracker.setIsSignedId(!!session); if (session) { - return session.accessToken; + return session.providerInfo.accessToken; } return ''; } - async getIdToken(options: IdTokenOptions = {}) { + async getIdToken(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); this.sessionStateTracker.setIsSignedId(!!session); if (session) { - return session.idToken; + return session.providerInfo.idToken; } return ''; } @@ -142,13 +155,18 @@ class GoogleAuth this.sessionStateTracker.setIsSignedId(false); } - async getProfile(options: ProfileInfoOptions = {}) { + async getBackstageIdentity( + options: AuthRequestOptions = {}, + ): Promise { const session = await this.sessionManager.getSession(options); this.sessionStateTracker.setIsSignedId(!!session); - if (!session) { - return undefined; - } - return session.profile; + return session?.backstageIdentity; + } + + async getProfile(options: AuthRequestOptions = {}) { + const session = await this.sessionManager.getSession(options); + this.sessionStateTracker.setIsSignedId(!!session); + return session?.profile; } static normalizeScopes(scopes?: string | string[]): Set { diff --git a/packages/core-api/src/apis/implementations/auth/google/types.ts b/packages/core-api/src/apis/implementations/auth/google/types.ts index ea251c7006..3b40932980 100644 --- a/packages/core-api/src/apis/implementations/auth/google/types.ts +++ b/packages/core-api/src/apis/implementations/auth/google/types.ts @@ -14,12 +14,15 @@ * limitations under the License. */ -import { ProfileInfo } from '../../../definitions'; +import { ProfileInfo, BackstageIdentity } from '../../../definitions'; export type GoogleSession = { + providerInfo: { + idToken: string; + accessToken: string; + scopes: Set; + expiresAt: Date; + }; profile: ProfileInfo; - idToken: string; - accessToken: string; - scopes: Set; - expiresAt: Date; + backstageIdentity: BackstageIdentity; }; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index a20a9b27f8..27a63ea04a 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -17,7 +17,6 @@ import React, { ComponentType, FC, useMemo, - useCallback, useState, ReactElement, } from 'react'; @@ -258,24 +257,14 @@ export class PrivateAppImpl implements BackstageApp { component: ComponentType; children: ReactElement; }> = ({ component: Component, children }) => { - const [done, setDone] = useState(false); + const [result, setResult] = useState(); - const onResult = useCallback( - (result: SignInResult) => { - if (done) { - throw new Error('Identity result callback was called twice'); - } - this.identityApi.setSignInResult(result); - setDone(true); - }, - [done], - ); - - if (done) { + if (result) { + this.identityApi.setSignInResult(result); return children; } - return ; + return ; }; const AppRouter: FC<{}> = ({ children }) => { @@ -293,6 +282,10 @@ export class PrivateAppImpl implements BackstageApp { if (!SignInPageComponent) { this.identityApi.setSignInResult({ userId: 'guest', + profile: { + email: 'guest@example.com', + displayName: 'Guest', + }, }); return ( diff --git a/packages/core-api/src/app/AppIdentity.ts b/packages/core-api/src/app/AppIdentity.ts index c38ece53df..69ee5d28ac 100644 --- a/packages/core-api/src/app/AppIdentity.ts +++ b/packages/core-api/src/app/AppIdentity.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IdentityApi } from '../apis'; +import { IdentityApi, ProfileInfo } from '../apis'; import { SignInResult } from './types'; /** @@ -24,6 +24,7 @@ import { SignInResult } from './types'; export class AppIdentity implements IdentityApi { private hasIdentity = false; private userId?: string; + private profile?: ProfileInfo; private idTokenFunc?: () => Promise; private logoutFunc?: () => Promise; @@ -36,6 +37,15 @@ export class AppIdentity implements IdentityApi { return this.userId!; } + getProfile(): ProfileInfo { + if (!this.hasIdentity) { + throw new Error( + 'Tried to access IdentityApi profile before app was loaded', + ); + } + return this.profile!; + } + async getIdToken(): Promise { if (!this.hasIdentity) { throw new Error( @@ -55,6 +65,7 @@ export class AppIdentity implements IdentityApi { location.reload(); } + // This is indirectly called by the sign-in page to continue into the app. setSignInResult(result: SignInResult) { if (this.hasIdentity) { return; @@ -62,8 +73,12 @@ export class AppIdentity implements IdentityApi { if (!result.userId) { throw new Error('Invalid sign-in result, userId not set'); } + if (!result.profile) { + throw new Error('Invalid sign-in result, profile not set'); + } this.hasIdentity = true; this.userId = result.userId; + this.profile = result.profile; this.idTokenFunc = result.getIdToken; this.logoutFunc = result.logout; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 33b884fd58..6b022c2736 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -18,7 +18,7 @@ import { ComponentType } from 'react'; import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; -import { AppTheme, ConfigApi } from '../apis/definitions'; +import { AppTheme, ConfigApi, ProfileInfo } from '../apis/definitions'; import { AppConfig } from '@backstage/config'; export type BootErrorPageProps = { @@ -31,6 +31,9 @@ export type SignInResult = { * User ID that will be returned by the IdentityApi */ userId: string; + + profile: ProfileInfo; + /** * Function used to retrieve an ID token for the signed in user. */ diff --git a/packages/core-api/src/lib/loginPopup.test.ts b/packages/core-api/src/lib/loginPopup.test.ts index 755438fead..55f1408e8d 100644 --- a/packages/core-api/src/lib/loginPopup.test.ts +++ b/packages/core-api/src/lib/loginPopup.test.ts @@ -59,7 +59,7 @@ describe('showLoginPopup', () => { // None of these should be accepted listener({ source: popupMock } as MessageEvent); listener({ origin: 'my-origin' } as MessageEvent); - listener({ data: { type: 'auth-result' } } as MessageEvent); + listener({ data: { type: 'authorization_response' } } as MessageEvent); listener({ source: popupMock, origin: 'my-origin', @@ -68,26 +68,26 @@ describe('showLoginPopup', () => { listener({ source: popupMock, origin: 'my-origin', - data: { type: 'not-auth-result', payload: {} }, + data: { type: 'not-auth-result', response: {} }, } as MessageEvent); await expect(Promise.race([payloadPromise, 'waiting'])).resolves.toBe( 'waiting', ); - const myPayload = {}; + const myResponse = {}; // This should be accepted as a valid sessions response listener({ source: popupMock, origin: 'my-origin', data: { - type: 'auth-result', - payload: myPayload, + type: 'authorization_response', + response: myResponse, }, } as MessageEvent); - await expect(payloadPromise).resolves.toBe(myPayload); + await expect(payloadPromise).resolves.toBe(myResponse); expect(openSpy).toBeCalledTimes(1); expect(addEventListenerSpy).toBeCalledTimes(1); @@ -118,7 +118,7 @@ describe('showLoginPopup', () => { source: popupMock, origin: 'my-origin', data: { - type: 'auth-result', + type: 'authorization_response', error: { message: 'NOPE', name: 'NopeError', diff --git a/packages/core-api/src/lib/loginPopup.ts b/packages/core-api/src/lib/loginPopup.ts index b4af94be49..10d6efd7a4 100644 --- a/packages/core-api/src/lib/loginPopup.ts +++ b/packages/core-api/src/lib/loginPopup.ts @@ -46,11 +46,11 @@ export type LoginPopupOptions = { type AuthResult = | { - type: 'auth-result'; - payload: any; + type: 'authorization_response'; + response: unknown; } | { - type: 'auth-result'; + type: 'authorization_response'; error: { name: string; message: string; @@ -58,12 +58,13 @@ type AuthResult = }; /** - * Show a popup pointing to a URL that starts an auth flow. + * Show a popup pointing to a URL that starts an auth flow. Implementing the receiving + * end of the postMessage mechanism outlined in https://tools.ietf.org/html/draft-sakimura-oauth-wmrm-00 * * The redirect handler of the flow should use postMessage to communicate back * to the app window. The message posted to the app must match the AuthResult type. * - * The returned promise resolves to the contents of the message that was posted from the auth popup. + * The returned promise resolves to the response of the message that was posted from the auth popup. */ export function showLoginPopup(options: LoginPopupOptions): Promise { return new Promise((resolve, reject) => { @@ -91,7 +92,7 @@ export function showLoginPopup(options: LoginPopupOptions): Promise { return; } const { data } = event; - if (data.type !== 'auth-result') { + if (data.type !== 'authorization_response') { return; } const authResult = data as AuthResult; @@ -103,7 +104,7 @@ export function showLoginPopup(options: LoginPopupOptions): Promise { // error.extra = authResult.error.extra; reject(error); } else { - resolve(authResult.payload); + resolve(authResult.response); } done(); }; diff --git a/packages/core/package.json b/packages/core/package.json index 212fe7ab35..82ea259ba1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.10", - "@backstage/core-api": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/config": "^0.1.1-alpha.12", + "@backstage/core-api": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -54,8 +54,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -68,6 +68,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx index b81ba2aaf7..73659eb63d 100644 --- a/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx +++ b/packages/core/src/layout/Sidebar/Settings/OAuthProviderSettings.tsx @@ -41,22 +41,29 @@ export const OAuthProviderSettings: FC = ({ const [signedIn, setSignedIn] = useState(false); useEffect(() => { + let didCancel = false; + const checkSession = async () => { const session = await api.getAccessToken('', { optional: true }); - setSignedIn(!!session); + if (!didCancel) { + setSignedIn(!!session); + } }; let subscription: Subscription; const observeSession = () => { subscription = api .sessionState$() .subscribe((sessionState: SessionState) => { - setSignedIn(sessionState === SessionState.SignedIn); + if (!didCancel) { + setSignedIn(sessionState === SessionState.SignedIn); + } }); }; checkSession(); observeSession(); return () => { + didCancel = true; subscription.unsubscribe(); }; }, [api]); diff --git a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx index 139d79213f..19ee00eee1 100644 --- a/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx +++ b/packages/core/src/layout/Sidebar/Settings/OIDCProviderSettings.tsx @@ -41,9 +41,13 @@ export const OIDCProviderSettings: FC = ({ const [signedIn, setSignedIn] = useState(false); useEffect(() => { + let didCancel = false; + const checkSession = async () => { const session = await api.getIdToken({ optional: true }); - setSignedIn(!!session); + if (!didCancel) { + setSignedIn(!!session); + } }; let subscription: Subscription; @@ -51,13 +55,16 @@ export const OIDCProviderSettings: FC = ({ subscription = api .sessionState$() .subscribe((sessionState: SessionState) => { - setSignedIn(sessionState === SessionState.SignedIn); + if (!didCancel) { + setSignedIn(sessionState === SessionState.SignedIn); + } }); }; checkSession(); observeSession(); return () => { + didCancel = true; subscription.unsubscribe(); }; }, [api]); diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx index 06c36bf9ce..101ae7010b 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx @@ -14,19 +14,12 @@ * limitations under the License. */ -import React, { FC, useState, useRef, useEffect } from 'react'; +import React, { FC, useRef } from 'react'; import { makeStyles, Avatar, Divider } from '@material-ui/core'; -import { - ProfileInfo, - useApi, - googleAuthApiRef, - Subscription, - SessionState, -} from '@backstage/core-api'; +import { useApi, identityApiRef } from '@backstage/core-api'; import { SidebarItem } from '../Items'; import ExpandLess from '@material-ui/icons/ExpandLess'; import ExpandMore from '@material-ui/icons/ExpandMore'; -import AccountCircleIcon from '@material-ui/icons/AccountCircle'; const useStyles = makeStyles({ avatar: { @@ -39,71 +32,26 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ open, setOpen, }) => { - const [profile, setProfile] = useState(); const ref = useRef(); // for scrolling down when collapse item opens - const googleAuth = useApi(googleAuthApiRef); const classes = useStyles(); + const profile = useApi(identityApiRef).getProfile(); const handleClick = () => { setOpen(!open); setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300); }; - useEffect(() => { - const fetchProfile = async () => { - await googleAuth - .getProfile({ optional: true }) - .then((userProfile?: ProfileInfo) => { - setProfile(userProfile); - }); - }; - - let subscription: Subscription; - const observeSession = () => { - subscription = googleAuth - .sessionState$() - .subscribe(async (sessionState: SessionState) => { - if (sessionState === SessionState.SignedIn) { - await fetchProfile(); - } else { - setProfile(undefined); - } - }); - }; - - fetchProfile(); - observeSession(); - return () => { - subscription.unsubscribe(); - }; - }, [googleAuth]); - - // Handle main auth info that is shown on the collapsible SidebarItem - let avatar; - let displayName = 'Guest'; - if (profile) { - const email = profile.email; - const name = profile.name; - const imageUrl = profile.picture; - const emailTrimmed = email.split('@')[0]; - const displayEmail = - emailTrimmed.charAt(0).toUpperCase() + emailTrimmed.slice(1); - displayName = name ?? displayEmail; - avatar = imageUrl - ? () => ( - - ) - : () => ; - } + const displayName = profile.displayName ?? profile.email; + const SignInAvatar = () => ( + + {displayName[0]} + + ); return ( <> - + {open ? : } diff --git a/packages/core/src/layout/SignInPage/customProvider.tsx b/packages/core/src/layout/SignInPage/customProvider.tsx index d24b054b16..c6cb9b789d 100644 --- a/packages/core/src/layout/SignInPage/customProvider.tsx +++ b/packages/core/src/layout/SignInPage/customProvider.tsx @@ -56,6 +56,9 @@ const Component: ProviderComponent = ({ onResult }) => { const handleResult = ({ userId, idToken }: Data) => { onResult({ userId, + profile: { + email: `${userId}@example.com`, + }, getIdToken: idToken ? async () => idToken : undefined, }); }; diff --git a/packages/core/src/layout/SignInPage/googleProvider.tsx b/packages/core/src/layout/SignInPage/googleProvider.tsx index 9613437af8..632b81ad17 100644 --- a/packages/core/src/layout/SignInPage/googleProvider.tsx +++ b/packages/core/src/layout/SignInPage/googleProvider.tsx @@ -18,16 +18,7 @@ import React from 'react'; import { Grid, Typography, Button } from '@material-ui/core'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { - useApi, - googleAuthApiRef, - errorApiRef, - ProfileInfo, -} from '@backstage/core-api'; - -function parseUserId(profile: ProfileInfo) { - return profile!.email.replace(/@.*/, ''); -} +import { useApi, googleAuthApiRef, errorApiRef } from '@backstage/core-api'; const Component: ProviderComponent = ({ onResult }) => { const googleAuthApi = useApi(googleAuthApiRef); @@ -35,12 +26,17 @@ const Component: ProviderComponent = ({ onResult }) => { const handleLogin = async () => { try { - await googleAuthApi.getIdToken({ instantPopup: true }); + const identity = await googleAuthApi.getBackstageIdentity({ + instantPopup: true, + }); + const profile = await googleAuthApi.getProfile(); onResult({ - userId: parseUserId(profile!), - getIdToken: () => googleAuthApi.getIdToken(), + userId: identity!.id, + profile: profile!, + getIdToken: () => + googleAuthApi.getBackstageIdentity().then(i => i!.idToken), logout: async () => { await googleAuthApi.logout(); }, @@ -69,11 +65,21 @@ const Component: ProviderComponent = ({ onResult }) => { const loader: ProviderLoader = async apis => { const googleAuthApi = apis.get(googleAuthApiRef)!; - const profile = await googleAuthApi.getProfile({ optional: true }); + const identity = await googleAuthApi.getBackstageIdentity({ + optional: true, + }); + + if (!identity) { + return undefined; + } + + const profile = await googleAuthApi.getProfile(); return { - userId: parseUserId(profile!), - getIdToken: () => googleAuthApi.getIdToken(), + userId: identity.id, + profile: profile!, + getIdToken: () => + googleAuthApi.getBackstageIdentity().then(i => i!.idToken), logout: async () => { await googleAuthApi.logout(); }, diff --git a/packages/core/src/layout/SignInPage/guestProvider.tsx b/packages/core/src/layout/SignInPage/guestProvider.tsx index 78d0191ba9..85784eb205 100644 --- a/packages/core/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core/src/layout/SignInPage/guestProvider.tsx @@ -19,6 +19,14 @@ import { Grid, Typography, Button } from '@material-ui/core'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +const result = { + userId: 'guest', + profile: { + email: 'guest@example.com', + displayName: 'Guest', + }, +}; + const Component: ProviderComponent = ({ onResult }) => ( ( @@ -45,7 +53,7 @@ const Component: ProviderComponent = ({ onResult }) => ( ); const loader: ProviderLoader = async () => { - return { userId: 'guest' }; + return result; }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index d60c8ff281..96dbc30a89 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -93,8 +93,9 @@ export const useSignInProviders = ( } if (result) { handleWrappedResult(result); + } else { + setLoading(false); } - setLoading(false); }) .catch(error => { if (didCancel) { diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 922b661170..65f92242da 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", @@ -50,6 +50,6 @@ "@types/node": "^12.0.0" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } 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, + }), +}); diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 65b2ec5aaa..3b61ae7af8 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -22,6 +22,7 @@ builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); builder.add(identityApiRef, { getUserId: () => 'guest', + getProfile: () => ({ email: 'guest@example.com' }), getIdToken: () => undefined, logout: async () => {}, }); diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 5a59426538..060241d383 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "description": "Storybook build for core package", "private": true, "scripts": { @@ -14,7 +14,7 @@ ] }, "dependencies": { - "@backstage/theme": "^0.1.1-alpha.10" + "@backstage/theme": "^0.1.1-alpha.12" }, "devDependencies": { "@storybook/addon-actions": "^5.3.17", diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 52448d7a1f..4258aee12e 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-cli", "description": "CLI for running TechDocs locally.", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": true, "publishConfig": { "access": "public" @@ -28,7 +28,7 @@ "techdocs": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10" + "@backstage/cli": "^0.1.1-alpha.12" }, "files": [ "bin", diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 9fc4951df9..60e0a6bdbe 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -40,6 +40,6 @@ "@types/node": "^12.0.0" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ff6f7b85fa..51ab99fcb1 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/core-api": "^0.1.1-alpha.10", - "@backstage/test-utils-core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/core-api": "^0.1.1-alpha.12", + "@backstage/test-utils-core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", @@ -48,6 +48,6 @@ "@types/node": "^12.0.0" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/packages/theme/package.json b/packages/theme/package.json index 04bb5f4467..61bd0443d0 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -31,9 +31,9 @@ "@material-ui/core": "^4.9.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10" + "@backstage/cli": "^0.1.1-alpha.12" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index a00fdcb249..2de098c7eb 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.10", - "@backstage/config": "^0.1.1-alpha.10", - "@backstage/config-loader": "^0.1.1-alpha.10", + "@backstage/backend-common": "^0.1.1-alpha.12", + "@backstage/config": "^0.1.1-alpha.12", + "@backstage/config-loader": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "body-parser": "^1.19.0", "compression": "^1.7.4", @@ -46,7 +46,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/jwt-decode": "2.2.1", @@ -57,6 +57,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts index 48952e6dad..f92875fd48 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts @@ -23,7 +23,23 @@ import { verifyNonce, OAuthProvider, } from './OAuthProvider'; -import { AuthResponse, OAuthProviderHandlers } from '../providers/types'; +import { + WebMessageResponse, + OAuthProviderHandlers, + OAuthResponse, +} from '../providers/types'; + +const mockResponseData: OAuthResponse = { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, +}; describe('OAuthProvider Utils', () => { describe('verifyNonce', () => { @@ -38,6 +54,7 @@ describe('OAuthProvider Utils', () => { verifyNonce(mockRequest, 'providera'); }).toThrowError('Missing nonce'); }); + it('should throw error if state nonce missing', () => { const mockRequest = ({ cookies: { @@ -49,6 +66,7 @@ describe('OAuthProvider Utils', () => { verifyNonce(mockRequest, 'providera'); }).toThrowError('Missing nonce'); }); + it('should throw error if nonce mismatch', () => { const mockRequest = ({ cookies: { @@ -62,6 +80,7 @@ describe('OAuthProvider Utils', () => { verifyNonce(mockRequest, 'providera'); }).toThrowError('Invalid nonce'); }); + it('should not throw any error if nonce matches', () => { const mockRequest = ({ cookies: { @@ -85,13 +104,22 @@ describe('OAuthProvider Utils', () => { setHeader: jest.fn().mockReturnThis(), } as unknown) as express.Response; - const data: AuthResponse = { - type: 'auth-result', - payload: { - accessToken: 'ACCESS_TOKEN', - idToken: 'ID_TOKEN', - expiresInSeconds: 10, - scope: 'email', + const data: WebMessageResponse = { + type: 'authorization_response', + response: { + providerInfo: { + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 10, + scope: 'email', + }, + profile: { + email: 'foo@bar.com', + }, + backstageIdentity: { + id: 'a', + idToken: 'a.b.c', + }, }, }; const jsonData = JSON.stringify(data); @@ -111,8 +139,8 @@ describe('OAuthProvider Utils', () => { setHeader: jest.fn().mockReturnThis(), } as unknown) as express.Response; - const data: AuthResponse = { - type: 'auth-result', + const data: WebMessageResponse = { + type: 'authorization_response', error: new Error('Unknown error occured'), }; const jsonData = JSON.stringify(data); @@ -161,16 +189,12 @@ describe('OAuthProvider', () => { } async handler() { return { - user: {}, - info: { - refreshToken: 'token', - }, + response: mockResponseData, + refreshToken: 'token', }; } async refresh() { - return { - accessToken: 'token', - }; + return mockResponseData; } } const providerInstance = new MyAuthProvider(); @@ -323,11 +347,13 @@ describe('OAuthProvider', () => { await oauthProvider.refresh(mockRequest, mockResponse); expect(mockResponse.send).toHaveBeenCalledTimes(1); - expect(mockResponse.send).toHaveBeenCalledWith( - expect.objectContaining({ - accessToken: 'token', - }), - ); + expect(mockResponse.send).toHaveBeenCalledWith({ + ...mockResponseData, + backstageIdentity: { + id: mockResponseData.profile.email, + idToken: 'my-id-token', + }, + }); }); it('handles refresh without capabilities', async () => { diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts index 38065c3380..dac31af451 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.ts @@ -21,6 +21,7 @@ import { AuthResponse, AuthProviderRouteHandlers, OAuthProviderHandlers, + WebMessageResponse, } from '../providers/types'; import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../identity'; @@ -53,9 +54,9 @@ export const verifyNonce = (req: express.Request, providerId: string) => { export const postMessageResponse = ( res: express.Response, appOrigin: string, - data: AuthResponse, + response: WebMessageResponse, ) => { - const jsonData = JSON.stringify(data); + const jsonData = JSON.stringify(response); const base64Data = Buffer.from(jsonData, 'utf8').toString('base64'); res.setHeader('Content-Type', 'text/html'); @@ -96,7 +97,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers { this.basePath = url.pathname; } - async start(req: express.Request, res: express.Response): Promise { + async start(req: express.Request, res: express.Response): Promise { // retrieve scopes from request const scope = req.query.scope?.toString() ?? ''; @@ -108,14 +109,15 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // set a nonce cookie before redirecting to oauth provider this.setNonceCookie(res, nonce); - const options = { + const queryParameters = { scope, - accessType: 'offline', - prompt: 'consent', state: nonce, }; - const { url, status } = await this.providerHandlers.start(req, options); + const { url, status } = await this.providerHandlers.start( + req, + queryParameters, + ); res.statusCode = status || 302; res.setHeader('Location', url); @@ -126,16 +128,17 @@ export class OAuthProvider implements AuthProviderRouteHandlers { async frameHandler( req: express.Request, res: express.Response, - ): Promise { + ): Promise { try { // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); - const { user, info } = await this.providerHandlers.handler(req); + const { response, refreshToken } = await this.providerHandlers.handler( + req, + ); if (!this.options.disableRefresh) { // throw error if missing refresh token - const { refreshToken } = info; if (!refreshToken) { throw new Error('Missing refresh token'); } @@ -144,19 +147,24 @@ export class OAuthProvider implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - user.userIdToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: user.profile.email }, + const id = response.profile.email; + const idToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: id }, }); + const fullResponse: AuthResponse = { + ...response, + backstageIdentity: { id, idToken }, + }; // post message back to popup if successful return postMessageResponse(res, this.options.appOrigin, { - type: 'auth-result', - payload: user, + type: 'authorization_response', + response: fullResponse, }); } catch (error) { // post error message back to popup if failure return postMessageResponse(res, this.options.appOrigin, { - type: 'auth-result', + type: 'authorization_response', error: { name: error.name, message: error.message, @@ -165,27 +173,30 @@ export class OAuthProvider implements AuthProviderRouteHandlers { } } - async logout(req: express.Request, res: express.Response): Promise { + async logout(req: express.Request, res: express.Response): Promise { if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); + res.status(401).send('Invalid X-Requested-With header'); + return; } if (!this.options.disableRefresh) { // remove refresh token cookie before logout this.removeRefreshTokenCookie(res); } - return res.send('logout!'); + res.send('logout!'); } - async refresh(req: express.Request, res: express.Response): Promise { + async refresh(req: express.Request, res: express.Response): Promise { if (!ensuresXRequestedWith(req)) { - return res.status(401).send('Invalid X-Requested-With header'); + res.status(401).send('Invalid X-Requested-With header'); + return; } if (!this.providerHandlers.refresh || this.options.disableRefresh) { - return res.send( + res.send( `Refresh token not supported for provider: ${this.options.providerId}`, ); + return; } try { @@ -200,18 +211,20 @@ export class OAuthProvider implements AuthProviderRouteHandlers { const scope = req.query.scope?.toString() ?? ''; // get new access_token - const refreshInfo = await this.providerHandlers.refresh( - refreshToken, - scope, - ); + const response = await this.providerHandlers.refresh(refreshToken, scope); - refreshInfo.userIdToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: refreshInfo.profile?.email }, + const id = response.profile.email; + const idToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: id }, }); + const fullResponse: AuthResponse = { + ...response, + backstageIdentity: { id, idToken }, + }; - return res.send(refreshInfo); + res.send(fullResponse); } catch (error) { - return res.status(401).send(`${error.message}`); + res.status(401).send(`${error.message}`); } } diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts index 071c949a21..d8f6d25d0f 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.test.ts @@ -82,8 +82,8 @@ describe('PassportStrategyHelper', () => { expect(spyAuthenticate).toBeCalledTimes(1); await expect(frameHandlerStrategyPromise).resolves.toStrictEqual( expect.objectContaining({ - user: { accessToken: 'ACCESS_TOKEN' }, - info: { refreshToken: 'REFRESH_TOKEN' }, + response: { accessToken: 'ACCESS_TOKEN' }, + privateInfo: { refreshToken: 'REFRESH_TOKEN' }, }), ); }); diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 1e8dfca5ed..46a386f419 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -26,42 +26,52 @@ import { export const makeProfileInfo = ( profile: passport.Profile, - params: any, + idToken?: string, ): ProfileInfo => { - const { displayName: name } = profile; + const { displayName } = profile; - let email = ''; - if (profile.emails) { + let email: string | undefined = undefined; + if (profile.emails && profile.emails.length > 0) { const [firstEmail] = profile.emails; email = firstEmail.value; } - if (!email && params.id_token) { - try { - const decoded: { email: string } = jwtDecoder(params.id_token); - email = decoded.email; - } catch (e) { - console.error('Failed to parse id token and get profile info'); - } - } - - let picture = ''; + let picture: string | undefined = undefined; if (profile.photos) { const [firstPhoto] = profile.photos; picture = firstPhoto.value; } + if ((!email || !picture) && idToken) { + try { + const decoded: Record = jwtDecoder(idToken); + + if (!email && decoded.email) { + email = decoded.email; + } + if (!picture && decoded.picture) { + picture = decoded.picture; + } + } catch (e) { + throw new Error(`Failed to parse id token and get profile info, ${e}`); + } + } + + if (!email) { + throw new Error('No email received in profile info'); + } + return { - name, email, picture, + displayName, }; }; export const executeRedirectStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, - options: any, + options: Record, ): Promise => { return new Promise(resolve => { const strategy = Object.create(providerStrategy); @@ -73,30 +83,32 @@ export const executeRedirectStrategy = async ( }); }; -export const executeFrameHandlerStrategy = async ( +export const executeFrameHandlerStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, ) => { - return new Promise<{ user: any; info: any }>((resolve, reject) => { - const strategy = Object.create(providerStrategy); - strategy.success = (user: any, info: any) => { - resolve({ user, info }); - }; - strategy.fail = ( - info: { type: 'success' | 'error'; message?: string }, - // _status: number, - ) => { - reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); - }; - strategy.error = (error: Error) => { - reject(new Error(`Authentication failed, ${error}`)); - }; - strategy.redirect = () => { - reject(new Error('Unexpected redirect')); - }; + return new Promise<{ response: T; privateInfo: PrivateInfo }>( + (resolve, reject) => { + const strategy = Object.create(providerStrategy); + strategy.success = (response: any, privateInfo: any) => { + resolve({ response, privateInfo }); + }; + strategy.fail = ( + info: { type: 'success' | 'error'; message?: string }, + // _status: number, + ) => { + reject(new Error(`Authentication rejected, ${info.message ?? ''}`)); + }; + strategy.error = (error: Error) => { + reject(new Error(`Authentication failed, ${error}`)); + }; + strategy.redirect = () => { + reject(new Error('Unexpected redirect')); + }; - strategy.authenticate(req, {}); - }); + strategy.authenticate(req, {}); + }, + ); }; export const executeRefreshTokenStrategy = async ( @@ -151,7 +163,7 @@ export const executeRefreshTokenStrategy = async ( export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, - params: any, + idToken?: string, ): Promise => { return new Promise((resolve, reject) => { const anyStrategy = (providerStrategy as unknown) as ProviderStrategy; @@ -162,7 +174,7 @@ export const executeFetchUserProfileStrategy = async ( reject(error); } - const profile = makeProfileInfo(passportProfile, params); + const profile = makeProfileInfo(passportProfile, idToken); resolve(profile); }, ); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index b5f9d7e46b..3ae9858bb1 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -19,16 +19,17 @@ import { Strategy as GithubStrategy } from 'passport-github2'; import { executeFrameHandlerStrategy, executeRedirectStrategy, + makeProfileInfo, } from '../../lib/PassportStrategyHelper'; import { OAuthProviderHandlers, AuthProviderConfig, RedirectInfo, - AuthInfoBase, - AuthInfoPrivate, EnvironmentProviderConfig, OAuthProviderOptions, OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; import { @@ -44,25 +45,42 @@ export class GithubAuthProvider implements OAuthProviderHandlers { constructor(options: OAuthProviderOptions) { this._strategy = new GithubStrategy( { ...options }, - (accessToken: any, _: any, params: any, profile: any, done: any) => { + ( + accessToken: any, + _: any, + params: any, + rawProfile: any, + done: PassportDoneCallback, + ) => { + const profile = makeProfileInfo(rawProfile); done(undefined, { + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, profile, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, }); }, ); } - async start(req: express.Request, options: any): Promise { + async start( + req: express.Request, + options: Record, + ): Promise { return await executeRedirectStrategy(req, this._strategy, options); } - async handler( - req: express.Request, - ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { - return await executeFrameHandlerStrategy(req, this._strategy); + async handler(req: express.Request): Promise<{ response: OAuthResponse }> { + const result = await executeFrameHandlerStrategy( + req, + this._strategy, + ); + + return { + response: result.response, + }; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index f41fbfe787..f12b248021 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -25,14 +25,13 @@ import { } from '../../lib/PassportStrategyHelper'; import { OAuthProviderHandlers, - AuthInfoBase, - AuthInfoPrivate, RedirectInfo, AuthProviderConfig, - AuthInfoWithProfile, EnvironmentProviderConfig, OAuthProviderOptions, OAuthProviderConfig, + OAuthResponse, + PassportDoneCallback, } from '../types'; import { OAuthProvider } from '../../lib/OAuthProvider'; import passport from 'passport'; @@ -43,6 +42,10 @@ import { import { Logger } from 'winston'; import { TokenIssuer } from '../../identity'; +type PrivateInfo = { + refreshToken: string; +}; + export class GoogleAuthProvider implements OAuthProviderHandlers { private readonly _strategy: GoogleStrategy; @@ -56,18 +59,20 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { accessToken: any, refreshToken: any, params: any, - profile: passport.Profile, - done: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profileInfo = makeProfileInfo(profile, params); + const profile = makeProfileInfo(rawProfile, params.id_token); done( undefined, { - profile: profileInfo, - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, + providerInfo: { + idToken: params.id_token, + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + profile, }, { refreshToken, @@ -77,20 +82,33 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { ); } - async start(req: express.Request, options: any): Promise { - return await executeRedirectStrategy(req, this._strategy, options); + async start( + req: express.Request, + options: Record, + ): Promise { + const providerOptions = { + ...options, + accessType: 'offline', + prompt: 'consent', + }; + return await executeRedirectStrategy(req, this._strategy, providerOptions); } async handler( req: express.Request, - ): Promise<{ user: AuthInfoBase; info: AuthInfoPrivate }> { - return await executeFrameHandlerStrategy(req, this._strategy); + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const result = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, this._strategy); + + return { + response: result.response, + refreshToken: result.privateInfo.refreshToken, + }; } - async refresh( - refreshToken: string, - scope: string, - ): Promise { + async refresh(refreshToken: string, scope: string): Promise { const { accessToken, params } = await executeRefreshTokenStrategy( this._strategy, refreshToken, @@ -100,14 +118,16 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { const profile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params, + params.id_token, ); return { - accessToken, - idToken: params.id_token, - expiresInSeconds: params.expires_in, - scope: params.scope, + providerInfo: { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, profile, }; } diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 621b75078c..68b84c7e33 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -15,7 +15,11 @@ */ import express from 'express'; -import { Strategy as SamlStrategy } from 'passport-saml'; +import { + Strategy as SamlStrategy, + Profile as SamlProfile, + VerifyWithoutRequest, +} from 'passport-saml'; import { executeFrameHandlerStrategy, executeRedirectStrategy, @@ -25,6 +29,8 @@ import { AuthProviderRouteHandlers, EnvironmentProviderConfig, SAMLProviderConfig, + PassportDoneCallback, + ProfileInfo, } from '../types'; import { postMessageResponse } from '../../lib/OAuthProvider'; import { @@ -32,30 +38,39 @@ import { EnvironmentHandler, } from '../../lib/EnvironmentHandler'; import { Logger } from 'winston'; +import { TokenIssuer } from '../../identity'; + +type SamlInfo = { + userId: string; + profile: ProfileInfo; +}; export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly strategy: SamlStrategy; + private readonly tokenIssuer: TokenIssuer; constructor(options: SAMLProviderOptions) { - this.strategy = new SamlStrategy( - { ...options }, - (profile: any, done: any) => { - // TODO: There's plenty more validation and profile handling to do here, - // this provider is currently only intended to validate the provider pattern - // for non-oauth auth flows. - // TODO: This flow doesn't issue an identity token that can be used to validate - // the identity of the user in other backends, which we need in some form. - done(undefined, { - email: profile.email, - firstName: profile.firstName, - lastName: profile.lastName, - displayName: profile.displayName, - }); - }, - ); + this.tokenIssuer = options.tokenIssuer; + this.strategy = new SamlStrategy({ ...options }, (( + profile: SamlProfile, + done: PassportDoneCallback, + ) => { + // TODO: There's plenty more validation and profile handling to do here, + // this provider is currently only intended to validate the provider pattern + // for non-oauth auth flows. + // TODO: This flow doesn't issue an identity token that can be used to validate + // the identity of the user in other backends, which we need in some form. + done(undefined, { + userId: profile.ID!, + profile: { + email: profile.email!, + displayName: profile.displayName as string, + }, + }); + }) as VerifyWithoutRequest); } - async start(req: express.Request, res: express.Response): Promise { + async start(req: express.Request, res: express.Response): Promise { const { url } = await executeRedirectStrategy(req, this.strategy, {}); res.redirect(url); } @@ -63,17 +78,28 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { async frameHandler( req: express.Request, res: express.Response, - ): Promise { + ): Promise { try { - const { user } = await executeFrameHandlerStrategy(req, this.strategy); + const { + response: { userId, profile }, + } = await executeFrameHandlerStrategy(req, this.strategy); + + const id = userId; + const idToken = await this.tokenIssuer.issueToken({ + claims: { sub: id }, + }); return postMessageResponse(res, 'http://localhost:3000', { - type: 'auth-result', - payload: user, + type: 'authorization_response', + response: { + providerInfo: {}, + profile, + backstageIdentity: { id, idToken }, + }, }); } catch (error) { return postMessageResponse(res, 'http://localhost:3000', { - type: 'auth-result', + type: 'authorization_response', error: { name: error.name, message: error.message, @@ -82,7 +108,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } - async logout(_req: express.Request, res: express.Response): Promise { + async logout(_req: express.Request, res: express.Response): Promise { res.send('noop'); } } @@ -91,12 +117,14 @@ type SAMLProviderOptions = { entryPoint: string; issuer: string; path: string; + tokenIssuer: TokenIssuer; }; export function createSamlProvider( _authProviderConfig: AuthProviderConfig, providerConfig: EnvironmentProviderConfig, logger: Logger, + tokenIssuer: TokenIssuer, ) { const envProviders: EnvironmentHandlers = {}; @@ -106,6 +134,7 @@ export function createSamlProvider( entryPoint: config.entryPoint, issuer: config.issuer, path: '/auth/saml/handler/frame', + tokenIssuer, }; if (!opts.entryPoint || !opts.issuer) { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 3660751c1c..81773bf9dc 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -89,25 +89,30 @@ export interface OAuthProviderHandlers { * @param {express.Request} req * @param options */ - start(req: express.Request, options: any): Promise; + start( + req: express.Request, + options: Record, + ): Promise; /** * Handles the redirect from the auth provider when the user has signed in. * @param {express.Request} req */ - handler(req: express.Request): Promise; + handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken?: string }>; /** * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. * @param {string} refreshToken * @param {string} scope */ - refresh?(refreshToken: string, scope: string): Promise; + refresh?(refreshToken: string, scope: string): Promise; /** * (Optional) Sign out of the auth provider. */ - logout?(): Promise; + logout?(): Promise; } /** @@ -134,7 +139,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - start(req: express.Request, res: express.Response): Promise; + start(req: express.Request, res: express.Response): Promise; /** * Once the user signs in or consents in the OAuth screen, the auth provider redirects to the @@ -149,7 +154,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - frameHandler(req: express.Request, res: express.Response): Promise; + frameHandler(req: express.Request, res: express.Response): Promise; /** * (Optional) If the auth provider supports refresh tokens then this method handles @@ -163,7 +168,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - refresh?(req: express.Request, res: express.Response): Promise; + refresh?(req: express.Request, res: express.Response): Promise; /** * (Optional) Handles sign out requests @@ -174,7 +179,7 @@ export interface AuthProviderRouteHandlers { * @param {express.Request} req * @param {express.Response} res */ - logout?(req: express.Request, res: express.Response): Promise; + logout?(req: express.Request, res: express.Response): Promise; } export type AuthProviderFactory = ( @@ -184,7 +189,30 @@ export type AuthProviderFactory = ( issuer: TokenIssuer, ) => AuthProviderRouteHandlers; -export type AuthInfoBase = { +export type AuthResponse = { + providerInfo: ProviderInfo; + profile: ProfileInfo; + backstageIdentity: BackstageIdentity; +}; + +export type OAuthResponse = Omit< + AuthResponse, + 'backstageIdentity' +>; + +export type BackstageIdentity = { + /** + * The backstage user ID. + */ + id: string; + + /** + * An ID token that can be used to authenticate the user within Backstage. + */ + idToken: string; +}; + +export type OAuthProviderInfo = { /** * An access token issued for the signed in user. */ @@ -203,14 +231,7 @@ export type AuthInfoBase = { scope: string; }; -export type AuthInfoWithProfile = AuthInfoBase & { - /** - * Profile information of the signed in user. - */ - profile: ProfileInfo | undefined; -}; - -export type AuthInfoPrivate = { +export type OAuthPrivateInfo = { /** * A refresh token issued for the signed in user. */ @@ -221,16 +242,22 @@ export type AuthInfoPrivate = { * Payload sent as a post message after the auth request is complete. * If successful then has a valid payload with Auth information else contains an error. */ -export type AuthResponse = +export type WebMessageResponse = | { - type: 'auth-result'; - payload: AuthInfoBase | AuthInfoWithProfile; + type: 'authorization_response'; + response: AuthResponse; } | { - type: 'auth-result'; + type: 'authorization_response'; error: Error; }; +export type PassportDoneCallback = ( + err?: Error, + response?: Res, + privateInfo?: Private, +) => void; + export type RedirectInfo = { /** * URL to redirect to @@ -242,6 +269,12 @@ export type RedirectInfo = { status?: number; }; +/** + * Used to display login information to user, i.e. sidebar popup. + * + * It is also temporarily used as the profile of the signed-in user's Backstage + * identity, but we want to replace that with data from identity and/org catalog service + */ export type ProfileInfo = { /** * Email ID of the signed in user. @@ -250,12 +283,12 @@ export type ProfileInfo = { /** * Display name that can be presented to the signed in user. */ - name: string; + displayName?: string; /** * URL to an image that can be used as the display image or avatar of the * signed in user. */ - picture: string; + picture?: string; }; export type RefreshTokenResponse = { diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 68b2d31032..28718e85a1 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -67,12 +67,6 @@ export async function createRouter( clientId: process.env.AUTH_GOOGLE_CLIENT_ID!, clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!, }, - production: { - appOrigin: 'http://localhost:3000', - secure: false, - clientId: '', - clientSecret: '', - }, }, github: { development: { diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index b3d18b220b..1bfd72bdee 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "mock-data": "./scripts/mock-data" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.10", - "@backstage/catalog-model": "^0.1.1-alpha.10", + "@backstage/backend-common": "^0.1.1-alpha.12", + "@backstage/catalog-model": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -36,10 +36,10 @@ "winston": "^3.2.1", "yaml": "^1.9.2", "yn": "^4.0.0", - "yup": "^0.28.5" + "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", @@ -49,7 +49,7 @@ "supertest": "^4.0.2" }, "files": [ - "dist/**/*.{js,d.ts}", + "dist", "migrations/**/*.{js,d.ts}" ] } diff --git a/plugins/catalog-backend/scripts/mock-data b/plugins/catalog-backend/scripts/mock-data index 76ca5cf7c2..01675db363 100755 --- a/plugins/catalog-backend/scripts/mock-data +++ b/plugins/catalog-backend/scripts/mock-data @@ -14,5 +14,5 @@ for URL in \ --location \ --request POST 'localhost:7000/catalog/locations' \ --header 'Content-Type: application/json' \ - --data-raw "{\"type\": \"github\", \"target\": \"https://github.com/spotify/backstage/blob/master/plugins/catalog-backend/examples/${URL}\"}" + --data-raw "{\"type\": \"github\", \"target\": \"https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/${URL}\"}" done diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 352f4a613e..c841b9dc7a 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.10", - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.10", - "@backstage/plugin-sentry": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/catalog-model": "^0.1.1-alpha.12", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.12", + "@backstage/plugin-sentry": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -38,9 +38,9 @@ "swr": "^0.2.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.3.0", @@ -54,6 +54,6 @@ "whatwg-fetch": "^3.0.0" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index f57e470e57..e5db5b9230 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -19,23 +19,24 @@ import { Content, errorApiRef, Header, + HeaderLabel, HeaderTabs, Page, pageTheme, + PageTheme, Progress, useApi, - PageTheme, } from '@backstage/core'; import { SentryIssuesWidget } from '@backstage/plugin-sentry'; import { Grid } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { FC, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { catalogApiRef } from '../..'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; -import { useParams, useNavigate } from 'react-router-dom'; const REDIRECT_DELAY = 1000; function headerProps( @@ -138,7 +139,19 @@ export const EntityPage: FC<{}> = () => { return (
- {entity && } + {entity && ( + <> + + + + + )}
{loading && } diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 4ae6eca890..a938b9728b 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -45,8 +45,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -57,6 +57,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 17a6b8cebc..755f0e50c3 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,9 +32,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -44,6 +44,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 663a6505a5..bbf9b9c5d8 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -43,6 +43,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } 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/package.json b/plugins/graphiql/package.json index 16e47552a3..0277363ad1 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "private": false, "publishConfig": { "access": "public", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -43,9 +43,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -57,6 +57,6 @@ "react-router-dom": "6.0.0-alpha.5" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } 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 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + 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); } diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index cb1f8dffd5..bc2e82bf94 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-identity-backend", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.10", + "@backstage/backend-common": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", @@ -33,10 +33,10 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index cf9722dde5..67d8bf143e 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,9 +33,9 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", - "@backstage/test-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", + "@backstage/test-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -45,6 +45,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index c40e4e8367..5c879ff8a1 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.10", - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/plugin-catalog": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/catalog-model": "^0.1.1-alpha.12", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/plugin-catalog": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,8 +36,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -47,6 +47,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 34593db501..061f6ca814 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "mock-data": "./scripts/mock-data" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.10", - "@backstage/catalog-model": "^0.1.1-alpha.10", - "@backstage/config": "^0.1.1-alpha.10", + "@backstage/backend-common": "^0.1.1-alpha.12", + "@backstage/catalog-model": "^0.1.1-alpha.12", + "@backstage/config": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", @@ -39,7 +39,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "@types/dockerode": "^2.5.32", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", @@ -49,6 +49,6 @@ "yaml": "^1.10.0" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 04260819ed..313242f2c0 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -43,6 +43,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index c8fa9a8403..b897a80e9a 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry-backend", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.10", + "@backstage/backend-common": "^0.1.1-alpha.12", "@types/express": "^4.17.6", "axios": "^0.19.2", "compression": "^1.7.4", @@ -34,10 +34,10 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index f72fdcd229..969445c1ec 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,8 +34,8 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -45,6 +45,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 9f077233ac..660ad735bc 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/test-utils-core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/test-utils-core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,8 +36,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -49,6 +49,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 40d94f6ced..068eb1ee22 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -31,8 +31,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -42,6 +42,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 8484afdb6b..3fb10b64e3 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.10", + "version": "0.1.1-alpha.12", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.10", - "@backstage/theme": "^0.1.1-alpha.10", + "@backstage/core": "^0.1.1-alpha.12", + "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,8 +32,8 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.10", - "@backstage/dev-utils": "^0.1.1-alpha.10", + "@backstage/cli": "^0.1.1-alpha.12", + "@backstage/dev-utils": "^0.1.1-alpha.12", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", @@ -43,6 +43,6 @@ "jest-fetch-mock": "^3.0.3" }, "files": [ - "dist/**/*.{js,d.ts}" + "dist" ] } diff --git a/yarn.lock b/yarn.lock index 9bfd738603..c9128c5350 100644 --- a/yarn.lock +++ b/yarn.lock @@ -884,14 +884,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f" - integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.5.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6": version "7.10.2" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== @@ -2426,10 +2419,10 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" -"@rollup/plugin-commonjs@^12.0.0": - version "12.0.0" - resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-12.0.0.tgz#e2f308ae6057499e0f413f878fff7c3a0fdc02a1" - integrity sha512-8+mDQt1QUmN+4Y9D3yCG8AJNewuTSLYPJVzKKUZ+lGeQrI+bV12Tc5HCyt2WdlnG6ihIL/DPbKRJlB40DX40mw== +"@rollup/plugin-commonjs@^13.0.0": + version "13.0.0" + resolved "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-13.0.0.tgz#8a1d684ba6848afe8b9e3d85649d4b2f6f7217ec" + integrity sha512-Anxc3qgkAi7peAyesTqGYidG5GRim9jtg8xhmykNaZkImtvjA7Wsqep08D2mYsqw1IF7rA3lYfciLgzUSgRoqw== dependencies: "@rollup/pluginutils" "^3.0.8" commondir "^1.0.1" @@ -2446,14 +2439,16 @@ dependencies: "@rollup/pluginutils" "^3.0.8" -"@rollup/plugin-node-resolve@^7.1.1": - version "7.1.3" - resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca" - integrity sha512-RxtSL3XmdTAE2byxekYLnx+98kEUOrPHF/KRVjLH+DEIHy6kjIw7YINQzn+NXiH/NTrQLAwYs0GWB+csWygA9Q== +"@rollup/plugin-node-resolve@^8.1.0": + version "8.1.0" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-8.1.0.tgz#1da5f3d0ccabc8f66f5e3c74462aad76cfd96c47" + integrity sha512-ovq7ZM3JJYUUmEjjO+H8tnUdmQmdQudJB7xruX8LFZ1W2q8jXdPUS6SsIYip8ByOApu4RR7729Am9WhCeCMiHA== dependencies: "@rollup/pluginutils" "^3.0.8" "@types/resolve" "0.0.8" builtin-modules "^3.1.0" + deep-freeze "^0.0.1" + deepmerge "^4.2.2" is-module "^1.0.0" resolve "^1.14.2" @@ -2570,15 +2565,15 @@ uuid "^3.3.2" "@storybook/addon-links@^5.3.17": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-5.3.18.tgz#6b4aec83e158c000221bfe1f43e4a473f0727193" - integrity sha512-rDpsAQUr60BEM83TBh7mwKjaZvqTR+CAAMLAxXN+J8DMoOjymjd2sjq4UVJsOQVTnw3wjIc4+xpAL/eYqkJAIQ== + version "5.3.19" + resolved "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-5.3.19.tgz#3c23e886d44b56978ae254fed3bf8be54c877178" + integrity sha512-gn9u8lebREfRsyzxoDPG0O+kOf5aJ0BhzcCJGZZdqha0F6OWHhh8vJYZZvjJ/Qwze+Qt2zjrgWm+Q6+JLD8ugQ== dependencies: - "@storybook/addons" "5.3.18" - "@storybook/client-logger" "5.3.18" - "@storybook/core-events" "5.3.18" + "@storybook/addons" "5.3.19" + "@storybook/client-logger" "5.3.19" + "@storybook/core-events" "5.3.19" "@storybook/csf" "0.0.1" - "@storybook/router" "5.3.18" + "@storybook/router" "5.3.19" core-js "^3.0.1" global "^4.3.2" prop-types "^15.7.2" @@ -2604,19 +2599,6 @@ regenerator-runtime "^0.13.3" util-deprecate "^1.0.2" -"@storybook/addons@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.18.tgz#5cbba6407ef7a802041c5ee831473bc3bed61f64" - integrity sha512-ZQjDgTUDFRLvAiBg2d8FgPgghfQ+9uFyXQbtiGlTBLinrPCeQd7J86qiUES0fcGoohCCw0wWKtvB0WF2z1XNDg== - dependencies: - "@storybook/api" "5.3.18" - "@storybook/channels" "5.3.18" - "@storybook/client-logger" "5.3.18" - "@storybook/core-events" "5.3.18" - core-js "^3.0.1" - global "^4.3.2" - util-deprecate "^1.0.2" - "@storybook/addons@5.3.19", "@storybook/addons@^5.3.17": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-5.3.19.tgz#3a7010697afd6df9a41b8c8a7351d9a06ff490a4" @@ -2630,32 +2612,6 @@ global "^4.3.2" util-deprecate "^1.0.2" -"@storybook/api@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.18.tgz#95582ab90d947065e0e34ed603650a3630dcbd16" - integrity sha512-QXaccNCARHzPWOuxYndiebGWBZmwiUvRgB9ji0XTJBS3y8K0ZPb5QyuqiKPaEWUj8dBA8rzdDtkW3Yt95Namaw== - dependencies: - "@reach/router" "^1.2.1" - "@storybook/channels" "5.3.18" - "@storybook/client-logger" "5.3.18" - "@storybook/core-events" "5.3.18" - "@storybook/csf" "0.0.1" - "@storybook/router" "5.3.18" - "@storybook/theming" "5.3.18" - "@types/reach__router" "^1.2.3" - core-js "^3.0.1" - fast-deep-equal "^2.0.1" - global "^4.3.2" - lodash "^4.17.15" - memoizerific "^1.11.3" - prop-types "^15.6.2" - react "^16.8.3" - semver "^6.0.0" - shallow-equal "^1.1.0" - store2 "^2.7.1" - telejson "^3.2.0" - util-deprecate "^1.0.2" - "@storybook/api@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/api/-/api-5.3.19.tgz#77f15e9e2eee59fe1ddeaba1ef39bc34713a6297" @@ -2693,13 +2649,6 @@ global "^4.3.2" telejson "^3.2.0" -"@storybook/channels@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/channels/-/channels-5.3.18.tgz#490c9eaa8292b0571c0f665052b12addf7c35f21" - integrity sha512-scP/6td/BJSEOgfN+qaYGDf3E793xye7tIw6W+sYqwg+xdMFO39wVXgVZNpQL6sLEwpJZTaPywCjC6p6ksErqQ== - dependencies: - core-js "^3.0.1" - "@storybook/channels@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-5.3.19.tgz#65ad7cd19d70aa5eabbb2e5e39ceef5e510bcb7f" @@ -2730,13 +2679,6 @@ ts-dedent "^1.1.0" util-deprecate "^1.0.2" -"@storybook/client-logger@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-5.3.18.tgz#27c9d09d788965db0164be6e168bc3f03adbf88f" - integrity sha512-RZjxw4uqZX3Yk27IirbB/pQG+wRsQSSRlKqYa8KQ5bSanm4IrcV9VA1OQbuySW9njE+CexAnakQJ/fENdmurNg== - dependencies: - core-js "^3.0.1" - "@storybook/client-logger@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-5.3.19.tgz#fbbd186e82102eaca1d6a5cca640271cae862921" @@ -2771,13 +2713,6 @@ simplebar-react "^1.0.0-alpha.6" ts-dedent "^1.1.0" -"@storybook/core-events@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-5.3.18.tgz#e5d335f8a2c7dd46502b8f505006f1e111b46d49" - integrity sha512-uQ6NYJ5WODXK8DJ7m8y3yUAtWB3n+6XtYztjY+tdkCsLYvTYDXNS+epV+f5Hu9+gB+/Dm+b5Su4jDD+LZB2QWA== - dependencies: - core-js "^3.0.1" - "@storybook/core-events@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-5.3.19.tgz#18020cd52e0d8ef0973a8e9622a10d5f99796f79" @@ -2910,21 +2845,6 @@ ts-dedent "^1.1.0" webpack "^4.33.0" -"@storybook/router@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/router/-/router-5.3.18.tgz#8ab22f1f2f7f957e78baf992030707a62289076e" - integrity sha512-6B2U2C75KTSVaCuYYgcubeJGcCSnwsXuEf50hEd5mGqWgHZfojCtGvB7Ko4X+0h8rEC+eNA4p7YBOhlUv9WNrQ== - dependencies: - "@reach/router" "^1.2.1" - "@storybook/csf" "0.0.1" - "@types/reach__router" "^1.2.3" - core-js "^3.0.1" - global "^4.3.2" - lodash "^4.17.15" - memoizerific "^1.11.3" - qs "^6.6.0" - util-deprecate "^1.0.2" - "@storybook/router@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/router/-/router-5.3.19.tgz#0f783b85658f99e4007f74347ad7ef17dbf7fc3a" @@ -2956,24 +2876,6 @@ prop-types "^15.7.2" regenerator-runtime "^0.13.3" -"@storybook/theming@5.3.18": - version "5.3.18" - resolved "https://registry.npmjs.org/@storybook/theming/-/theming-5.3.18.tgz#35e78de79d9cf8f1248af0dd1c7fa60555761312" - integrity sha512-lfFTeLoYwLMKg96N3gn0umghMdAHgJBGuk2OM8Ll84yWtdl9RGnzfiI1Fl7Cr5k95dCF7drLJlJCao1VxUkFSA== - dependencies: - "@emotion/core" "^10.0.20" - "@emotion/styled" "^10.0.17" - "@storybook/client-logger" "5.3.18" - core-js "^3.0.1" - deep-object-diff "^1.1.0" - emotion-theming "^10.0.19" - global "^4.3.2" - memoizerific "^1.11.3" - polished "^3.3.1" - prop-types "^15.7.2" - resolve-from "^5.0.0" - ts-dedent "^1.1.0" - "@storybook/theming@5.3.19": version "5.3.19" resolved "https://registry.npmjs.org/@storybook/theming/-/theming-5.3.19.tgz#177d9819bd64f7a1a6ea2f1920ffa5baf9a5f467" @@ -7434,6 +7336,11 @@ deep-extend@^0.6.0: resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== +deep-freeze@^0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84" + integrity sha1-OgsABd4YZygZ39OM0x+RF5yJPoQ= + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -19802,10 +19709,10 @@ yn@^4.0.0: resolved "https://registry.npmjs.org/yn/-/yn-4.0.0.tgz#611480051ea43b510da1dfdbe177ed159f00a979" integrity sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg== -yup@^0.28.5: - version "0.28.5" - resolved "https://registry.npmjs.org/yup/-/yup-0.28.5.tgz#85cabb4000d3623ef69be81551190692e631a8a5" - integrity sha512-7JZcvpUGUxMKoaEtcoMEM8lCWRaueGNH/A3EhL/UWqfbFm3uloiI+x59Yq4nzhbbYWUTwAsCteaZOJ+VbqI1uw== +yup@^0.29.1: + version "0.29.1" + resolved "https://registry.npmjs.org/yup/-/yup-0.29.1.tgz#35d25aab470a0c3950f66040ba0ff4b1b6efe0d9" + integrity sha512-U7mPIbgfQWI6M3hZCJdGFrr+U0laG28FxMAKIgNvgl7OtyYuUoc4uy9qCWYHZjh49b8T7Ug8NNDdiMIEytcXrQ== dependencies: "@babel/runtime" "^7.9.6" fn-name "~3.0.0"