* refactor: implement custom ExpoImage wrapper for cache control Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app * refactor(ios): convert Gekidou to CocoaPods Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0 * npm audit * update fastlane * feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository * fix tests by mocking @mattermost/intune * feat: implement Intune MAM integration with comprehensive security enforcement Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls * fix alerts when no server database is present * Unify cache strategy * fix emit config changed after it was stored in the db * Handle Mid-Session Enrollment Detection * fix ADALLogOverrideDisabled missing in Fastfile * fix flow for initial enrollment * fix and add unit tests * enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release * Update intune submodule with addressed feedback * fix validate-intune-clean workflow * feat(intune): add comprehensive error handling and SAML+Entra support Add production-ready error handling for native Entra authentication with user-friendly i18n messages, comprehensive test coverage, and support for Entra login when server requires SAML. * update i18n * update intune submodule * update build-pr token * fix race condition between server auth and intune enrollment * fix CI workflow to build with intune * use deploy key for intune submodule * set the config directly in the submodule .git * debug injection * try setting GIT_SSH_COMMAND * remove action debug * fix server url input * match pod cache with intune hash * Fastfile and envs * have workflows check for intune/.git * have ci cache intune frameworks as well * update Fastlane to set no-cache to artifacts uploaded * fix s3 upload * fix pblist template * Attempt to remove the cache control for PR uploads to s3 * use hash from commit for S3 path * Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain * Fix surface errors from intune login * fix postinstall scripts * use cacheKey for draft md images * remove unnecessary double await during test * Have isMinimumLicenseTier accept valid license sku tier as target * Add missing Auth error messages * remove the last period for intune errors in i18n * do not call unenroll with wipe on manual logout * Fix tests and Intune error messages * do not filter any SSO type regardless of which is used for Intune * fix 412 to not retry * fix tests, app logs sharing and share_extension avatar cache * apply setScreenCapturePolicy on license change Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * re-apply screen capture on enrollment Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * use userData from intunr login and prevent getMe Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * Check for Biometrics and Jailbreak as we used to --------- Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
173 lines
6.3 KiB
TypeScript
173 lines
6.3 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
|
import DatabaseManager from '@database/manager';
|
|
import {DEFAULT_LOCALE} from '@i18n';
|
|
import {getRecentCustomStatuses} from '@queries/servers/system';
|
|
import {getCurrentUser, getUserById} from '@queries/servers/user';
|
|
import {logError} from '@utils/log';
|
|
|
|
import {addRecentReaction} from './reactions';
|
|
|
|
import type Model from '@nozbe/watermelondb/Model';
|
|
import type UserModel from '@typings/database/models/servers/user';
|
|
|
|
export async function setCurrentUserStatus(serverUrl: string, status: string): Promise<{error?: unknown}> {
|
|
try {
|
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const user = await getCurrentUser(database);
|
|
if (!user) {
|
|
throw new Error(`No current user for ${serverUrl}`);
|
|
}
|
|
|
|
if (user.status !== status) {
|
|
user.prepareStatus(status);
|
|
await operator.batchRecords([user], 'setCurrentUserStatus');
|
|
}
|
|
|
|
return {};
|
|
} catch (error) {
|
|
logError('Failed setCurrentUserStatus', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
export async function updateLocalCustomStatus(serverUrl: string, user: UserModel, customStatus?: UserCustomStatus): Promise<{error?: unknown}> {
|
|
try {
|
|
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const models: Model[] = [];
|
|
const currentProps = {...user.props, customStatus: customStatus || {}};
|
|
const userModel = user.prepareUpdate((u: UserModel) => {
|
|
u.props = currentProps;
|
|
});
|
|
|
|
models.push(userModel);
|
|
if (customStatus) {
|
|
const {models: customStatusModels} = await updateRecentCustomStatuses(serverUrl, customStatus, true);
|
|
if (customStatusModels?.length) {
|
|
models.push(...customStatusModels);
|
|
}
|
|
|
|
if (customStatus.emoji) {
|
|
const recentEmojis = await addRecentReaction(serverUrl, [customStatus.emoji], true);
|
|
if (Array.isArray(recentEmojis)) {
|
|
models.push(...recentEmojis);
|
|
}
|
|
}
|
|
}
|
|
|
|
await operator.batchRecords(models, 'updateLocalCustomStatus');
|
|
|
|
return {};
|
|
} catch (error) {
|
|
logError('Failed updateLocalCustomStatus', error);
|
|
return {error};
|
|
}
|
|
}
|
|
|
|
export const updateRecentCustomStatuses = async (serverUrl: string, customStatus: UserCustomStatus, prepareRecordsOnly = false, remove = false): Promise<{models?: Model[]; error?: unknown}> => {
|
|
try {
|
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const recentStatuses = await getRecentCustomStatuses(database);
|
|
const index = recentStatuses.findIndex((cs) => (
|
|
cs.emoji === customStatus.emoji &&
|
|
cs.text === customStatus.text &&
|
|
cs.duration === customStatus.duration
|
|
));
|
|
|
|
if (index !== -1) {
|
|
recentStatuses.splice(index, 1);
|
|
}
|
|
|
|
if (!remove) {
|
|
recentStatuses.unshift(customStatus);
|
|
}
|
|
|
|
const models = await operator.handleSystem({
|
|
systems: [{
|
|
id: SYSTEM_IDENTIFIERS.RECENT_CUSTOM_STATUS,
|
|
value: JSON.stringify(recentStatuses),
|
|
}],
|
|
prepareRecordsOnly,
|
|
});
|
|
return {models};
|
|
} catch (error) {
|
|
logError('Failed updateRecentCustomStatuses', error);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const updateLocalUser = async (
|
|
serverUrl: string,
|
|
userDetails: Partial<UserProfile> & { status?: string},
|
|
userId?: string,
|
|
) => {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
|
|
let user: UserModel | undefined;
|
|
|
|
if (userId) {
|
|
user = await getUserById(database, userId);
|
|
} else {
|
|
user = await getCurrentUser(database);
|
|
}
|
|
|
|
if (user) {
|
|
const u = user;
|
|
await database.write(async () => {
|
|
await u.update((userRecord: UserModel) => {
|
|
userRecord.authService = userDetails.auth_service ?? u.authService;
|
|
userRecord.email = userDetails.email ?? u.email;
|
|
userRecord.firstName = userDetails.first_name ?? u.firstName;
|
|
userRecord.lastName = userDetails.last_name ?? u.lastName;
|
|
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? u.lastPictureUpdate;
|
|
userRecord.locale = userDetails.locale ?? u.locale;
|
|
userRecord.nickname = userDetails.nickname ?? u.nickname;
|
|
userRecord.notifyProps = userDetails.notify_props ?? u.notifyProps;
|
|
userRecord.position = userDetails?.position ?? u.position;
|
|
userRecord.props = userDetails.props ?? u.props;
|
|
userRecord.roles = userDetails.roles ?? u.roles;
|
|
userRecord.status = userDetails?.status ?? u.status;
|
|
userRecord.timezone = userDetails.timezone ?? u.timezone;
|
|
userRecord.username = userDetails.username ?? u.username;
|
|
});
|
|
});
|
|
}
|
|
return {user};
|
|
} catch (error) {
|
|
logError('Failed updateLocalUser', error);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const storeProfile = async (serverUrl: string, profile: UserProfile) => {
|
|
try {
|
|
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const user = await getUserById(database, profile.id);
|
|
if (user) {
|
|
return {user};
|
|
}
|
|
|
|
const records = await operator.handleUsers({
|
|
users: [profile],
|
|
prepareRecordsOnly: false,
|
|
});
|
|
|
|
return {user: records[0]};
|
|
} catch (error) {
|
|
logError('Failed storeProfile', error);
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const getCurrentUserLocale = async (serverUrl: string): Promise<string> => {
|
|
try {
|
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
|
const user = await getCurrentUser(database);
|
|
return user?.locale || DEFAULT_LOCALE;
|
|
} catch {
|
|
return DEFAULT_LOCALE;
|
|
}
|
|
};
|