From 6cfcf8a4a759fad3b1775f8002eeadc65c675f47 Mon Sep 17 00:00:00 2001 From: Renan Mendes Carvalho Date: Wed, 30 Aug 2023 10:26:16 +0200 Subject: [PATCH] feature(home-plugin): Adds user scope to storage calls This patch adds the userEntityRef to the storage key ensuring they are scoped per user. Signed-off-by: Renan Mendes Carvalho --- plugins/home/api-report.md | 7 +++++- .../home/src/api/CoreStorageVisitsApi.test.ts | 14 +++++++++++- plugins/home/src/api/CoreStorageVisitsApi.ts | 22 ++++++++++++++----- .../src/api/LocalStorageVisitsApi.test.ts | 18 +++++++++++---- plugins/home/src/api/LocalStorageVisitsApi.ts | 21 +++++++++++++----- 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 3352a291bc..9eddb81da5 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -14,6 +14,7 @@ import { CardSettings as CardSettings_2 } from '@backstage/plugin-home-react'; import { ComponentParts as ComponentParts_2 } from '@backstage/plugin-home-react'; import { ComponentRenderer as ComponentRenderer_2 } from '@backstage/plugin-home-react'; import { createCardExtension as createCardExtension_2 } from '@backstage/plugin-home-react'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; @@ -79,12 +80,14 @@ export const ComponentTabs: (props: { export class CoreStorageVisitsApi extends VisitsApiFactory { constructor({ storageApi, + identityApi, randomUUID, limit, }: { storageApi: StorageApi; randomUUID?: Window['crypto']['randomUUID']; limit?: number; + identityApi: IdentityApi; }); } @@ -181,10 +184,12 @@ export class LocalStorageVisitsApi extends VisitsApiFactory { localStorage, randomUUID, limit, - }?: { + identityApi, + }: { localStorage?: Window['localStorage']; randomUUID?: Window['crypto']['randomUUID']; limit?: number; + identityApi: IdentityApi; }); } diff --git a/plugins/home/src/api/CoreStorageVisitsApi.test.ts b/plugins/home/src/api/CoreStorageVisitsApi.test.ts index 9185ece2c6..e14f71ad60 100644 --- a/plugins/home/src/api/CoreStorageVisitsApi.test.ts +++ b/plugins/home/src/api/CoreStorageVisitsApi.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; import { CoreStorageVisitsApi } from './CoreStorageVisitsApi'; import { MockStorageApi } from '@backstage/test-utils'; @@ -24,6 +25,14 @@ describe('new CoreStorageVisitsApi({ storageApi: MockStorageApi.create() })', () () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; + const mockIdentityApi: IdentityApi = { + signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: async () => + ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), + getCredentials: jest.fn(), + }; + beforeEach(() => { window.crypto.randomUUID = mockRandomUUID; }); @@ -32,9 +41,10 @@ describe('new CoreStorageVisitsApi({ storageApi: MockStorageApi.create() })', () window.localStorage.clear(); }); - it('instantiates with no configuration', () => { + it('instantiates', () => { const api = new CoreStorageVisitsApi({ storageApi: MockStorageApi.create(), + identityApi: mockIdentityApi, }); expect(api).toBeTruthy(); }); @@ -42,6 +52,7 @@ describe('new CoreStorageVisitsApi({ storageApi: MockStorageApi.create() })', () it('saves a visit', async () => { const api = new CoreStorageVisitsApi({ storageApi: MockStorageApi.create(), + identityApi: mockIdentityApi, }); const visit = { pathname: '/catalog/default/component/playback-order', @@ -58,6 +69,7 @@ describe('new CoreStorageVisitsApi({ storageApi: MockStorageApi.create() })', () it('retrieves visits', async () => { const api = new CoreStorageVisitsApi({ storageApi: MockStorageApi.create(), + identityApi: mockIdentityApi, }); const visit = { pathname: '/catalog/default/component/playback-order', diff --git a/plugins/home/src/api/CoreStorageVisitsApi.ts b/plugins/home/src/api/CoreStorageVisitsApi.ts index 8b54fcc416..ae7fac14e1 100644 --- a/plugins/home/src/api/CoreStorageVisitsApi.ts +++ b/plugins/home/src/api/CoreStorageVisitsApi.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { StorageApi } from '@backstage/core-plugin-api'; +import { IdentityApi, StorageApi } from '@backstage/core-plugin-api'; import { Visit } from './VisitsApi'; import { VisitsApiFactory } from './VisitsApiFactory'; @@ -23,30 +23,40 @@ import { VisitsApiFactory } from './VisitsApiFactory'; */ export class CoreStorageVisitsApi extends VisitsApiFactory { private readonly storageApi: StorageApi; - private readonly storageKey = '@backstage/plugin-home:visits'; + private readonly storageKeyPrefix = '@backstage/plugin-home:visits'; + private readonly identityApi: IdentityApi; constructor({ storageApi, + identityApi, randomUUID = window?.crypto?.randomUUID, limit = 100, }: { storageApi: StorageApi; randomUUID?: Window['crypto']['randomUUID']; limit?: number; + identityApi: IdentityApi; }) { super({ randomUUID, limit }); this.storageApi = storageApi; + this.identityApi = identityApi; this.retrieveAll = async (): Promise> => { let visits: Array; + const { userEntityRef } = await this.identityApi.getBackstageIdentity(); + const storageKey = `${this.storageKeyPrefix}:${userEntityRef}`; + try { - visits = - this.storageApi.snapshot>(this.storageKey).value ?? []; + visits = this.storageApi.snapshot>(storageKey).value ?? []; } catch { visits = []; } return visits; }; - this.persistAll = async (visits: Array) => - this.storageApi.set>(this.storageKey, visits); + this.persistAll = async (visits: Array) => { + const { userEntityRef } = await this.identityApi.getBackstageIdentity(); + const storageKey = `${this.storageKeyPrefix}:${userEntityRef}`; + + return this.storageApi.set>(storageKey, visits); + }; } } diff --git a/plugins/home/src/api/LocalStorageVisitsApi.test.ts b/plugins/home/src/api/LocalStorageVisitsApi.test.ts index 8cf94e6c25..4e2e3f9c7a 100644 --- a/plugins/home/src/api/LocalStorageVisitsApi.test.ts +++ b/plugins/home/src/api/LocalStorageVisitsApi.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; import { LocalStorageVisitsApi } from './LocalStorageVisitsApi'; describe('new LocalStorageVisitsApi()', () => { @@ -23,21 +24,30 @@ describe('new LocalStorageVisitsApi()', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; + const mockIdentityApi: IdentityApi = { + signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: async () => + ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), + getCredentials: jest.fn(), + }; + beforeEach(() => { window.crypto.randomUUID = mockRandomUUID; }); afterEach(() => { window.localStorage.clear(); + jest.resetAllMocks(); }); - it('instantiates with no configuration', () => { - const api = new LocalStorageVisitsApi(); + it('instantiates with only identitiyApi', () => { + const api = new LocalStorageVisitsApi({ identityApi: mockIdentityApi }); expect(api).toBeTruthy(); }); it('saves a visit', async () => { - const api = new LocalStorageVisitsApi(); + const api = new LocalStorageVisitsApi({ identityApi: mockIdentityApi }); const visit = { pathname: '/catalog/default/component/playback-order', entityRef: 'component:default/playback-order', @@ -51,7 +61,7 @@ describe('new LocalStorageVisitsApi()', () => { }); it('retrieves visits', async () => { - const api = new LocalStorageVisitsApi(); + const api = new LocalStorageVisitsApi({ identityApi: mockIdentityApi }); const visit = { pathname: '/catalog/default/component/playback-order', entityRef: 'component:default/playback-order', diff --git a/plugins/home/src/api/LocalStorageVisitsApi.ts b/plugins/home/src/api/LocalStorageVisitsApi.ts index 5999930e3a..21a8e5cb6f 100644 --- a/plugins/home/src/api/LocalStorageVisitsApi.ts +++ b/plugins/home/src/api/LocalStorageVisitsApi.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { IdentityApi } from '@backstage/core-plugin-api'; import { Visit } from './VisitsApi'; import { VisitsApiFactory } from './VisitsApiFactory'; @@ -22,31 +23,41 @@ import { VisitsApiFactory } from './VisitsApiFactory'; */ export class LocalStorageVisitsApi extends VisitsApiFactory { private readonly localStorage: Window['localStorage']; - private readonly storageKey = '@backstage/plugin-home:visits'; + private readonly storageKeyPrefix = '@backstage/plugin-home:visits'; + private readonly identityApi: IdentityApi; constructor({ localStorage = window?.localStorage, randomUUID = window?.crypto?.randomUUID, limit = 100, + identityApi, }: { localStorage?: Window['localStorage']; randomUUID?: Window['crypto']['randomUUID']; limit?: number; - } = {}) { + identityApi: IdentityApi; + }) { super({ randomUUID, limit }); this.localStorage = localStorage; + this.identityApi = identityApi; this.retrieveAll = async (): Promise> => { let visits: Array; + const { userEntityRef } = await this.identityApi.getBackstageIdentity(); + const storageKey = `${this.storageKeyPrefix}:${userEntityRef}`; + try { - visits = JSON.parse(this.localStorage.getItem(this.storageKey) ?? '[]'); + visits = JSON.parse(this.localStorage.getItem(storageKey) ?? '[]'); } catch { visits = []; } return visits; }; this.persistAll = async (visits: Array) => { - this.localStorage.setItem( - this.storageKey, + const { userEntityRef } = await this.identityApi.getBackstageIdentity(); + const storageKey = `${this.storageKeyPrefix}:${userEntityRef}`; + + return this.localStorage.setItem( + storageKey, JSON.stringify(visits.splice(0, this.limit)), ); };