) {
return function withApisWrapper(
WrappedComponent: React.ComponentType
,
) {
- const Hoc: FC> = (props) => {
+ const Hoc: FC> = props => {
const apiHolder = useContext(Context);
if (!apiHolder) {
diff --git a/packages/core/src/api/apis/ApiRef.test.ts b/packages/core-api/src/apis/ApiRef.test.ts
similarity index 100%
rename from packages/core/src/api/apis/ApiRef.test.ts
rename to packages/core-api/src/apis/ApiRef.test.ts
diff --git a/packages/core/src/api/apis/ApiRef.ts b/packages/core-api/src/apis/ApiRef.ts
similarity index 100%
rename from packages/core/src/api/apis/ApiRef.ts
rename to packages/core-api/src/apis/ApiRef.ts
diff --git a/packages/core/src/api/apis/ApiRegistry.test.ts b/packages/core-api/src/apis/ApiRegistry.test.ts
similarity index 100%
rename from packages/core/src/api/apis/ApiRegistry.test.ts
rename to packages/core-api/src/apis/ApiRegistry.test.ts
diff --git a/packages/core/src/api/apis/ApiRegistry.ts b/packages/core-api/src/apis/ApiRegistry.ts
similarity index 100%
rename from packages/core/src/api/apis/ApiRegistry.ts
rename to packages/core-api/src/apis/ApiRegistry.ts
diff --git a/packages/core/src/api/apis/ApiTestRegistry.test.ts b/packages/core-api/src/apis/ApiTestRegistry.test.ts
similarity index 100%
rename from packages/core/src/api/apis/ApiTestRegistry.test.ts
rename to packages/core-api/src/apis/ApiTestRegistry.test.ts
diff --git a/packages/core/src/api/apis/ApiTestRegistry.ts b/packages/core-api/src/apis/ApiTestRegistry.ts
similarity index 100%
rename from packages/core/src/api/apis/ApiTestRegistry.ts
rename to packages/core-api/src/apis/ApiTestRegistry.ts
diff --git a/packages/core/src/api/apis/definitions/AlertApi.ts b/packages/core-api/src/apis/definitions/AlertApi.ts
similarity index 100%
rename from packages/core/src/api/apis/definitions/AlertApi.ts
rename to packages/core-api/src/apis/definitions/AlertApi.ts
diff --git a/packages/core/src/api/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts
similarity index 100%
rename from packages/core/src/api/apis/definitions/AppThemeApi.ts
rename to packages/core-api/src/apis/definitions/AppThemeApi.ts
diff --git a/packages/core/src/api/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts
similarity index 100%
rename from packages/core/src/api/apis/definitions/ErrorApi.ts
rename to packages/core-api/src/apis/definitions/ErrorApi.ts
diff --git a/packages/core/src/api/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts
similarity index 100%
rename from packages/core/src/api/apis/definitions/FeatureFlagsApi.ts
rename to packages/core-api/src/apis/definitions/FeatureFlagsApi.ts
diff --git a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts
similarity index 98%
rename from packages/core/src/api/apis/definitions/OAuthRequestApi.ts
rename to packages/core-api/src/apis/definitions/OAuthRequestApi.ts
index 5a0f399a6d..6fefbd1b9c 100644
--- a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts
+++ b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { IconComponent } from '../../../icons';
+import { IconComponent } from '../../icons';
import { Observable } from '../../types';
import { createApiRef } from '../ApiRef';
diff --git a/packages/core/src/api/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts
similarity index 98%
rename from packages/core/src/api/apis/definitions/auth.ts
rename to packages/core-api/src/apis/definitions/auth.ts
index 15c9e5d632..c066c83b73 100644
--- a/packages/core/src/api/apis/definitions/auth.ts
+++ b/packages/core-api/src/apis/definitions/auth.ts
@@ -85,7 +85,10 @@ export type OAuthApi = {
* will be prompted 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.
*/
- getAccessToken(scope?: OAuthScope): Promise;
+ getAccessToken(
+ scope?: OAuthScope,
+ options?: AccessTokenOptions,
+ ): Promise;
/**
* Log out the user's session. This will reload the page.
diff --git a/packages/core/src/api/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts
similarity index 100%
rename from packages/core/src/api/apis/definitions/index.ts
rename to packages/core-api/src/apis/definitions/index.ts
diff --git a/packages/core/src/api/apis/helpers.ts b/packages/core-api/src/apis/helpers.ts
similarity index 100%
rename from packages/core/src/api/apis/helpers.ts
rename to packages/core-api/src/apis/helpers.ts
diff --git a/packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts
similarity index 91%
rename from packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts
rename to packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts
index 901d3b57cc..91a606059d 100644
--- a/packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts
+++ b/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { AlertApi, AlertMessage } from '../../../..';
-import { PublishSubject } from '../lib';
+import { AlertApi, AlertMessage } from '../..';
+import { PublishSubject } from '../../../lib';
import { Observable } from '../../../types';
/**
diff --git a/packages/core/src/api/apis/implementations/AlertApi/index.ts b/packages/core-api/src/apis/implementations/AlertApi/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/AlertApi/index.ts
rename to packages/core-api/src/apis/implementations/AlertApi/index.ts
diff --git a/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.test.ts b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.test.ts
rename to packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts
diff --git a/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts
similarity index 97%
rename from packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts
rename to packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts
index 0f554ed7f1..43b52a254b 100644
--- a/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts
+++ b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts
@@ -15,7 +15,7 @@
*/
import { AppThemeApi, AppTheme } from '../../definitions';
-import { BehaviorSubject } from '../lib';
+import { BehaviorSubject } from '../../../lib';
import { Observable } from '../../../types';
const STORAGE_KEY = 'theme';
diff --git a/packages/core/src/api/apis/implementations/AppThemeApi/index.ts b/packages/core-api/src/apis/implementations/AppThemeApi/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/AppThemeApi/index.ts
rename to packages/core-api/src/apis/implementations/AppThemeApi/index.ts
diff --git a/packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts b/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts
similarity index 94%
rename from packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts
rename to packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts
index 903463d7de..162b28f334 100644
--- a/packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts
+++ b/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ErrorApi, ErrorContext, AlertApi } from '../../../..';
+import { ErrorApi, ErrorContext, AlertApi } from '../..';
/**
* Decorates an ErrorApi by also forwarding error messages
diff --git a/packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts
similarity index 91%
rename from packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts
rename to packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts
index 6729050401..2ebf9db7a8 100644
--- a/packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts
+++ b/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ErrorApi, ErrorContext } from '../../../..';
-import { PublishSubject } from '../lib';
+import { ErrorApi, ErrorContext } from '../..';
+import { PublishSubject } from '../../../lib';
import { Observable } from '../../../types';
/**
diff --git a/packages/core/src/api/apis/implementations/ErrorApi/index.ts b/packages/core-api/src/apis/implementations/ErrorApi/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/ErrorApi/index.ts
rename to packages/core-api/src/apis/implementations/ErrorApi/index.ts
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts
similarity index 98%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts
index aa5609b1e9..6287f35015 100644
--- a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts
+++ b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { BehaviorSubject } from '../lib';
+import { BehaviorSubject } from '../../../lib';
import { Observable } from '../../../types';
type RequestQueueEntry = {
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts
similarity index 98%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts
index 1168d6abd4..23e9e1ddc4 100644
--- a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts
+++ b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts
@@ -21,7 +21,7 @@ import {
AuthRequesterOptions,
} from '../../definitions';
import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
-import { BehaviorSubject } from '../lib';
+import { BehaviorSubject } from '../../../lib';
import { Observable } from '../../../types';
/**
diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/index.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/OAuthRequestApi/index.ts
rename to packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts
diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts
rename to packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts
diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
similarity index 86%
rename from packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts
rename to packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
index dbcf53dbe0..582afbcfea 100644
--- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts
+++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts
@@ -15,16 +15,17 @@
*/
import GoogleIcon from '@material-ui/icons/AcUnit';
-import { DefaultAuthConnector } from '../../lib/AuthConnector';
+import { DefaultAuthConnector } from '../../../../lib/AuthConnector';
import { GoogleSession } from './types';
import {
OAuthApi,
OpenIdConnectApi,
IdTokenOptions,
+ AccessTokenOptions,
} from '../../../definitions/auth';
import { OAuthRequestApi, AuthProvider } from '../../../definitions';
-import { SessionManager } from '../../lib/AuthSessionManager/types';
-import { RefreshingAuthSessionManager } from '../../lib/AuthSessionManager';
+import { SessionManager } from '../../../../lib/AuthSessionManager/types';
+import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager';
type CreateOptions = {
// TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth
@@ -95,19 +96,23 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi {
constructor(private readonly sessionManager: SessionManager) {}
- async getAccessToken(scope?: string | string[]) {
+ async getAccessToken(
+ scope?: string | string[],
+ options?: AccessTokenOptions,
+ ) {
const normalizedScopes = GoogleAuth.normalizeScopes(scope);
const session = await this.sessionManager.getSession({
- optional: false,
+ ...options,
scopes: normalizedScopes,
});
- return session.accessToken;
+ if (session) {
+ return session.accessToken;
+ }
+ return '';
}
- async getIdToken({ optional }: IdTokenOptions = {}) {
- const session = await this.sessionManager.getSession({
- optional: optional || false,
- });
+ async getIdToken(options: IdTokenOptions = {}) {
+ const session = await this.sessionManager.getSession(options);
if (session) {
return session.idToken;
}
diff --git a/packages/core/src/api/apis/implementations/auth/google/index.ts b/packages/core-api/src/apis/implementations/auth/google/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/auth/google/index.ts
rename to packages/core-api/src/apis/implementations/auth/google/index.ts
diff --git a/packages/core/src/api/apis/implementations/auth/google/types.ts b/packages/core-api/src/apis/implementations/auth/google/types.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/auth/google/types.ts
rename to packages/core-api/src/apis/implementations/auth/google/types.ts
diff --git a/packages/core/src/api/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/auth/index.ts
rename to packages/core-api/src/apis/implementations/auth/index.ts
diff --git a/packages/core/src/api/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/index.ts
rename to packages/core-api/src/apis/implementations/index.ts
diff --git a/packages/core/src/api/apis/index.ts b/packages/core-api/src/apis/index.ts
similarity index 100%
rename from packages/core/src/api/apis/index.ts
rename to packages/core-api/src/apis/index.ts
diff --git a/packages/core/src/api/apis/types.ts b/packages/core-api/src/apis/types.ts
similarity index 100%
rename from packages/core/src/api/apis/types.ts
rename to packages/core-api/src/apis/types.ts
diff --git a/packages/core/src/api/app/App.tsx b/packages/core-api/src/app/App.tsx
similarity index 80%
rename from packages/core/src/api/app/App.tsx
rename to packages/core-api/src/app/App.tsx
index afdb6ffc58..844c38ecce 100644
--- a/packages/core/src/api/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -17,19 +17,13 @@
import React, { ComponentType, FC } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { AppContextProvider } from './AppContext';
-import { BackstageApp, AppOptions, AppComponents } from './types';
+import { BackstageApp, AppComponents } from './types';
import { BackstagePlugin } from '../plugin';
import { FeatureFlagsRegistryItem } from './FeatureFlags';
import { featureFlagsApiRef } from '../apis/definitions';
-import ErrorPage from '../../layout/ErrorPage';
import { AppThemeProvider } from './AppThemeProvider';
-import {
- IconComponent,
- SystemIcons,
- SystemIconKey,
- defaultSystemIcons,
-} from '../../icons';
+import { IconComponent, SystemIcons, SystemIconKey } from '../icons';
import {
ApiHolder,
ApiProvider,
@@ -38,7 +32,6 @@ import {
AppThemeSelector,
appThemeApiRef,
} from '../apis';
-import { lightTheme, darkTheme } from '@backstage/theme';
import { ApiAggregator } from '../apis/ApiAggregator';
type FullAppOptions = {
@@ -49,7 +42,7 @@ type FullAppOptions = {
themes: AppTheme[];
};
-class AppImpl implements BackstageApp {
+export class PrivateAppImpl implements BackstageApp {
private readonly apis: ApiHolder;
private readonly icons: SystemIcons;
private readonly plugins: BackstagePlugin[];
@@ -175,40 +168,3 @@ class AppImpl implements BackstageApp {
}
}
}
-
-/**
- * Creates a new Backstage App.
- */
-export function createApp(options?: AppOptions) {
- const DefaultNotFoundPage = () => (
-
- );
-
- const apis = options?.apis ?? ApiRegistry.from([]);
- const icons = { ...defaultSystemIcons, ...options?.icons };
- const plugins = options?.plugins ?? [];
- const components = {
- NotFoundErrorPage: DefaultNotFoundPage,
- ...options?.components,
- };
- const themes = options?.themes ?? [
- {
- id: 'light',
- title: 'Light Theme',
- variant: 'light',
- theme: lightTheme,
- },
- {
- id: 'dark',
- title: 'Dark Theme',
- variant: 'dark',
- theme: darkTheme,
- },
- ];
-
- const app = new AppImpl({ apis, icons, plugins, components, themes });
-
- app.verify();
-
- return app;
-}
diff --git a/packages/core/src/api/app/AppContext.tsx b/packages/core-api/src/app/AppContext.tsx
similarity index 100%
rename from packages/core/src/api/app/AppContext.tsx
rename to packages/core-api/src/app/AppContext.tsx
diff --git a/packages/core/src/api/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx
similarity index 91%
rename from packages/core/src/api/app/AppThemeProvider.tsx
rename to packages/core-api/src/app/AppThemeProvider.tsx
index 6e68fe9ba0..775d8293ba 100644
--- a/packages/core/src/api/app/AppThemeProvider.tsx
+++ b/packages/core-api/src/app/AppThemeProvider.tsx
@@ -27,20 +27,20 @@ function resolveTheme(
themes: AppTheme[],
) {
if (themeId !== undefined) {
- const selectedTheme = themes.find((theme) => theme.id === themeId);
+ const selectedTheme = themes.find(theme => theme.id === themeId);
if (selectedTheme) {
return selectedTheme;
}
}
if (shouldPreferDark) {
- const darkTheme = themes.find((theme) => theme.variant === 'dark');
+ const darkTheme = themes.find(theme => theme.variant === 'dark');
if (darkTheme) {
return darkTheme;
}
}
- const lightTheme = themes.find((theme) => theme.variant === 'light');
+ const lightTheme = themes.find(theme => theme.variant === 'light');
if (lightTheme) {
return lightTheme;
}
diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core-api/src/app/FeatureFlags.test.tsx
similarity index 99%
rename from packages/core/src/api/app/FeatureFlags.test.tsx
rename to packages/core-api/src/app/FeatureFlags.test.tsx
index ca4a819f13..99974f9b1d 100644
--- a/packages/core/src/api/app/FeatureFlags.test.tsx
+++ b/packages/core-api/src/app/FeatureFlags.test.tsx
@@ -147,7 +147,7 @@ describe('FeatureFlags', () => {
it('should get the correct values', () => {
const getByName = (name: string) =>
- featureFlags.getRegisteredFlags().find((flag) => flag.name === name);
+ featureFlags.getRegisteredFlags().find(flag => flag.name === name);
expect(getByName('registered-flag-0')).toBeUndefined();
expect(getByName('registered-flag-1')).toEqual({
diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx
similarity index 95%
rename from packages/core/src/api/app/FeatureFlags.tsx
rename to packages/core-api/src/app/FeatureFlags.tsx
index 6af10f228e..11c084d3ed 100644
--- a/packages/core/src/api/app/FeatureFlags.tsx
+++ b/packages/core-api/src/app/FeatureFlags.tsx
@@ -127,12 +127,12 @@ export interface FeatureFlagsRegistryItem {
export class FeatureFlagsRegistry extends Array {
static from(entries: FeatureFlagsRegistryItem[]) {
- Array.from(entries).forEach((entry) => validateFlagName(entry.name));
+ Array.from(entries).forEach(entry => validateFlagName(entry.name));
return new FeatureFlagsRegistry(...entries);
}
push(...entries: FeatureFlagsRegistryItem[]): number {
- Array.from(entries).forEach((entry) => validateFlagName(entry.name));
+ Array.from(entries).forEach(entry => validateFlagName(entry.name));
return super.push(...entries);
}
@@ -143,7 +143,7 @@ export class FeatureFlagsRegistry extends Array {
)[]
): FeatureFlagsRegistryItem[] {
const _concat = super.concat(...entries);
- Array.from(_concat).forEach((entry) => validateFlagName(entry.name));
+ Array.from(_concat).forEach(entry => validateFlagName(entry.name));
return _concat;
}
diff --git a/packages/core/src/api/app/index.ts b/packages/core-api/src/app/index.ts
similarity index 95%
rename from packages/core/src/api/app/index.ts
rename to packages/core-api/src/app/index.ts
index c40fa9e9f8..003b71e353 100644
--- a/packages/core/src/api/app/index.ts
+++ b/packages/core-api/src/app/index.ts
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-export { createApp } from './App';
export { FeatureFlags } from './FeatureFlags';
export { useApp } from './AppContext';
export * from './types';
diff --git a/packages/core/src/api/app/types.ts b/packages/core-api/src/app/types.ts
similarity index 97%
rename from packages/core/src/api/app/types.ts
rename to packages/core-api/src/app/types.ts
index 75a750ce6e..ea3a812557 100644
--- a/packages/core/src/api/app/types.ts
+++ b/packages/core-api/src/app/types.ts
@@ -15,7 +15,7 @@
*/
import { ComponentType } from 'react';
-import { IconComponent, SystemIconKey, SystemIcons } from '../../icons';
+import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
import { BackstagePlugin } from '../plugin';
import { ApiHolder } from '../apis';
import { AppTheme } from '../apis/definitions';
diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx
new file mode 100644
index 0000000000..488973b664
--- /dev/null
+++ b/packages/core-api/src/icons/icons.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SvgIconProps } from '@material-ui/core';
+import PeopleIcon from '@material-ui/icons/People';
+import PersonIcon from '@material-ui/icons/Person';
+import React, { FC } from 'react';
+import { useApp } from '../app/AppContext';
+import { IconComponent, SystemIconKey, SystemIcons } from './types';
+
+export const defaultSystemIcons: SystemIcons = {
+ user: PersonIcon,
+ group: PeopleIcon,
+};
+
+const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
+ const Component: FC = props => {
+ const app = useApp();
+ const Icon = app.getSystemIcon(key);
+ return ;
+ };
+ return Component;
+};
+
+export const UserIcon = overridableSystemIcon('user');
+export const GroupIcon = overridableSystemIcon('group');
diff --git a/packages/core-api/src/icons/index.ts b/packages/core-api/src/icons/index.ts
new file mode 100644
index 0000000000..4c97d27176
--- /dev/null
+++ b/packages/core-api/src/icons/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './icons';
+export * from './types';
diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts
new file mode 100644
index 0000000000..30e0ba53b2
--- /dev/null
+++ b/packages/core-api/src/icons/types.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentType } from 'react';
+import { SvgIconProps } from '@material-ui/core';
+
+export type IconComponent = ComponentType;
+export type SystemIconKey = 'user' | 'group';
+export type SystemIcons = { [key in SystemIconKey]: IconComponent };
diff --git a/packages/core-api/src/index.ts b/packages/core-api/src/index.ts
new file mode 100644
index 0000000000..f08e65809e
--- /dev/null
+++ b/packages/core-api/src/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './public';
+import * as privateExports from './private';
+export default privateExports;
diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts
similarity index 83%
rename from packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts
rename to packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts
index d4f95f7907..3d8f7abeb9 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts
+++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts
@@ -16,7 +16,7 @@
import ProviderIcon from '@material-ui/icons/AcUnit';
import { DefaultAuthConnector } from './DefaultAuthConnector';
-import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
+import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi';
import * as loginPopup from '../loginPopup';
const anyFetch = fetch as any;
@@ -86,7 +86,7 @@ describe('DefaultAuthConnector', () => {
...defaultOptions,
oauthRequestApi: mockOauth,
});
- const promise = helper.createSession(new Set(['a', 'b']));
+ const promise = helper.createSession({ scopes: new Set(['a', 'b']) });
await mockOauth.rejectAll();
await expect(promise).rejects.toMatchObject({ name: 'RejectedError' });
});
@@ -106,7 +106,9 @@ describe('DefaultAuthConnector', () => {
oauthRequestApi: mockOauth,
});
- const sessionPromise = helper.createSession(new Set(['a', 'b']));
+ const sessionPromise = helper.createSession({
+ scopes: new Set(['a', 'b']),
+ });
await mockOauth.triggerAll();
@@ -123,6 +125,26 @@ describe('DefaultAuthConnector', () => {
});
});
+ it('should instantly show popup if option is set', async () => {
+ const popupSpy = jest
+ .spyOn(loginPopup, 'showLoginPopup')
+ .mockResolvedValue('my-session');
+ const helper = new DefaultAuthConnector({
+ ...defaultOptions,
+ oauthRequestApi: new MockOAuthApi(),
+ sessionTransform: str => str,
+ });
+
+ const sessionPromise = helper.createSession({
+ scopes: new Set(),
+ instantPopup: true,
+ });
+
+ expect(popupSpy).toBeCalledTimes(1);
+
+ await expect(sessionPromise).resolves.toBe('my-session');
+ });
+
it('should use join func to join scopes', async () => {
const mockOauth = new MockOAuthApi();
const popupSpy = jest
@@ -134,7 +156,7 @@ describe('DefaultAuthConnector', () => {
oauthRequestApi: mockOauth,
});
- helper.createSession(new Set(['a', 'b']));
+ helper.createSession({ scopes: new Set(['a', 'b']) });
await mockOauth.triggerAll();
diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts
similarity index 88%
rename from packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts
rename to packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts
index ca8a9cd2ae..21d2530c5a 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts
+++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-import { AuthRequester } from '../../..';
-import { OAuthRequestApi, AuthProvider } from '../../../definitions';
+import { AuthRequester } from '../../apis';
+import { OAuthRequestApi, AuthProvider } from '../../apis/definitions';
import { showLoginPopup } from '../loginPopup';
-import { AuthConnector } from './types';
+import { AuthConnector, CreateSessionOptions } from './types';
const DEFAULT_BASE_PATH = '/api/auth/';
@@ -68,6 +68,7 @@ export class DefaultAuthConnector
private readonly basePath: string;
private readonly environment: string;
private readonly provider: AuthProvider & { id: string };
+ private readonly joinScopesFunc: (scopes: Set) => string;
private readonly authRequester: AuthRequester;
private readonly sessionTransform: (response: any) => Promise;
@@ -84,18 +85,22 @@ export class DefaultAuthConnector
this.authRequester = oauthRequestApi.createAuthRequester({
provider,
- onAuthRequest: scopes => this.showPopup(joinScopes(scopes)),
+ onAuthRequest: scopes => this.showPopup(scopes),
});
this.apiOrigin = apiOrigin;
this.basePath = basePath;
this.environment = environment;
this.provider = provider;
+ this.joinScopesFunc = joinScopes;
this.sessionTransform = sessionTransform;
}
- async createSession(scopes: Set): Promise {
- return this.authRequester(scopes);
+ async createSession(options: CreateSessionOptions): Promise {
+ if (options.instantPopup) {
+ return this.showPopup(options.scopes);
+ }
+ return this.authRequester(options.scopes);
}
async refreshSession(): Promise {
@@ -142,7 +147,8 @@ export class DefaultAuthConnector
}
}
- private async showPopup(scope: string): Promise {
+ private async showPopup(scopes: Set): Promise {
+ const scope = this.joinScopesFunc(scopes);
const popupUrl = this.buildUrl('/start', { scope });
const payload = await showLoginPopup({
diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.test.ts
rename to packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts
diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.ts
rename to packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts
diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/index.ts b/packages/core-api/src/lib/AuthConnector/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/AuthConnector/index.ts
rename to packages/core-api/src/lib/AuthConnector/index.ts
diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts b/packages/core-api/src/lib/AuthConnector/types.ts
similarity index 84%
rename from packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts
rename to packages/core-api/src/lib/AuthConnector/types.ts
index 146c31cbe1..46175a265f 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts
+++ b/packages/core-api/src/lib/AuthConnector/types.ts
@@ -14,12 +14,17 @@
* limitations under the License.
*/
+export type CreateSessionOptions = {
+ scopes: Set;
+ instantPopup?: boolean;
+};
+
/**
* An AuthConnector is responsible for realizing auth session actions
* by for example communicating with a backend or interacting with the user.
*/
export type AuthConnector = {
- createSession(scopes: Set): Promise;
+ createSession(options: CreateSessionOptions): Promise;
refreshSession(): Promise;
removeSession(): Promise;
};
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.test.ts
rename to packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts
similarity index 90%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.ts
rename to packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts
index 14796c6e11..8d318f5bff 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts
@@ -18,6 +18,7 @@ import {
SessionManager,
SessionScopesFunc,
SessionShouldRefreshFunc,
+ GetSessionOptions,
} from './types';
import { SessionScopeHelper } from './common';
@@ -59,18 +60,7 @@ export class AuthSessionStore implements SessionManager {
});
}
- async getSession(options: {
- optional: false;
- scopes?: Set;
- }): Promise;
- async getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise;
- async getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise {
+ async getSession(options: GetSessionOptions): Promise {
const { scopes } = options;
const session = this.loadSession();
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts
similarity index 88%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts
rename to packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts
index 8684c932b2..ee7cbab2ae 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts
@@ -113,6 +113,23 @@ describe('RefreshingAuthSessionManager', () => {
expect(refreshSession).toBeCalledTimes(1);
});
+ it('should forward option to instantly show auth popup', async () => {
+ const createSession = jest.fn();
+ const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE'));
+ const manager = new RefreshingAuthSessionManager({
+ connector: { createSession, refreshSession },
+ ...defaultOptions,
+ } as any);
+
+ expect(await manager.getSession({ instantPopup: true })).toBe(undefined);
+ expect(createSession).toBeCalledTimes(1);
+ expect(createSession).toHaveBeenCalledWith({
+ scopes: new Set(),
+ instantPopup: true,
+ });
+ expect(refreshSession).toBeCalledTimes(1);
+ });
+
it('should remove session and reload', async () => {
// This is a workaround that is used by Facebook and the Jest core team
// It is a limitation with the newest versions of JSDOM, and newer browser standards
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
similarity index 90%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
rename to packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
index cbd6d5ca46..64df3deca4 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
@@ -18,6 +18,7 @@ import {
SessionManager,
SessionScopesFunc,
SessionShouldRefreshFunc,
+ GetSessionOptions,
} from './types';
import { AuthConnector } from '../AuthConnector';
import { SessionScopeHelper, hasScopes } from './common';
@@ -60,18 +61,7 @@ export class RefreshingAuthSessionManager implements SessionManager {
this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });
}
- async getSession(options: {
- optional: false;
- scopes?: Set;
- }): Promise;
- async getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise;
- async getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise {
+ async getSession(options: GetSessionOptions): Promise {
if (
this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)
) {
@@ -115,9 +105,10 @@ export class RefreshingAuthSessionManager implements SessionManager {
}
// We can call authRequester multiple times, the returned session will contain all requested scopes.
- this.currentSession = await this.connector.createSession(
- this.helper.getExtendedScope(this.currentSession, options.scopes),
- );
+ this.currentSession = await this.connector.createSession({
+ ...options,
+ scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),
+ });
return this.currentSession;
}
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts
similarity index 98%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts
rename to packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts
index dc13aec0c2..7d47fa91df 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts
@@ -62,7 +62,7 @@ describe('StaticAuthSessionManager', () => {
it('should only request auth once for same scopes', async () => {
const createSession = jest
.fn()
- .mockImplementation(scopes => [...scopes].join(' '));
+ .mockImplementation(({ scopes }) => [...scopes].join(' '));
const manager = new StaticAuthSessionManager({
connector: { createSession, ...baseConnector },
...defaultOptions,
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts
similarity index 81%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts
rename to packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts
index 8d057ecc58..6e6db47a99 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { SessionManager } from './types';
+import { SessionManager, GetSessionOptions } from './types';
import { AuthConnector } from '../AuthConnector';
import { SessionScopeHelper } from './common';
@@ -43,18 +43,7 @@ export class StaticAuthSessionManager implements SessionManager {
this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes });
}
- async getSession(options: {
- optional: false;
- scopes?: Set;
- }): Promise;
- async getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise;
- async getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise {
+ async getSession(options: GetSessionOptions): Promise {
if (
this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes)
) {
@@ -67,9 +56,10 @@ export class StaticAuthSessionManager implements SessionManager {
}
// We can call authRequester multiple times, the returned session will contain all requested scopes.
- this.currentSession = await this.connector.createSession(
- this.helper.getExtendedScope(this.currentSession, options.scopes),
- );
+ this.currentSession = await this.connector.createSession({
+ ...options,
+ scopes: this.helper.getExtendedScope(this.currentSession, options.scopes),
+ });
return this.currentSession;
}
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/common.ts b/packages/core-api/src/lib/AuthSessionManager/common.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/common.ts
rename to packages/core-api/src/lib/AuthSessionManager/common.ts
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/index.ts
rename to packages/core-api/src/lib/AuthSessionManager/index.ts
diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts
similarity index 87%
rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts
rename to packages/core-api/src/lib/AuthSessionManager/types.ts
index 5ebc8643e1..d28a751fbf 100644
--- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts
+++ b/packages/core-api/src/lib/AuthSessionManager/types.ts
@@ -14,17 +14,19 @@
* limitations under the License.
*/
+export type GetSessionOptions = {
+ optional?: boolean;
+ instantPopup?: boolean;
+ scopes?: Set;
+};
+
/**
* A sessions manager keeps track of the current session and makes sure that
* multiple simultaneous requests for sessions with different scope are handled
* in a correct way.
*/
export type SessionManager = {
- getSession(options: { optional: false; scopes?: Set }): Promise;
- getSession(options: {
- optional?: boolean;
- scopes?: Set;
- }): Promise;
+ getSession(options: GetSessionOptions): Promise;
removeSession(): Promise;
};
diff --git a/packages/core-api/src/lib/index.ts b/packages/core-api/src/lib/index.ts
new file mode 100644
index 0000000000..10f213b50f
--- /dev/null
+++ b/packages/core-api/src/lib/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './subjects';
+export * from './loginPopup';
+export * from './AuthConnector';
+export * from './AuthSessionManager';
diff --git a/packages/core/src/api/apis/implementations/lib/loginPopup.test.ts b/packages/core-api/src/lib/loginPopup.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/loginPopup.test.ts
rename to packages/core-api/src/lib/loginPopup.test.ts
diff --git a/packages/core/src/api/apis/implementations/lib/loginPopup.ts b/packages/core-api/src/lib/loginPopup.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/loginPopup.ts
rename to packages/core-api/src/lib/loginPopup.ts
diff --git a/packages/core/src/api/apis/implementations/lib/subjects.test.ts b/packages/core-api/src/lib/subjects.test.ts
similarity index 100%
rename from packages/core/src/api/apis/implementations/lib/subjects.test.ts
rename to packages/core-api/src/lib/subjects.test.ts
diff --git a/packages/core/src/api/apis/implementations/lib/subjects.ts b/packages/core-api/src/lib/subjects.ts
similarity index 88%
rename from packages/core/src/api/apis/implementations/lib/subjects.ts
rename to packages/core-api/src/lib/subjects.ts
index b33df70a65..9ebde6ebbc 100644
--- a/packages/core/src/api/apis/implementations/lib/subjects.ts
+++ b/packages/core-api/src/lib/subjects.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { Observable } from '../../../types';
+import { Observable } from '../types';
import ObservableImpl from 'zen-observable';
// TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects.
@@ -33,7 +33,7 @@ export class PublishSubject
private isClosed = false;
private terminatingError?: Error;
- private readonly observable = new ObservableImpl((subscriber) => {
+ private readonly observable = new ObservableImpl(subscriber => {
if (this.isClosed) {
if (this.terminatingError) {
subscriber.error(this.terminatingError);
@@ -61,7 +61,7 @@ export class PublishSubject
if (this.isClosed) {
throw new Error('PublishSubject is closed');
}
- this.subscribers.forEach((subscriber) => subscriber.next(value));
+ this.subscribers.forEach(subscriber => subscriber.next(value));
}
error(error: Error) {
@@ -70,7 +70,7 @@ export class PublishSubject
}
this.isClosed = true;
this.terminatingError = error;
- this.subscribers.forEach((subscriber) => subscriber.error(error));
+ this.subscribers.forEach(subscriber => subscriber.error(error));
}
complete() {
@@ -78,7 +78,7 @@ export class PublishSubject
throw new Error('PublishSubject is closed');
}
this.isClosed = true;
- this.subscribers.forEach((subscriber) => subscriber.complete());
+ this.subscribers.forEach(subscriber => subscriber.complete());
}
subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription;
@@ -126,7 +126,7 @@ export class BehaviorSubject
this.currentValue = value;
}
- private readonly observable = new ObservableImpl((subscriber) => {
+ private readonly observable = new ObservableImpl(subscriber => {
if (this.isClosed) {
if (this.terminatingError) {
subscriber.error(this.terminatingError);
@@ -157,7 +157,7 @@ export class BehaviorSubject
throw new Error('BehaviorSubject is closed');
}
this.currentValue = value;
- this.subscribers.forEach((subscriber) => subscriber.next(value));
+ this.subscribers.forEach(subscriber => subscriber.next(value));
}
error(error: Error) {
@@ -166,7 +166,7 @@ export class BehaviorSubject
}
this.isClosed = true;
this.terminatingError = error;
- this.subscribers.forEach((subscriber) => subscriber.error(error));
+ this.subscribers.forEach(subscriber => subscriber.error(error));
}
complete() {
@@ -174,7 +174,7 @@ export class BehaviorSubject
throw new Error('BehaviorSubject is closed');
}
this.isClosed = true;
- this.subscribers.forEach((subscriber) => subscriber.complete());
+ this.subscribers.forEach(subscriber => subscriber.complete());
}
subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription;
diff --git a/packages/core/src/api/plugin/Plugin.tsx b/packages/core-api/src/plugin/Plugin.tsx
similarity index 100%
rename from packages/core/src/api/plugin/Plugin.tsx
rename to packages/core-api/src/plugin/Plugin.tsx
diff --git a/packages/core/src/api/plugin/index.ts b/packages/core-api/src/plugin/index.ts
similarity index 100%
rename from packages/core/src/api/plugin/index.ts
rename to packages/core-api/src/plugin/index.ts
diff --git a/packages/core/src/api/plugin/types.ts b/packages/core-api/src/plugin/types.ts
similarity index 100%
rename from packages/core/src/api/plugin/types.ts
rename to packages/core-api/src/plugin/types.ts
diff --git a/packages/core/src/api/apis/implementations/lib/index.ts b/packages/core-api/src/private.ts
similarity index 93%
rename from packages/core/src/api/apis/implementations/lib/index.ts
rename to packages/core-api/src/private.ts
index aa2202858c..65462d8d51 100644
--- a/packages/core/src/api/apis/implementations/lib/index.ts
+++ b/packages/core-api/src/private.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export * from './subjects';
+export { PrivateAppImpl } from './app/App';
diff --git a/packages/core-api/src/public.ts b/packages/core-api/src/public.ts
new file mode 100644
index 0000000000..0a30936c52
--- /dev/null
+++ b/packages/core-api/src/public.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './apis';
+export * from './app';
+export * from './icons';
+export * from './plugin';
+export * from './routing';
+export * from './types';
diff --git a/packages/core/src/api/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts
similarity index 100%
rename from packages/core/src/api/routing/RouteRef.ts
rename to packages/core-api/src/routing/RouteRef.ts
diff --git a/packages/core/src/api/routing/index.ts b/packages/core-api/src/routing/index.ts
similarity index 100%
rename from packages/core/src/api/routing/index.ts
rename to packages/core-api/src/routing/index.ts
diff --git a/packages/core/src/api/routing/types.ts b/packages/core-api/src/routing/types.ts
similarity index 95%
rename from packages/core/src/api/routing/types.ts
rename to packages/core-api/src/routing/types.ts
index 0b6d1c711f..491665bbcf 100644
--- a/packages/core/src/api/routing/types.ts
+++ b/packages/core-api/src/routing/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { IconComponent } from '../../icons';
+import { IconComponent } from '../icons';
export type RouteRef = {
path: string;
diff --git a/packages/core-api/src/setupTests.ts b/packages/core-api/src/setupTests.ts
new file mode 100644
index 0000000000..e34bc46f4b
--- /dev/null
+++ b/packages/core-api/src/setupTests.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '@testing-library/jest-dom';
+require('jest-fetch-mock').enableMocks();
diff --git a/packages/core/src/api/types.ts b/packages/core-api/src/types.ts
similarity index 100%
rename from packages/core/src/api/types.ts
rename to packages/core-api/src/types.ts
diff --git a/packages/core/package.json b/packages/core/package.json
index b1800f5db3..a89ffc0b92 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -28,6 +28,7 @@
"clean": "backstage-cli clean"
},
"dependencies": {
+ "@backstage/core-api": "0.1.1-alpha.6",
"@backstage/theme": "^0.1.1-alpha.6",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
@@ -46,20 +47,17 @@
"prop-types": "^15.7.2",
"rc-progress": "^3.0.0",
"react": "^16.12.0",
- "react-addons-text-content": "0.0.4",
"react-dom": "^16.12.0",
- "react-helmet": "5.2.1",
+ "react-helmet": "6.0.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-sparklines": "^1.7.0",
"react-syntax-highlighter": "^12.2.1",
- "react-use": "^14.2.0",
- "zen-observable": "^0.8.15"
+ "react-use": "^14.2.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.6",
"@backstage/test-utils": "^0.1.1-alpha.6",
- "@backstage/test-utils-core": "^0.1.1-alpha.6",
"@testing-library/jest-dom": "^5.7.0",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^10.2.4",
diff --git a/packages/core/src/api/createApp.tsx b/packages/core/src/api/createApp.tsx
new file mode 100644
index 0000000000..d0deed667a
--- /dev/null
+++ b/packages/core/src/api/createApp.tsx
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import privateExports, {
+ AppOptions,
+ ApiRegistry,
+ defaultSystemIcons,
+} from '@backstage/core-api';
+
+import ErrorPage from '../layout/ErrorPage';
+import { lightTheme, darkTheme } from '@backstage/theme';
+
+const { PrivateAppImpl } = privateExports;
+
+// createApp is defined in core, and not core-api, since we need access
+// to the components inside core to provide defaults.
+// The actual implementation of the app class still lives in core-api,
+// as it needs to be used by dev- and test-utils.
+
+/**
+ * Creates a new Backstage App.
+ */
+export function createApp(options?: AppOptions) {
+ const DefaultNotFoundPage = () => (
+
+ );
+
+ const apis = options?.apis ?? ApiRegistry.from([]);
+ const icons = { ...defaultSystemIcons, ...options?.icons };
+ const plugins = options?.plugins ?? [];
+ const components = {
+ NotFoundErrorPage: DefaultNotFoundPage,
+ ...options?.components,
+ };
+ const themes = options?.themes ?? [
+ {
+ id: 'light',
+ title: 'Light Theme',
+ variant: 'light',
+ theme: lightTheme,
+ },
+ {
+ id: 'dark',
+ title: 'Dark Theme',
+ variant: 'dark',
+ theme: darkTheme,
+ },
+ ];
+
+ const app = new PrivateAppImpl({ apis, icons, plugins, components, themes });
+
+ app.verify();
+
+ return app;
+}
diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts
index cde010293d..b8136305b0 100644
--- a/packages/core/src/api/index.ts
+++ b/packages/core/src/api/index.ts
@@ -14,8 +14,4 @@
* limitations under the License.
*/
-export * from './apis';
-export * from './app';
-export * from './routing';
-export * from './plugin';
-export * from './types';
+export { createApp } from './createApp';
diff --git a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx
index 85b41f2e0a..30940f68ac 100644
--- a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx
+++ b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx
@@ -18,7 +18,7 @@ import React, { FC, useEffect, useState } from 'react';
import { Snackbar, IconButton } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { Alert } from '@material-ui/lab';
-import { AlertMessage, useApi, alertApiRef } from '../../api';
+import { AlertMessage, useApi, alertApiRef } from '@backstage/core-api';
type Props = {};
diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
index fc7c262882..c77dc5bd43 100644
--- a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
+++ b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx
@@ -31,7 +31,7 @@ const defaultProps = {
showLineNumbers: false,
};
-const CodeSnippet: FC = (props) => {
+const CodeSnippet: FC = props => {
const { text, language, showLineNumbers } = {
...defaultProps,
...props,
diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx
index 80c11f6b4a..3dc5854ab3 100644
--- a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx
+++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx
@@ -16,7 +16,12 @@
import React from 'react';
import CopyTextButton from '.';
-import { ApiProvider, errorApiRef, ApiRegistry, ErrorApi } from '../../api';
+import {
+ ApiProvider,
+ errorApiRef,
+ ApiRegistry,
+ ErrorApi,
+} from '@backstage/core-api';
export default {
title: 'CopyTextButton',
diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx
index 7bc59d1f48..e0f7271014 100644
--- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx
+++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx
@@ -18,7 +18,12 @@ import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import CopyTextButton from './CopyTextButton';
-import { ApiRegistry, errorApiRef, ApiProvider, ErrorApi } from '../../api';
+import {
+ ApiRegistry,
+ errorApiRef,
+ ApiProvider,
+ ErrorApi,
+} from '@backstage/core-api';
jest.mock('popper.js', () => {
const PopperJS = jest.requireActual('popper.js');
diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx
index 5af62f4380..1c5028fa9c 100644
--- a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx
+++ b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx
@@ -19,9 +19,9 @@ import { IconButton, makeStyles, Tooltip } from '@material-ui/core';
import PropTypes from 'prop-types';
import CopyIcon from '@material-ui/icons/FileCopy';
import { BackstageTheme } from '@backstage/theme';
-import { errorApiRef, useApi } from '../../api';
+import { errorApiRef, useApi } from '@backstage/core-api';
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles(theme => ({
button: {
'&:hover': {
backgroundColor: theme.palette.highlight,
@@ -56,7 +56,7 @@ const defaultProps = {
tooltipText: 'Text copied to clipboard',
};
-const CopyTextButton: FC = (props) => {
+const CopyTextButton: FC = props => {
const { text, tooltipDelay, tooltipText } = {
...defaultProps,
...props,
@@ -66,7 +66,7 @@ const CopyTextButton: FC = (props) => {
const inputRef = useRef(null);
const [open, setOpen] = useState(false);
- const handleCopyClick: MouseEventHandler = (e) => {
+ const handleCopyClick: MouseEventHandler = e => {
e.stopPropagation();
setOpen(true);
diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx
new file mode 100644
index 0000000000..e19495a099
--- /dev/null
+++ b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import DismissableBanner from './DismissableBanner';
+import { Link, Typography } from '@material-ui/core';
+
+export default {
+ title: 'DismissableBanner',
+ component: DismissableBanner,
+};
+
+const containerStyle = { width: '70%' };
+
+export const Default = () => (
+
+
+
+);
+
+export const Error = () => (
+
+
+
+);
+
+export const EmojisIncluded = () => (
+
+
+
+);
+
+export const WithLink = () => (
+
+
+ This is a dismissable banner with a link:{' '}
+
+ example.com
+
+
+ }
+ variant="info"
+ />
+
+);
diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.test.js b/packages/core/src/components/DismissableBanner/DismissableBanner.test.js
new file mode 100644
index 0000000000..485b6226d2
--- /dev/null
+++ b/packages/core/src/components/DismissableBanner/DismissableBanner.test.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+// import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
+import { renderWithEffects, wrapInThemedTestApp } from '@backstage/test-utils';
+// import { createSetting } from 'shared/apis/settings';
+import DismissableBanner from './DismissableBanner';
+
+describe(' ', () => {
+ it('renders the message and the popover', async () => {
+ /*
+ const mockSetting = createSetting({
+ id: 'mockSetting',
+ defaultValue: true,
+ });
+ */
+
+ const rendered = await renderWithEffects(
+ wrapInThemedTestApp(
+ ,
+ ),
+ );
+ rendered.getByText('test message');
+
+ // fireEvent.click(rendered.getByTitle('Permanently dismiss this message'));
+ // await waitForElementToBeRemoved(rendered.queryByText('test message'));
+ });
+});
diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx
new file mode 100644
index 0000000000..7190ee8725
--- /dev/null
+++ b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { FC, ReactNode, useState } from 'react';
+import classNames from 'classnames';
+import { makeStyles, Theme } from '@material-ui/core';
+import Snackbar from '@material-ui/core/Snackbar';
+import SnackbarContent from '@material-ui/core/SnackbarContent';
+import IconButton from '@material-ui/core/IconButton';
+import Close from '@material-ui/icons/Close';
+// import { useSetting, Setting } from 'shared/apis/settings';
+
+const useStyles = makeStyles((theme: Theme) => ({
+ root: {
+ position: 'relative',
+ padding: theme.spacing(0),
+ marginBottom: theme.spacing(6),
+ marginTop: -theme.spacing(3),
+ display: 'flex',
+ flexFlow: 'row nowrap',
+ },
+ icon: {
+ fontSize: 20,
+ },
+ content: {
+ width: '100%',
+ maxWidth: 'inherit',
+ },
+ message: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ info: {
+ backgroundColor: theme.palette.primary.main,
+ },
+ error: {
+ backgroundColor: theme.palette.error.dark,
+ },
+}));
+
+type Props = {
+ variant: 'info' | 'error';
+ // setting: Setting;
+ message: ReactNode;
+};
+
+const DismissableBanner: FC = ({ variant, /* setting, */ message }) => {
+ // const [show, setShown, loading] = useSetting(setting);
+ const [show, setShown] = useState(true);
+ const classes = useStyles();
+
+ const handleClick = () => {
+ setShown(false);
+ };
+
+ return (
+
+
+
+ ,
+ ]}
+ />
+
+ );
+};
+
+export default DismissableBanner;
diff --git a/packages/core/src/components/DismissableBanner/index.ts b/packages/core/src/components/DismissableBanner/index.ts
new file mode 100644
index 0000000000..28091e858b
--- /dev/null
+++ b/packages/core/src/components/DismissableBanner/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { default } from './DismissableBanner';
diff --git a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx
index 36079854fd..d7f1e2387a 100644
--- a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx
+++ b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx
@@ -25,7 +25,7 @@ describe(' ', () => {
jest.spyOn(window.performance, 'now').mockReturnValue(5);
jest
.spyOn(window, 'requestAnimationFrame')
- .mockImplementation((cb) => cb(20));
+ .mockImplementation(cb => cb(20));
});
afterEach(() => {
diff --git a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx
index 8e3d98c998..cc7d99660b 100644
--- a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx
+++ b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx
@@ -23,9 +23,9 @@ import {
Theme,
} from '@material-ui/core';
import React, { FC, useState } from 'react';
-import { PendingAuthRequest } from '../../api';
+import { PendingAuthRequest } from '@backstage/core-api';
-const useItemStyles = makeStyles((theme) => ({
+const useItemStyles = makeStyles(theme => ({
root: {
paddingLeft: theme.spacing(3),
},
diff --git a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
index 617cce2aee..07078c3fa2 100644
--- a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
+++ b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
@@ -27,9 +27,9 @@ import {
import React, { FC, useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import LoginRequestListItem from './LoginRequestListItem';
-import { useApi, oauthRequestApiRef } from '../../api';
+import { useApi, oauthRequestApiRef } from '@backstage/core-api';
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles(theme => ({
dialog: {
paddingTop: theme.spacing(1),
},
@@ -53,7 +53,7 @@ export const OAuthRequestDialog: FC = () => {
);
const handleRejectAll = () => {
- requests.forEach((request) => request.reject());
+ requests.forEach(request => request.reject());
};
return (
@@ -69,7 +69,7 @@ export const OAuthRequestDialog: FC = () => {
- {requests.map((request) => (
+ {requests.map(request => (
((theme) => ({
+const useStyles = makeStyles(theme => ({
status: {
fontWeight: 500,
'&::before': {
@@ -63,26 +63,26 @@ const useStyles = makeStyles((theme) => ({
},
}));
-export const StatusOK: FC<{}> = (props) => {
+export const StatusOK: FC<{}> = props => {
const classes = useStyles(props);
return ;
};
-export const StatusWarning: FC<{}> = (props) => {
+export const StatusWarning: FC<{}> = props => {
const classes = useStyles(props);
return (
);
};
-export const StatusError: FC<{}> = (props) => {
+export const StatusError: FC<{}> = props => {
const classes = useStyles(props);
return (
);
};
-export const StatusPending: FC<{}> = (props) => {
+export const StatusPending: FC<{}> = props => {
const classes = useStyles(props);
return (
= (props) => {
);
};
-export const StatusRunning: FC<{}> = (props) => {
+export const StatusRunning: FC<{}> = props => {
const classes = useStyles(props);
return (
= (props) => {
);
};
-export const StatusAborted: FC<{}> = (props) => {
+export const StatusAborted: FC<{}> = props => {
const classes = useStyles(props);
return (
', () => {
,
);
const keys = Object.keys(metadata);
- keys.forEach((value) => {
+ keys.forEach(value => {
expect(getByText(startCase(value))).toBeInTheDocument();
expect(getByText(metadata[value])).toBeInTheDocument();
});
@@ -49,7 +49,7 @@ describe(' ', () => {
);
const keys = Object.keys(metadata);
- keys.forEach((value) => {
+ keys.forEach(value => {
expect(getByText(startCase(value))).toBeInTheDocument();
expect(getByText(metadata[value].toString())).toBeInTheDocument();
});
@@ -61,10 +61,10 @@ describe(' ', () => {
,
);
const keys = Object.keys(metadata);
- keys.forEach((value) => {
+ keys.forEach(value => {
expect(getByText(startCase(value))).toBeInTheDocument();
});
- metadata.arrayField.forEach((value) => {
+ metadata.arrayField.forEach(value => {
expect(getByText(value)).toBeInTheDocument();
});
});
@@ -85,7 +85,7 @@ describe(' ', () => {
);
const keys = Object.keys(metadata.config);
- keys.forEach((value) => {
+ keys.forEach(value => {
expect(
getByText(startCase(value), { exact: false }),
).toBeInTheDocument();
diff --git a/packages/core/src/icons/icons.tsx b/packages/core/src/icons/icons.tsx
index ca3ef32859..e8834a6162 100644
--- a/packages/core/src/icons/icons.tsx
+++ b/packages/core/src/icons/icons.tsx
@@ -18,7 +18,7 @@ import { SvgIconProps } from '@material-ui/core';
import PeopleIcon from '@material-ui/icons/People';
import PersonIcon from '@material-ui/icons/Person';
import React, { FC } from 'react';
-import { useApp } from '../api/app/AppContext';
+import { useApp } from '@backstage/core-api';
import { IconComponent, SystemIconKey, SystemIcons } from './types';
export const defaultSystemIcons: SystemIcons = {
@@ -27,7 +27,7 @@ export const defaultSystemIcons: SystemIcons = {
};
const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
- const Component: FC = (props) => {
+ const Component: FC = props => {
const app = useApp();
const Icon = app.getSystemIcon(key);
return ;
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index fc6e2988bc..be9cdc432d 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+export * from '@backstage/core-api';
+
export * from './api';
export { default as Page } from './layout/Page';
export { gradients, pageTheme } from './layout/Page';
@@ -21,6 +23,7 @@ export type { PageTheme } from './layout/Page';
export { default as CodeSnippet } from './components/CodeSnippet';
export { default as Content } from './layout/Content/Content';
export { default as ContentHeader } from './layout/ContentHeader/ContentHeader';
+export { default as DismissableBanner } from './components/DismissableBanner';
export { default as Header } from './layout/Header/Header';
export { default as HeaderLabel } from './layout/HeaderLabel';
export { default as HomepageTimer } from './layout/HomepageTimer';
diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx
index 6f5a1b24cc..703c27e32c 100644
--- a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx
+++ b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx
@@ -20,7 +20,9 @@ import ContentHeader from './ContentHeader';
import { wrapInThemedTestApp } from '@backstage/test-utils';
jest.mock('react-helmet', () => {
- return ({ defaultTitle }: any) => defaultTitle: {defaultTitle}
;
+ return {
+ Helmet: ({ defaultTitle }: any) => defaultTitle: {defaultTitle}
,
+ };
});
describe(' ', () => {
diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.tsx
index 409cfe6b90..1a51d5d19b 100644
--- a/packages/core/src/layout/ContentHeader/ContentHeader.tsx
+++ b/packages/core/src/layout/ContentHeader/ContentHeader.tsx
@@ -20,7 +20,7 @@
import React, { ComponentType, Fragment, FC } from 'react';
import { Typography, makeStyles } from '@material-ui/core';
-import Helmet from 'react-helmet';
+import { Helmet } from 'react-helmet';
const useStyles = makeStyles(theme => ({
container: {
diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
index 14241c1ee7..747f2f6036 100644
--- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx
+++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
@@ -26,7 +26,7 @@ interface IErrorPageProps {
statusMessage: string;
}
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles(theme => ({
container: {
padding: theme.spacing(8),
},
diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx
new file mode 100644
index 0000000000..e1198d7b65
--- /dev/null
+++ b/packages/core/src/layout/Header/Header.stories.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import Header from '.';
+import HeaderLabel from '../HeaderLabel';
+import Page, { pageTheme } from '../Page';
+
+export default {
+ title: 'Header',
+ component: Header,
+};
+
+const labels = (
+ <>
+
+
+
+ >
+);
+
+export const Home = () => (
+
+
+
+);
+
+export const HomeWithSubtitle = () => (
+
+);
+
+export const Tool = () => (
+
+
+
+);
+
+export const Service = () => (
+
+
+
+);
+
+export const Website = () => (
+
+
+
+);
+
+export const Library = () => (
+
+
+
+);
+
+export const App = () => (
+
+
+
+);
+
+export const Other = () => (
+
+
+
+);
diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx
index 85b15e33aa..afedf2ad35 100644
--- a/packages/core/src/layout/Header/Header.test.tsx
+++ b/packages/core/src/layout/Header/Header.test.tsx
@@ -20,7 +20,9 @@ import { wrapInThemedTestApp } from '@backstage/test-utils';
import Header from './Header';
jest.mock('react-helmet', () => {
- return ({ defaultTitle }: any) => defaultTitle: {defaultTitle}
;
+ return {
+ Helmet: ({ defaultTitle }: any) => defaultTitle: {defaultTitle}
,
+ };
});
describe('', () => {
diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx
index ab3d0080e4..47fcc324a7 100644
--- a/packages/core/src/layout/Header/Header.tsx
+++ b/packages/core/src/layout/Header/Header.tsx
@@ -15,15 +15,14 @@
*/
import React, { Fragment, ReactNode, CSSProperties, FC } from 'react';
-import Helmet from 'react-helmet';
+import { Helmet } from 'react-helmet';
import { Typography, Tooltip, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { Theme } from '../Page/Page';
-// import { Link } from 'shared/components';
import Waves from './Waves';
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles(theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
@@ -149,7 +148,7 @@ const SubtitleFragment: FC = ({ classes, subtitle }) => {
}
return (
-
+
{subtitle}
);
@@ -175,7 +174,7 @@ export const Header: FC = ({
- {(theme) => (
+ {theme => (
diff --git a/packages/core/src/layout/Page/PageThemeProvider.ts b/packages/core/src/layout/Page/PageThemeProvider.ts
index a767033029..9c31fcb4ac 100644
--- a/packages/core/src/layout/Page/PageThemeProvider.ts
+++ b/packages/core/src/layout/Page/PageThemeProvider.ts
@@ -89,4 +89,19 @@ export const pageTheme: Record = {
tool: {
gradient: gradients.purpleBlue,
},
+ service: {
+ gradient: gradients.green,
+ },
+ website: {
+ gradient: gradients.purple,
+ },
+ library: {
+ gradient: gradients.sunset,
+ },
+ other: {
+ gradient: gradients.brown,
+ },
+ app: {
+ gradient: gradients.redOrange,
+ },
};
diff --git a/packages/core/src/layout/Sidebar/Intro.tsx b/packages/core/src/layout/Sidebar/Intro.tsx
index 595697c466..b4bb56588e 100644
--- a/packages/core/src/layout/Sidebar/Intro.tsx
+++ b/packages/core/src/layout/Sidebar/Intro.tsx
@@ -26,7 +26,7 @@ import {
} from './config';
import { SidebarDivider } from './Items';
-const useStyles = makeStyles((theme) => ({
+const useStyles = makeStyles(theme => ({
introCard: {
color: '#b5b5b5',
// XXX (@koroeskohr): should I be using a Mui theme variable?
@@ -74,7 +74,7 @@ type IntroCardProps = {
onClose: () => void;
};
-export const IntroCard: FC = (props) => {
+export const IntroCard: FC = props => {
const classes = useStyles();
const { text, onClose } = props;
const handleClose = () => onClose();
@@ -109,7 +109,7 @@ type SidebarIntroCardProps = {
onDismiss: () => void;
};
-const SidebarIntroCard: FC = (props) => {
+const SidebarIntroCard: FC = props => {
const { text, onDismiss } = props;
const [collapsing, setCollapsing] = useState(false);
const startDismissing = () => {
@@ -138,10 +138,10 @@ export const SidebarIntro: FC = () => {
});
const dismissStarred = () => {
- setDismissedIntro((state) => ({ ...state, starredItemsDismissed: true }));
+ setDismissedIntro(state => ({ ...state, starredItemsDismissed: true }));
};
const dismissRecentlyViewed = () => {
- setDismissedIntro((state) => ({
+ setDismissedIntro(state => ({
...state,
recentlyViewedItemsDismissed: true,
}));
diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx
index bd5faa5631..4696f53a37 100644
--- a/packages/core/src/layout/Sidebar/Items.tsx
+++ b/packages/core/src/layout/Sidebar/Items.tsx
@@ -29,7 +29,7 @@ import { NavLink } from 'react-router-dom';
import { sidebarConfig, SidebarContext } from './config';
import { IconComponent } from '../../icons';
-const useStyles = makeStyles((theme) => {
+const useStyles = makeStyles(theme => {
const {
selectedIndicatorWidth,
drawerWidthClosed,
@@ -139,7 +139,7 @@ export const SidebarItem: FC = ({
Boolean(match && !disableSelected)}
+ isActive={match => Boolean(match && !disableSelected)}
exact
to={to}
onClick={onClick}
@@ -153,7 +153,7 @@ export const SidebarItem: FC = ({
Boolean(match && !disableSelected)}
+ isActive={match => Boolean(match && !disableSelected)}
exact
to={to}
onClick={onClick}
@@ -175,11 +175,11 @@ type SidebarSearchFieldProps = {
onSearch: (input: string) => void;
};
-export const SidebarSearchField: FC = (props) => {
+export const SidebarSearchField: FC = props => {
const [input, setInput] = useState('');
const classes = useStyles();
- const handleEnter: KeyboardEventHandler = (ev) => {
+ const handleEnter: KeyboardEventHandler = ev => {
if (ev.key === 'Enter') {
props.onSearch(input);
}
diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx
index 79df2852d9..750f764c2d 100644
--- a/packages/core/src/layout/Sidebar/Page.tsx
+++ b/packages/core/src/layout/Sidebar/Page.tsx
@@ -44,7 +44,7 @@ export const SidebarPinStateContext = createContext(
},
);
-export const SidebarPage: FC<{}> = (props) => {
+export const SidebarPage: FC<{}> = props => {
const [isPinned, setIsPinned] = useState(LocalStorage.getSidebarPinState());
useEffect(() => {
diff --git a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx
index 7f4c8890f9..32bc9d9632 100644
--- a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx
+++ b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx
@@ -19,7 +19,7 @@ import { useObservable } from 'react-use';
import LightIcon from '@material-ui/icons/WbSunny';
import DarkIcon from '@material-ui/icons/Brightness2';
import AutoIcon from '@material-ui/icons/BrightnessAuto';
-import { appThemeApiRef, useApi } from '../../api';
+import { appThemeApiRef, useApi } from '@backstage/core-api';
import { SidebarItem } from './Items';
export const SidebarThemeToggle: FC<{}> = () => {
diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js
index 5f9bde16c3..b8f78f5d2c 100644
--- a/packages/storybook/.storybook/main.js
+++ b/packages/storybook/.storybook/main.js
@@ -12,7 +12,7 @@ module.exports = {
'@storybook/addon-storysource',
'storybook-dark-mode/register',
],
- webpackFinal: async (config) => {
+ webpackFinal: async config => {
const coreSrc = path.resolve(__dirname, '../../core/src');
// Mirror config in packages/cli/src/lib/bundler
diff --git a/packages/storybook/package.json b/packages/storybook/package.json
index 209b7371dc..883fb1452a 100644
--- a/packages/storybook/package.json
+++ b/packages/storybook/package.json
@@ -22,6 +22,6 @@
"@storybook/addon-storysource": "^5.3.18",
"@storybook/addons": "^5.3.17",
"@storybook/react": "^5.3.17",
- "storybook-dark-mode": "^0.4.1"
+ "storybook-dark-mode": "^0.6.1"
}
}
diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts
index b692a35216..327bf0260b 100644
--- a/plugins/auth-backend/src/providers/google/provider.test.ts
+++ b/plugins/auth-backend/src/providers/google/provider.test.ts
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-import { GoogleAuthProvider, THOUSAND_DAYS_MS } from './provider';
+import {
+ GoogleAuthProvider,
+ THOUSAND_DAYS_MS,
+ TEN_MINUTES_MS,
+} from './provider';
import passport from 'passport';
import express from 'express';
import * as utils from './../utils';
@@ -52,11 +56,16 @@ describe('GoogleAuthProvider', () => {
});
describe('start authentication handler', () => {
- const mockResponse = ({} as unknown) as express.Response;
+ const mockResponse = ({
+ send: jest.fn().mockReturnThis(),
+ status: jest.fn().mockReturnThis(),
+ cookie: jest.fn().mockReturnThis(),
+ } as unknown) as express.Response;
const mockNext: express.NextFunction = jest.fn();
it('should initiate authenticate request with provided scopes', () => {
const mockRequest = ({
+ header: () => 'XMLHttpRequest',
query: {
scope: 'a,b',
},
@@ -75,11 +84,36 @@ describe('GoogleAuthProvider', () => {
scope: 'a,b',
accessType: 'offline',
prompt: 'consent',
+ state: expect.any(String),
});
});
+ it('should set a nonce cookie', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ query: {
+ scope: 'a,b',
+ },
+ } as unknown) as express.Request;
+
+ const googleAuthProvider = new GoogleAuthProvider(
+ googleAuthProviderConfig,
+ );
+ googleAuthProvider.start(mockRequest, mockResponse, mockNext);
+ expect(mockResponse.cookie).toBeCalledTimes(1);
+ expect(mockResponse.cookie).toBeCalledWith(
+ 'google-nonce',
+ expect.any(String),
+ expect.objectContaining({
+ maxAge: TEN_MINUTES_MS,
+ path: `/auth/${googleAuthProviderConfig.provider}/handler`,
+ }),
+ );
+ });
+
it('should throw error if no scopes provided', () => {
const mockRequest = ({
+ header: () => 'XMLHttpRequest',
query: {},
} as unknown) as express.Request;
@@ -93,7 +127,9 @@ describe('GoogleAuthProvider', () => {
});
describe('logout handler', () => {
- const mockRequest = ({} as unknown) as express.Request;
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ } as unknown) as express.Request;
it('should perform logout and respond with 200', () => {
const mockResponse: any = ({
@@ -122,7 +158,6 @@ describe('GoogleAuthProvider', () => {
});
describe('redirect frame handler', () => {
- const mockRequest = ({} as unknown) as express.Request;
const mockResponse: any = ({
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
@@ -131,6 +166,14 @@ describe('GoogleAuthProvider', () => {
const mockNext: express.NextFunction = jest.fn();
it('should call authenticate and post a response', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ cookies: { 'google-nonce': 'NONCE' },
+ query: {
+ state: 'NONCE',
+ },
+ } as unknown) as express.Request;
+
const spyPostMessage = jest
.spyOn(utils, 'postMessageResponse')
.mockImplementation(() => jest.fn());
@@ -164,6 +207,14 @@ describe('GoogleAuthProvider', () => {
});
it('should respond with a error message if no refresh token returned', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ cookies: { 'google-nonce': 'NONCE' },
+ query: {
+ state: 'NONCE',
+ },
+ } as unknown) as express.Request;
+
const spyPassport = jest
.spyOn(passport, 'authenticate')
.mockImplementation((_x, callbackFunc) => {
@@ -190,6 +241,14 @@ describe('GoogleAuthProvider', () => {
});
it('should respond with a error message if auth failed', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ cookies: { 'google-nonce': 'NONCE' },
+ query: {
+ state: 'NONCE',
+ },
+ } as unknown) as express.Request;
+
const spyPassport = jest
.spyOn(passport, 'authenticate')
.mockImplementation((_x, callbackFunc) => {
@@ -214,6 +273,60 @@ describe('GoogleAuthProvider', () => {
error: new Error('Google auth failed, Error: TokenError'),
});
});
+
+ it('should respond with a error message if cookie nonce is missing', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ cookies: {},
+ query: { state: 'NONCE' },
+ } as unknown) as express.Request;
+
+ const googleAuthProvider = new GoogleAuthProvider(
+ googleAuthProviderConfig,
+ );
+
+ googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
+ expect(mockResponse.send).toBeCalledTimes(1);
+ expect(mockResponse.send).toBeCalledWith('Missing nonce');
+ expect(mockResponse.status).toBeCalledTimes(1);
+ expect(mockResponse.status).toBeCalledWith(401);
+ });
+
+ it('should respond with a error message if state nonce is missing', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ cookies: { 'google-nonce': 'NONCE' },
+ query: {},
+ } as unknown) as express.Request;
+
+ const googleAuthProvider = new GoogleAuthProvider(
+ googleAuthProviderConfig,
+ );
+
+ googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
+ expect(mockResponse.send).toBeCalledTimes(1);
+ expect(mockResponse.send).toBeCalledWith('Missing nonce');
+ expect(mockResponse.status).toBeCalledTimes(1);
+ expect(mockResponse.status).toBeCalledWith(401);
+ });
+
+ it('should respond with a error message if nonce mismatch', () => {
+ const mockRequest = ({
+ header: () => 'XMLHttpRequest',
+ cookies: { 'google-nonce': 'NONCA' },
+ query: { state: 'NONCEB' },
+ } as unknown) as express.Request;
+
+ const googleAuthProvider = new GoogleAuthProvider(
+ googleAuthProviderConfig,
+ );
+
+ googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext);
+ expect(mockResponse.send).toBeCalledTimes(1);
+ expect(mockResponse.send).toBeCalledWith('Invalid nonce');
+ expect(mockResponse.status).toBeCalledTimes(1);
+ expect(mockResponse.status).toBeCalledWith(401);
+ });
});
describe('strategy handler', () => {
@@ -245,6 +358,7 @@ describe('GoogleAuthProvider', () => {
it('should respond with a 401', () => {
const mockRequest = ({
cookies: jest.fn(),
+ header: () => 'XMLHttpRequest',
} as unknown) as express.Request;
const googleAuthProvider = new GoogleAuthProvider(
@@ -262,6 +376,7 @@ describe('GoogleAuthProvider', () => {
describe('refresh token cookie, no scope', () => {
const mockRequest = ({
+ header: () => 'XMLHttpRequest',
cookies: { 'google-refresh-token': 'REFRESH_TOKEN' },
query: {},
} as unknown) as express.Request;
@@ -349,6 +464,7 @@ describe('GoogleAuthProvider', () => {
describe('refresh token cookie and scope', () => {
const mockRequest = ({
+ header: () => 'XMLHttpRequest',
cookies: { 'google-refresh-token': 'REFRESH_TOKEN' },
query: {
scope: 'a,b',
@@ -387,6 +503,20 @@ describe('GoogleAuthProvider', () => {
scope: 'a,b',
});
});
+
+ it('ensures x-requested-with header', () => {
+ const mockHeaderRequest = ({
+ header: () => 'TEST',
+ } as unknown) as express.Request;
+
+ googleAuthProvider.refresh(mockHeaderRequest, mockResponse);
+ expect(mockResponse.send).toBeCalledTimes(1);
+ expect(mockResponse.send).toBeCalledWith(
+ 'Invalid X-Requested-With header',
+ );
+ expect(mockResponse.status).toBeCalledTimes(1);
+ expect(mockResponse.status).toBeCalledWith(401);
+ });
});
});
});
diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts
index 096260e044..cb080e2fd3 100644
--- a/plugins/auth-backend/src/providers/google/provider.ts
+++ b/plugins/auth-backend/src/providers/google/provider.ts
@@ -16,6 +16,7 @@
import passport from 'passport';
import express, { CookieOptions } from 'express';
+import crypto from 'crypto';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import refresh from 'passport-oauth2-refresh';
import {
@@ -23,10 +24,11 @@ import {
AuthProviderRouteHandlers,
AuthProviderConfig,
} from './../types';
-import { postMessageResponse } from './../utils';
+import { postMessageResponse, ensuresXRequestedWith } from './../utils';
import { InputError } from '@backstage/backend-common';
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
+export const TEN_MINUTES_MS = 600 * 1000;
export class GoogleAuthProvider
implements AuthProvider, AuthProviderRouteHandlers {
private readonly providerConfig: AuthProviderConfig;
@@ -39,6 +41,19 @@ export class GoogleAuthProvider
res: express.Response,
next: express.NextFunction,
) {
+ const nonce = crypto.randomBytes(16).toString('base64');
+
+ const options: CookieOptions = {
+ maxAge: TEN_MINUTES_MS,
+ secure: false,
+ sameSite: 'none',
+ domain: 'localhost',
+ path: `/auth/${this.providerConfig.provider}/handler`,
+ httpOnly: true,
+ };
+
+ res.cookie(`${this.providerConfig.provider}-nonce`, nonce, options);
+
const scope = req.query.scope?.toString() ?? '';
if (!scope) {
throw new InputError('missing scope parameter');
@@ -47,6 +62,7 @@ export class GoogleAuthProvider
scope,
accessType: 'offline',
prompt: 'consent',
+ state: nonce,
})(req, res, next);
}
@@ -55,6 +71,17 @@ export class GoogleAuthProvider
res: express.Response,
next: express.NextFunction,
) {
+ const cookieNonce = req.cookies[`${this.providerConfig.provider}-nonce`];
+ const stateNonce = req.query.state;
+
+ if (!cookieNonce || !stateNonce) {
+ return res.status(401).send('Missing nonce');
+ }
+
+ if (cookieNonce !== stateNonce) {
+ return res.status(401).send('Invalid nonce');
+ }
+
return passport.authenticate('google', (err, user) => {
if (err) {
return postMessageResponse(res, {
@@ -95,7 +122,11 @@ export class GoogleAuthProvider
})(req, res, next);
}
- async logout(_req: express.Request, res: express.Response) {
+ async logout(req: express.Request, res: express.Response) {
+ if (!ensuresXRequestedWith(req)) {
+ return res.status(401).send('Invalid X-Requested-With header');
+ }
+
const options: CookieOptions = {
maxAge: 0,
secure: false,
@@ -110,6 +141,10 @@ export class GoogleAuthProvider
}
async refresh(req: express.Request, res: express.Response) {
+ if (!ensuresXRequestedWith(req)) {
+ return res.status(401).send('Invalid X-Requested-With header');
+ }
+
const refreshToken =
req.cookies[`${this.providerConfig.provider}-refresh-token`];
diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts
index 68fe61c7a6..1b33391c27 100644
--- a/plugins/auth-backend/src/providers/index.ts
+++ b/plugins/auth-backend/src/providers/index.ts
@@ -20,11 +20,11 @@ import { ProviderFactories } from './factories';
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
const router = Router();
- router.get('/start', provider.start);
- router.get('/handler/frame', provider.frameHandler);
- router.get('/logout', provider.logout);
+ router.get('/start', provider.start.bind(provider));
+ router.get('/handler/frame', provider.frameHandler.bind(provider));
+ router.get('/logout', provider.logout.bind(provider));
if (provider.refresh) {
- router.get('/refresh', provider.refresh);
+ router.get('/refresh', provider.refresh.bind(provider));
}
return router;
};
diff --git a/plugins/auth-backend/src/providers/utils.ts b/plugins/auth-backend/src/providers/utils.ts
index 7fb906ad7c..83229e55d4 100644
--- a/plugins/auth-backend/src/providers/utils.ts
+++ b/plugins/auth-backend/src/providers/utils.ts
@@ -39,3 +39,12 @@ export const postMessageResponse = (