-
Notifications
You must be signed in to change notification settings - Fork 13.9k
feat: Deeplink login for Mobile and Desktop clients #40601
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
7296b45
4f5ffab
c5399e1
0e82452
de5cb91
416c8fc
73bc61d
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 |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export const buildDeepLinkURL = (resumeToken: string, userId: string) => { | ||
| const url = new URL(window.location.href); | ||
| const { host } = url; | ||
| return `rocketchat://auth?host=http://${host}&token=${resumeToken}&userId=${userId}`; | ||
|
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. P1: Build the deeplink query with Prompt for AI agents |
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { useRouter } from '@rocket.chat/ui-contexts'; | ||
| import { useEffect } from 'react'; | ||
|
|
||
| import { buildDeepLinkURL } from '../../../lib/buildAuthDeeplinkURL'; | ||
|
|
||
| export const useLoginOtherClients = () => { | ||
| const router = useRouter(); | ||
|
|
||
| useEffect(() => { | ||
| const { resumeToken, loginClient, userId } = router.getSearchParameters(); | ||
|
|
||
| if (!resumeToken || !userId) { | ||
| return; | ||
| } | ||
|
|
||
| if (loginClient !== 'desktop' && loginClient !== 'mobile') { | ||
| return; | ||
| } | ||
|
|
||
| const loginURL = buildDeepLinkURL(resumeToken, userId); | ||
| window.location.href = loginURL; | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| router.navigate('/home', { replace: true }); | ||
| }, 0); | ||
|
|
||
| return () => clearTimeout(timeout); | ||
| }, [router]); | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,12 +7,17 @@ export const useLoginViaQuery = () => { | |||||||||||||
|
|
||||||||||||||
| useEffect(() => { | ||||||||||||||
| const handleLogin = async () => { | ||||||||||||||
| const { resumeToken } = router.getSearchParameters(); | ||||||||||||||
| const { resumeToken, loginClient } = router.getSearchParameters(); | ||||||||||||||
|
|
||||||||||||||
| if (!resumeToken) { | ||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| //Case handled by useLoginOtherClients, we don't want to login here. | ||||||||||||||
| if (loginClient) { | ||||||||||||||
|
yash-rajpal marked this conversation as resolved.
|
||||||||||||||
| return; | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+17
to
+19
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. Guard only supported clients before skipping query-token login. Line 17 currently bypasses Proposed fix- if (loginClient) {
+ if (loginClient === 'desktop' || loginClient === 'mobile') {
return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| await loginWithToken(resumeToken); | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { useRouter, useUserId } from '@rocket.chat/ui-contexts'; | ||
| import { useEffect } from 'react'; | ||
|
|
||
| import { buildDeepLinkURL } from '../../../lib/buildAuthDeeplinkURL'; | ||
| import { readStoredLoginToken } from '../../../lib/sdk/ddpSdk'; | ||
|
|
||
| export const useShareSessionWithOtherClients = () => { | ||
| const router = useRouter(); | ||
| const userId = useUserId(); | ||
|
|
||
| useEffect(() => { | ||
| if (!userId) { | ||
| return; | ||
| } | ||
|
|
||
| const loginToken = readStoredLoginToken(); | ||
|
|
||
| if (!loginToken) { | ||
| return; | ||
| } | ||
|
|
||
| const { resumeToken, loginClient } = router.getSearchParameters(); | ||
|
|
||
| if (resumeToken) { | ||
| return; | ||
| } | ||
|
|
||
| if (loginClient !== 'desktop' && loginClient !== 'mobile') { | ||
| return; | ||
| } | ||
|
|
||
| const loginURL = buildDeepLinkURL(loginToken, userId); | ||
| window.location.href = loginURL; | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| router.navigate('/home', { replace: true }); | ||
| }, 100); | ||
|
|
||
| return () => clearTimeout(timeout); | ||
| }, [router, userId]); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import 'express-session'; | ||
|
|
||
| declare module 'express-session' { | ||
| interface SessionData { | ||
| loginClient?: string; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import type { IRocketChatDesktop } from '@rocket.chat/desktop-api'; | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| RocketChatDesktop?: IRocketChatDesktop; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||
| import { ButtonGroup, Divider } from '@rocket.chat/fuselage'; | ||||||||||
| import { Button, ButtonGroup, Divider } from '@rocket.chat/fuselage'; | ||||||||||
| import { useLoginServices, useSetting } from '@rocket.chat/ui-contexts'; | ||||||||||
| import type { Dispatch, ReactElement, SetStateAction } from 'react'; | ||||||||||
| import { useTranslation } from 'react-i18next'; | ||||||||||
|
|
@@ -21,18 +21,35 @@ const LoginServices = ({ | |||||||||
| return null; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| const isDesktopApp = !!window.RocketChatDesktop?.openInBrowser; | ||||||||||
|
|
||||||||||
| const handleLoginOnWeb = () => { | ||||||||||
| if (!isDesktopApp) { | ||||||||||
| return; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| window.RocketChatDesktop?.openInBrowser(`${window.location.href}?loginClient=desktop`); | ||||||||||
|
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. P1: Build the deeplink URL with Prompt for AI agents
Suggested change
|
||||||||||
| }; | ||||||||||
|
Comment on lines
+26
to
+32
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. Build the desktop redirect URL via Line 31 can generate invalid URLs ( Proposed fix const handleLoginOnWeb = () => {
if (!isDesktopApp) {
return;
}
- window.RocketChatDesktop?.openInBrowser(`${window.location.href}?loginClient=desktop`);
+ const url = new URL(window.location.href);
+ url.searchParams.set('loginClient', 'desktop');
+ window.RocketChatDesktop?.openInBrowser(url.toString());
};🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <> | ||||||||||
| {showFormLogin && ( | ||||||||||
| <Divider mb={24} p={0}> | ||||||||||
| {t('registration.component.form.divider')} | ||||||||||
| </Divider> | ||||||||||
| )} | ||||||||||
| <ButtonGroup vertical stretch small> | ||||||||||
| {services.map((service) => ( | ||||||||||
| <LoginServicesButton disabled={disabled} key={service.service} {...service} setError={setError} /> | ||||||||||
| ))} | ||||||||||
| </ButtonGroup> | ||||||||||
| {!isDesktopApp && ( | ||||||||||
| <ButtonGroup vertical stretch small> | ||||||||||
| {services.map((service) => ( | ||||||||||
| <LoginServicesButton disabled={disabled} key={service.service} {...service} setError={setError} /> | ||||||||||
| ))} | ||||||||||
| </ButtonGroup> | ||||||||||
| )} | ||||||||||
| {isDesktopApp && ( | ||||||||||
| <Button width='100%' primary onClick={handleLoginOnWeb}> | ||||||||||
| {t('registration.component.login.onWeb')} | ||||||||||
| </Button> | ||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||
| )} | ||||||||||
| </> | ||||||||||
| ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| { | ||
| "extends": "./tsconfig.json", | ||
| "include": ["./src/**/*"], | ||
| "include": ["./src/**/*", "./global.d.ts"], | ||
| "exclude": ["./src/**/*.spec.ts", "./src/**/*.stories.tsx"] | ||
| } |
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.
Preserve origin protocol and URL-encode deeplink query params.
This auth deeplink currently hardcodes
http://and interpolates raw query values. That can break HTTPS instances and corrupttokenparsing for reserved characters.Proposed fix
export const buildDeepLinkURL = (resumeToken: string, userId: string) => { - const url = new URL(window.location.href); - const { host } = url; - return `rocketchat://auth?host=http://${host}&token=${resumeToken}&userId=${userId}`; + const { origin } = new URL(window.location.href); + const params = new URLSearchParams({ + host: origin, + token: resumeToken, + userId, + }); + return `rocketchat://auth?${params.toString()}`; };📝 Committable suggestion
🤖 Prompt for AI Agents