refactor: apply review suggestions
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
c884b9a478
commit
ffd71105a6
@@ -2,4 +2,4 @@
|
||||
'@backstage/plugin-auth-react': patch
|
||||
---
|
||||
|
||||
Update the default cookie base path and create a experimental redirect to root and protected app components, the components should be used only when the public entry is enabled.
|
||||
Update the default cookie base path and create a experimental redirect to root and app mode components, the components authenticate and keep a cookie refresh loop on the client side.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
'@backstage/core-app-api': patch
|
||||
---
|
||||
|
||||
Clear the app auth cookie when the when the user sign out.
|
||||
Clear the app auth cookie after a sign out.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': patch
|
||||
'@backstage/backend-test-utils': patch
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Add a `removeUserCookie` to the http auth service interface.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: enable-public-entry
|
||||
title: Enabling public entry point
|
||||
title: Enabling a public entry point
|
||||
description: A guide for how to experiment with public and protected Backstage app bundles
|
||||
---
|
||||
|
||||
@@ -21,11 +21,14 @@ With that, Backstage's cli and backend will detect public entry point and serve
|
||||
|
||||
- The tutorial will only work for those using backstage-cli to build and serve their Backstage app.
|
||||
|
||||
## Trying out this feature
|
||||
## Step-by-step
|
||||
|
||||
1. Add a `index-public-experimental.tsx` to your app `src` folder;
|
||||
1. Create a `index-public-experimental.tsx` in your app `src` folder;
|
||||
:::note
|
||||
The filename is a convention, so it is not currently configurable.
|
||||
:::
|
||||
|
||||
2. Prepare an unauthenticated version of your application. This will be the public entry point for your site:
|
||||
2. This file is the public entry point for your application, and it should only contain what unauthenticated users should see:
|
||||
|
||||
```tsx title="in packages/app/src/index-public-experimental.tsx"
|
||||
import React from 'react';
|
||||
@@ -42,7 +45,7 @@ With that, Backstage's cli and backend will detect public entry point and serve
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { RedirectToRoot } from '@backstage/plugin-auth-react';
|
||||
import { CookieAuthRootRedirect } from '@backstage/plugin-auth-react';
|
||||
import { providers } from '../src/identityProviders';
|
||||
import { AuthProxyDiscoveryApi } from '../src/AuthProxyDiscoveryApi';
|
||||
|
||||
@@ -76,7 +79,7 @@ With that, Backstage's cli and backend will detect public entry point and serve
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* This is a special component that does the magic to redirect users to access the home page of your authenticated application version */}
|
||||
<RedirectToRoot />
|
||||
<CookieAuthRootRedirect />
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
@@ -84,35 +87,12 @@ With that, Backstage's cli and backend will detect public entry point and serve
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
```
|
||||
|
||||
3) Then ensure your main entry is also protected by the experimental app provider, as shown in the following example:
|
||||
3. The frontend will handle cookie refreshing automatically, so you don't have to worry about it;
|
||||
|
||||
```tsx title="in packages/app/src/index-public-experimental.tsx"
|
||||
// ...
|
||||
export default app.createRoot(
|
||||
<>
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* For now, this is just a temporary solution to ensure the user remains logged in properly and has access to the main app content. */}
|
||||
<ExperimentalAppProtection>
|
||||
<VisitListener />
|
||||
<Root>{routes}</Root>
|
||||
</ExperimentalAppProtection>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
```
|
||||
4. You're now ready to build and serve your frontend and backend as usual;
|
||||
|
||||
4. You're now ready to build your front-end app:
|
||||
5. After that, access your backend index endpoint to see the public app being served (note that only a minimal app is being served).
|
||||
|
||||
```sh
|
||||
yarn workspace example-app build
|
||||
```
|
||||
6. Log in and you will be redirected to the main app home page (check the protected bundle being served from the app-backend after the redirect).
|
||||
|
||||
5. And also serve it from your Backstage app backend:
|
||||
|
||||
```sh
|
||||
yarn start-backend:next
|
||||
```
|
||||
|
||||
6. Finally, access http://localhost:7007 to see the public app being served (note that only a minimal app is being served). Log in and you will be redirected to the main app home page (check the protected bundle being served from the app-backend after the redirect).
|
||||
That's it!
|
||||
|
||||
@@ -108,7 +108,6 @@ import { DevToolsPage } from '@backstage/plugin-devtools';
|
||||
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
|
||||
import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
|
||||
import { NotificationsPage } from '@backstage/plugin-notifications';
|
||||
import { ExperimentalAppProtection } from '@backstage/plugin-auth-react';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
@@ -283,10 +282,8 @@ export default app.createRoot(
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
<ExperimentalAppProtection>
|
||||
<VisitListener />
|
||||
<Root>{routes}</Root>
|
||||
</ExperimentalAppProtection>
|
||||
<VisitListener />
|
||||
<Root>{routes}</Root>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
OAuthRequestDialog,
|
||||
SignInPage,
|
||||
} from '@backstage/core-components';
|
||||
import { RedirectToRoot } from '@backstage/plugin-auth-react';
|
||||
import { CookieAuthRootRedirect } from '@backstage/plugin-auth-react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { providers } from '../src/identityProviders';
|
||||
@@ -59,7 +59,7 @@ const App = app.createRoot(
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
<RedirectToRoot />
|
||||
<CookieAuthRootRedirect />
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
|
||||
+33
-19
@@ -181,6 +181,13 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
|
||||
let credentials: BackstageCredentials<BackstageUserPrincipal>;
|
||||
if (options?.credentials) {
|
||||
if (this.#auth.isPrincipal(options.credentials, 'none')) {
|
||||
res.clearCookie(
|
||||
BACKSTAGE_AUTH_COOKIE,
|
||||
await this.#getCookieOptions(res.req),
|
||||
);
|
||||
return { expiresAt: new Date() };
|
||||
}
|
||||
if (!this.#auth.isPrincipal(options.credentials, 'user')) {
|
||||
throw new AuthenticationError(
|
||||
'Refused to issue cookie for non-user principal',
|
||||
@@ -196,16 +203,6 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
return { expiresAt: existingExpiresAt };
|
||||
}
|
||||
|
||||
const originHeader = res.req.headers.origin;
|
||||
const origin =
|
||||
!originHeader || originHeader === 'null' ? undefined : originHeader;
|
||||
|
||||
// https://backstage.example.com/api/catalog
|
||||
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
|
||||
this.#pluginId,
|
||||
);
|
||||
const externalBaseUrl = new URL(origin ?? externalBaseUrlStr);
|
||||
|
||||
const { token, expiresAt } = await this.#auth.getLimitedUserToken(
|
||||
credentials,
|
||||
);
|
||||
@@ -213,20 +210,41 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
throw new Error('User credentials is unexpectedly missing token');
|
||||
}
|
||||
|
||||
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
|
||||
...(await this.#getCookieOptions(res.req)),
|
||||
expires: expiresAt,
|
||||
});
|
||||
|
||||
return { expiresAt };
|
||||
}
|
||||
|
||||
async #getCookieOptions(req: Request): Promise<{
|
||||
domain: string;
|
||||
httpOnly: true;
|
||||
secure: boolean;
|
||||
priority: 'high';
|
||||
sameSite: 'none' | 'lax';
|
||||
}> {
|
||||
const originHeader = req.headers.origin;
|
||||
const origin =
|
||||
!originHeader || originHeader === 'null' ? undefined : originHeader;
|
||||
|
||||
const externalBaseUrlStr = await this.#discovery.getExternalBaseUrl(
|
||||
this.#pluginId,
|
||||
);
|
||||
const externalBaseUrl = new URL(origin ?? externalBaseUrlStr);
|
||||
|
||||
const secure =
|
||||
externalBaseUrl.protocol === 'https:' ||
|
||||
externalBaseUrl.hostname === 'localhost';
|
||||
|
||||
res.cookie(BACKSTAGE_AUTH_COOKIE, token, {
|
||||
return {
|
||||
domain: externalBaseUrl.hostname,
|
||||
httpOnly: true,
|
||||
expires: expiresAt,
|
||||
secure,
|
||||
priority: 'high',
|
||||
sameSite: secure ? 'none' : 'lax',
|
||||
});
|
||||
|
||||
return { expiresAt };
|
||||
};
|
||||
}
|
||||
|
||||
async #existingCookieExpiration(req: Request): Promise<Date | undefined> {
|
||||
@@ -254,10 +272,6 @@ class DefaultHttpAuthService implements HttpAuthService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
removeUserCookie(res: Response): void {
|
||||
res.clearCookie(BACKSTAGE_AUTH_COOKIE);
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ export const httpRouterServiceFactory = createServiceFactory(
|
||||
) {
|
||||
// Only add the cookie refresh middleware once
|
||||
hasRegistedCookieAuthRefreshMiddleware = true;
|
||||
router.use(createCookieAuthRefreshMiddleware({ httpAuth }));
|
||||
router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -259,10 +259,6 @@ class HttpAuthCompat implements HttpAuthService {
|
||||
async issueUserCookie(_res: Response): Promise<{ expiresAt: Date }> {
|
||||
return { expiresAt: new Date(Date.now() + 3600_000) };
|
||||
}
|
||||
|
||||
removeUserCookie(res: Response): void {
|
||||
res.clearCookie('backstage-auth');
|
||||
}
|
||||
}
|
||||
|
||||
export class UserInfoCompat implements UserInfoService {
|
||||
|
||||
@@ -325,13 +325,11 @@ export interface HttpAuthService {
|
||||
issueUserCookie(
|
||||
res: Response_2,
|
||||
options?: {
|
||||
credentials?: BackstageCredentials<BackstageUserPrincipal>;
|
||||
credentials?: BackstageCredentials;
|
||||
},
|
||||
): Promise<{
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
// (undocumented)
|
||||
removeUserCookie(res: Response_2): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import {
|
||||
BackstageCredentials,
|
||||
BackstagePrincipalTypes,
|
||||
BackstageUserPrincipal,
|
||||
} from './AuthService';
|
||||
import { BackstageCredentials, BackstagePrincipalTypes } from './AuthService';
|
||||
|
||||
/** @public */
|
||||
export interface HttpAuthService {
|
||||
@@ -34,9 +30,7 @@ export interface HttpAuthService {
|
||||
issueUserCookie(
|
||||
res: Response,
|
||||
options?: {
|
||||
credentials?: BackstageCredentials<BackstageUserPrincipal>;
|
||||
credentials?: BackstageCredentials;
|
||||
},
|
||||
): Promise<{ expiresAt: Date }>;
|
||||
|
||||
removeUserCookie(res: Response): void;
|
||||
}
|
||||
|
||||
@@ -140,8 +140,4 @@ export class MockHttpAuthService implements HttpAuthService {
|
||||
|
||||
return { expiresAt: new Date(Date.now() + 3600_000) };
|
||||
}
|
||||
|
||||
removeUserCookie(res: Response): void {
|
||||
res.clearCookie(MOCK_AUTH_COOKIE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +275,6 @@ export namespace mockServices {
|
||||
export const mock = simpleMock(coreServices.httpAuth, () => ({
|
||||
credentials: jest.fn(),
|
||||
issueUserCookie: jest.fn(),
|
||||
removeUserCookie: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -70,11 +70,7 @@ export async function buildBundle(options: BuildOptions) {
|
||||
`⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`,
|
||||
),
|
||||
);
|
||||
configs.push(
|
||||
await createConfig(publicPaths, {
|
||||
...commonConfigOptions,
|
||||
}),
|
||||
);
|
||||
configs.push(await createConfig(publicPaths, commonConfigOptions));
|
||||
}
|
||||
|
||||
const isCi = yn(process.env.CI, { default: false });
|
||||
|
||||
@@ -216,12 +216,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be
|
||||
);
|
||||
}
|
||||
const compiler = publicPaths
|
||||
? webpack([
|
||||
config,
|
||||
await createConfig(publicPaths, {
|
||||
...commonConfigOptions,
|
||||
}),
|
||||
])
|
||||
? webpack([config, await createConfig(publicPaths, commonConfigOptions)])
|
||||
: webpack(config);
|
||||
|
||||
webpackServer = new WebpackDevServer(
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-auth-react": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@types/prop-types": "^15.7.3",
|
||||
|
||||
@@ -864,7 +864,7 @@ describe('Integration Test', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7007/app/.backstage/v1-cookie',
|
||||
'http://localhost:7007/app/.backstage/auth/v1/cookie',
|
||||
{ method: 'DELETE' },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import { isReactRouterBeta } from './isReactRouterBeta';
|
||||
import { RouteTracker } from '../routing/RouteTracker';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy';
|
||||
import { AppMode } from '@backstage/plugin-auth-react';
|
||||
|
||||
/**
|
||||
* Get the app base path from the configured app baseUrl.
|
||||
@@ -145,7 +146,10 @@ export function AppRouter(props: AppRouterProps) {
|
||||
<RouterComponent>
|
||||
<RouteTracker routeObjects={routeObjects} />
|
||||
<Routes>
|
||||
<Route path={mountPath} element={<>{props.children}</>} />
|
||||
<Route
|
||||
path={mountPath}
|
||||
element={<AppMode>{props.children}</AppMode>}
|
||||
/>
|
||||
</Routes>
|
||||
</RouterComponent>
|
||||
);
|
||||
@@ -154,7 +158,7 @@ export function AppRouter(props: AppRouterProps) {
|
||||
return (
|
||||
<RouterComponent basename={basePath}>
|
||||
<RouteTracker routeObjects={routeObjects} />
|
||||
{props.children}
|
||||
<AppMode>{props.children}</AppMode>
|
||||
</RouterComponent>
|
||||
);
|
||||
}
|
||||
@@ -168,7 +172,10 @@ export function AppRouter(props: AppRouterProps) {
|
||||
appIdentityProxy={appIdentityProxy}
|
||||
>
|
||||
<Routes>
|
||||
<Route path={mountPath} element={<>{props.children}</>} />
|
||||
<Route
|
||||
path={mountPath}
|
||||
element={<AppMode>{props.children}</AppMode>}
|
||||
/>
|
||||
</Routes>
|
||||
</SignInPageWrapper>
|
||||
</RouterComponent>
|
||||
@@ -182,7 +189,7 @@ export function AppRouter(props: AppRouterProps) {
|
||||
component={SignInPageComponent}
|
||||
appIdentityProxy={appIdentityProxy}
|
||||
>
|
||||
{props.children}
|
||||
<AppMode>{props.children}</AppMode>
|
||||
</SignInPageWrapper>
|
||||
</RouterComponent>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-auth-react": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
|
||||
@@ -42,6 +42,7 @@ import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { RouteTracker } from '../routing/RouteTracker';
|
||||
import { getBasePath } from '../routing/getBasePath';
|
||||
import { AppMode } from '@backstage/plugin-auth-react';
|
||||
|
||||
export const AppRoot = createExtension({
|
||||
namespace: 'app',
|
||||
@@ -190,7 +191,7 @@ export function AppRouter(props: AppRouterProps) {
|
||||
return (
|
||||
<RouterComponent>
|
||||
<RouteTracker routeObjects={routeObjects} />
|
||||
{children}
|
||||
<AppMode>{children}</AppMode>
|
||||
</RouterComponent>
|
||||
);
|
||||
}
|
||||
@@ -202,7 +203,7 @@ export function AppRouter(props: AppRouterProps) {
|
||||
component={SignInPageComponent}
|
||||
appIdentityProxy={appIdentityProxy}
|
||||
>
|
||||
{children}
|
||||
<AppMode>{children}</AppMode>
|
||||
</SignInPageWrapper>
|
||||
</RouterComponent>
|
||||
);
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('appPlugin', () => {
|
||||
fetch(`http://localhost:${server.port()}/api/app/derp.html`).then(res =>
|
||||
res.text(),
|
||||
),
|
||||
).resolves.toBe('winning');
|
||||
).resolves.toMatch('winning');
|
||||
await expect(
|
||||
fetch(`http://localhost:${server.port()}`).then(res => res.text()),
|
||||
).resolves.toBe('winning');
|
||||
).resolves.toMatch('winning');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,12 +168,13 @@ export async function createRouter(
|
||||
|
||||
const publicDistDir = resolvePath(appDistDir, 'public');
|
||||
|
||||
const enablePublicEntryPoint = await fs.pathExists(publicDistDir);
|
||||
const enablePublicEntryPoint =
|
||||
(await fs.pathExists(publicDistDir)) && auth && httpAuth;
|
||||
|
||||
if (enablePublicEntryPoint && auth && httpAuth) {
|
||||
const publicRouter = Router();
|
||||
|
||||
publicRouter.use(createCookieAuthRefreshMiddleware({ httpAuth }));
|
||||
publicRouter.use(createCookieAuthRefreshMiddleware({ auth, httpAuth }));
|
||||
|
||||
publicRouter.use(async (req, _res, next) => {
|
||||
const credentials = await httpAuth.credentials(req, {
|
||||
@@ -214,6 +215,7 @@ export async function createRouter(
|
||||
|
||||
publicRouter.use(
|
||||
await createEntryPointRouter({
|
||||
appMode: 'public',
|
||||
logger: logger.child({ entry: 'public' }),
|
||||
rootDir: publicDistDir,
|
||||
assetStore: assetStore?.withNamespace('public'),
|
||||
@@ -226,6 +228,7 @@ export async function createRouter(
|
||||
|
||||
router.use(
|
||||
await createEntryPointRouter({
|
||||
appMode: enablePublicEntryPoint ? 'protected' : 'public',
|
||||
logger: logger.child({ entry: 'main' }),
|
||||
rootDir: appDistDir,
|
||||
assetStore,
|
||||
@@ -243,6 +246,7 @@ async function createEntryPointRouter({
|
||||
rootDir,
|
||||
assetStore,
|
||||
staticFallbackHandler,
|
||||
appMode,
|
||||
appConfigs,
|
||||
injectedConfigPath,
|
||||
}: {
|
||||
@@ -250,6 +254,7 @@ async function createEntryPointRouter({
|
||||
rootDir: string;
|
||||
assetStore?: StaticAssetsStore;
|
||||
staticFallbackHandler?: express.Handler;
|
||||
appMode: 'public' | 'protected';
|
||||
appConfigs?: AppConfig[];
|
||||
injectedConfigPath?: string;
|
||||
}) {
|
||||
@@ -303,14 +308,21 @@ async function createEntryPointRouter({
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const indexHtmlContent = Buffer.from(
|
||||
(await fs.readFile(resolvePath(rootDir, 'index.html'), 'utf8')).replace(
|
||||
/<head>/,
|
||||
`<head><meta name="backstage-app-mode" content="${appMode}" />`,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
router.get('/*', (_req, res) => {
|
||||
res.sendFile(resolvePath(rootDir, 'index.html'), {
|
||||
headers: {
|
||||
// The Cache-Control header instructs the browser to not cache the index.html since it might
|
||||
// link to static assets from recently deployed versions.
|
||||
'cache-control': CACHE_CONTROL_NO_CACHE,
|
||||
},
|
||||
});
|
||||
// The Cache-Control header instructs the browser to not cache the index.html since it might
|
||||
// link to static assets from recently deployed versions.
|
||||
res.setHeader('Cache-Control', CACHE_CONTROL_NO_CACHE);
|
||||
res.setHeader('Content-Type', 'text/html;charset=utf-8');
|
||||
res.send(indexHtmlContent);
|
||||
});
|
||||
|
||||
return router;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { BackstageIdentityResponse as BackstageIdentityResponse_2 } from '@backstage/plugin-auth-node';
|
||||
import { BackstageSignInResult as BackstageSignInResult_2 } from '@backstage/plugin-auth-node';
|
||||
import { Config } from '@backstage/config';
|
||||
@@ -152,6 +153,7 @@ export type CookieConfigurer = (ctx: {
|
||||
|
||||
// @public
|
||||
export function createCookieAuthRefreshMiddleware(options: {
|
||||
auth: AuthService;
|
||||
httpAuth: HttpAuthService;
|
||||
}): Router;
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ describe('createCookieAuthRefreshMiddleware', () => {
|
||||
let app: express.Express;
|
||||
|
||||
beforeAll(async () => {
|
||||
const auth = mockServices.auth();
|
||||
const httpAuth = mockServices.httpAuth();
|
||||
const router = createCookieAuthRefreshMiddleware({ httpAuth });
|
||||
const router = createCookieAuthRefreshMiddleware({ auth, httpAuth });
|
||||
app = express().use(router);
|
||||
});
|
||||
|
||||
@@ -33,7 +34,7 @@ describe('createCookieAuthRefreshMiddleware', () => {
|
||||
});
|
||||
|
||||
it('should issue the user cookie', async () => {
|
||||
const response = await request(app).get('/.backstage/v1-cookie');
|
||||
const response = await request(app).get('/.backstage/auth/v1/cookie');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['set-cookie'][0]).toMatch(
|
||||
`backstage-auth=${mockCredentials.limitedUser.token()}`,
|
||||
@@ -41,7 +42,7 @@ describe('createCookieAuthRefreshMiddleware', () => {
|
||||
});
|
||||
|
||||
it('should remove the user cookie', async () => {
|
||||
const response = await request(app).delete('/.backstage/v1-cookie');
|
||||
const response = await request(app).delete('/.backstage/auth/v1/cookie');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.header['set-cookie'][0]).toMatch('backstage-auth=');
|
||||
});
|
||||
|
||||
@@ -14,19 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import { AuthService, HttpAuthService } from '@backstage/backend-plugin-api';
|
||||
import { Router } from 'express';
|
||||
|
||||
const WELL_KNOWN_COOKIE_PATH_V1 = '/.backstage/v1-cookie';
|
||||
const WELL_KNOWN_COOKIE_PATH_V1 = '/.backstage/auth/v1/cookie';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Creates a middleware that can be used to refresh the cookie for the user.
|
||||
*/
|
||||
export function createCookieAuthRefreshMiddleware(options: {
|
||||
auth: AuthService;
|
||||
httpAuth: HttpAuthService;
|
||||
}) {
|
||||
const { httpAuth } = options;
|
||||
const { auth, httpAuth } = options;
|
||||
const router = Router();
|
||||
|
||||
// Endpoint that sets the cookie for the user
|
||||
@@ -37,7 +38,8 @@ export function createCookieAuthRefreshMiddleware(options: {
|
||||
|
||||
// Endpoint that removes the cookie for the user
|
||||
router.delete(WELL_KNOWN_COOKIE_PATH_V1, async (_, res) => {
|
||||
httpAuth.removeUserCookie(res);
|
||||
const credentials = await auth.getNoneCredentials();
|
||||
await httpAuth.issueUserCookie(res, { credentials });
|
||||
res.send(200);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
// @public
|
||||
export function AppMode(props: { children: ReactNode }): JSX.Element;
|
||||
|
||||
// @public
|
||||
export function CookieAuthRefreshProvider(
|
||||
props: CookieAuthRefreshProviderProps,
|
||||
@@ -19,12 +22,7 @@ export type CookieAuthRefreshProviderProps = {
|
||||
};
|
||||
|
||||
// @public
|
||||
export function ExperimentalAppProtection(props: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element;
|
||||
|
||||
// @public
|
||||
export function RedirectToRoot(): React_2.JSX.Element | null;
|
||||
export function CookieAuthRootRedirect(): React_2.JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export function useCookieAuthRefresh(options: {
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@react-hookz/web": "^24.0.0",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { render, screen, waitFor } from '@testing-library/react';
|
||||
import { AppMode } from './AppMode';
|
||||
import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api';
|
||||
import { componentsApiRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const now = 1710316886171;
|
||||
const tenMinutesInMilliseconds = 10 * 60 * 1000;
|
||||
const tenMinutesFromNowInMilliseconds = now + tenMinutesInMilliseconds;
|
||||
const expiresAt = new Date(tenMinutesFromNowInMilliseconds).toISOString();
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => {
|
||||
return {
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
useApp: jest.fn().mockReturnValue({
|
||||
getComponents: () => ({ Progress: () => <div data-testid="progress" /> }),
|
||||
}),
|
||||
useApi: jest.fn(ref => {
|
||||
if (ref === discoveryApiRef) {
|
||||
return {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'),
|
||||
};
|
||||
}
|
||||
|
||||
if (ref === fetchApiRef) {
|
||||
return {
|
||||
fetch: jest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue({ expiresAt }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (ref === componentsApiRef) {
|
||||
return {
|
||||
getComponent: jest
|
||||
.fn()
|
||||
.mockReturnValue(() => <div data-testid="progress" />),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Attempted to use an unmocked API reference: ${ref}`);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('AppMode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useFakeTimers({ now });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should render the children when app mode is undefined', async () => {
|
||||
render(<AppMode>Test content</AppMode>);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the children the app mode is public', async () => {
|
||||
render(
|
||||
<>
|
||||
<meta name="backstage-app-mode" content="public" />
|
||||
<AppMode>Test content</AppMode>
|
||||
</>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the children wrapped in the CookieAuthRefreshProvider', async () => {
|
||||
render(
|
||||
<>
|
||||
<meta name="backstage-app-mode" content="protected" />
|
||||
<AppMode>Test content</AppMode>
|
||||
</>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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, { ReactNode, useEffect, useState } from 'react';
|
||||
import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
|
||||
import { CompatAppProgress } from '../CompatAppProgress';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A provider that will protect the app when running in protected experimental mode.
|
||||
*/
|
||||
export function AppMode(props: { children: ReactNode }): JSX.Element {
|
||||
const { children } = props;
|
||||
|
||||
const [appMode, setAppMode] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('meta[name="backstage-app-mode"]');
|
||||
setAppMode(element?.getAttribute('content') ?? 'public');
|
||||
}, [setAppMode]);
|
||||
|
||||
if (!appMode) {
|
||||
return <CompatAppProgress />;
|
||||
}
|
||||
|
||||
if (appMode === 'protected') {
|
||||
return (
|
||||
<CookieAuthRefreshProvider pluginId="app">
|
||||
{children}
|
||||
</CookieAuthRefreshProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { RedirectToRoot } from './RedirectToRoot';
|
||||
export { AppMode } from './AppMode';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { useApp } from '@backstage/core-plugin-api';
|
||||
import { useVersionedContext } from '@backstage/version-bridge';
|
||||
|
||||
import {
|
||||
coreComponentRefs,
|
||||
useComponentRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
function LegacyAppProgress() {
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
function NewAppProgress() {
|
||||
const Progress = useComponentRef(coreComponentRefs.progress);
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
export function CompatAppProgress() {
|
||||
const isInNewApp = !useVersionedContext<{ 1: unknown }>('app-context');
|
||||
return isInNewApp ? <NewAppProgress /> : <LegacyAppProgress />;
|
||||
}
|
||||
+1
-1
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ExperimentalAppProtection } from './ExperimentalAppProtection';
|
||||
export { CompatAppProgress } from './CompatAppProgress';
|
||||
+1
-1
@@ -119,7 +119,7 @@ describe('CookieAuthRefreshProvider', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/.backstage/v1-cookie',
|
||||
'http://localhost:7000/techdocs/api/.backstage/auth/v1/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
+2
-4
@@ -16,9 +16,9 @@
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { useCookieAuthRefresh } from '../../hooks';
|
||||
import { CompatAppProgress } from '../CompatAppProgress/CompatAppProgress';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -41,13 +41,11 @@ export function CookieAuthRefreshProvider(
|
||||
props: CookieAuthRefreshProviderProps,
|
||||
): JSX.Element {
|
||||
const { children, ...options } = props;
|
||||
const app = useApp();
|
||||
const { Progress } = app.getComponents();
|
||||
|
||||
const result = useCookieAuthRefresh(options);
|
||||
|
||||
if (result.status === 'loading') {
|
||||
return <Progress />;
|
||||
return <CompatAppProgress />;
|
||||
}
|
||||
|
||||
if (result.status === 'error') {
|
||||
|
||||
+4
-4
@@ -18,9 +18,9 @@ import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
import { identityApiRef } from '@backstage/core-plugin-api';
|
||||
import { RedirectToRoot } from './RedirectToRoot';
|
||||
import { CookieAuthRootRedirect } from './CookieAuthRootRedirect';
|
||||
|
||||
describe('RedirectToRoot', () => {
|
||||
describe('CookieAuthRootRedirect', () => {
|
||||
const identityApiMock = { getCredentials: jest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -32,7 +32,7 @@ describe('RedirectToRoot', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[identityApiRef, identityApiMock]]}>
|
||||
<RedirectToRoot />
|
||||
<CookieAuthRootRedirect />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('RedirectToRoot', () => {
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[identityApiRef, identityApiMock]]}>
|
||||
<RedirectToRoot />
|
||||
<CookieAuthRootRedirect />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
* @public
|
||||
* A component that redirects to the root of the app after a successful sign-in.
|
||||
*/
|
||||
export function RedirectToRoot() {
|
||||
export function CookieAuthRootRedirect() {
|
||||
const identityApi = useApi(identityApiRef);
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { CookieAuthRootRedirect } from './CookieAuthRootRedirect';
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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 { screen, waitFor } from '@testing-library/react';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import {
|
||||
MockConfigApi,
|
||||
TestApiProvider,
|
||||
renderInTestApp,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/test-utils';
|
||||
import { ExperimentalAppProtection } from './ExperimentalAppProtection';
|
||||
import {
|
||||
configApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
describe('ExperimentalAppProtection', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
const configApiMock = new MockConfigApi({
|
||||
backend: {
|
||||
baseUrl: 'http://localhost:7000',
|
||||
},
|
||||
});
|
||||
|
||||
const fetchApiMock = {
|
||||
fetch: jest.fn(),
|
||||
};
|
||||
|
||||
const discoveryApiMock = {
|
||||
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000/app'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render the progress component while loading', async () => {
|
||||
fetchApiMock.fetch.mockReturnValueOnce(new Promise(() => {}));
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[fetchApiRef, fetchApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(screen.getByTestId('progress')).toBeVisible();
|
||||
expect(screen.queryByText('Test Content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children even when there is an error', async () => {
|
||||
const error = new Error('Failed to fetch');
|
||||
fetchApiMock.fetch.mockRejectedValueOnce(error);
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[fetchApiRef, fetchApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Failed to fetch')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children even when the public index is not available', async () => {
|
||||
fetchApiMock.fetch.mockResolvedValueOnce({ ok: false });
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[fetchApiRef, fetchApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children also when the public index is available', async () => {
|
||||
fetchApiMock.fetch.mockResolvedValueOnce({ ok: true });
|
||||
await renderInTestApp(
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
expect(screen.queryByTestId('progress')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the children wrapped in the CookieAuthRefreshProvider', async () => {
|
||||
worker.use(
|
||||
rest.get('http://localhost:7000/public/index.html', (_, res, ctx) => {
|
||||
return res(ctx.status(200));
|
||||
}),
|
||||
rest.get(
|
||||
'http://localhost:7000/app/.backstage/v1-cookie',
|
||||
(_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({ expiresAt: Date.now() + 10 * 60 * 1000 }),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[configApiRef, configApiMock],
|
||||
[discoveryApiRef, discoveryApiMock],
|
||||
]}
|
||||
>
|
||||
<ExperimentalAppProtection>Test content</ExperimentalAppProtection>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* 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, { ReactNode } from 'react';
|
||||
import {
|
||||
configApiRef,
|
||||
fetchApiRef,
|
||||
useApi,
|
||||
useApp,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { CookieAuthRefreshProvider } from '@backstage/plugin-auth-react';
|
||||
import { useAsync, useMountEffect } from '@react-hookz/web';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* A provider that will protect the app when running in public experimental mode.
|
||||
*/
|
||||
export function ExperimentalAppProtection(props: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
const { children } = props;
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const Components = useApp().getComponents();
|
||||
|
||||
const [state, actions] = useAsync(async () => {
|
||||
const baseUrl = configApi.getString('backend.baseUrl');
|
||||
const response = await fetchApi.fetch(`${baseUrl}/public/index.html`);
|
||||
return response.ok;
|
||||
});
|
||||
|
||||
useMountEffect(actions.execute);
|
||||
|
||||
if (state.status === 'not-executed' || state.status === 'loading') {
|
||||
return <Components.Progress />;
|
||||
}
|
||||
|
||||
// Request failed, or the public index is not available
|
||||
if (state.status === 'error' || !state.result) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// The public index is available
|
||||
// That means the app is running in public experimental mode
|
||||
return (
|
||||
<CookieAuthRefreshProvider pluginId="app">
|
||||
{children}
|
||||
</CookieAuthRefreshProvider>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,6 @@
|
||||
// The index file in ./components/ is typically responsible for selecting
|
||||
// which components are public API and should be exported from the package.
|
||||
|
||||
export * from './RedirectToRoot';
|
||||
export * from './CookieAuthRootRedirect';
|
||||
export * from './CookieAuthRefreshProvider';
|
||||
export * from './ExperimentalAppProtection';
|
||||
export * from './AppMode';
|
||||
|
||||
@@ -236,7 +236,7 @@ describe('useCookieAuthRefresh', () => {
|
||||
|
||||
await waitFor(() =>
|
||||
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
|
||||
'http://localhost:7000/techdocs/api/.backstage/v1-cookie',
|
||||
'http://localhost:7000/techdocs/api/.backstage/auth/v1/cookie',
|
||||
{ credentials: 'include' },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,13 +31,13 @@ import { ResponseError } from '@backstage/errors';
|
||||
export function useCookieAuthRefresh(options: {
|
||||
// The plugin id used for discovering the API origin
|
||||
pluginId: string;
|
||||
// The path used for calling the refresh cookie endpoint, default to '/.backstage/v1-cookie'
|
||||
// The path used for calling the refresh cookie endpoint, default to '/.backstage/auth/v1/cookie'
|
||||
path?: string;
|
||||
}):
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; error: Error; retry: () => void }
|
||||
| { status: 'success'; data: { expiresAt: string } } {
|
||||
const { pluginId, path = '/.backstage/v1-cookie' } = options ?? {};
|
||||
const { pluginId, path = '/.backstage/auth/v1/cookie' } = options ?? {};
|
||||
const fetchApi = useApi(fetchApiRef);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
|
||||
|
||||
@@ -3834,6 +3834,7 @@ __metadata:
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/plugin-auth-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
@@ -4193,6 +4194,7 @@ __metadata:
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/plugin-auth-react": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
@@ -5112,7 +5114,9 @@ __metadata:
|
||||
"@backstage/core-components": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@material-ui/core": ^4.9.13
|
||||
"@react-hookz/web": ^24.0.0
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
@@ -25863,7 +25867,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domhandler@npm:^5.0.1, domhandler@npm:^5.0.2":
|
||||
"domhandler@npm:^5.0.1, domhandler@npm:^5.0.2, domhandler@npm:^5.0.3":
|
||||
version: 5.0.3
|
||||
resolution: "domhandler@npm:5.0.3"
|
||||
dependencies:
|
||||
@@ -26269,7 +26273,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"entities@npm:^4.2.0, entities@npm:^4.3.0, entities@npm:^4.4.0":
|
||||
"entities@npm:^4.2.0, entities@npm:^4.4.0":
|
||||
version: 4.4.0
|
||||
resolution: "entities@npm:4.4.0"
|
||||
checksum: 84d250329f4b56b40fa93ed067b194db21e8815e4eb9b59f43a086f0ecd342814f6bc483de8a77da5d64e0f626033192b1b4f1792232a7ea6b970ebe0f3187c2
|
||||
@@ -30033,14 +30037,14 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"htmlparser2@npm:^8.0.0":
|
||||
version: 8.0.1
|
||||
resolution: "htmlparser2@npm:8.0.1"
|
||||
version: 8.0.2
|
||||
resolution: "htmlparser2@npm:8.0.2"
|
||||
dependencies:
|
||||
domelementtype: ^2.3.0
|
||||
domhandler: ^5.0.2
|
||||
domhandler: ^5.0.3
|
||||
domutils: ^3.0.1
|
||||
entities: ^4.3.0
|
||||
checksum: 06d5c71e8313597722bc429ae2a7a8333d77bd3ab07ccb916628384b37332027b047f8619448d8f4a3312b6609c6ea3302a4e77435d859e9e686999e6699ca39
|
||||
entities: ^4.4.0
|
||||
checksum: 29167a0f9282f181da8a6d0311b76820c8a59bc9e3c87009e21968264c2987d2723d6fde5a964d4b7b6cba663fca96ffb373c06d8223a85f52a6089ced942700
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user