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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
158 lines
6.3 KiB
TypeScript
158 lines
6.3 KiB
TypeScript
import type { UserStatus } from '@rocket.chat/core-typings';
|
|
|
|
import 'highlight.js/styles/github.css';
|
|
import { sdk } from '../lib/SDKClient';
|
|
import { onLoggedIn } from '../lib/loggedIn';
|
|
import { clearStoredCredentials, ensureConnectedAndAuthenticated, getDdpSdk, isAuthError } from '../lib/sdk/ddpSdk';
|
|
import { isSdkTransportEnabled } from '../lib/sdk/sdkTransportEnabled';
|
|
import { STORAGE_KEYS, getStoredItem } from '../lib/sdk/storage';
|
|
import { userIdStore } from '../lib/user';
|
|
import { removeLocalUserData, synchronizeUserData } from '../lib/userData';
|
|
import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent';
|
|
import { Users } from '../stores';
|
|
|
|
const sdkTransportEnabled = isSdkTransportEnabled();
|
|
|
|
let status: UserStatus | undefined = undefined;
|
|
|
|
const emitStatusChange = (next: UserStatus | undefined) => {
|
|
if (next === status) return;
|
|
status = next;
|
|
fireGlobalEvent('status-changed', status);
|
|
};
|
|
|
|
if (!sdkTransportEnabled) {
|
|
// Flag off: develop's exact onLoggedIn handler — single Accounts.onLogin
|
|
// hook, no SDK auth gate, no syncOnce dedup, no userIdStore.subscribe
|
|
// belt-and-braces.
|
|
onLoggedIn(async () => {
|
|
const uid = userIdStore.getState();
|
|
if (!uid) return;
|
|
|
|
const user = await synchronizeUserData(uid);
|
|
if (!user) return;
|
|
|
|
const utcOffset = -new Date().getTimezoneOffset() / 60;
|
|
if (user.utcOffset !== utcOffset) {
|
|
void sdk.rest.post('/v1/users.setPreferences', { data: { utcOffset } });
|
|
}
|
|
|
|
emitStatusChange(user.status);
|
|
});
|
|
} else {
|
|
const runUserDataSync = async (uid: string) => {
|
|
// synchronizeUserData opens a `stream-notify-user/${uid}/userData` sub
|
|
// over DDPSDK. The server rejects that sub with "not-allowed" until
|
|
// DDPSDK has completed loginWithToken on its own socket. Both
|
|
// runUserDataSync and ensureConnectedAndAuthenticated are subscribers
|
|
// of userIdStore, so without sequencing the sub races the auth and
|
|
// hits the rejection on every re-login. Await the SDK auth here so
|
|
// the sub fires authenticated.
|
|
const tokenBeforeSync = getStoredItem(STORAGE_KEYS.LOGIN_TOKEN);
|
|
try {
|
|
await ensureConnectedAndAuthenticated();
|
|
} catch {
|
|
// non-fatal: sdk.stream queues until DDPSDK eventually auths
|
|
}
|
|
|
|
let user: Awaited<ReturnType<typeof synchronizeUserData>>;
|
|
try {
|
|
user = await synchronizeUserData(uid);
|
|
} catch (error) {
|
|
// When the stored token is expired/revoked server-side, the userData
|
|
// stream sub + /v1/me come back 401/403, so synchronizeUserData throws
|
|
// and useUserDataSyncReady never flips true. useMainReady then stays
|
|
// false forever (uid is still truthy from the stale credentials), so
|
|
// Preload sits on PageLoading — and because Preload WRAPS
|
|
// AuthenticationCheck, the user never reaches LoginPage and the
|
|
// workspace looks stuck "loading"/grayed-out. Escalate the dead-session
|
|
// signal to a credential wipe so userId drops to null and the router
|
|
// falls through to the login screen. Token-stable guard: only clear
|
|
// when localStorage still holds the token we synced with, so a parallel
|
|
// re-auth that already rotated the token isn't kicked out.
|
|
if (isAuthError(error) && getStoredItem(STORAGE_KEYS.LOGIN_TOKEN) === tokenBeforeSync) {
|
|
clearStoredCredentials();
|
|
}
|
|
throw error;
|
|
}
|
|
if (!user) return;
|
|
|
|
const utcOffset = -new Date().getTimezoneOffset() / 60;
|
|
if (user.utcOffset !== utcOffset) {
|
|
void sdk.rest.post('/v1/users.setPreferences', { data: { utcOffset } });
|
|
}
|
|
|
|
emitStatusChange(user.status);
|
|
};
|
|
|
|
// Both `onLoggedIn` (from accounts-base) and `userIdStore.subscribe`
|
|
// (belt-and-braces in case the loggedInAndDataReadyCallback's user-await
|
|
// autorun gets wedged on logout → fresh login) fire for the same uid on a
|
|
// successful login. runUserDataSync calls userSetUtcOffset which is
|
|
// rate-limited on CI/prod, so without a shared guard the second call
|
|
// returns 400 too-many-requests and downstream REST calls (sessions/list
|
|
// etc.) start coming back 401 because the rate limiter throttles auth
|
|
// checks for the rest of the window. Use a single guarded sync gate, but
|
|
// reset it on failure so SAML/oauth/post-logout flows can retry — those
|
|
// flows depend on a second runUserDataSync after the SDK socket finishes
|
|
// authenticating, otherwise the userData stream subscription comes back
|
|
// nosub and synchronizeUserData throws, leaving useUserDataSyncReady
|
|
// false and the page stuck on PageLoading.
|
|
let lastSyncedUid: string | undefined;
|
|
const syncOnce = (uid: string | undefined): void => {
|
|
// Reset on logout transitions so a subsequent re-login (same uid or different)
|
|
// runs a fresh sync. Force-logout via the SDK loginWithToken wrap clears
|
|
// creds via Accounts._unstoreLoginToken() + setUserId(null), which does NOT
|
|
// fire Accounts.onLogout — so without this branch, lastSyncedUid stays set,
|
|
// the next login is deduped, runUserDataSync is skipped, and
|
|
// useUserDataSyncReady stays false (page wedged on PageLoading).
|
|
if (!uid) {
|
|
lastSyncedUid = undefined;
|
|
return;
|
|
}
|
|
if (uid === lastSyncedUid) return;
|
|
lastSyncedUid = uid;
|
|
void runUserDataSync(uid).catch((err) => {
|
|
console.warn('[startup] runUserDataSync failed; clearing dedup to allow a retry', err);
|
|
if (lastSyncedUid === uid) lastSyncedUid = undefined;
|
|
});
|
|
};
|
|
|
|
onLoggedIn(() => {
|
|
syncOnce(userIdStore.getState());
|
|
});
|
|
|
|
userIdStore.subscribe((uid) => {
|
|
syncOnce(uid);
|
|
});
|
|
|
|
syncOnce(userIdStore.getState());
|
|
|
|
getDdpSdk().account.onLogout(() => {
|
|
lastSyncedUid = undefined;
|
|
});
|
|
}
|
|
|
|
Users.use.subscribe(() => {
|
|
const uid = userIdStore.getState();
|
|
if (!uid) return;
|
|
const user = Users.use.getState().get(uid);
|
|
if (!user) return;
|
|
emitStatusChange(user.status);
|
|
});
|
|
|
|
getDdpSdk().account.onLogout(() => {
|
|
removeLocalUserData();
|
|
status = undefined;
|
|
});
|
|
|
|
// Session-resume failure (expired stored token on page load): Meteor has already
|
|
// cleared Meteor.loginToken before this module runs, userId stays null, and no
|
|
// Accounts.onLogout callback fires for this scenario. Detect via the combination
|
|
// of missing token and missing uid at module init and clean up residual keys
|
|
// (e.g. E2EE public_key / private_key). Do NOT subscribe to userIdStore for this —
|
|
// the valid-session resume path is async and would clobber a valid token mid-flight.
|
|
if (!userIdStore.getState() && localStorage.getItem('Meteor.loginToken') === null) {
|
|
removeLocalUserData();
|
|
}
|