Gekidou session expired notification (#6639)

* Fix crash on Android when session expired notification is presented

* react-native-notification patch for schedule fix on android and open local notification on iOS

* Fix android scheduled session notification crash

* patch react-native-navigation to support blur/focus AppState on Android

* remove schedule session expired notification from login entry point

* schedule session expired notification actions

* add session manager

* Handle open session expired notification
This commit is contained in:
Elias Nahum
2022-09-18 06:57:55 -04:00
committed by GitHub
parent af77f74902
commit 4c389a49fa
23 changed files with 541 additions and 222 deletions

View File

@@ -2,14 +2,11 @@
// See LICENSE.txt for license information.
import {switchToChannelById} from '@actions/remote/channel';
import {getSessions} from '@actions/remote/session';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
import {isTablet} from '@utils/helpers';
import {logWarning} from '@utils/log';
import {scheduleExpiredNotification} from '@utils/notification';
import {deferredAppEntryActions, entry} from './gql_common';
@@ -50,27 +47,6 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
return {error: clData.error};
}
// schedule local push notification if needed
if (clData.config) {
if (clData.config.ExtendSessionLengthWithActivity !== 'true') {
const timeOut = setTimeout(async () => {
clearTimeout(timeOut);
let sessions: Session[]|undefined;
try {
sessions = await getSessions(serverUrl, 'me');
} catch (e) {
logWarning('Failed to get user sessions', e);
return;
}
if (sessions && Array.isArray(sessions)) {
scheduleExpiredNotification(sessions, clData.config?.SiteName || serverUrl, user.locale);
}
}, 500);
}
}
const entryData = await entry(serverUrl, '', '');
if ('error' in entryData) {

View File

@@ -1,18 +1,23 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceEventEmitter} from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import {DeviceEventEmitter, Platform} from 'react-native';
import {Database, Events} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getServerCredentials} from '@init/credentials';
import PushNotifications from '@init/push_notifications';
import NetworkManager from '@managers/network_manager';
import WebsocketManager from '@managers/websocket_manager';
import {getDeviceToken} from '@queries/app/global';
import {getCurrentUserId, getCommonSystemValues} from '@queries/servers/system';
import {queryServerName} from '@queries/app/servers';
import {getCurrentUserId, getCommonSystemValues, getExpiredSession} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import EphemeralStore from '@store/ephemeral_store';
import {logWarning} from '@utils/log';
import {logWarning, logError} from '@utils/log';
import {scheduleExpiredNotification} from '@utils/notification';
import {getCSRFFromCookie} from '@utils/security';
import {getDeviceTimezone, isTimezoneEnabled} from '@utils/timezone';
@@ -91,7 +96,7 @@ export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientError
return {error: null};
};
export const getSessions = async (serverUrl: string, currentUserId: string) => {
export const fetchSessions = async (serverUrl: string, currentUserId: string) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
@@ -102,6 +107,7 @@ export const getSessions = async (serverUrl: string, currentUserId: string) => {
try {
return await client.getSessions(currentUserId);
} catch (e) {
logError('fetchSessions', e);
await forceLogoutIfNecessary(serverUrl, e as ClientError);
}
@@ -166,7 +172,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
}
};
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false) => {
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false, skipEvents = false) => {
if (!skipServerLogout) {
try {
const client = NetworkManager.getClient(serverUrl);
@@ -177,7 +183,65 @@ export const logout = async (serverUrl: string, skipServerLogout = false, remove
}
}
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
if (!skipEvents) {
DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
}
};
export const cancelSessionNotification = async (serverUrl: string) => {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const expiredSession = await getExpiredSession(database);
const rechable = (await NetInfo.fetch()).isInternetReachable;
if (expiredSession?.notificationId && rechable) {
PushNotifications.cancelScheduleNotification(parseInt(expiredSession.notificationId, 10));
operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
value: '',
}],
prepareRecordsOnly: false,
});
}
} catch (e) {
logError('cancelSessionNotification', e);
}
};
export const scheduleSessionNotification = async (serverUrl: string) => {
try {
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const sessions = await fetchSessions(serverUrl, 'me');
const user = await getCurrentUser(database);
const serverName = await queryServerName(appDatabase, serverUrl);
await cancelSessionNotification(serverUrl);
if (sessions) {
const session = await findSession(serverUrl, sessions);
if (session) {
const sessionId = session.id;
const notificationId = scheduleExpiredNotification(serverUrl, session, serverName, user?.locale);
operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.SESSION_EXPIRATION,
value: {
id: sessionId,
notificationId,
expiresAt: session.expires_at,
},
}],
prepareRecordsOnly: false,
});
}
}
} catch (e) {
logError('scheduleExpiredNotification', e);
await forceLogoutIfNecessary(serverUrl, e as ClientError);
}
};
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
@@ -251,3 +315,47 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
return {error: error as ClientError, failed: false, time: 0};
}
};
async function findSession(serverUrl: string, sessions: Session[]) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const {database: appDatabase} = DatabaseManager.getAppDatabaseAndOperator();
const expiredSession = await getExpiredSession(database);
const deviceToken = await getDeviceToken(appDatabase);
// First try and find the session by the given identifier hyqddef7jjdktqiyy36gxa8sqy
let session = sessions.find((s) => s.id === expiredSession?.id);
if (session) {
return session;
}
// Next try and find the session by deviceId
if (deviceToken) {
session = sessions.find((s) => s.device_id === deviceToken);
if (session) {
return session;
}
}
// Next try and find the session by the CSRF token
const csrfToken = await getCSRFFromCookie(serverUrl);
if (csrfToken) {
session = sessions.find((s) => s.props?.csrf === csrfToken);
if (session) {
return session;
}
}
// Next try and find the session based on the OS
// if multiple sessions exists with the same os type this can be inaccurate
session = sessions.find((s) => s.props?.os.toLowerCase() === Platform.OS);
if (session) {
return session;
}
} catch (e) {
logError('findSession', e);
}
// At this point we did not find the session
return undefined;
}