diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index fbba2a0430..10388cea28 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -295,8 +295,8 @@ export async function createRouter( router.use('/static/docs', publisher.docsRouter()); // Endpoint that sets the cookie for the user - router.get('/cookie', async (req, res) => { - const { expiresAt } = await httpAuth.issueUserCookie(req); + router.get('/cookie', async (_, res) => { + const { expiresAt } = await httpAuth.issueUserCookie(res); res.json({ expiresAt: expiresAt.toISOString() }); }); diff --git a/plugins/techdocs-react/src/api.ts b/plugins/techdocs-react/src/api.ts index 6d726fae01..a4bb6ccf34 100644 --- a/plugins/techdocs-react/src/api.ts +++ b/plugins/techdocs-react/src/api.ts @@ -24,6 +24,7 @@ import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; * @public */ export interface TechDocsApi { + issueUserCookie(): Promise<{ expiresAt: string }>; getApiOrigin(): Promise; getTechDocsMetadata(entityId: CompoundEntityRef): Promise; getEntityMetadata( diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 8680e14468..e7b97a2bce 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -51,6 +51,70 @@ export class TechDocsClient implements TechDocsApi { this.fetchApi = options.fetchApi; } + private async getCookie(): Promise<{ expiresAt: string }> { + const apiOrigin = await this.getApiOrigin(); + const requestUrl = `${apiOrigin}/cookie`; + const response = await this.fetchApi.fetch(`${requestUrl}`, { + credentials: 'include', + }); + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + return await response.json(); + } + + private async refreshCookie(options: { + expiresAt?: Date | null; + timeoutId: number | null; + }): Promise<{ expiresAt: string }> { + // Always clear the previous timeout + if (options.timeoutId) clearTimeout(options.timeoutId); + + // Only issue a new cookie when we don't have an expiry date or it's in the past + if (!options.expiresAt || options.expiresAt.getTime() < Date.now()) { + return this.getCookie().then(response => { + const expiresAt = new Date(response.expiresAt); + expiresAt.setMinutes(expiresAt.getMinutes() - 5); + localStorage.setItem( + 'backstage-auth-cookie-last-refresh-expiration-date', + expiresAt.toISOString(), + ); + const timeoutId = setTimeout(this.refreshCookie, expiresAt.getTime()); + localStorage.setItem( + 'backstage-auth-cookie-last-refresh-timeout-id', + timeoutId.toString(), + ); + return { expiresAt: expiresAt.toISOString() }; + }); + } + + // Otherwise, set a new timeout to refresh the cookie with the current expiration date + const timeoutId = setTimeout( + this.refreshCookie, + options.expiresAt.getTime(), + ); + localStorage.setItem( + 'backstage-auth-cookie-last-refresh-timeout-id', + timeoutId.toString(), + ); + + return { expiresAt: options.expiresAt.toISOString() }; + } + + async issueUserCookie() { + const expiresAt = localStorage.getItem( + 'backstage-auth-cookie-last-refresh-expiration-date', + ); + const timeoutId = localStorage.getItem( + 'backstage-auth-cookie-last-refresh-timeout-id', + ); + + return this.refreshCookie({ + expiresAt: expiresAt ? new Date(expiresAt) : null, + timeoutId: timeoutId ? Number(timeoutId) : null, + }); + } + async getApiOrigin(): Promise { return await this.discoveryApi.getBaseUrl('techdocs'); } diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index e81a455cea..dc8803c962 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -14,15 +14,16 @@ * limitations under the License. */ -import React, { ReactNode, Children, ReactElement } from 'react'; +import React, { ReactNode, Children, ReactElement, useState } from 'react'; import { useOutlet } from 'react-router-dom'; -import { Page } from '@backstage/core-components'; +import { ErrorPanel, Page } from '@backstage/core-components'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { TECHDOCS_ADDONS_WRAPPER_KEY, TECHDOCS_ADDONS_KEY, TechDocsReaderPageProvider, + techdocsApiRef, } from '@backstage/plugin-techdocs-react'; import { TechDocsReaderPageRenderFunction } from '../../../types'; @@ -33,8 +34,11 @@ import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader'; import { rootDocsRouteRef } from '../../../routes'; import { getComponentData, + useApi, + useApp, useRouteRefParams, } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; /* An explanation for the multiple ways of customizing the TechDocs reader page @@ -143,6 +147,27 @@ export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => { ); }; +function TechDocsAuthProvider({ children }: { children: ReactNode }) { + const app = useApp(); + const { Progress } = app.getComponents(); + + const techdocsApi = useApi(techdocsApiRef); + + const { loading, error } = useAsync(async () => { + return await techdocsApi.issueUserCookie(); + }, [techdocsApi]); + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return children; +} + /** * @public */ @@ -177,29 +202,33 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => { // As explained above, "page" is configuration 4 and is 1 return ( - - {(page as JSX.Element) || } - + + + {(page as JSX.Element) || } + + ); } // As explained above, a render function is configuration 3 and React element is 2 return ( - - {({ metadata, entityMetadata, onReady }) => ( -
- - {children instanceof Function - ? children({ - entityRef, - techdocsMetadataValue: metadata.value, - entityMetadataValue: entityMetadata.value, - onReady, - }) - : children} - -
- )} -
+ + + {({ metadata, entityMetadata, onReady }) => ( +
+ + {children instanceof Function + ? children({ + entityRef, + techdocsMetadataValue: metadata.value, + entityMetadataValue: entityMetadata.value, + onReady, + }) + : children} + +
+ )} +
+
); };