Merge pull request #3777 from alfonso-salces/MOBILE-4405

Mobile 4405 - Implement auto logout
main
Noel De Martin 2023-09-26 15:44:04 +02:00 committed by GitHub
commit 038bcfaa30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 594 additions and 0 deletions

View File

@ -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<typeof setInterval>;
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<void> {
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<void> {
await CoreSites.setSiteLoggedOut(siteId, true);
}
/**
* Saves stored timestamp.
*/
protected async setTimestamp(): Promise<void> {
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<boolean> {
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<boolean> {
if (!site.id) {
return false;
}
const storage = CoreStorage.forSite(site);
const savedTimestamp = await storage.get<number>(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<boolean> {
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<CoreAutoLogoutType, CoreAutoLogoutType.CUSTOM>;
};
/**
* 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);

View File

@ -158,6 +158,7 @@ import { ADDON_PRIVATEFILES_SERVICES } from '@addons/privatefiles/privatefiles.m
import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module'; import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/components.module';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { CoreAutoLogoutService } from '@features/autologout/services/autologout';
/** /**
* Service to provide functionalities regarding compiling dynamic HTML and Javascript. * Service to provide functionalities regarding compiling dynamic HTML and Javascript.
@ -268,6 +269,7 @@ export class CoreCompileProvider {
injectLibraries(instance: any, extraProviders: Type<unknown>[] = []): void { injectLibraries(instance: any, extraProviders: Type<unknown>[] = []): void {
const providers = [ const providers = [
...CORE_SERVICES, ...CORE_SERVICES,
CoreAutoLogoutService,
...CORE_BLOCK_SERVICES, ...CORE_BLOCK_SERVICES,
...CORE_COMMENTS_SERVICES, ...CORE_COMMENTS_SERVICES,
...CORE_CONTENTLINKS_SERVICES, ...CORE_CONTENTLINKS_SERVICES,

View File

@ -18,7 +18,11 @@ import { CoreCronDelegate } from '@services/cron';
import { CoreFilepool } from '@services/filepool'; import { CoreFilepool } from '@services/filepool';
import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreLocalNotifications } from '@services/local-notifications';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreStorage } from '@services/storage';
/**
* Init databases instances.
*/
export default async function(): Promise<void> { export default async function(): Promise<void> {
await Promise.all([ await Promise.all([
CoreApp.initializeDatabase(), CoreApp.initializeDatabase(),
@ -27,5 +31,6 @@ export default async function(): Promise<void> {
CoreFilepool.initializeDatabase(), CoreFilepool.initializeDatabase(),
CoreLocalNotifications.initializeDatabase(), CoreLocalNotifications.initializeDatabase(),
CoreSites.initializeDatabase(), CoreSites.initializeDatabase(),
CoreStorage.initializeDatabase(),
]); ]);
} }

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreAutoLogout } from '@features/autologout/services/autologout';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreFilepool } from '@services/filepool'; import { CoreFilepool } from '@services/filepool';
import { CoreLang } from '@services/lang'; import { CoreLang } from '@services/lang';
@ -31,5 +32,6 @@ export default async function(): Promise<void> {
CoreNetwork.initialize(), CoreNetwork.initialize(),
CoreUpdateManager.initialize(), CoreUpdateManager.initialize(),
CoreTimeUtils.initialize(), CoreTimeUtils.initialize(),
CoreAutoLogout.initialize(),
]); ]);
} }

View File

@ -18,7 +18,13 @@ import { CORE_SITE_SCHEMAS } from '@services/sites';
import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './filepool'; import { SITE_SCHEMA as FILEPOOL_SITE_SCHEMA } from './filepool';
import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './sites'; import { SITE_SCHEMA as SITES_SITE_SCHEMA } from './sites';
import { SITE_SCHEMA as SYNC_SITE_SCHEMA } from './sync'; 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[] { export function getDatabaseProviders(): Provider[] {
return [{ return [{
provide: CORE_SITE_SCHEMAS, provide: CORE_SITE_SCHEMAS,
@ -26,6 +32,7 @@ export function getDatabaseProviders(): Provider[] {
FILEPOOL_SITE_SCHEMA, FILEPOOL_SITE_SCHEMA,
SITES_SITE_SCHEMA, SITES_SITE_SCHEMA,
SYNC_SITE_SCHEMA, SYNC_SITE_SCHEMA,
STORAGE_SITE_SCHEMA,
], ],
multi: true, multi: true,
}]; }];

View File

@ -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;
};

View File

@ -65,6 +65,7 @@ import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest
import { CoreLang, CoreLangFormat } from '@services/lang'; import { CoreLang, CoreLangFormat } from '@services/lang';
import { CoreNative } from '@features/native/services/native'; import { CoreNative } from '@features/native/services/native';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreAutoLogoutType, CoreAutoLogout } from '@features/autologout/services/autologout';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS'); export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; 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. * @returns Promise resolved if a session is restored.
*/ */
async restoreSession(): Promise<void> { async restoreSession(): Promise<void> {
await this.handleAutoLogout();
if (this.sessionRestored) { if (this.sessionRestored) {
return Promise.reject(new CoreError('Session already restored.')); 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<void> {
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. * Mark a site as logged out so the user needs to authenticate again.
* *

View File

@ -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<CoreStorageTable>;
constructor(@Optional() @Inject(null) lazyTableConstructor?: () => Promise<CoreStorageTable>) {
this.table = asyncInstance(lazyTableConstructor);
}
/**
* Initialize database.
*/
async initializeDatabase(): Promise<void> {
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<void> {
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<T=unknown>(key: string): Promise<T | null>;
async get<T>(key: string, defaultValue: T): Promise<T>;
async get<T=unknown>(key: string, defaultValue: T | null = null): Promise<T | null> {
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<T=unknown>(key: string): Promise<T | null>;
async getFromDB<T>(key: string, defaultValue: T): Promise<T>;
async getFromDB<T=unknown>(key: string, defaultValue: T | null = null): Promise<T | null> {
try {
const db = CoreApp.getDB();
const { value } = await db.getRecord<CoreStorageRecord>(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<void> {
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<boolean> {
return this.table.hasAny({ key });
}
/**
* Remove value.
*
* @param key Data key.
*/
async remove(key: string): Promise<void> {
await this.table.deleteByPrimaryKey({ key });
}
/**
* Get the core_storage table of the current site.
*
* @returns CoreStorageService instance with the core_storage table.
*/
forCurrentSite(): AsyncInstance<Omit<CoreStorageService, 'forSite' | 'forCurrentSite'>> {
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<Omit<CoreStorageService, 'forSite' | 'forCurrentSite'>> {
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<string, AsyncInstance<CoreStorageService>> = {};
const TABLE_INSTANCES: WeakMap<SQLiteDB, Promise<CoreStorageTable>> = new WeakMap();
/**
* Helper function to get a storage table for the given database.
*
* @param database Database.
* @returns Storage table.
*/
function getStorageTable(database: SQLiteDB): Promise<CoreStorageTable> {
const existingTable = TABLE_INSTANCES.get(database);
if (existingTable) {
return existingTable;
}
const table = new Promise<CoreStorageTable>((resolve, reject) => {
const tableProxy = new CoreDatabaseTableProxy<CoreStorageRecord, 'key'>(
{ 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<CoreStorageRecord, 'key'>;