frontend-defaults: move public sign-in app to alpha app module

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-08-01 10:15:21 +02:00
parent 22de964c62
commit e8828e82b2
9 changed files with 102 additions and 74 deletions
+19 -3
View File
@@ -11,9 +11,7 @@
]
},
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
"access": "public"
},
"repository": {
"type": "git",
@@ -22,6 +20,21 @@
},
"license": "Apache-2.0",
"sideEffects": false,
"exports": {
".": "./src/index.ts",
"./alpha": "./src/alpha/index.ts",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
"src/alpha/index.ts"
],
"package.json": [
"package.json"
]
}
},
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
@@ -47,12 +60,15 @@
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
"@react-hookz/web": "^24.0.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/frontend-defaults": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
+12
View File
@@ -0,0 +1,12 @@
## API Report File for "@backstage/plugin-app"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { FrontendModule } from '@backstage/frontend-plugin-api';
// @alpha
export const appModulePublicSignIn: FrontendModule;
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,124 @@
/*
* 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 {
SignInPageBlueprint,
createFrontendModule,
} from '@backstage/frontend-plugin-api';
import { render, screen, waitFor } from '@testing-library/react';
import { useEffect } from 'react';
import { appModulePublicSignIn } from './appModulePublicSignIn';
import { mockApis } from '@backstage/test-utils';
import { createApp } from '@backstage/frontend-defaults';
describe('appModulePublicSignIn', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should render a sign-in page', async () => {
const app = createApp({
configLoader: async () => ({ config: mockApis.config() }),
features: [
appModulePublicSignIn,
createFrontendModule({
pluginId: 'app',
extensions: [
SignInPageBlueprint.make({
params: {
loader: async () => () => <div>Sign in page</div>,
},
}),
],
}),
],
});
render(app.createRoot());
await expect(
screen.findByText('Sign in page'),
).resolves.toBeInTheDocument();
});
it('should render the form redirect on sign-in', async () => {
const submitSpy = jest
.spyOn(HTMLFormElement.prototype, 'submit')
.mockReturnValue();
const app = createApp({
configLoader: async () => ({ config: mockApis.config() }),
features: [
appModulePublicSignIn,
createFrontendModule({
pluginId: 'app',
extensions: [
SignInPageBlueprint.make({
params: {
loader:
async () =>
({ onSignInSuccess }) => {
useEffect(() => {
onSignInSuccess(
mockApis.identity({ token: 'mock-token' }),
);
}, [onSignInSuccess]);
return <div />;
},
},
}),
],
}),
],
});
const { baseElement } = render(app.createRoot());
await waitFor(() => {
expect(submitSpy).toHaveBeenCalled();
});
expect(baseElement).toMatchInlineSnapshot(`
<body
data-theme-mode="light"
data-theme-name="backstage"
>
<div>
<form
action="http://localhost/"
method="POST"
style="visibility: hidden;"
>
<input
name="type"
type="hidden"
value="sign-in"
/>
<input
name="token"
type="hidden"
value="mock-token"
/>
<input
type="submit"
value="Continue"
/>
</form>
</div>
</body>
`);
});
});
@@ -0,0 +1,126 @@
/*
* Copyright 2025 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 appPlugin from '@backstage/plugin-app';
import { useAsync, useMountEffect } from '@react-hookz/web';
import {
coreExtensionData,
createFrontendModule,
identityApiRef,
useApi,
} from '@backstage/frontend-plugin-api';
// This is a copy of the CookieAuthRedirect component from the auth-react
// plugin, to avoid a dependency on that package. Long-term we want this to be
// the only implementation and remove the one in auth-react once the old frontend system is gone.
// TODO(Rugvip): Should this be part of the app plugin instead? since it owns the backend part of it.
/** @internal */
export function InternalCookieAuthRedirect() {
const identityApi = useApi(identityApiRef);
const [state, actions] = useAsync(async () => {
const { token } = await identityApi.getCredentials();
if (!token) {
throw new Error('Expected Backstage token in sign-in response');
}
return token;
});
useMountEffect(actions.execute);
if (state.status === 'error' && state.error) {
return <>An error occurred: {state.error.message}</>;
}
if (state.status === 'success' && state.result) {
return (
<form
ref={form => form?.submit()}
action={window.location.href}
method="POST"
style={{ visibility: 'hidden' }}
>
<input type="hidden" name="type" value="sign-in" />
<input type="hidden" name="token" value={state.result} />
<input type="submit" value="Continue" />
</form>
);
}
return null;
}
/**
* This module is intended for use in public sign-in page apps, in the
* `index-public-experimental.tsx` file.
*
* @remarks
*
* This module is used to enable the public sign-in flow where the build output
* is split into one small publicly accessible app, and the full app protected
* by auth.
*
* This module overrides the `app/layout` extension, which means that most
* extension typically installed in an app will be ignored. However, you can
* still for example install API and root element extensions.
*
* A typical setup of this app will only install a custom sign-in page.
*
* @example
*
*#### In `index-public-experimental.tsx`
*
*```ts
*import { createApp } from '@backstage/frontend-defaults
*import { appModulePublicSignIn } from '@backstage/plugin-app/alpha';
*import { appModuleSignInPage } from './appModuleSignInPage';
*
*const app = createApp({
* features: [appModuleSignInPage, appModulePublicSignIn],
*});
*```
*
*#### In `appModuleSignInPage.tsx`
*
*```tsx
*import { createFrontendModule, SignInPageBlueprint } from '@backstage/frontend-plugin-api';
*
*export const appModuleSignInPage = createFrontendModule({
* pluginId: 'app',
* extensions: [
* SignInPageBlueprint.make({
* params: {
* ...
* }
* }),
* ],
*})
*```
*
* @alpha
*/
export const appModulePublicSignIn = createFrontendModule({
pluginId: 'app',
extensions: [
appPlugin.getExtension('app/layout').override({
factory: () => [
coreExtensionData.reactElement(<InternalCookieAuthRedirect />),
],
}),
],
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2025 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 { appModulePublicSignIn } from './appModulePublicSignIn';