diff --git a/plugins/auth/package.json b/plugins/auth/package.json
index 6dadc78423..54e87cb7b1 100644
--- a/plugins/auth/package.json
+++ b/plugins/auth/package.json
@@ -47,9 +47,7 @@
"test": "backstage-cli package test"
},
"dependencies": {
- "@backstage/core-compat-api": "workspace:^",
"@backstage/core-components": "workspace:^",
- "@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
@@ -60,12 +58,11 @@
},
"devDependencies": {
"@backstage/cli": "workspace:^",
- "@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/frontend-defaults": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
- "@testing-library/react": "^14.0.0",
+ "@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/react": "^18.0.0",
"msw": "^1.0.0",
diff --git a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx
index 3234450a1d..0f071025a2 100644
--- a/plugins/auth/src/components/ConsentPage/ConsentPage.tsx
+++ b/plugins/auth/src/components/ConsentPage/ConsentPage.tsx
@@ -13,8 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
+
import {
Box,
Button,
@@ -38,13 +38,7 @@ import {
EmptyState,
ResponseErrorPanel,
} from '@backstage/core-components';
-import {
- alertApiRef,
- useApi,
- fetchApiRef,
- discoveryApiRef,
-} from '@backstage/core-plugin-api';
-import { isError } from '@backstage/errors';
+import { useConsentSession } from './useConsentSession';
const useStyles = makeStyles(theme => ({
authCard: {
@@ -89,260 +83,164 @@ const useStyles = makeStyles(theme => ({
},
}));
-interface Session {
- id: string;
- clientName?: string;
- clientId: string;
- redirectUri: string;
- scopes?: string[];
- responseType?: string;
- state?: string;
- nonce?: string;
- codeChallenge?: string;
- codeChallengeMethod?: string;
- expiresAt?: string;
-}
+const ConsentPageLayout = ({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) => (
+
+
+ {children}
+
+);
export const ConsentPage = () => {
const classes = useStyles();
const { sessionId } = useParams<{ sessionId: string }>();
- const alertApi = useApi(alertApiRef);
- const fetchApi = useApi(fetchApiRef);
- const discoveryApi = useApi(discoveryApiRef);
- const [session, setSession] = useState(null);
- const [loading, setLoading] = useState(true);
- const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState(null);
- const [completed, setCompleted] = useState<
- | {
- action: 'approve' | 'reject';
- }
- | undefined
- >(undefined);
-
- useEffect(() => {
- const fetchSession = async () => {
- if (!sessionId) return;
-
- try {
- const baseUrl = await discoveryApi.getBaseUrl('auth');
- const response = await fetchApi.fetch(
- `${baseUrl}/v1/sessions/${sessionId}`,
- );
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- const data = await response.json();
- setSession(data);
- } catch (err) {
- setError(isError(err) ? err.message : 'Failed to load consent request');
- } finally {
- setLoading(false);
- }
- };
-
- fetchSession();
- }, [sessionId, discoveryApi, fetchApi]);
-
- const handleAction = useCallback(
- async (action: 'approve' | 'reject') => {
- if (!session) return;
-
- setSubmitting(true);
- try {
- const baseUrl = await discoveryApi.getBaseUrl('auth');
- const response = await fetchApi.fetch(
- `${baseUrl}/v1/sessions/${session.id}/${action}`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- },
- );
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- const result = await response.json();
-
- setCompleted({
- action,
- });
-
- if (result.redirectUrl) {
- window.location.href = result.redirectUrl;
- }
- } catch (err) {
- alertApi.post({
- message: isError(err) ? err.message : `Failed to ${action} consent`,
- severity: 'error',
- });
- } finally {
- setSubmitting(false);
- }
- },
- [session, discoveryApi, fetchApi, alertApi],
- );
+ const { state, handleAction } = useConsentSession({ sessionId });
if (!sessionId) {
return (
-
-
-
-
-
-
+
+
+
);
}
- if (loading) {
+ if (state.status === 'loading') {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
- if (error ?? !session) {
+ if (state.status === 'error') {
return (
-
-
-
-
-
-
+
+
+
);
}
- if (completed) {
+ if (state.status === 'completed') {
return (
-
-
-
-
-
-
- {completed.action === 'approve' ? (
-
- ) : (
-
- )}
-
- {completed.action === 'approve'
- ? 'Authorization Approved'
- : 'Authorization Denied'}
-
-
- {completed.action === 'approve'
- ? 'You have successfully authorized the application to access your Backstage account.'
- : 'You have denied the application access to your Backstage account.'}
-
-
- Redirecting to the application...
-
-
-
-
-
-
- );
- }
-
- const appName = session.clientName ?? session.clientId;
-
- return (
-
-
-
+
-
-
-
- {appName}
-
- wants to access your Backstage account
-
-
-
-
-
-
- }
- className={classes.securityWarning}
- >
-
- Security Notice: By authorizing this
- application, you are granting it access to your Backstage
- account. The application will receive an access token that
- allows it to act on your behalf.
+
+ {state.action === 'approve' ? (
+
+ ) : (
+
+ )}
+
+ {state.action === 'approve'
+ ? 'Authorization Approved'
+ : 'Authorization Denied'}
+
+
+ {state.action === 'approve'
+ ? 'You have successfully authorized the application to access your Backstage account.'
+ : 'You have denied the application access to your Backstage account.'}
-
-
- Callback URL:
-
- {session.redirectUri}
-
-
-
-
- Make sure you trust this application and recognize the callback
- URL above. Only authorize applications you trust.
+ Redirecting to the application...
-
-
-
-
-
-
-
+
+ );
+ }
+
+ const session = state.session;
+ const isSubmitting = state.status === 'submitting';
+ const appName = session.clientName ?? session.clientId;
+
+ return (
+
+
+
+
+
+
+ {appName}
+
+ wants to access your Backstage account
+
+
+
+
+
+
+ }
+ className={classes.securityWarning}
+ >
+
+ Security Notice: By authorizing this application,
+ you are granting it access to your Backstage account. The
+ application will receive an access token that allows it to act on
+ your behalf.
+
+
+
+ Callback URL:
+
+ {session.redirectUri}
+
+
+
+
+
+ Make sure you trust this application and recognize the callback
+ URL above. Only authorize applications you trust.
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/plugins/auth/src/components/ConsentPage/useConsentSession.ts b/plugins/auth/src/components/ConsentPage/useConsentSession.ts
new file mode 100644
index 0000000000..a19faae6fe
--- /dev/null
+++ b/plugins/auth/src/components/ConsentPage/useConsentSession.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 {
+ useApi,
+ alertApiRef,
+ fetchApiRef,
+ discoveryApiRef,
+} from '@backstage/frontend-plugin-api';
+import { useCallback } from 'react';
+import useAsync from 'react-use/esm/useAsync';
+import useAsyncFn from 'react-use/esm/useAsyncFn';
+import { isError } from '@backstage/errors';
+
+interface Session {
+ id: string;
+ clientName?: string;
+ clientId: string;
+ redirectUri: string;
+ scopes?: string[];
+ responseType?: string;
+ state?: string;
+ nonce?: string;
+ codeChallenge?: string;
+ codeChallengeMethod?: string;
+ expiresAt?: string;
+}
+
+type ConsentState =
+ | { status: 'loading' }
+ | { status: 'error'; error: string }
+ | { status: 'loaded'; session: Session }
+ | { status: 'submitting'; session: Session; action: 'approve' | 'reject' }
+ | { status: 'completed'; action: 'approve' | 'reject' };
+
+export const useConsentSession = (opts: { sessionId?: string }) => {
+ const alertApi = useApi(alertApiRef);
+ const fetchApi = useApi(fetchApiRef);
+ const discoveryApi = useApi(discoveryApiRef);
+ const { sessionId } = opts;
+
+ const sessionState = useAsync(async () => {
+ if (!sessionId) {
+ throw new Error('Session ID is missing');
+ }
+
+ const baseUrl = await discoveryApi.getBaseUrl('auth');
+ const response = await fetchApi.fetch(
+ `${baseUrl}/v1/sessions/${sessionId}`,
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ return (await response.json()) as Session;
+ }, [sessionId]);
+
+ const [actionState, handleActionInternal] = useAsyncFn(
+ async (action: 'approve' | 'reject', session: Session) => {
+ const baseUrl = await discoveryApi.getBaseUrl('auth');
+ const response = await fetchApi.fetch(
+ `${baseUrl}/v1/sessions/${session.id}/${action}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ if (result.redirectUrl) {
+ window.location.href = result.redirectUrl;
+ }
+
+ return { action, redirectUrl: result.redirectUrl };
+ },
+ [discoveryApi, fetchApi],
+ );
+
+ const getConsentState = (): ConsentState => {
+ if (actionState.value) {
+ return { status: 'completed', action: actionState.value.action };
+ }
+ if (actionState.loading && sessionState.value) {
+ return {
+ status: 'submitting',
+ session: sessionState.value,
+ action: 'approve', // This will be set properly when called
+ };
+ }
+ if (sessionState.error) {
+ return {
+ status: 'error',
+ error: isError(sessionState.error)
+ ? sessionState.error.message
+ : 'Failed to load consent request',
+ };
+ }
+ if (sessionState.value) {
+ return { status: 'loaded', session: sessionState.value };
+ }
+ return { status: 'loading' };
+ };
+
+ const state = getConsentState();
+ return {
+ state,
+ handleAction: useCallback(
+ async (action: 'approve' | 'reject') => {
+ if (state.status !== 'loaded') return;
+
+ try {
+ await handleActionInternal(action, state.session);
+ } catch (err) {
+ alertApi.post({
+ message: isError(err) ? err.message : `Failed to ${action} consent`,
+ severity: 'error',
+ });
+ }
+ },
+ [state, handleActionInternal, alertApi],
+ ),
+ };
+};
diff --git a/plugins/auth/src/plugin.tsx b/plugins/auth/src/plugin.tsx
index 9b52960f4c..4ed5d57223 100644
--- a/plugins/auth/src/plugin.tsx
+++ b/plugins/auth/src/plugin.tsx
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { compatWrapper } from '@backstage/core-compat-api';
import {
createFrontendPlugin,
PageBlueprint,
@@ -24,8 +23,7 @@ export const AuthPage = PageBlueprint.make({
params: {
path: '/oauth2',
routeRef: rootRouteRef,
- loader: () =>
- import('./components/Router').then(m => compatWrapper()),
+ loader: () => import('./components/Router').then(m => ),
},
});
diff --git a/yarn.lock b/yarn.lock
index 4d15a81ad2..9d3c3af1f7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4269,10 +4269,7 @@ __metadata:
resolution: "@backstage/plugin-auth@workspace:plugins/auth"
dependencies:
"@backstage/cli": "workspace:^"
- "@backstage/core-app-api": "workspace:^"
- "@backstage/core-compat-api": "workspace:^"
"@backstage/core-components": "workspace:^"
- "@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/frontend-defaults": "workspace:^"
@@ -4283,18 +4280,22 @@ __metadata:
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
"@testing-library/jest-dom": "npm:^6.0.0"
- "@testing-library/react": "npm:^14.0.0"
+ "@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.0.0"
"@types/react": "npm:^18.0.0"
msw: "npm:^1.0.0"
react: "npm:^18.0.2"
react-dom: "npm:^18.0.2"
+ react-router-dom: "npm:^6.3.0"
react-use: "npm:^17.2.4"
peerDependencies:
"@types/react": ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
react-router-dom: ^6.3.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
languageName: unknown
linkType: soft
@@ -19521,22 +19522,6 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/dom@npm:^9.0.0":
- version: 9.3.4
- resolution: "@testing-library/dom@npm:9.3.4"
- dependencies:
- "@babel/code-frame": "npm:^7.10.4"
- "@babel/runtime": "npm:^7.12.5"
- "@types/aria-query": "npm:^5.0.1"
- aria-query: "npm:5.1.3"
- chalk: "npm:^4.1.0"
- dom-accessibility-api: "npm:^0.5.9"
- lz-string: "npm:^1.5.0"
- pretty-format: "npm:^27.0.2"
- checksum: 10/510da752ea76f4a10a0a4e3a77917b0302cf03effe576cd3534cab7e796533ee2b0e9fb6fb11b911a1ebd7c70a0bb6f235bf4f816c9b82b95b8fe0cddfd10975
- languageName: node
- linkType: hard
-
"@testing-library/jest-dom@npm:6.5.0":
version: 6.5.0
resolution: "@testing-library/jest-dom@npm:6.5.0"
@@ -19589,20 +19574,6 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/react@npm:^14.0.0":
- version: 14.3.1
- resolution: "@testing-library/react@npm:14.3.1"
- dependencies:
- "@babel/runtime": "npm:^7.12.5"
- "@testing-library/dom": "npm:^9.0.0"
- "@types/react-dom": "npm:^18.0.0"
- peerDependencies:
- react: ^18.0.0
- react-dom: ^18.0.0
- checksum: 10/83359dcdf9eaf067839f34604e1a181cbc14fc09f3a07672403700fcc6a900c4b8054ad1114fc24b4b9f89d84e2a09e1b7c9afce2306b1d4b4c9e30eb1cb12de
- languageName: node
- linkType: hard
-
"@testing-library/react@npm:^16.0.0":
version: 16.3.0
resolution: "@testing-library/react@npm:16.3.0"
@@ -24005,15 +23976,6 @@ __metadata:
languageName: node
linkType: hard
-"aria-query@npm:5.1.3":
- version: 5.1.3
- resolution: "aria-query@npm:5.1.3"
- dependencies:
- deep-equal: "npm:^2.0.5"
- checksum: 10/e5da608a7c4954bfece2d879342b6c218b6b207e2d9e5af270b5e38ef8418f02d122afdc948b68e32649b849a38377785252059090d66fa8081da95d1609c0d2
- languageName: node
- linkType: hard
-
"aria-query@npm:5.3.0":
version: 5.3.0
resolution: "aria-query@npm:5.3.0"
@@ -24030,7 +23992,7 @@ __metadata:
languageName: node
linkType: hard
-"array-buffer-byte-length@npm:^1.0.0, array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2":
+"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2":
version: 1.0.2
resolution: "array-buffer-byte-length@npm:1.0.2"
dependencies:
@@ -27790,32 +27752,6 @@ __metadata:
languageName: node
linkType: hard
-"deep-equal@npm:^2.0.5":
- version: 2.2.3
- resolution: "deep-equal@npm:2.2.3"
- dependencies:
- array-buffer-byte-length: "npm:^1.0.0"
- call-bind: "npm:^1.0.5"
- es-get-iterator: "npm:^1.1.3"
- get-intrinsic: "npm:^1.2.2"
- is-arguments: "npm:^1.1.1"
- is-array-buffer: "npm:^3.0.2"
- is-date-object: "npm:^1.0.5"
- is-regex: "npm:^1.1.4"
- is-shared-array-buffer: "npm:^1.0.2"
- isarray: "npm:^2.0.5"
- object-is: "npm:^1.1.5"
- object-keys: "npm:^1.1.1"
- object.assign: "npm:^4.1.4"
- regexp.prototype.flags: "npm:^1.5.1"
- side-channel: "npm:^1.0.4"
- which-boxed-primitive: "npm:^1.0.2"
- which-collection: "npm:^1.0.1"
- which-typed-array: "npm:^1.1.13"
- checksum: 10/1ce49d0b71d0f14d8ef991a742665eccd488dfc9b3cada069d4d7a86291e591c92d2589c832811dea182b4015736b210acaaebce6184be356c1060d176f5a05f
- languageName: node
- linkType: hard
-
"deep-equal@npm:~1.0.1":
version: 1.0.1
resolution: "deep-equal@npm:1.0.1"
@@ -29024,7 +28960,7 @@ __metadata:
languageName: node
linkType: hard
-"es-get-iterator@npm:^1.0.2, es-get-iterator@npm:^1.1.3":
+"es-get-iterator@npm:^1.0.2":
version: 1.1.3
resolution: "es-get-iterator@npm:1.1.3"
dependencies:
@@ -31621,7 +31557,7 @@ __metadata:
languageName: node
linkType: hard
-"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0":
+"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0":
version: 1.3.0
resolution: "get-intrinsic@npm:1.3.0"
dependencies:
@@ -33554,7 +33490,7 @@ __metadata:
languageName: node
linkType: hard
-"is-array-buffer@npm:^3.0.2, is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5":
+"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5":
version: 3.0.5
resolution: "is-array-buffer@npm:3.0.5"
dependencies:
@@ -34027,7 +33963,7 @@ __metadata:
languageName: node
linkType: hard
-"is-regex@npm:^1.1.4, is-regex@npm:^1.2.1":
+"is-regex@npm:^1.2.1":
version: 1.2.1
resolution: "is-regex@npm:1.2.1"
dependencies:
@@ -34076,7 +34012,7 @@ __metadata:
languageName: node
linkType: hard
-"is-shared-array-buffer@npm:^1.0.2, is-shared-array-buffer@npm:^1.0.4":
+"is-shared-array-buffer@npm:^1.0.4":
version: 1.0.4
resolution: "is-shared-array-buffer@npm:1.0.4"
dependencies:
@@ -43815,7 +43751,7 @@ __metadata:
languageName: node
linkType: hard
-"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.3":
+"regexp.prototype.flags@npm:^1.5.3":
version: 1.5.4
resolution: "regexp.prototype.flags@npm:1.5.4"
dependencies:
@@ -45319,7 +45255,7 @@ __metadata:
languageName: node
linkType: hard
-"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6, side-channel@npm:^1.1.0":
+"side-channel@npm:^1.0.6, side-channel@npm:^1.1.0":
version: 1.1.0
resolution: "side-channel@npm:1.1.0"
dependencies:
@@ -49486,7 +49422,7 @@ __metadata:
languageName: node
linkType: hard
-"which-boxed-primitive@npm:^1.0.2, which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
+"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
version: 1.1.1
resolution: "which-boxed-primitive@npm:1.1.1"
dependencies:
@@ -49520,7 +49456,7 @@ __metadata:
languageName: node
linkType: hard
-"which-collection@npm:^1.0.1, which-collection@npm:^1.0.2":
+"which-collection@npm:^1.0.2":
version: 1.0.2
resolution: "which-collection@npm:1.0.2"
dependencies:
@@ -49542,7 +49478,7 @@ __metadata:
languageName: node
linkType: hard
-"which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2":
+"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2":
version: 1.1.19
resolution: "which-typed-array@npm:1.1.19"
dependencies: