import type { LoginServiceConfiguration } from '@rocket.chat/core-typings'; import { capitalize } from '@rocket.chat/tools'; import { AuthenticationContext, useSetting } from '@rocket.chat/ui-contexts'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ContextType, ReactNode } from 'react'; import { useMemo, useSyncExternalStore } from 'react'; import { useLDAPAndCrowdCollisionWarning } from './hooks/useLDAPAndCrowdCollisionWarning'; import { capitalize as capitalizeService } from '../../../lib/utils/stringUtils'; import { loginServices } from '../../lib/loginServices'; import { getDdpSdk } from '../../lib/sdk/ddpSdk'; import { STORAGE_KEYS, getStoredItem, removeStoredItem } from '../../lib/sdk/storage'; export type LoginMethods = keyof typeof Meteor extends infer T ? (T extends `loginWith${string}` ? T : never) : never; export type AuthenticationProviderProps = { children: ReactNode; }; const callLoginMethod = ( options: { loginToken?: string; token?: string; iframe?: boolean }, userCallback: ((err?: any) => void) | undefined, ) => { Accounts.callLoginMethod({ methodArguments: [options], userCallback, }); }; // Bridge Accounts.loggingIn() — Meteor's Tracker-reactive flag — into a // non-reactive subscribe/getSnapshot pair for useSyncExternalStore. We hook // `_setLoggingIn` (Meteor's internal flip, also accessed in // apps/meteor/client/meteor/overrides/killMeteorStream.ts) to fan out // transitions without entering a Tracker computation. const loggingInListeners = new Set<() => void>(); let loggingInBridgeInstalled = false; const installLoggingInBridge = (): void => { if (loggingInBridgeInstalled) return; loggingInBridgeInstalled = true; const wrap = Accounts as unknown as { _setLoggingIn?: (v: boolean) => void }; const original = wrap._setLoggingIn; if (typeof original !== 'function') return; wrap._setLoggingIn = function (this: typeof Accounts, v: boolean) { original.call(this, v); loggingInListeners.forEach((cb) => cb()); }; }; const subscribeLoggingIn = (cb: () => void): (() => void) => { installLoggingInBridge(); loggingInListeners.add(cb); return () => { loggingInListeners.delete(cb); }; }; const getLoggingInSnapshot = (): boolean => Accounts.loggingIn(); const AuthenticationProvider = ({ children }: AuthenticationProviderProps) => { const isLdapEnabled = useSetting('LDAP_Enable', false); const isCrowdEnabled = useSetting('CROWD_Enable', false); const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword'; useLDAPAndCrowdCollisionWarning(); const isLoggingIn = useSyncExternalStore(subscribeLoggingIn, getLoggingInSnapshot); const contextValue = useMemo( (): ContextType => ({ isLoggingIn, loginWithToken: (token: string, callback): Promise => new Promise((resolve, reject) => Meteor.loginWithToken(token, (err) => { if (err) { console.error(err); callback?.(err); return reject(err); } resolve(undefined); }), ), loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string): Promise => new Promise((resolve, reject) => { Meteor[loginMethod](user, password, (error) => { if (error) { reject(error); return; } resolve(); }); }), loginWithService: (serviceConfig: T): (() => Promise) => { const loginMethods: Record = { 'meteor-developer': 'MeteorDeveloperAccount', }; const { service: serviceName } = serviceConfig; const clientConfig = ('clientConfig' in serviceConfig && serviceConfig.clientConfig) || {}; const loginWithService = `loginWith${loginMethods[serviceName] || capitalize(String(serviceName || ''))}`; const method: (config: unknown, cb: (error: any) => void) => Promise = (Meteor as any)[loginWithService]; if (!method) { return () => Promise.reject(new Error('Login method not found')); } return () => new Promise((resolve, reject) => { method(clientConfig, (error: any): void => { if (!error) { resolve(true); return; } reject(error); }); }); }, loginWithCustomOauth: (service: string, options: { redirectUrl: string }, callback) => { const methodName = `loginWith${capitalizeService(service, true)}`; const method = (Meteor as any)[methodName] as ((options: { redirectUrl: string }, cb?: (response: unknown) => void) => void) | undefined; if (!method) { return; } method.call(Meteor, options, callback); }, loginWithIframe: (token: string, callback) => new Promise((resolve, reject) => { callLoginMethod({ iframe: true, token }, (error) => { if (error) { console.error(error); callback?.(error); return reject(error); } resolve(); }); }), loginWithTokenRoute: (token: string, callback) => new Promise((resolve, reject) => { callLoginMethod({ token }, (error) => { if (error) { console.error(error); callback?.(error); return reject(error); } resolve(); }); }), getLoginToken: () => getStoredItem(STORAGE_KEYS.LOGIN_TOKEN), wipeLocalAuth: () => { removeStoredItem(STORAGE_KEYS.USER_ID); removeStoredItem(STORAGE_KEYS.LOGIN_TOKEN); removeStoredItem(STORAGE_KEYS.LOGIN_TOKEN_EXPIRES); try { Meteor.connection.setUserId(null); } catch { // ignore } }, unstoreLoginToken: (callback) => getDdpSdk().account.onLogout(callback), queryLoginServices: { getCurrentValue: () => loginServices.getLoginServiceButtons(), subscribe: (onStoreChange: () => void) => loginServices.on('changed', onStoreChange), }, }), [isLoggingIn, loginMethod], ); return {children}; }; export default AuthenticationProvider;