From eec0cc7a73e30c6547213887eea2e397db5fd4ee Mon Sep 17 00:00:00 2001 From: Sean Wu Date: Thu, 18 Feb 2021 13:54:01 +0800 Subject: [PATCH] Remove all user data on logging out. --- src/app/features/profile/profile.page.ts | 22 ++++-- .../database/database.service.spec.ts | 10 +++ .../services/database/database.service.ts | 4 ++ .../capacitor-filesystem-table.spec.ts | 46 +++++++++++++ .../capacitor-filesystem-table.ts | 25 +++++-- .../shared/services/database/table/table.ts | 1 + .../image-store/image-store.service.spec.ts | 45 ++++++++----- .../image-store/image-store.service.ts | 11 ++- .../preference-manager.service.spec.ts | 18 +++++ .../preference-manager.service.ts | 6 ++ .../capacitor-storage-preferences.spec.ts | 11 +++ .../capacitor-storage-preferences.ts | 67 +++++++++++++------ src/assets/i18n/en-us.json | 1 - src/assets/i18n/zh-tw.json | 1 - 14 files changed, 211 insertions(+), 57 deletions(-) diff --git a/src/app/features/profile/profile.page.ts b/src/app/features/profile/profile.page.ts index 92d0b45c2..4031dc6f1 100644 --- a/src/app/features/profile/profile.page.ts +++ b/src/app/features/profile/profile.page.ts @@ -1,6 +1,5 @@ import { Component } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { Router } from '@angular/router'; import { Plugins } from '@capacitor/core'; import { ToastController } from '@ionic/angular'; import { TranslocoService } from '@ngneat/transloco'; @@ -9,7 +8,10 @@ import { defer } from 'rxjs'; import { catchError, concatMapTo } from 'rxjs/operators'; import { BlockingActionService } from '../../shared/services/blocking-action/blocking-action.service'; import { WebCryptoApiSignatureProvider } from '../../shared/services/collector/signature/web-crypto-api-signature-provider/web-crypto-api-signature-provider.service'; +import { Database } from '../../shared/services/database/database.service'; import { DiaBackendAuthService } from '../../shared/services/dia-backend/auth/dia-backend-auth.service'; +import { ImageStore } from '../../shared/services/image-store/image-store.service'; +import { PreferenceManager } from '../../shared/services/preference-manager/preference-manager.service'; const { Clipboard } = Plugins; @@ -26,7 +28,9 @@ export class ProfilePage { readonly privateKey$ = this.webCryptoApiSignatureProvider.getPrivateKey$(); constructor( - private readonly router: Router, + private readonly database: Database, + private readonly preferenceManager: PreferenceManager, + private readonly imageStore: ImageStore, private readonly blockingActionService: BlockingActionService, private readonly toastController: ToastController, private readonly translocoService: TranslocoService, @@ -44,7 +48,10 @@ export class ProfilePage { logout() { const action$ = this.diaBackendAuthService.logout$().pipe( - concatMapTo(defer(() => this.router.navigate(['/login']))), + concatMapTo(defer(() => this.imageStore.clear())), + concatMapTo(defer(() => this.database.clear())), + concatMapTo(defer(() => this.preferenceManager.clear())), + concatMapTo(defer(reloadApp)), catchError(err => this.toastController .create({ message: JSON.stringify(err.error), duration: 4000 }) @@ -52,10 +59,13 @@ export class ProfilePage { ) ); this.blockingActionService - .run$(action$, { - message: this.translocoService.translate('talkingToTheServer'), - }) + .run$(action$) .pipe(untilDestroyed(this)) .subscribe(); } } + +// Reload the app to force app to re-run the initialization in AppModule. +function reloadApp() { + location.href = 'index.html'; +} diff --git a/src/app/shared/services/database/database.service.spec.ts b/src/app/shared/services/database/database.service.spec.ts index 03fd02825..acbfcc027 100644 --- a/src/app/shared/services/database/database.service.spec.ts +++ b/src/app/shared/services/database/database.service.spec.ts @@ -23,4 +23,14 @@ describe('Database', () => { const id = 'id'; expect(database.getTable(id)).toBe(database.getTable(id)); }); + + it('should clear all tables', async () => { + const id = 'id'; + const table = database.getTable(id); + await table.insert([{ a: 1 }]); + + await database.clear(); + + expect(await table.queryAll()).toEqual([]); + }); }); diff --git a/src/app/shared/services/database/database.service.ts b/src/app/shared/services/database/database.service.ts index 763d62a05..7d34fffd9 100644 --- a/src/app/shared/services/database/database.service.ts +++ b/src/app/shared/services/database/database.service.ts @@ -28,4 +28,8 @@ export class Database { this.tables.set(id, created); return created; } + + async clear() { + return Promise.all([...this.tables.values()].map(table => table.clear())); + } } diff --git a/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts b/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts index 0eb9948fa..6d85a9b28 100644 --- a/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts +++ b/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.spec.ts @@ -212,6 +212,52 @@ describe('CapacitorFilesystemTable', () => { }); }); + it('should wipe all data after clear', async () => { + await table.insert([TUPLE1, TUPLE2]); + + await table.clear(); + + expect(await table.queryAll()).toEqual([]); + }); + + it('should be able to reinitialize after clear', async () => { + await table.insert([TUPLE1, TUPLE2]); + await table.clear(); + + await table.insert([TUPLE1]); + + expect(await table.queryAll()).toEqual([TUPLE1]); + }); + + it('should clear idempotently', async () => { + await table.insert([TUPLE1]); + + await table.clear(); + await table.clear(); + + expect(await table.queryAll()).toEqual([]); + }); + + it('should emit empty data after clear', async done => { + let counter = 0; + + table.queryAll$.subscribe(value => { + if (counter === 0) { + expect(value).toEqual([]); + } else if (counter === 1) { + expect(value).toEqual([TUPLE1]); + } else if (counter === 2) { + expect(value).toEqual([]); + done(); + } + counter += 1; + }); + + await table.insert([TUPLE1]); + + await table.clear(); + }); + it('should update proofs', async done => { const tupleCount = 100; const sourceTuple: TestTuple[] = [...Array(tupleCount).keys()].map( diff --git a/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts b/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts index bd9383c1a..330d659e1 100644 --- a/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts +++ b/src/app/shared/services/database/table/capacitor-filesystem-table/capacitor-filesystem-table.ts @@ -178,17 +178,28 @@ export class CapacitorFilesystemTable implements Table { }); } + async clear() { + await this.destroy(); + return this.tuples$.next([]); + } + async drop() { - this.hasInitialized = false; - if (await this.hasCreatedJson()) { - await this.filesystemPlugin.deleteFile({ - directory: this.directory, - path: `${this.rootDir}/${this.id}.json`, - }); - } + await this.destroy(); return this.tuples$.complete(); } + private async destroy() { + return this.mutex.runExclusive(async () => { + this.hasInitialized = false; + if (await this.hasCreatedJson()) { + await this.filesystemPlugin.deleteFile({ + directory: this.directory, + path: `${this.rootDir}/${this.id}.json`, + }); + } + }); + } + private static readonly initializationMutex = new Mutex(); } diff --git a/src/app/shared/services/database/table/table.ts b/src/app/shared/services/database/table/table.ts index c4c995768..14ae73559 100644 --- a/src/app/shared/services/database/table/table.ts +++ b/src/app/shared/services/database/table/table.ts @@ -11,6 +11,7 @@ export interface Table { ): Promise; delete(tuples: T[], comparator?: (x: T, y: T) => boolean): Promise; update(tuples: T[], comparator: (x: T, y: T) => boolean): Promise; + clear(): Promise; drop(): Promise; } diff --git a/src/app/shared/services/image-store/image-store.service.spec.ts b/src/app/shared/services/image-store/image-store.service.spec.ts index a6517de6a..6a3bd38eb 100644 --- a/src/app/shared/services/image-store/image-store.service.spec.ts +++ b/src/app/shared/services/image-store/image-store.service.spec.ts @@ -10,11 +10,6 @@ const { Filesystem } = Plugins; describe('ImageStore', () => { let store: ImageStore; - const sampleFile = - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; - const sampleIndex = - '93ae7d494fad0fb30cbf3ae746a39c4bc7a0f8bbf87fbb587a3f3c01f3c5ce20'; - const sampleMimeType: MimeType = 'image/png'; beforeEach(() => { TestBed.configureTestingModule({ @@ -29,21 +24,21 @@ describe('ImageStore', () => { it('should be created', () => expect(store).toBeTruthy()); it('should check if file exists', async () => { - expect(await store.exists(sampleIndex)).toBeFalse(); + expect(await store.exists(INDEX)).toBeFalse(); }); it('should write file with Base64', async () => { - const index = await store.write(sampleFile, sampleMimeType); + const index = await store.write(FILE, MIME_TYPE); expect(await store.exists(index)).toBeTrue(); }); it('should read file with index', async () => { - const index = await store.write(sampleFile, sampleMimeType); - expect(await store.read(index)).toEqual(sampleFile); + const index = await store.write(FILE, MIME_TYPE); + expect(await store.read(index)).toEqual(FILE); }); it('should delete file with index', async () => { - const index = await store.write(sampleFile, sampleMimeType); + const index = await store.write(FILE, MIME_TYPE); await store.delete(index); @@ -51,8 +46,8 @@ describe('ImageStore', () => { }); it('should delete file with index and thumbnail', async () => { - const index = await store.write(sampleFile, sampleMimeType); - await store.readThumbnail(index, sampleMimeType); + const index = await store.write(FILE, MIME_TYPE); + await store.readThumbnail(index, MIME_TYPE); await store.delete(index); @@ -60,10 +55,10 @@ describe('ImageStore', () => { }); it('should remove all files after drop', async () => { - const index1 = await store.write(sampleFile, sampleMimeType); + const index1 = await store.write(FILE, MIME_TYPE); const anotherFile = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII='; - const index2 = await store.write(anotherFile, sampleMimeType); + const index2 = await store.write(anotherFile, MIME_TYPE); await store.drop(); @@ -78,7 +73,7 @@ describe('ImageStore', () => { ); const indexes = await Promise.all( - files.map(file => store.write(file, sampleMimeType)) + files.map(file => store.write(file, MIME_TYPE)) ); for (const index of indexes) { @@ -94,7 +89,7 @@ describe('ImageStore', () => { const indexes = []; for (const file of files) { - indexes.push(await store.write(file, sampleMimeType)); + indexes.push(await store.write(file, MIME_TYPE)); } await Promise.all(indexes.map(index => store.delete(index))); @@ -105,8 +100,22 @@ describe('ImageStore', () => { }); it('should read thumbnail', async () => { - const index = await store.write(sampleFile, sampleMimeType); - const thumbnailFile = await store.readThumbnail(index, sampleMimeType); + const index = await store.write(FILE, MIME_TYPE); + const thumbnailFile = await store.readThumbnail(index, MIME_TYPE); expect(thumbnailFile).toBeTruthy(); }); + + it('should clear all files', async () => { + const index = await store.write(FILE, MIME_TYPE); + + await store.clear(); + + expect(await store.exists(index)).toBeFalse(); + }); }); + +const FILE = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; +const INDEX = + '93ae7d494fad0fb30cbf3ae746a39c4bc7a0f8bbf87fbb587a3f3c01f3c5ce20'; +const MIME_TYPE: MimeType = 'image/png'; diff --git a/src/app/shared/services/image-store/image-store.service.ts b/src/app/shared/services/image-store/image-store.service.ts index 8bbbf6f7f..86cd61169 100644 --- a/src/app/shared/services/image-store/image-store.service.ts +++ b/src/app/shared/services/image-store/image-store.service.ts @@ -219,9 +219,10 @@ export class ImageStore { return index; } - async drop() { + async clear() { await this.initialize(); - await this.thumbnailTable.drop(); + await this.extensionTable.clear(); + await this.thumbnailTable.clear(); return this.mutex.runExclusive(async () => { this.hasInitialized = false; await this.filesystemPlugin.rmdir({ @@ -231,6 +232,12 @@ export class ImageStore { }); }); } + + async drop() { + await this.clear(); + await this.extensionTable.drop(); + await this.thumbnailTable.drop(); + } } interface ImageExtension extends Tuple { diff --git a/src/app/shared/services/preference-manager/preference-manager.service.spec.ts b/src/app/shared/services/preference-manager/preference-manager.service.spec.ts index 8620368b7..5f8e25e1c 100644 --- a/src/app/shared/services/preference-manager/preference-manager.service.spec.ts +++ b/src/app/shared/services/preference-manager/preference-manager.service.spec.ts @@ -23,4 +23,22 @@ describe('PreferenceManager', () => { const id = 'id'; expect(manager.getPreferences(id)).toBe(manager.getPreferences(id)); }); + + it('should clear all preferences', async () => { + const key = 'key'; + const defaultValue = 99; + const preference1 = manager.getPreferences('id1'); + const preference2 = manager.getPreferences('id2'); + await preference1.setNumber(key, 1); + await preference2.setNumber(key, 2); + + await manager.clear(); + + expect(await preference1.getNumber(key, defaultValue)).toEqual( + defaultValue + ); + expect(await preference2.getNumber(key, defaultValue)).toEqual( + defaultValue + ); + }); }); diff --git a/src/app/shared/services/preference-manager/preference-manager.service.ts b/src/app/shared/services/preference-manager/preference-manager.service.ts index 292ce277d..890edd186 100644 --- a/src/app/shared/services/preference-manager/preference-manager.service.ts +++ b/src/app/shared/services/preference-manager/preference-manager.service.ts @@ -27,4 +27,10 @@ export class PreferenceManager { this.preferencesMap.set(id, created); return created; } + + async clear() { + return Promise.all( + [...this.preferencesMap.values()].map(preferences => preferences.clear()) + ); + } } diff --git a/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts b/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts index fb4e250b0..40408386c 100644 --- a/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts +++ b/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.spec.ts @@ -213,4 +213,15 @@ describe('CapacitorStoragePreferences', () => { done(); }); }); + + it('should clear idempotently', async () => { + const key = 'key'; + const expected = 2; + await preferences.setNumber(key, 1); + + await preferences.clear(); + await preferences.clear(); + + expect(await preferences.getNumber(key, expected)).toEqual(expected); + }); }); diff --git a/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts b/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts index e785c817a..7f0ee944c 100644 --- a/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts +++ b/src/app/shared/services/preference-manager/preferences/capacitor-storage-preferences/capacitor-storage-preferences.ts @@ -1,7 +1,9 @@ import { StoragePlugin } from '@capacitor/core'; import { Mutex } from 'async-mutex'; +import { isEqual } from 'lodash'; import { BehaviorSubject, defer, Observable } from 'rxjs'; -import { concatMap } from 'rxjs/operators'; +import { concatMap, distinctUntilChanged } from 'rxjs/operators'; +import { isNonNullable } from '../../../../../utils/rx-operators/rx-operators'; import { Preferences } from '../preferences'; export class CapacitorStoragePreferences implements Preferences { @@ -25,10 +27,15 @@ export class CapacitorStoragePreferences implements Preferences { return this.get$(key, defaultValue); } - get$(key: string, defaultValue: T) { + get$(key: string, defaultValue: boolean): Observable; + get$(key: string, defaultValue: number): Observable; + get$(key: string, defaultValue: string): Observable; + get$(key: string, defaultValue: SupportedTypes): Observable { return defer(() => this.initializeValue(key, defaultValue)).pipe( // tslint:disable-next-line: no-non-null-assertion - concatMap(() => this.subjects.get(key)!.asObservable() as Observable) + concatMap(() => this.subjects.get(key)!.asObservable()), + isNonNullable(), + distinctUntilChanged(isEqual) ); } @@ -42,30 +49,35 @@ export class CapacitorStoragePreferences implements Preferences { return this.get(key, defaultValue); } - private async get( + private async get(key: string, defaultValue: boolean): Promise; + private async get(key: string, defaultValue: number): Promise; + private async get(key: string, defaultValue: string): Promise; + private async get( key: string, - defaultValue: T - ) { + defaultValue: SupportedTypes + ): Promise { await this.initializeValue(key, defaultValue); // tslint:disable-next-line: no-non-null-assertion - return this.subjects.get(key)!.value as T; + return this.subjects.get(key)!.value; } - private async initializeValue( - key: string, - defaultValue: boolean | number | string - ) { + private async initializeValue(key: string, defaultValue: SupportedTypes) { if (this.subjects.has(key)) { + const subject$ = this.subjects.get(key); + if (subject$?.value === undefined) { + subject$?.next(defaultValue); + } return; } const value = await this.loadValue(key, defaultValue); - this.subjects.set(key, new BehaviorSubject(value)); + this.subjects.set( + key, + // tslint:disable-next-line: rxjs-no-explicit-generics + new BehaviorSubject(value) + ); } - private async loadValue( - key: string, - defaultValue: boolean | number | string - ) { + private async loadValue(key: string, defaultValue: SupportedTypes) { const rawValue = ( await this.storagePlugin.get({ key: this.toStorageKey(key) }) ).value; @@ -93,20 +105,29 @@ export class CapacitorStoragePreferences implements Preferences { return this.set(key, value); } - async set(key: string, value: T) { + async set(key: string, value: boolean): Promise; + async set(key: string, value: number): Promise; + async set(key: string, value: string): Promise; + async set(key: string, value: SupportedTypes): Promise { return this.mutex.runExclusive(async () => { await this.storeValue(key, value); if (!this.subjects.has(key)) { - this.subjects.set(key, new BehaviorSubject(value)); + this.subjects.set( + key, + // tslint:disable-next-line: rxjs-no-explicit-generics + new BehaviorSubject(value) + ); } // tslint:disable-next-line: no-non-null-assertion - const subject$ = this.subjects.get(key)! as BehaviorSubject; + const subject$ = this.subjects.get( + key + )! as BehaviorSubject; subject$.next(value); return value; }); } - private async storeValue(key: string, value: boolean | number | string) { + private async storeValue(key: string, value: SupportedTypes) { return this.storagePlugin.set({ key: this.toStorageKey(key), value: `${value}`, @@ -114,10 +135,10 @@ export class CapacitorStoragePreferences implements Preferences { } async clear() { - for (const key of this.subjects.keys()) { + for (const [key, subject$] of this.subjects) { await this.storagePlugin.remove({ key: this.toStorageKey(key) }); + subject$.next(undefined); } - this.subjects.clear(); return this; } @@ -125,3 +146,5 @@ export class CapacitorStoragePreferences implements Preferences { return `${this.id}_${key}`; } } + +type SupportedTypes = boolean | number | string; diff --git a/src/assets/i18n/en-us.json b/src/assets/i18n/en-us.json index 7b7f8d854..667151e6a 100644 --- a/src/assets/i18n/en-us.json +++ b/src/assets/i18n/en-us.json @@ -32,7 +32,6 @@ "signUp": "Sign Up", "required": "Required", "tooShort": "Too Short", - "talkingToTheServer": "Talking to the Server", "unknownError": "Unknown Error", "low": "Low", "high": "High", diff --git a/src/assets/i18n/zh-tw.json b/src/assets/i18n/zh-tw.json index 8f3bfd877..5ca6a8f0b 100644 --- a/src/assets/i18n/zh-tw.json +++ b/src/assets/i18n/zh-tw.json @@ -32,7 +32,6 @@ "signUp": "註冊", "required": "必填", "tooShort": "太短", - "talkingToTheServer": "與伺服器連線中", "unknownError": "未知的錯誤", "low": "低", "high": "高",