Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
179 lines
4.9 KiB
TypeScript
179 lines
4.9 KiB
TypeScript
import type { ILivechatAgent, IUser, Serialized } from '@rocket.chat/core-typings';
|
|
import { createTransformFromUpdateFilter } from '@rocket.chat/mongo-adapter';
|
|
import { create } from 'zustand';
|
|
|
|
import { sdk } from './SDKClient';
|
|
import { Users } from '../stores';
|
|
|
|
export const useUserDataSyncReady = create(() => false);
|
|
|
|
type RawUserData = Serialized<
|
|
Pick<
|
|
IUser,
|
|
| '_id'
|
|
| 'type'
|
|
| 'name'
|
|
| 'username'
|
|
| 'emails'
|
|
| 'status'
|
|
| 'statusDefault'
|
|
| 'statusText'
|
|
| 'statusExpiresAt'
|
|
| 'statusConnection'
|
|
| 'avatarOrigin'
|
|
| 'utcOffset'
|
|
| 'language'
|
|
| 'settings'
|
|
| 'roles'
|
|
| 'active'
|
|
| 'defaultRoom'
|
|
| 'customFields'
|
|
| 'oauth'
|
|
| 'createdAt'
|
|
| '_updatedAt'
|
|
| 'avatarETag'
|
|
> & { statusLivechat?: ILivechatAgent['statusLivechat'] }
|
|
>;
|
|
|
|
const updateUser = (userData: IUser): void => {
|
|
const user = Users.state.get(userData._id);
|
|
|
|
if (!user?._updatedAt || user._updatedAt.getTime() < userData._updatedAt.getTime()) {
|
|
Users.state.store(userData);
|
|
return;
|
|
}
|
|
|
|
// delete data already on user's collection as those are newer
|
|
for (const key of Object.keys(user)) {
|
|
delete userData[key as keyof IUser];
|
|
}
|
|
|
|
Users.state.update(
|
|
({ _id }) => _id === user._id,
|
|
(user) => ({ ...user, ...userData }),
|
|
);
|
|
};
|
|
|
|
let cancel: undefined | (() => void);
|
|
export const synchronizeUserData = async (uid: IUser['_id']): Promise<RawUserData | void> => {
|
|
if (!uid) return;
|
|
|
|
// Remove data from any other user that we may have retained
|
|
Users.state.remove((record) => record._id !== uid);
|
|
cancel?.();
|
|
|
|
const result = sdk.stream('notify-user', [`${uid}/userData`], (data) => {
|
|
switch (data.type) {
|
|
case 'inserted': {
|
|
const { type, id, ...user } = data;
|
|
Users.state.store(user.data);
|
|
break;
|
|
}
|
|
|
|
case 'updated': {
|
|
const transform = createTransformFromUpdateFilter<IUser>({ $unset: data.unset as Record<string, 1>, $set: data.diff });
|
|
Users.state.update(({ _id }) => _id === uid, transform);
|
|
break;
|
|
}
|
|
|
|
case 'removed':
|
|
Users.state.delete(uid);
|
|
break;
|
|
}
|
|
});
|
|
|
|
cancel = result.stop;
|
|
await result.ready();
|
|
|
|
const { success: _success, ldap, lastLogin, services: rawServices, ...fromMe } = await sdk.rest.get('/v1/me');
|
|
|
|
if (fromMe._id) {
|
|
const existingUser = Users.state.get(uid);
|
|
const { email: _meEmail, ...meFields } = fromMe;
|
|
const { email, cloud, resume, email2fa, emailCode, ...services } = (rawServices ?? {}) as NonNullable<IUser['services']>;
|
|
|
|
const mergedEmail2fa =
|
|
email2fa &&
|
|
(() => {
|
|
const { changedAt: apiChangedAt, ...email2faRest } = email2fa;
|
|
let changedAt: Date | undefined;
|
|
if (apiChangedAt != null) {
|
|
const parsed = new Date(apiChangedAt as string | number | Date);
|
|
if (!Number.isNaN(parsed.getTime())) {
|
|
changedAt = parsed;
|
|
}
|
|
}
|
|
if (changedAt == null && existingUser?.services?.email2fa?.changedAt != null) {
|
|
const prev = existingUser.services.email2fa.changedAt;
|
|
changedAt = prev instanceof Date ? prev : new Date(prev);
|
|
}
|
|
return {
|
|
...email2faRest,
|
|
...(changedAt != null ? { changedAt } : {}),
|
|
};
|
|
})();
|
|
|
|
updateUser({
|
|
type: existingUser?.type ?? 'user',
|
|
active: existingUser?.active ?? true,
|
|
roles: existingUser?.roles ?? [],
|
|
...existingUser,
|
|
...meFields,
|
|
...(rawServices && {
|
|
services: {
|
|
...(services ? { ...services } : {}),
|
|
...(resume
|
|
? {
|
|
resume: {
|
|
...(resume.loginTokens && {
|
|
loginTokens: resume.loginTokens.map((token) => ({
|
|
...token,
|
|
when: new Date('when' in token && token.when ? token.when : 0),
|
|
createdAt: 'createdAt' in token && token.createdAt ? new Date(token.createdAt) : new Date(0),
|
|
twoFactorAuthorizedUntil: token.twoFactorAuthorizedUntil ? new Date(token.twoFactorAuthorizedUntil) : undefined,
|
|
})),
|
|
}),
|
|
},
|
|
}
|
|
: {}),
|
|
...(cloud
|
|
? {
|
|
cloud: {
|
|
...cloud,
|
|
expiresAt: new Date(cloud.expiresAt),
|
|
},
|
|
}
|
|
: {}),
|
|
...(emailCode ? { ...emailCode, expire: new Date(emailCode.expire) } : {}),
|
|
...(mergedEmail2fa ? { email2fa: mergedEmail2fa } : {}),
|
|
...(email?.verificationTokens && {
|
|
email: {
|
|
verificationTokens: email.verificationTokens.map((token) => ({
|
|
...token,
|
|
when: new Date(token.when),
|
|
})),
|
|
},
|
|
}),
|
|
},
|
|
}),
|
|
...(lastLogin && {
|
|
lastLogin: new Date(lastLogin),
|
|
}),
|
|
...(meFields.statusExpiresAt && {
|
|
statusExpiresAt: new Date(meFields.statusExpiresAt),
|
|
}),
|
|
ldap: Boolean(ldap),
|
|
createdAt: meFields.createdAt != null ? new Date(meFields.createdAt) : (existingUser?.createdAt ?? new Date()),
|
|
_updatedAt: new Date(meFields._updatedAt ?? existingUser?._updatedAt ?? Date.now()),
|
|
} as IUser);
|
|
}
|
|
useUserDataSyncReady.setState(true);
|
|
|
|
return fromMe as unknown as RawUserData;
|
|
};
|
|
|
|
export const removeLocalUserData = () => {
|
|
Users.state.replaceAll([]);
|
|
localStorage.clear();
|
|
};
|