refactor: use cross tab timeout

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-03-07 18:46:06 +01:00
parent 3f14e9fc5f
commit a57f830fe9
2 changed files with 66 additions and 37 deletions
@@ -292,7 +292,9 @@ export async function createRouter(
const { kind, namespace, name } = req.params;
const entityName = { kind, namespace, name };
const credentials = await httpAuth.credentials(req);
const credentials = await httpAuth.credentials(req, {
allowLimitedAccess: true,
});
const { token } = await auth.getPluginRequestToken({
onBehalfOf: credentials,
@@ -321,8 +323,12 @@ export async function createRouter(
router.use('/static/docs', publisher.docsRouter());
// Endpoint that sets the cookie for the user
router.get('/cookie', async (_, res) => {
const { expiresAt } = await httpAuth.issueUserCookie(res);
router.get('/cookie', async (req, res) => {
const credentials = await httpAuth.credentials(req, {
allow: ['user'],
allowLimitedAccess: true,
});
const { expiresAt } = await httpAuth.issueUserCookie(res, { credentials });
res.json({ expiresAt: expiresAt.toISOString() });
});
+57 -34
View File
@@ -31,6 +31,8 @@ import {
} from '@backstage/plugin-techdocs-react';
import { EventSourcePolyfill } from 'event-source-polyfill';
const EXPIRES_AT_STORAGE_KEY = 'backstage-techdocs-cookie-expires-at';
/**
* API to talk to `techdocs-backend`.
*
@@ -40,7 +42,10 @@ export class TechDocsClient implements TechDocsApi {
public configApi: Config;
public discoveryApi: DiscoveryApi;
private fetchApi: FetchApi;
private cookieRefreshTimeoutId: NodeJS.Timeout | undefined;
private locked = false;
private readonly channel = new BroadcastChannel(
'backstage-techdocs-cookie-refresh',
);
constructor(options: {
configApi: Config;
@@ -50,6 +55,20 @@ export class TechDocsClient implements TechDocsApi {
this.configApi = options.configApi;
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
this.channel.onmessage = event => {
const { action, payload } = event.data;
if (action === 'REFRESH_COOKIE') {
this.locked = payload.locked;
}
};
}
set expiresAt(value: string) {
localStorage.setItem(EXPIRES_AT_STORAGE_KEY, value);
}
get expiresAt(): string | null {
return localStorage.getItem(EXPIRES_AT_STORAGE_KEY);
}
private async getCookie(): Promise<{ expiresAt: string }> {
@@ -64,42 +83,46 @@ export class TechDocsClient implements TechDocsApi {
return await response.json();
}
public async issueUserCookie(): Promise<{ expiresAt: string }> {
const expiresAtStorageKey = 'backstage-auth-cookie-last-refresh-expires-at';
const formatedExpiresAt = localStorage.getItem(expiresAtStorageKey);
const expiresAt = formatedExpiresAt ? new Date(formatedExpiresAt) : null;
private refreshUserCookie(options: { expiresAt: string }) {
const { expiresAt } = options;
const tenMinutesInMilliseconds = 10 * 60000;
const intervalId = setInterval(() => {
if (this.locked) return;
if (Date.parse(expiresAt) - Date.now() - tenMinutesInMilliseconds > 0)
return;
// get a new cookie if it doesn't exist or has expired
if (
// first time issuing a cookie or user deleted the stored expiration date
!expiresAt ||
// the application was reloaded and the cookie was not refreshed
expiresAt.getTime() < Date.now()
) {
return this.getCookie().then(response => {
const newExpiresAt = new Date(response.expiresAt);
// refresh the cookie 5 minutes before it expires
newExpiresAt.setMinutes(newExpiresAt.getMinutes() - 5);
// store the expiration date to keep it even if the application is reloaded
localStorage.setItem(expiresAtStorageKey, newExpiresAt.toISOString());
// maintain the timeout id per tab instance so we know that there is a timeout running
this.cookieRefreshTimeoutId = setTimeout(
this.issueUserCookie,
newExpiresAt.getTime(),
);
return { expiresAt: newExpiresAt.toISOString() };
this.channel.postMessage({
action: 'REFRESH_COOKIE',
payload: { locked: true },
});
this.issueUserCookie({ force: true })
.then(() => clearInterval(intervalId))
.finally(() => {
this.locked = false;
this.channel.postMessage({
action: 'REFRESH_COOKIE',
payload: { locked: false },
});
});
}, tenMinutesInMilliseconds);
return { expiresAt };
}
public async issueUserCookie(
options: {
force?: boolean;
} = {},
): Promise<{ expiresAt: string }> {
const { force } = options;
if (force || !this.expiresAt || Date.parse(this.expiresAt) < Date.now()) {
return this.getCookie().then(({ expiresAt }) => {
this.expiresAt = expiresAt;
return this.refreshUserCookie({ expiresAt });
});
}
// restart timeout when the cookie is still valid but the application was reloaded
if (!this.cookieRefreshTimeoutId) {
this.cookieRefreshTimeoutId = setTimeout(
this.issueUserCookie,
expiresAt.getTime(),
);
}
return { expiresAt: expiresAt.toISOString() };
return this.refreshUserCookie({ expiresAt: this.expiresAt });
}
async getApiOrigin(): Promise<string> {