From 4993f9a71b8d3fe444d8d69b7afa3c27f54610df Mon Sep 17 00:00:00 2001 From: Alfonso Salces Date: Wed, 9 Aug 2023 15:04:44 +0200 Subject: [PATCH 1/2] MOBILE-4405 storage: Create storage service --- src/core/initializers/initialize-databases.ts | 5 + src/core/services/database/index.ts | 7 + src/core/services/database/storage.ts | 55 +++++ src/core/services/storage.ts | 222 ++++++++++++++++++ 4 files changed, 289 insertions(+) create mode 100644 src/core/services/database/storage.ts create mode 100644 src/core/services/storage.ts diff --git a/src/core/initializers/initialize-databases.ts b/src/core/initializers/initialize-databases.ts index 4785c945539..1a0eb20ee78 100644 --- a/src/core/initializers/initialize-databases.ts +++ b/src/core/initializers/initialize-databases.ts @@ -18,7 +18,11 @@ import { CoreCronDelegate } from '@services/cron'; import { CoreFilepool } from '@services/filepool'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreSites } from '@services/sites'; +import { CoreStorage } from '@services/storage'; +/** + * Init databases instances. + */ export default async function(): Promise { await Promise.all([ CoreApp.initializeDatabase(), @@ -27,5 +31,6 @@ export default async function(): Promise { CoreFilepool.initializeDatabase(), CoreLocalNotifications.initializeDatabase(), CoreSites.initializeDatabase(), + CoreStorage.initializeDatabase(), ]); } diff --git a/src/core/services/database/index.ts b/src/core/services/database/index.ts index ab8d9bd6861..e55368ffe31 100644 --- a/src/core/services/database/index.ts +++ b/src/core/services/database/index.ts @@ -18,7 +18,13 @@ import { CORE_SITE_SCHEMAS } from '@services/sites'; import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './filepool'; import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './sites'; import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './sync'; +import { SITE_SCHEMA as STORAGE_SITE_SCHEMA } from './storage'; +/** + * Give database providers. + * + * @returns database providers + */ export function getDatabaseProviders(): Provider[] { return [{ provide: CORE_SITE_SCHEMAS, @@ -26,6 +32,7 @@ export function getDatabaseProviders(): Provider[] { FILEPOOL_SITE_SCHEMA, SITES_SITE_SCHEMA, SYNC_SITE_SCHEMA, + STORAGE_SITE_SCHEMA, ], multi: true, }]; diff --git a/src/core/services/database/storage.ts b/src/core/services/database/storage.ts new file mode 100644 index 00000000000..6744b673da7 --- /dev/null +++ b/src/core/services/database/storage.ts @@ -0,0 +1,55 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { SQLiteDBTableSchema } from '@classes/sqlitedb'; +import { CoreAppSchema } from '@services/app'; +import { CoreSiteSchema } from '@services/sites'; + +export const TABLE_NAME = 'core_storage'; + +export const TABLE_SCHEMA: SQLiteDBTableSchema = { + name: TABLE_NAME, + columns: [ + { + name: 'key', + type: 'TEXT', + primaryKey: true, + }, + { + name: 'value', + type: 'TEXT', + notNull: true, + }, + ], +}; + +export const APP_SCHEMA: CoreAppSchema = { + name: 'CoreStorageService', + version: 1, + tables: [TABLE_SCHEMA], +}; + +export const SITE_SCHEMA: CoreSiteSchema = { + name: 'CoreStorageService', + version: 1, + tables: [TABLE_SCHEMA], +}; + +/** + * Storage table record type. + */ +export type CoreStorageRecord = { + key: string; + value: string; +}; diff --git a/src/core/services/storage.ts b/src/core/services/storage.ts new file mode 100644 index 00000000000..a2fb8fc380f --- /dev/null +++ b/src/core/services/storage.ts @@ -0,0 +1,222 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Inject, Injectable, Optional } from '@angular/core'; + +import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; +import { CoreApp } from '@services/app'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { makeSingleton } from '@singletons'; +import { SQLiteDB } from '@classes/sqlitedb'; + +import { APP_SCHEMA, CoreStorageRecord, TABLE_NAME } from './database/storage'; +import { CoreSites } from './sites'; +import { CoreSite } from '@classes/site'; + +/** + * Service to store data using key-value pairs. + * + * The data can be scoped to a single site using CoreStorage.forSite(site), and it will be automatically cleared + * when the site is deleted. + * + * For tabular data, use CoreAppProvider.getDB() or CoreSite.getDb(). + */ +@Injectable({ providedIn: 'root' }) +export class CoreStorageService { + + table: AsyncInstance; + + constructor(@Optional() @Inject(null) lazyTableConstructor?: () => Promise) { + this.table = asyncInstance(lazyTableConstructor); + } + + /** + * Initialize database. + */ + async initializeDatabase(): Promise { + try { + await CoreApp.createTablesFromSchema(APP_SCHEMA); + } catch (e) { + // Ignore errors. + } + + await this.initializeTable(CoreApp.getDB()); + } + + /** + * Initialize table. + * + * @param database Database. + */ + async initializeTable(database: SQLiteDB): Promise { + const table = await getStorageTable(database); + + this.table.setInstance(table); + } + + /** + * Get value. + * + * @param key Data key. + * @param defaultValue Value to return if the key wasn't found. + * @returns Data value. + */ + async get(key: string): Promise; + async get(key: string, defaultValue: T): Promise; + async get(key: string, defaultValue: T | null = null): Promise { + try { + const { value } = await this.table.getOneByPrimaryKey({ key }); + + return JSON.parse(value); + } catch (error) { + return defaultValue; + } + } + + /** + * Get value directly from the database, without using any optimizations.. + * + * @param key Data key. + * @param defaultValue Value to return if the key wasn't found. + * @returns Data value. + */ + async getFromDB(key: string): Promise; + async getFromDB(key: string, defaultValue: T): Promise; + async getFromDB(key: string, defaultValue: T | null = null): Promise { + try { + const db = CoreApp.getDB(); + const { value } = await db.getRecord(TABLE_NAME, { key }); + + return JSON.parse(value); + } catch (error) { + return defaultValue; + } + } + + /** + * Set value. + * + * @param key Data key. + * @param value Data value. + */ + async set(key: string, value: unknown): Promise { + await this.table.insert({ key, value: JSON.stringify(value) }); + } + + /** + * Check if value exists. + * + * @param key Data key. + * @returns Whether key exists or not. + */ + async has(key: string): Promise { + return this.table.hasAny({ key }); + } + + /** + * Remove value. + * + * @param key Data key. + */ + async remove(key: string): Promise { + await this.table.deleteByPrimaryKey({ key }); + } + + /** + * Get the core_storage table of the current site. + * + * @returns CoreStorageService instance with the core_storage table. + */ + forCurrentSite(): AsyncInstance> { + return asyncInstance(async () => { + const siteId = await CoreSites.getStoredCurrentSiteId(); + const site = await CoreSites.getSite(siteId); + + if (!(siteId in SERVICE_INSTANCES)) { + SERVICE_INSTANCES[siteId] = asyncInstance(async () => { + const instance = new CoreStorageService(); + await instance.initializeTable(site.getDb()); + + return instance; + }); + } + + return await SERVICE_INSTANCES[siteId].getInstance(); + }); + } + + /** + * Get the core_storage table for the provided site. + * + * @param site Site from which we will obtain the storage. + * @returns CoreStorageService instance with the core_storage table. + */ + forSite(site: CoreSite): AsyncInstance> { + const siteId = site.getId(); + + return asyncInstance(async () => { + if (!(siteId in SERVICE_INSTANCES)) { + const instance = new CoreStorageService(); + await instance.initializeTable(site.getDb()); + + SERVICE_INSTANCES[siteId] = asyncInstance(() => instance); + } + + return await SERVICE_INSTANCES[siteId].getInstance(); + }); + } + +} + +export const CoreStorage = makeSingleton(CoreStorageService); + +const SERVICE_INSTANCES: Record> = {}; +const TABLE_INSTANCES: WeakMap> = new WeakMap(); + +/** + * Helper function to get a storage table for the given database. + * + * @param database Database. + * @returns Storage table. + */ +function getStorageTable(database: SQLiteDB): Promise { + const existingTable = TABLE_INSTANCES.get(database); + + if (existingTable) { + return existingTable; + } + + const table = new Promise((resolve, reject) => { + const tableProxy = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + database, + TABLE_NAME, + ['key'], + ); + + tableProxy.initialize() + .then(() => resolve(tableProxy)) + .catch(reject); + }); + + TABLE_INSTANCES.set(database, table); + + return table; +} + +/** + * Storage table. + */ +type CoreStorageTable = CoreDatabaseTable; From 56caf062d8137a6c9a543eb48e98d6720a03bc02 Mon Sep 17 00:00:00 2001 From: Alfonso Salces Date: Wed, 9 Aug 2023 15:05:17 +0200 Subject: [PATCH 2/2] MOBILE-4405 autologout: Create autologout service --- .../autologout/services/autologout.ts | 274 ++++++++++++++++++ src/core/features/compile/services/compile.ts | 2 + src/core/initializers/initialize-services.ts | 2 + src/core/services/sites.ts | 27 ++ 4 files changed, 305 insertions(+) create mode 100644 src/core/features/autologout/services/autologout.ts diff --git a/src/core/features/autologout/services/autologout.ts b/src/core/features/autologout/services/autologout.ts new file mode 100644 index 00000000000..4d721cacf80 --- /dev/null +++ b/src/core/features/autologout/services/autologout.ts @@ -0,0 +1,274 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSite } from '@classes/site'; +import { CorePlatform } from '@services/platform'; +import { CoreSites } from '@services/sites'; +import { CoreStorage } from '@services/storage'; +import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { Subscription } from 'rxjs'; + +/** + * Auto logout service + */ +@Injectable({ providedIn: 'root' }) +export class CoreAutoLogoutService { + + /** + * Timestamp indicating the last time the application was in the foreground. + */ + protected static readonly TIMESTAMP_DB_KEY = 'CoreAutoLogoutTimestamp'; + + /** + * How often we will store a timestamp (in miliseconds). + */ + protected static readonly DEFAULT_TIMESTAMP_STORE_TIME = 10000; + + /** + * Grace period if you return to the application too soon (in miliseconds). + */ + protected static readonly GRACE_PERIOD = 30000; + + protected platformResumeSubscription?: Subscription; + protected platformPauseSubscription?: Subscription; + protected interval?: ReturnType; + protected backgroundTimestamp?: number; + + /** + * Initialize. + */ + initialize(): void { + CoreEvents.on(CoreEvents.LOGIN, async() => await this.refreshListeners()); + CoreEvents.on(CoreEvents.LOGOUT, async() => { + this.cancelListeners(); + const storage = CoreStorage.forCurrentSite(); + await storage.remove(CoreAutoLogoutService.TIMESTAMP_DB_KEY); + }); + } + + /** + * Refresh listeners for auto logout. + */ + async refreshListeners(): Promise { + if (!CoreSites.isLoggedIn()) { + return; + } + + const site = CoreSites.getCurrentSite(); + + if (!site) { + return; + } + + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + this.cancelListeners(); + + if (!autoLogoutType || autoLogoutType === CoreAutoLogoutType.NEVER) { + return; + } + + if (autoLogoutType === CoreAutoLogoutType.CUSTOM) { + await this.setTimestamp(); + this.setInterval(); + } + + this.platformPauseSubscription = CorePlatform.pause.subscribe(async () => { + this.backgroundTimestamp = new Date().getTime(); + this.clearInterval(); + }); + + this.platformResumeSubscription = CorePlatform.resume.subscribe(async () => { + if (autoLogoutType !== CoreAutoLogoutType.CUSTOM) { + await this.handleAppClosed(site); + + return; + } + + const autoLogoutTime = Number(site.getStoredConfig('tool_mobile_autologouttime')); + const loggedOut = await this.handleSessionClosed(autoLogoutTime, site); + + if (!loggedOut) { + await this.setTimestamp(); + this.setInterval(); + } + }); + } + + /** + * Set site logged out. + * + * @param siteId site id. + */ + protected async logout(siteId: string): Promise { + await CoreSites.setSiteLoggedOut(siteId, true); + } + + /** + * Saves stored timestamp. + */ + protected async setTimestamp(): Promise { + const date = new Date().getTime(); + const storage = CoreStorage.forCurrentSite(); + await storage.set(CoreAutoLogoutService.TIMESTAMP_DB_KEY, date); + } + + /** + * Gives if auto logout can be displayed. + * + * @returns true if can display, false if not. + */ + async canShowPreference(): Promise { + const site = CoreSites.getCurrentSite(); + + if (!site) { + return false; + } + + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + + return autoLogoutType !== CoreAutoLogoutType.NEVER; + } + + /** + * Cancel uncompleted listeners. + */ + protected cancelListeners(): void { + this.clearInterval(); + this.platformResumeSubscription?.unsubscribe(); + this.platformPauseSubscription?.unsubscribe(); + delete this.platformPauseSubscription; + delete this.platformResumeSubscription; + } + + /** + * Set interval. + */ + protected setInterval(): void { + this.interval = setInterval(async () => await this.setTimestamp(), CoreAutoLogoutService.DEFAULT_TIMESTAMP_STORE_TIME); + } + + /** + * Clear interval. + */ + protected clearInterval(): void { + if (!this.interval) { + return; + } + + clearInterval(this.interval); + delete this.interval; + } + + /** + * Logout user if his session is expired. + * + * @param sessionDuration Session duration. + * @param site Current site. + * @returns Whether site has been logged out. + */ + async handleSessionClosed(sessionDuration: number, site: CoreSite): Promise { + if (!site.id) { + return false; + } + + const storage = CoreStorage.forSite(site); + const savedTimestamp = await storage.get(CoreAutoLogoutService.TIMESTAMP_DB_KEY); + + if (!savedTimestamp) { + return false; + } + + // Get expiration time from site preferences as miliseconds. + const expirationDate = savedTimestamp + ((sessionDuration || 0) * 1000); + await storage.remove(CoreAutoLogoutService.TIMESTAMP_DB_KEY); + + if (new Date().getTime() < expirationDate) { + return false; + } + + await this.logout(site.id); + + return true; + } + + /** + * Logout if user closed the app. + * + * @param site Current site. + * @returns Whether site has been logged out. + */ + async handleAppClosed(site: CoreSite): Promise { + if (!site.id) { + return false; + } + + if ( + this.backgroundTimestamp && + (this.backgroundTimestamp + CoreAutoLogoutService.GRACE_PERIOD) > new Date().getTime() + ) { + delete this.backgroundTimestamp; + + return false; + } + + await this.logout(site.id); + + return true; + } + + getConfig(): { autoLogoutType: CoreAutoLogoutType; autoLogoutTime: number } { + const site = CoreSites.getRequiredCurrentSite(); + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + const autoLogoutTime = Number(site.getStoredConfig('tool_mobile_autologouttime')); + + return { autoLogoutType, autoLogoutTime }; + } + +} + +export type CoreAutoLogoutSessionConfig = { + type: CoreAutoLogoutType.CUSTOM; + sessionDuration: number; +}; + +export type CoreAutoLogoutOtherConfig = { + type: Exclude; +}; + +/** + * Possible automatic logout cases. + */ +export enum CoreAutoLogoutType { + /** + * Disabled automatic logout. + */ + NEVER = 0, + + /** + * When the user closes the app, in next login he need to login again. + */ + INMEDIATE = 1, + + /** + * This applies when session time is set. If the user closes the app more time than the specified, + * then, the user must login again. + */ + CUSTOM = 2, +}; + +export type CoreAutoLogoutConfig = CoreAutoLogoutSessionConfig | CoreAutoLogoutOtherConfig; + +export const CoreAutoLogout = makeSingleton(CoreAutoLogoutService); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index b9c5ae548b2..fb655955996 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -158,6 +158,7 @@ import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.m import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; +import { CoreAutoLogoutService } from '@features/autologout/services/autologout'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -268,6 +269,7 @@ export class CoreCompileProvider { injectLibraries(instance: any, extraProviders: Type[] = []): void { const providers = [ ...CORE_SERVICES, + CoreAutoLogoutService, ...CORE_BLOCK_SERVICES, ...CORE_COMMENTS_SERVICES, ...CORE_CONTENTLINKS_SERVICES, diff --git a/src/core/initializers/initialize-services.ts b/src/core/initializers/initialize-services.ts index 521afe5343f..77d7b4ae8ae 100644 --- a/src/core/initializers/initialize-services.ts +++ b/src/core/initializers/initialize-services.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreAutoLogout } from '@features/autologout/services/autologout'; import { CoreConfig } from '@services/config'; import { CoreFilepool } from '@services/filepool'; import { CoreLang } from '@services/lang'; @@ -31,5 +32,6 @@ export default async function(): Promise { CoreNetwork.initialize(), CoreUpdateManager.initialize(), CoreTimeUtils.initialize(), + CoreAutoLogout.initialize(), ]); } diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 29e383242e1..a722fc1404d 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -65,6 +65,7 @@ import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest import { CoreLang, CoreLangFormat } from '@services/lang'; import { CoreNative } from '@features/native/services/native'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreAutoLogoutType, CoreAutoLogout } from '@features/autologout/services/autologout'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; @@ -1461,6 +1462,8 @@ export class CoreSitesProvider { * @returns Promise resolved if a session is restored. */ async restoreSession(): Promise { + await this.handleAutoLogout(); + if (this.sessionRestored) { return Promise.reject(new CoreError('Session already restored.')); } @@ -1477,6 +1480,30 @@ export class CoreSitesProvider { } } + /** + * Handle auto logout by checking autologout type and time if its required. + */ + async handleAutoLogout(): Promise { + await CoreUtils.ignoreErrors(( async () => { + const siteId = await this.getStoredCurrentSiteId(); + const site = await this.getSite(siteId); + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + const autoLogoutTime = Number(site.getStoredConfig('tool_mobile_autologouttime')); + + if (autoLogoutType === CoreAutoLogoutType.NEVER || !site.id) { + return; + } + + if (autoLogoutType === CoreAutoLogoutType.CUSTOM) { + await CoreAutoLogout.handleSessionClosed(autoLogoutTime, site); + + return; + } + + await CoreAutoLogout.handleAppClosed(site); + })()); + } + /** * Mark a site as logged out so the user needs to authenticate again. *