From e1f6eb4bc07e1073b4ee683b8e211521fa728b05 Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 17 Jul 2024 21:35:44 +0300 Subject: [PATCH 1/6] added session and persistant storages --- src/application/services/useAuthRequired.ts | 11 ++- src/infrastructure/auth.repository.ts | 4 +- src/infrastructure/index.ts | 2 +- .../storage/abstract/persistant.ts | 75 +++++++++++++++++++ .../storage/abstract/session.ts | 29 +++++++ .../storage/abstract/subscribable.ts | 31 +------- src/infrastructure/storage/auth.ts | 24 +++--- src/infrastructure/storage/openedPage.ts | 13 +++- src/infrastructure/storage/user.ts | 4 +- 9 files changed, 140 insertions(+), 53 deletions(-) create mode 100644 src/infrastructure/storage/abstract/persistant.ts create mode 100644 src/infrastructure/storage/abstract/session.ts diff --git a/src/application/services/useAuthRequired.ts b/src/application/services/useAuthRequired.ts index 72b93bc3..78671cfe 100644 --- a/src/application/services/useAuthRequired.ts +++ b/src/application/services/useAuthRequired.ts @@ -1,6 +1,7 @@ import { useAppState } from './useAppState.ts'; import useAuth from './useAuth.ts'; import { useRouter } from 'vue-router'; +import { until } from '@vueuse/core'; /** * Function that is used in App for checking user authorization @@ -18,15 +19,17 @@ export default function useAuthRequired(): void { * When oauth will work, it will be treated as he authorized manually * @returns true if user is authorized, false otherwise */ - function isUserAuthorized(): boolean { - return (user.value !== null && user.value !== undefined); + async function isUserAuthorized(): Promise { + await until(user).not.toBe(undefined); + + return user.value !== null; } /** * For each route check if auth is required */ - router.beforeEach((actualRoute, _, next) => { - if (actualRoute.meta.authRequired === true && !isUserAuthorized()) { + router.beforeEach(async (actualRoute, _, next) => { + if (actualRoute.meta.authRequired === true && !(await isUserAuthorized())) { /** * If auth is required and user is not autorized * Then show google auth popup and redirect user to auth page diff --git a/src/infrastructure/auth.repository.ts b/src/infrastructure/auth.repository.ts index 6fa7602e..3aaa1af3 100644 --- a/src/infrastructure/auth.repository.ts +++ b/src/infrastructure/auth.repository.ts @@ -28,7 +28,9 @@ export default class AuthRepository implements AuthRepositoryInterface { * Specify whether we have auth session (refresh token) */ public hasSession(): boolean { - return this.authStorage.getRefreshToken() !== null; + const refreshToken = this.authStorage.getRefreshToken(); + + return refreshToken !== null && refreshToken !== ''; } /** diff --git a/src/infrastructure/index.ts b/src/infrastructure/index.ts index 4516cf0b..75debe3e 100644 --- a/src/infrastructure/index.ts +++ b/src/infrastructure/index.ts @@ -78,10 +78,10 @@ export function init(noteApiUrl: string, eventBus: EventBus): Repositories { * Init storages */ const noteStore = new NoteStore(); - const authStore = new AuthStore(); const userStore = new UserStore(); const editorToolsStore = new EditorToolsStore(); const openedPagesStore = new OpenedPagesStore(); + const authStore = new AuthStore(); /** * Init transport diff --git a/src/infrastructure/storage/abstract/persistant.ts b/src/infrastructure/storage/abstract/persistant.ts new file mode 100644 index 00000000..5041b326 --- /dev/null +++ b/src/infrastructure/storage/abstract/persistant.ts @@ -0,0 +1,75 @@ +/* eslint-disable-next-line boundaries/element-types */ +import { SubscribableStore } from './subscribable'; + +type StoreDataKeys = string[]; + +export class PersistantStore> extends SubscribableStore { + protected readonly storeDataKeys: StoreDataKeys; + + /** + * Proxy for data stored in store. + * Used to watch data changes + */ + protected data: StoreData; + + private storage = window.localStorage; + + constructor(storeDataKeys: StoreDataKeys) { + super(); + this.storeDataKeys = storeDataKeys; + this.data = new Proxy(this.loadInitialData(), this._data); + }; + + protected get _data(): ProxyHandler { + return { + get: (target, prop) => { + const item = this.storage.getItem(prop as string); + + return item !== null ? JSON.parse(item) : undefined; + }, + + set: (target, prop, value, receiver) => { + /** + * Set new property value for proxy usage + */ + Reflect.set(target, prop, value, receiver); + + /** + * Set new property value for storage + */ + this.storage.setItem(prop as string, JSON.stringify(value)); + + this.onDataChange([{ prop: prop as keyof StoreData, + newValue: value }]); + + return true; + }, + }; + } + + /** + * Funciton for loading initial data from window.localStorage to proxy object + * @returns data stored in localStorage in normalized form for proxy + */ + private loadInitialData(): StoreData { + const storedData = {} as StoreData; + + if (this.storage === undefined) { + return {} as StoreData; + } + + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); + + if (key !== null && this.storeDataKeys.includes(key)) { + const storedValue = this.storage.getItem(key); + + if (storedValue !== null) { + storedData[key as keyof StoreData] = JSON.parse(storedValue); + } + } + } + + return storedData; + } +} diff --git a/src/infrastructure/storage/abstract/session.ts b/src/infrastructure/storage/abstract/session.ts new file mode 100644 index 00000000..ab95edb1 --- /dev/null +++ b/src/infrastructure/storage/abstract/session.ts @@ -0,0 +1,29 @@ +/* eslint-disable-next-line boundaries/element-types */ +import { SubscribableStore } from './subscribable'; + +export class SessionStore> extends SubscribableStore { + /** + * Proxy for data stored in store. + * Used to watch data changes + */ + protected data = new Proxy({} as StoreData, this._data); + + /** + * Data proxy handler + */ + protected get _data(): ProxyHandler { + return { + set: (target, prop, value, receiver) => { + Reflect.set(target, prop, value, receiver); + + this.onDataChange([{ prop: prop as keyof StoreData, + newValue: value }]); + + return true; + }, + get(target, prop, receiver) { + return Reflect.get(target, prop, receiver); + }, + }; + } +} diff --git a/src/infrastructure/storage/abstract/subscribable.ts b/src/infrastructure/storage/abstract/subscribable.ts index 5afa825f..d3b08b10 100644 --- a/src/infrastructure/storage/abstract/subscribable.ts +++ b/src/infrastructure/storage/abstract/subscribable.ts @@ -24,40 +24,15 @@ type Change = { * Allows to subscribe to store data changes */ export abstract class SubscribableStore> { - /** - * Proxy for data stored in store. - * Used to watch data changes - */ - protected data = new Proxy({} as StoreData, this._data); - /** * List of subscribers on data change */ - private subscribers: PropChangeCallback[] = []; + protected subscribers: PropChangeCallback[] = []; /** * Using for accumulation of changes in store until subscriber appearse */ - private stashedChanges: Change[] = []; - - /** - * Data proxy handler - */ - protected get _data(): ProxyHandler { - return { - set: (target, prop, value, receiver) => { - Reflect.set(target, prop, value, receiver); - - this.onDataChange([{ prop: prop as keyof StoreData, - newValue: value }]); - - return true; - }, - get(target, prop, receiver) { - return Reflect.get(target, prop, receiver); - }, - }; - } + protected stashedChanges: Change[] = []; /** * Subscribe to store changes @@ -86,7 +61,7 @@ export abstract class SubscribableStore[]): void { + protected onDataChange(changes: Change[]): void { /** * If there are no subscribers stash current change */ diff --git a/src/infrastructure/storage/auth.ts b/src/infrastructure/storage/auth.ts index cae3854d..6413955e 100644 --- a/src/infrastructure/storage/auth.ts +++ b/src/infrastructure/storage/auth.ts @@ -1,24 +1,22 @@ +import { PersistantStore } from './abstract/persistant'; + +export type AuthStoreData = { + refreshToken: string; +}; + /** * Stores auth data in local storage */ -export default class AuthStorage { - /** - * Storage implementation - */ - private readonly storage: Storage; - - /** - * Creates storage instance - */ +export default class AuthStorage extends PersistantStore { constructor() { - this.storage = window.localStorage; + super(['refreshToken']); } /** * Returns refresh token */ public getRefreshToken(): string | null { - return this.storage.getItem('refreshToken'); + return this.data.refreshToken === undefined ? null : this.data.refreshToken; } /** @@ -26,13 +24,13 @@ export default class AuthStorage { * @param refreshToken - refresh token to save */ public setRefreshToken(refreshToken: string): void { - this.storage.setItem('refreshToken', refreshToken); + this.data.refreshToken = refreshToken; } /** * Removes refresh token */ public removeRefreshToken(): void { - this.storage.removeItem('refreshToken'); + this.data.refreshToken = ''; } } diff --git a/src/infrastructure/storage/openedPage.ts b/src/infrastructure/storage/openedPage.ts index eb1e0a0d..49b1b580 100644 --- a/src/infrastructure/storage/openedPage.ts +++ b/src/infrastructure/storage/openedPage.ts @@ -1,5 +1,5 @@ import type { OpenedPage } from '@/domain/entities/OpenedPage'; -import { SubscribableStore } from './abstract/subscribable'; +import { PersistantStore } from './abstract/persistant'; export type OpenedPagesStoreData = { /** @@ -11,10 +11,9 @@ export type OpenedPagesStoreData = { /** * Class to store all pages that are currently opened in workspace */ -export class OpenedPagesStore extends SubscribableStore { +export class OpenedPagesStore extends PersistantStore { constructor() { - super(); - this.data.openedPages = []; + super(['openedPages']); } /** @@ -22,6 +21,7 @@ export class OpenedPagesStore extends SubscribableStore { * @param page - page that had beed opened by user */ public addOpenedPage(page: OpenedPage): void { + console.log(this.data.openedPages, typeof this.data.openedPages); const uniquePageUrls = this.data.openedPages.map(currentPage => currentPage.url); if (!uniquePageUrls.includes(page.url) && page.url !== '/') { @@ -29,6 +29,11 @@ export class OpenedPagesStore extends SubscribableStore { ...this.data.openedPages, page, ]; + } else { + /** + * This used for calling proxy setter to update currently opened pages in domain + */ + this.data.openedPages = this.data.openedPages; } } diff --git a/src/infrastructure/storage/user.ts b/src/infrastructure/storage/user.ts index e5a1fb67..eadac8b2 100644 --- a/src/infrastructure/storage/user.ts +++ b/src/infrastructure/storage/user.ts @@ -1,5 +1,5 @@ import type { User } from '@/domain/entities/User'; -import { SubscribableStore } from './abstract/subscribable'; +import { SessionStore } from './abstract/session'; import type EditorTool from '@/domain/entities/EditorTool'; /** @@ -20,7 +20,7 @@ export type UserStoreData = { /** * Store for the user data */ -export class UserStore extends SubscribableStore { +export class UserStore extends SessionStore { /** * Returns user data */ From 100c2b81a4f42980e2b61b20360b1ca58089943f Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 17 Jul 2024 21:45:30 +0300 Subject: [PATCH 2/6] improved comments and types --- src/application/services/useAuthRequired.ts | 4 +++- .../storage/abstract/persistant.ts | 20 +++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/application/services/useAuthRequired.ts b/src/application/services/useAuthRequired.ts index 78671cfe..eefaf3a0 100644 --- a/src/application/services/useAuthRequired.ts +++ b/src/application/services/useAuthRequired.ts @@ -15,11 +15,13 @@ export default function useAuthRequired(): void { /** * Check if user is authorized - * If authorization is in process we treat user as unauthorized * When oauth will work, it will be treated as he authorized manually * @returns true if user is authorized, false otherwise */ async function isUserAuthorized(): Promise { + /** + * Wait until authorization process is finished + */ await until(user).not.toBe(undefined); return user.value !== null; diff --git a/src/infrastructure/storage/abstract/persistant.ts b/src/infrastructure/storage/abstract/persistant.ts index 5041b326..d00c7ba2 100644 --- a/src/infrastructure/storage/abstract/persistant.ts +++ b/src/infrastructure/storage/abstract/persistant.ts @@ -1,20 +1,24 @@ /* eslint-disable-next-line boundaries/element-types */ import { SubscribableStore } from './subscribable'; -type StoreDataKeys = string[]; - export class PersistantStore> extends SubscribableStore { - protected readonly storeDataKeys: StoreDataKeys; + /** + * Keys of the StoreData type + * Used to separate the needed local storage information + */ + protected readonly storeDataKeys: string[]; /** - * Proxy for data stored in store. - * Used to watch data changes + * Proxy for data stored */ protected data: StoreData; + /** + * Storage that would retain information when proxy is cleared + */ private storage = window.localStorage; - constructor(storeDataKeys: StoreDataKeys) { + constructor(storeDataKeys: string[]) { super(); this.storeDataKeys = storeDataKeys; this.data = new Proxy(this.loadInitialData(), this._data); @@ -54,10 +58,6 @@ export class PersistantStore> extends private loadInitialData(): StoreData { const storedData = {} as StoreData; - if (this.storage === undefined) { - return {} as StoreData; - } - for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); From 6e98d847fb5fcd168710877fe8848f5b4c8e2f7c Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 17 Jul 2024 22:18:39 +0300 Subject: [PATCH 3/6] added delete func to proxy --- src/infrastructure/storage/abstract/persistant.ts | 4 ++++ src/infrastructure/storage/abstract/session.ts | 4 ++++ src/infrastructure/storage/auth.ts | 7 +++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/storage/abstract/persistant.ts b/src/infrastructure/storage/abstract/persistant.ts index d00c7ba2..60f24f0a 100644 --- a/src/infrastructure/storage/abstract/persistant.ts +++ b/src/infrastructure/storage/abstract/persistant.ts @@ -48,6 +48,10 @@ export class PersistantStore> extends return true; }, + + deleteProperty: (target, property) => { + return Reflect.deleteProperty(target, property); + }, }; } diff --git a/src/infrastructure/storage/abstract/session.ts b/src/infrastructure/storage/abstract/session.ts index ab95edb1..4780a7bf 100644 --- a/src/infrastructure/storage/abstract/session.ts +++ b/src/infrastructure/storage/abstract/session.ts @@ -24,6 +24,10 @@ export class SessionStore> extends Sub get(target, prop, receiver) { return Reflect.get(target, prop, receiver); }, + + deleteProperty: (target, property) => { + return Reflect.deleteProperty(target, property); + }, }; } } diff --git a/src/infrastructure/storage/auth.ts b/src/infrastructure/storage/auth.ts index 6413955e..537794fd 100644 --- a/src/infrastructure/storage/auth.ts +++ b/src/infrastructure/storage/auth.ts @@ -1,7 +1,10 @@ import { PersistantStore } from './abstract/persistant'; export type AuthStoreData = { - refreshToken: string; + /** + * If user is authorized refresh token will be stored + */ + refreshToken?: string; }; /** @@ -31,6 +34,6 @@ export default class AuthStorage extends PersistantStore { * Removes refresh token */ public removeRefreshToken(): void { - this.data.refreshToken = ''; + delete this.data.refreshToken; } } From 931d90c08d52d0828cd9c6f3307a4d4c60c3ce74 Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 17 Jul 2024 22:22:51 +0300 Subject: [PATCH 4/6] minor improvements --- src/application/services/useAuthRequired.ts | 4 +++- src/infrastructure/auth.repository.ts | 3 ++- src/infrastructure/storage/abstract/persistant.ts | 8 ++++---- src/infrastructure/storage/openedPage.ts | 1 - 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/application/services/useAuthRequired.ts b/src/application/services/useAuthRequired.ts index eefaf3a0..33a0d544 100644 --- a/src/application/services/useAuthRequired.ts +++ b/src/application/services/useAuthRequired.ts @@ -31,7 +31,9 @@ export default function useAuthRequired(): void { * For each route check if auth is required */ router.beforeEach(async (actualRoute, _, next) => { - if (actualRoute.meta.authRequired === true && !(await isUserAuthorized())) { + const isAuthorized = await isUserAuthorized(); + + if (actualRoute.meta.authRequired === true && !isAuthorized) { /** * If auth is required and user is not autorized * Then show google auth popup and redirect user to auth page diff --git a/src/infrastructure/auth.repository.ts b/src/infrastructure/auth.repository.ts index 3aaa1af3..ac058609 100644 --- a/src/infrastructure/auth.repository.ts +++ b/src/infrastructure/auth.repository.ts @@ -2,6 +2,7 @@ import type AuthRepositoryInterface from '@/domain/auth.repository.interface'; import type NotesApiTransport from './transport/notes-api'; import type AuthStorage from './storage/auth'; import type AuthSession from '@/domain/entities/AuthSession'; +import { notEmpty } from './utils/empty'; /** * Facade for the auth data @@ -30,7 +31,7 @@ export default class AuthRepository implements AuthRepositoryInterface { public hasSession(): boolean { const refreshToken = this.authStorage.getRefreshToken(); - return refreshToken !== null && refreshToken !== ''; + return notEmpty(refreshToken); } /** diff --git a/src/infrastructure/storage/abstract/persistant.ts b/src/infrastructure/storage/abstract/persistant.ts index 60f24f0a..692e8ab6 100644 --- a/src/infrastructure/storage/abstract/persistant.ts +++ b/src/infrastructure/storage/abstract/persistant.ts @@ -6,7 +6,7 @@ export class PersistantStore> extends * Keys of the StoreData type * Used to separate the needed local storage information */ - protected readonly storeDataKeys: string[]; + protected readonly keysStored: string[]; /** * Proxy for data stored @@ -18,9 +18,9 @@ export class PersistantStore> extends */ private storage = window.localStorage; - constructor(storeDataKeys: string[]) { + constructor(keysStored: string[]) { super(); - this.storeDataKeys = storeDataKeys; + this.keysStored = keysStored; this.data = new Proxy(this.loadInitialData(), this._data); }; @@ -65,7 +65,7 @@ export class PersistantStore> extends for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); - if (key !== null && this.storeDataKeys.includes(key)) { + if (key !== null && this.keysStored.includes(key)) { const storedValue = this.storage.getItem(key); if (storedValue !== null) { diff --git a/src/infrastructure/storage/openedPage.ts b/src/infrastructure/storage/openedPage.ts index 49b1b580..1c48c8e5 100644 --- a/src/infrastructure/storage/openedPage.ts +++ b/src/infrastructure/storage/openedPage.ts @@ -21,7 +21,6 @@ export class OpenedPagesStore extends PersistantStore { * @param page - page that had beed opened by user */ public addOpenedPage(page: OpenedPage): void { - console.log(this.data.openedPages, typeof this.data.openedPages); const uniquePageUrls = this.data.openedPages.map(currentPage => currentPage.url); if (!uniquePageUrls.includes(page.url) && page.url !== '/') { From 3ee8929a3f2a0328ee71518961d03421e4539d47 Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 17 Jul 2024 22:26:37 +0300 Subject: [PATCH 5/6] deleteProperty also deleted prop from store --- src/infrastructure/storage/abstract/persistant.ts | 6 ++++-- src/infrastructure/storage/abstract/session.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/infrastructure/storage/abstract/persistant.ts b/src/infrastructure/storage/abstract/persistant.ts index 692e8ab6..45baa820 100644 --- a/src/infrastructure/storage/abstract/persistant.ts +++ b/src/infrastructure/storage/abstract/persistant.ts @@ -49,8 +49,10 @@ export class PersistantStore> extends return true; }, - deleteProperty: (target, property) => { - return Reflect.deleteProperty(target, property); + deleteProperty: (target, prop) => { + this.storage.removeItem(prop as string); + + return Reflect.deleteProperty(target, prop); }, }; } diff --git a/src/infrastructure/storage/abstract/session.ts b/src/infrastructure/storage/abstract/session.ts index 4780a7bf..e9bbd827 100644 --- a/src/infrastructure/storage/abstract/session.ts +++ b/src/infrastructure/storage/abstract/session.ts @@ -25,8 +25,8 @@ export class SessionStore> extends Sub return Reflect.get(target, prop, receiver); }, - deleteProperty: (target, property) => { - return Reflect.deleteProperty(target, property); + deleteProperty: (target, prop) => { + return Reflect.deleteProperty(target, prop); }, }; } From b4b1dcaf1d4a5de3992d0dcc687634a4ea6cd7d4 Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 17 Jul 2024 22:51:48 +0300 Subject: [PATCH 6/6] now on initial data load proxy change would be triggered --- .../storage/abstract/persistant.ts | 17 +++++++++-------- src/infrastructure/storage/openedPage.ts | 5 ----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/infrastructure/storage/abstract/persistant.ts b/src/infrastructure/storage/abstract/persistant.ts index 45baa820..4443d9b3 100644 --- a/src/infrastructure/storage/abstract/persistant.ts +++ b/src/infrastructure/storage/abstract/persistant.ts @@ -21,7 +21,12 @@ export class PersistantStore> extends constructor(keysStored: string[]) { super(); this.keysStored = keysStored; - this.data = new Proxy(this.loadInitialData(), this._data); + this.data = new Proxy({} as StoreData, this._data); + + /** + * Load data from local store to proxy + */ + this.loadInitialData(); }; protected get _data(): ProxyHandler { @@ -59,11 +64,9 @@ export class PersistantStore> extends /** * Funciton for loading initial data from window.localStorage to proxy object - * @returns data stored in localStorage in normalized form for proxy + * Data stored in localStorage would be normalized for proxy */ - private loadInitialData(): StoreData { - const storedData = {} as StoreData; - + private loadInitialData(): void { for (let i = 0; i < this.storage.length; i++) { const key = this.storage.key(i); @@ -71,11 +74,9 @@ export class PersistantStore> extends const storedValue = this.storage.getItem(key); if (storedValue !== null) { - storedData[key as keyof StoreData] = JSON.parse(storedValue); + this.data[key as keyof StoreData] = JSON.parse(storedValue); } } } - - return storedData; } } diff --git a/src/infrastructure/storage/openedPage.ts b/src/infrastructure/storage/openedPage.ts index 1c48c8e5..2a9ccff6 100644 --- a/src/infrastructure/storage/openedPage.ts +++ b/src/infrastructure/storage/openedPage.ts @@ -28,11 +28,6 @@ export class OpenedPagesStore extends PersistantStore { ...this.data.openedPages, page, ]; - } else { - /** - * This used for calling proxy setter to update currently opened pages in domain - */ - this.data.openedPages = this.data.openedPages; } }