From 81793abc0c563bb01de8ea45b3b33cf6c97f0336 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 12 Mar 2024 13:56:35 +0100 Subject: [PATCH] refactor: create a generic auth cookie provider Signed-off-by: Camila Belo --- plugins/auth-react/.eslintrc.js | 1 + plugins/auth-react/README.md | 5 ++ plugins/auth-react/catalog-info.yaml | 10 +++ plugins/auth-react/package.json | 44 ++++++++++ .../CookieAuthRefreshProvider.tsx | 51 ++++++++++++ .../CookieAuthRefreshProvider/index.ts | 17 ++++ plugins/auth-react/src/components/index.ts | 20 +++++ plugins/auth-react/src/hooks/index.ts | 20 +++++ .../src/hooks/useCookieAuthRefresh/index.ts | 17 ++++ .../useCookieAuthRefresh.tsx | 82 +++++++++++++++++++ plugins/auth-react/src/index.ts | 27 ++++++ plugins/auth-react/src/setupTests.ts | 16 ++++ plugins/auth-react/src/types.ts | 17 ++++ yarn.lock | 20 ++++- 14 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 plugins/auth-react/.eslintrc.js create mode 100644 plugins/auth-react/README.md create mode 100644 plugins/auth-react/catalog-info.yaml create mode 100644 plugins/auth-react/package.json create mode 100644 plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx create mode 100644 plugins/auth-react/src/components/CookieAuthRefreshProvider/index.ts create mode 100644 plugins/auth-react/src/components/index.ts create mode 100644 plugins/auth-react/src/hooks/index.ts create mode 100644 plugins/auth-react/src/hooks/useCookieAuthRefresh/index.ts create mode 100644 plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx create mode 100644 plugins/auth-react/src/index.ts create mode 100644 plugins/auth-react/src/setupTests.ts create mode 100644 plugins/auth-react/src/types.ts diff --git a/plugins/auth-react/.eslintrc.js b/plugins/auth-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-react/README.md b/plugins/auth-react/README.md new file mode 100644 index 0000000000..0137f95371 --- /dev/null +++ b/plugins/auth-react/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-auth-react + +Welcome to the web library package for the auth plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/auth-react/catalog-info.yaml b/plugins/auth-react/catalog-info.yaml new file mode 100644 index 0000000000..b75aa32c56 --- /dev/null +++ b/plugins/auth-react/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-react + title: '@backstage/plugin-auth-react' + description: Web library for the auth plugin +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json new file mode 100644 index 0000000000..089dafd7f6 --- /dev/null +++ b/plugins/auth-react/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-auth-react", + "description": "Web library for the auth plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@material-ui/core": "^4.9.13", + "@react-hookz/web": "^24.0.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^6.0.0", + "@testing-library/react": "^14.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx new file mode 100644 index 0000000000..941717f1e1 --- /dev/null +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.tsx @@ -0,0 +1,51 @@ +/* + * 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 { ErrorPanel } from '@backstage/core-components'; +import { ApiRef, useApp } from '@backstage/core-plugin-api'; +import { Button } from '@material-ui/core'; +import { useCookieAuthRefresh } from '../../hooks'; +import { AuthApi } from '../../types'; + +export function CookieAuthRefreshProvider({ + apiRef, + children, +}: { + apiRef: ApiRef; + children: ReactNode; +}) { + const app = useApp(); + const { Progress } = app.getComponents(); + + const { state, actions } = useCookieAuthRefresh({ apiRef }); + + if (state.status === 'error' && state.error) { + return ( + + + + ); + } + + if (state.status === 'loading') { + return ; + } + + return children; +} diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/index.ts b/plugins/auth-react/src/components/CookieAuthRefreshProvider/index.ts new file mode 100644 index 0000000000..968fa5c4b4 --- /dev/null +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/index.ts @@ -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 { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider'; diff --git a/plugins/auth-react/src/components/index.ts b/plugins/auth-react/src/components/index.ts new file mode 100644 index 0000000000..f561672096 --- /dev/null +++ b/plugins/auth-react/src/components/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +// The index file in ./components/ is typically responsible for selecting +// which components are public API and should be exported from the package. + +export * from './CookieAuthRefreshProvider'; diff --git a/plugins/auth-react/src/hooks/index.ts b/plugins/auth-react/src/hooks/index.ts new file mode 100644 index 0000000000..1257334498 --- /dev/null +++ b/plugins/auth-react/src/hooks/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +// The index file in ./hooks/ is typically responsible for selecting +// which hooks are public API and should be exported from the package. + +export * from './useCookieAuthRefresh'; diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/index.ts b/plugins/auth-react/src/hooks/useCookieAuthRefresh/index.ts new file mode 100644 index 0000000000..42b60840f8 --- /dev/null +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/index.ts @@ -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 { useCookieAuthRefresh } from './useCookieAuthRefresh'; diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx new file mode 100644 index 0000000000..338c662486 --- /dev/null +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.tsx @@ -0,0 +1,82 @@ +/* + * 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 { useEffect, useState, useCallback } from 'react'; +import { ApiRef, useApi } from '@backstage/core-plugin-api'; +import { useAsync, useMountEffect } from '@react-hookz/web'; +import { AuthApi } from '../../types'; + +type CookieAuthRefreshMessage = MessageEvent<{ + action: string; + payload: { + expiresAt: string; + }; +}>; + +export function useCookieAuthRefresh({ + apiRef, +}: { + apiRef: ApiRef; +}) { + const api = useApi(apiRef); + + const [channel] = useState( + () => new BroadcastChannel(`${apiRef.id}.auth.cookie.channel`), + ); + + const [state, actions] = useAsync(async () => await api.getCookie()); + + useMountEffect(actions.execute); + + const refresh = useCallback( + (options: { expiresAt: string }) => { + // Randomize the refreshing margin to avoid all tabs refreshing at the same time + const margin = (1 + 3 * Math.random()) * 60000; + const delay = Date.parse(options.expiresAt) - Date.now() - margin; + const timeout = setTimeout(actions.execute, delay); + return () => clearTimeout(timeout); + }, + [actions], + ); + + useEffect(() => { + if (!state.result) return () => {}; + + channel.postMessage({ + action: 'COOKIE_REFRESHED', + payload: state.result, + }); + + let cancel = refresh(state.result); + + const handleMessage = (event: CookieAuthRefreshMessage): void => { + const { action, payload } = event.data; + if (action === 'COOKIE_REFRESHED') { + cancel(); + cancel = refresh(payload); + } + }; + + channel.addEventListener('message', handleMessage); + + return () => { + cancel(); + channel.removeEventListener('message', handleMessage); + }; + }, [state, refresh, channel]); + + return { state, actions }; +} diff --git a/plugins/auth-react/src/index.ts b/plugins/auth-react/src/index.ts new file mode 100644 index 0000000000..30ac7cfe72 --- /dev/null +++ b/plugins/auth-react/src/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Web library for the auth plugin. + * + * @packageDocumentation + */ + +// In this package you might for example export components or hooks +// that are useful to other plugins or modules. + +export * from './components'; +export * from './hooks'; diff --git a/plugins/auth-react/src/setupTests.ts b/plugins/auth-react/src/setupTests.ts new file mode 100644 index 0000000000..658016ffdd --- /dev/null +++ b/plugins/auth-react/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * 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 '@testing-library/jest-dom'; diff --git a/plugins/auth-react/src/types.ts b/plugins/auth-react/src/types.ts new file mode 100644 index 0000000000..b8c74cec8e --- /dev/null +++ b/plugins/auth-react/src/types.ts @@ -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 type AuthApi = { getCookie(): Promise<{ expiresAt: string }> }; diff --git a/yarn.lock b/yarn.lock index 63e46381d6..c438e5f39d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5110,6 +5110,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-react@workspace:^, @backstage/plugin-auth-react@workspace:plugins/auth-react": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-react@workspace:plugins/auth-react" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@material-ui/core": ^4.9.13 + "@react-hookz/web": ^24.0.4 + "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^14.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-azure-devops-backend@workspace:^, @backstage/plugin-azure-devops-backend@workspace:plugins/azure-devops-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-azure-devops-backend@workspace:plugins/azure-devops-backend" @@ -9852,6 +9869,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" + "@backstage/plugin-auth-react": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" @@ -15707,7 +15725,7 @@ __metadata: languageName: node linkType: hard -"@react-hookz/web@npm:^24.0.0": +"@react-hookz/web@npm:^24.0.0, @react-hookz/web@npm:^24.0.4": version: 24.0.4 resolution: "@react-hookz/web@npm:24.0.4" dependencies: