-
Notifications
You must be signed in to change notification settings - Fork 13.9k
refactor(client): move Meteor auth calls behind AuthenticationContext #40477
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,105 +1,100 @@ | ||||||||||||||||||||||||||
| import type { UserStatus, IUser } from '@rocket.chat/core-typings'; | ||||||||||||||||||||||||||
| import type { UserStatus } from '@rocket.chat/core-typings'; | ||||||||||||||||||||||||||
| import { escapeRegExp } from '@rocket.chat/string-helpers'; | ||||||||||||||||||||||||||
| import { type LocationPathname, useSetting } from '@rocket.chat/ui-contexts'; | ||||||||||||||||||||||||||
| import { Meteor } from 'meteor/meteor'; | ||||||||||||||||||||||||||
| import { useEffect } from 'react'; | ||||||||||||||||||||||||||
| import { type LocationPathname, UserContext, useLoginWithCustomOauth, useLoginWithToken, useSetting } from '@rocket.chat/ui-contexts'; | ||||||||||||||||||||||||||
| import { useContext, useEffect } from 'react'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { AccountBox } from '../../../../app/ui-utils/client/lib/AccountBox'; | ||||||||||||||||||||||||||
| import { sdk } from '../../../../app/utils/client/lib/SDKClient'; | ||||||||||||||||||||||||||
| import { capitalize, ltrim, rtrim } from '../../../../lib/utils/stringUtils'; | ||||||||||||||||||||||||||
| import { baseURI } from '../../../lib/baseURI'; | ||||||||||||||||||||||||||
| import { loginServices } from '../../../lib/loginServices'; | ||||||||||||||||||||||||||
| import { settings } from '../../../lib/settings'; | ||||||||||||||||||||||||||
| import { getUser } from '../../../lib/user'; | ||||||||||||||||||||||||||
| import { router } from '../../../providers/RouterProvider'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const commands = { | ||||||||||||||||||||||||||
| 'go'(data: { path: string }) { | ||||||||||||||||||||||||||
| if (typeof data.path !== 'string' || data.path.trim().length === 0) { | ||||||||||||||||||||||||||
| return console.error('`path` not defined'); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const newUrl = new URL(`${rtrim(baseURI, '/')}/${ltrim(data.path, '/')}`); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const newParams = Array.from(newUrl.searchParams.entries()).reduce( | ||||||||||||||||||||||||||
| (ret, [key, value]) => { | ||||||||||||||||||||||||||
| ret[key] = value; | ||||||||||||||||||||||||||
| return ret; | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| {} as Record<string, string>, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const newPath = newUrl.pathname.replace( | ||||||||||||||||||||||||||
| new RegExp(`^${escapeRegExp(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX)}`), | ||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||
| ) as LocationPathname; | ||||||||||||||||||||||||||
| router.navigate({ | ||||||||||||||||||||||||||
| pathname: newPath, | ||||||||||||||||||||||||||
| search: { ...router.getSearchParameters(), ...newParams }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 'set-user-status'(data: { status: UserStatus }) { | ||||||||||||||||||||||||||
| AccountBox.setStatus(data.status); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 'call-custom-oauth-login'(data: { service: string; redirectUrl?: string | null }, event: MessageEvent) { | ||||||||||||||||||||||||||
| const customOAuthCallback = (response: unknown) => { | ||||||||||||||||||||||||||
| event.source?.postMessage( | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| event: 'custom-oauth-callback', | ||||||||||||||||||||||||||
| response, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| { targetOrigin: event.origin }, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| export const useIframeCommands = () => { | ||||||||||||||||||||||||||
| const iframeReceiveEnabled = useSetting('Iframe_Integration_receive_enable'); | ||||||||||||||||||||||||||
| const iframeReceiveOrigin = useSetting('Iframe_Integration_receive_origin', '*'); | ||||||||||||||||||||||||||
| const loginWithToken = useLoginWithToken(); | ||||||||||||||||||||||||||
| const loginWithCustomOauth = useLoginWithCustomOauth(); | ||||||||||||||||||||||||||
| const { logout } = useContext(UserContext); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const siteUrl = `${settings.peek('Site_Url') ?? ''}/`; | ||||||||||||||||||||||||||
| if (typeof data.redirectUrl !== 'string' || !data.redirectUrl.startsWith(siteUrl)) { | ||||||||||||||||||||||||||
| data.redirectUrl = null; | ||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||
| if (!iframeReceiveEnabled) { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (typeof data.service === 'string' && window.ServiceConfiguration) { | ||||||||||||||||||||||||||
| const customOauth = loginServices.getLoginService(data.service); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (customOauth) { | ||||||||||||||||||||||||||
| const customLoginWith = (Meteor as any)[`loginWith${capitalize(customOauth.service, true)}`]; | ||||||||||||||||||||||||||
| const customRedirectUri = data.redirectUrl || siteUrl; | ||||||||||||||||||||||||||
| customLoginWith.call(Meteor, { redirectUrl: customRedirectUri }, customOAuthCallback); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| const commands = { | ||||||||||||||||||||||||||
| 'go'(data: { path: string }) { | ||||||||||||||||||||||||||
| if (typeof data.path !== 'string' || data.path.trim().length === 0) { | ||||||||||||||||||||||||||
| return console.error('`path` not defined'); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const newUrl = new URL(`${rtrim(baseURI, '/')}/${ltrim(data.path, '/')}`); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const newParams = Array.from(newUrl.searchParams.entries()).reduce( | ||||||||||||||||||||||||||
| (ret, [key, value]) => { | ||||||||||||||||||||||||||
| ret[key] = value; | ||||||||||||||||||||||||||
| return ret; | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| {} as Record<string, string>, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const newPath = newUrl.pathname.replace( | ||||||||||||||||||||||||||
| new RegExp(`^${escapeRegExp(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX)}`), | ||||||||||||||||||||||||||
| '', | ||||||||||||||||||||||||||
| ) as LocationPathname; | ||||||||||||||||||||||||||
| router.navigate({ | ||||||||||||||||||||||||||
| pathname: newPath, | ||||||||||||||||||||||||||
| search: { ...router.getSearchParameters(), ...newParams }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 'login-with-token'(data: { token: string }) { | ||||||||||||||||||||||||||
| if (typeof data.token === 'string') { | ||||||||||||||||||||||||||
| Meteor.loginWithToken(data.token, () => { | ||||||||||||||||||||||||||
| console.log('Iframe command [login-with-token]: result', data); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| 'set-user-status'(data: { status: UserStatus }) { | ||||||||||||||||||||||||||
| AccountBox.setStatus(data.status); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async 'logout'() { | ||||||||||||||||||||||||||
| const user = getUser(); | ||||||||||||||||||||||||||
| Meteor.logout(() => { | ||||||||||||||||||||||||||
| if (!user) return; | ||||||||||||||||||||||||||
| 'call-custom-oauth-login'(data: { service: string; redirectUrl?: string | null }, event: MessageEvent) { | ||||||||||||||||||||||||||
| const customOAuthCallback = (response: unknown) => { | ||||||||||||||||||||||||||
| event.source?.postMessage( | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| event: 'custom-oauth-callback', | ||||||||||||||||||||||||||
| response, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| { targetOrigin: event.origin }, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const siteUrl = `${settings.peek('Site_Url') ?? ''}/`; | ||||||||||||||||||||||||||
| if (typeof data.redirectUrl !== 'string' || !data.redirectUrl.startsWith(siteUrl)) { | ||||||||||||||||||||||||||
| data.redirectUrl = null; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+65
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two edge cases worth tightening:
Normalizing the trailing slash (e.g. via 🛡️ Suggested hardening- const siteUrl = `${settings.peek('Site_Url') ?? ''}/`;
- if (typeof data.redirectUrl !== 'string' || !data.redirectUrl.startsWith(siteUrl)) {
- data.redirectUrl = null;
- }
+ const rawSiteUrl = settings.peek('Site_Url');
+ const siteUrl = typeof rawSiteUrl === 'string' && rawSiteUrl.length > 0 ? `${rtrim(rawSiteUrl, '/')}/` : '';
+ if (!siteUrl || typeof data.redirectUrl !== 'string' || !data.redirectUrl.startsWith(siteUrl)) {
+ data.redirectUrl = null;
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (typeof data.service === 'string' && window.ServiceConfiguration) { | ||||||||||||||||||||||||||
| const customOauth = loginServices.getLoginService(data.service); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (customOauth) { | ||||||||||||||||||||||||||
| const customRedirectUri = data.redirectUrl || siteUrl; | ||||||||||||||||||||||||||
| loginWithCustomOauth(capitalize(customOauth.service, true), { redirectUrl: customRedirectUri }, customOAuthCallback); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| sdk.call('logoutCleanUp', user as unknown as IUser); | ||||||||||||||||||||||||||
| return router.navigate('/home'); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| } as const; | ||||||||||||||||||||||||||
| 'login-with-token'(data: { token: string }) { | ||||||||||||||||||||||||||
| if (typeof data.token === 'string') { | ||||||||||||||||||||||||||
| void loginWithToken(data.token, () => { | ||||||||||||||||||||||||||
| console.log('Iframe command [login-with-token]: result', data); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
Comment on lines
+80
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not log raw login token payloads. Line 83 logs Safer logging change 'login-with-token'(data: { token: string }) {
if (typeof data.token === 'string') {
void loginWithToken(data.token, () => {
- console.log('Iframe command [login-with-token]: result', data);
+ console.log('Iframe command [login-with-token]: completed');
});
}
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| type CommandMessage<TCommandName extends keyof typeof commands = keyof typeof commands> = { | ||||||||||||||||||||||||||
| externalCommand: TCommandName; | ||||||||||||||||||||||||||
| } & Parameters<(typeof commands)[TCommandName]>[0]; | ||||||||||||||||||||||||||
| 'logout'() { | ||||||||||||||||||||||||||
| void logout(); | ||||||||||||||||||||||||||
| router.navigate('/home'); | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| } as const; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const useIframeCommands = () => { | ||||||||||||||||||||||||||
| const iframeReceiveEnabled = useSetting('Iframe_Integration_receive_enable'); | ||||||||||||||||||||||||||
| const iframeReceiveOrigin = useSetting('Iframe_Integration_receive_origin', '*'); | ||||||||||||||||||||||||||
| type CommandMessage<TCommandName extends keyof typeof commands = keyof typeof commands> = { | ||||||||||||||||||||||||||
| externalCommand: TCommandName; | ||||||||||||||||||||||||||
| } & Parameters<(typeof commands)[TCommandName]>[0]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||
| if (!iframeReceiveEnabled) { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const messageListener = (event: MessageEvent<CommandMessage>) => { | ||||||||||||||||||||||||||
| if (typeof event.data !== 'object' || typeof event.data.externalCommand !== 'string') { | ||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||
|
|
@@ -124,5 +119,5 @@ export const useIframeCommands = () => { | |||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||
| window.removeEventListener('message', messageListener); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }, [iframeReceiveEnabled, iframeReceiveOrigin]); | ||||||||||||||||||||||||||
| }, [iframeReceiveEnabled, iframeReceiveOrigin, loginWithToken, loginWithCustomOauth, logout]); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { useContext } from 'react'; | ||
|
|
||
| import { AuthenticationContext } from '../AuthenticationContext'; | ||
|
|
||
| export const useLoginToken = (): string | null => useContext(AuthenticationContext).getLoginToken(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { useContext } from 'react'; | ||
|
|
||
| import { AuthenticationContext } from '../AuthenticationContext'; | ||
|
|
||
| export const useLoginWithCustomOauth = () => useContext(AuthenticationContext).loginWithCustomOauth; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { useContext } from 'react'; | ||
|
|
||
| import { AuthenticationContext } from '../AuthenticationContext'; | ||
|
|
||
| export const useWipeLocalAuth = (): (() => void) => useContext(AuthenticationContext).wipeLocalAuth; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loginWithCustomOauthsilently no-ops on missing method; callback never fires.When
Meteor[methodName]is undefined the function returns without invokingcallback. Callers that await the callback (e.g., the iframecall-custom-oauth-logincommand path) won't receive any signal, so they can't surface or recover from the failure. The previous direct-Meteor call site would have thrown aTypeErrorinstead, which made the failure observable. Consider invokingcallbackwith an error so the caller can react.🛡️ Proposed fix
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; + callback?.(new Error(`Login method ${methodName} not found`)); + return; } method.call(Meteor, options, callback); },🤖 Prompt for AI Agents