mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-24 04:27:27 +08:00
feat: drop outdated session (#14373)
#### PR Dependency Tree * **PR #14373** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added client version tracking and validation to ensure application compatibility across authentication flows and sessions. * Enhanced OAuth authentication with improved version handling during sign-in and refresh operations. * **Bug Fixes** * Improved payment callback URL handling with safer defaults for redirect links. * **Tests** * Expanded test coverage for client version enforcement and session management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -4,11 +4,14 @@ import {
|
||||
type LoaderFunction,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import {
|
||||
buildAuthenticationDeepLink,
|
||||
buildOpenAppUrlRoute,
|
||||
} from '../../../modules/open-in-app';
|
||||
import { supportedClient } from './common';
|
||||
|
||||
interface LoaderData {
|
||||
@@ -44,13 +47,14 @@ export const loader: LoaderFunction = ({ request }) => {
|
||||
return redirect('/sign-in?error=Invalid callback parameters');
|
||||
}
|
||||
|
||||
const authParams = new URLSearchParams();
|
||||
authParams.set('method', 'magic-link');
|
||||
authParams.set('payload', JSON.stringify(payload));
|
||||
const urlToOpen = buildAuthenticationDeepLink({
|
||||
scheme: clientCheckResult.data,
|
||||
method: 'magic-link',
|
||||
payload,
|
||||
server: location.origin,
|
||||
});
|
||||
|
||||
return redirect(
|
||||
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||
);
|
||||
return redirect(buildOpenAppUrlRoute(urlToOpen));
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import {
|
||||
buildAuthenticationDeepLink,
|
||||
buildOpenAppUrlRoute,
|
||||
} from '../../../modules/open-in-app';
|
||||
import { supportedClient } from './common';
|
||||
|
||||
interface LoaderData {
|
||||
@@ -45,14 +49,14 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
return redirect('/sign-in?error=Invalid oauth callback parameters');
|
||||
}
|
||||
|
||||
const authParams = new URLSearchParams();
|
||||
authParams.set('method', 'oauth');
|
||||
authParams.set('payload', JSON.stringify(payload));
|
||||
authParams.set('server', location.origin);
|
||||
const urlToOpen = buildAuthenticationDeepLink({
|
||||
scheme: clientCheckResult.data,
|
||||
method: 'oauth',
|
||||
payload,
|
||||
server: location.origin,
|
||||
});
|
||||
|
||||
return redirect(
|
||||
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||
);
|
||||
return redirect(buildOpenAppUrlRoute(urlToOpen));
|
||||
} catch {
|
||||
return redirect('/sign-in?error=Invalid oauth callback parameters');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
buildAuthenticationDeepLink,
|
||||
buildOpenAppUrlRoute,
|
||||
normalizeOpenAppSignInNextParam,
|
||||
} from '@affine/core/modules/open-in-app';
|
||||
import { OpenInAppPage } from '@affine/core/modules/open-in-app/views/open-in-app-page';
|
||||
import {
|
||||
appSchemaUrl,
|
||||
@@ -7,8 +12,8 @@ import {
|
||||
channelToScheme,
|
||||
} from '@affine/core/utils/channel';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { AppContainer } from '../../components/app-container';
|
||||
|
||||
@@ -51,13 +56,16 @@ const OpenAppSignInRedirect = () => {
|
||||
const authService = useService(AuthService);
|
||||
const [params] = useSearchParams();
|
||||
const triggeredRef = useRef(false);
|
||||
const [urlToOpen, setUrlToOpen] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const maybeScheme = appSchemes.safeParse(params.get('scheme'));
|
||||
const scheme = maybeScheme.success
|
||||
? maybeScheme.data
|
||||
: channelToScheme[BUILD_CONFIG.appBuildType];
|
||||
const next = params.get('next') || undefined;
|
||||
const next = normalizeOpenAppSignInNextParam(
|
||||
params.get('next'),
|
||||
location.origin
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggeredRef.current) {
|
||||
@@ -68,23 +76,18 @@ const OpenAppSignInRedirect = () => {
|
||||
authService
|
||||
.createOpenAppSignInCode()
|
||||
.then(code => {
|
||||
const authParams = new URLSearchParams();
|
||||
authParams.set('method', 'open-app-signin');
|
||||
authParams.set(
|
||||
'payload',
|
||||
JSON.stringify(next ? { code, next } : { code })
|
||||
);
|
||||
authParams.set('server', location.origin);
|
||||
setUrlToOpen(`${scheme}://authentication?${authParams.toString()}`);
|
||||
const urlToOpen = buildAuthenticationDeepLink({
|
||||
scheme,
|
||||
method: 'open-app-signin',
|
||||
payload: next ? { code, next } : { code },
|
||||
server: location.origin,
|
||||
});
|
||||
navigate(buildOpenAppUrlRoute(urlToOpen), { replace: true });
|
||||
})
|
||||
.catch(console.error);
|
||||
}, [authService, next, scheme]);
|
||||
}, [authService, navigate, next, scheme]);
|
||||
|
||||
if (!urlToOpen) {
|
||||
return <AppContainer fallback />;
|
||||
}
|
||||
|
||||
return <OpenInAppPage urlToOpen={urlToOpen} />;
|
||||
return <AppContainer fallback />;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
buildAuthenticationDeepLink,
|
||||
buildOpenAppUrlRoute,
|
||||
normalizeOpenAppSignInNextParam,
|
||||
} from '../utils';
|
||||
|
||||
test('buildAuthenticationDeepLink', () => {
|
||||
const payload = { code: '1', next: '/workspace/123' };
|
||||
const url = buildAuthenticationDeepLink({
|
||||
scheme: 'affine',
|
||||
method: 'open-app-signin',
|
||||
payload,
|
||||
server: 'https://app.affine.local',
|
||||
});
|
||||
|
||||
const parsed = new URL(url);
|
||||
|
||||
expect(parsed.protocol).toBe('affine:');
|
||||
expect(parsed.hostname).toBe('authentication');
|
||||
expect(parsed.searchParams.get('method')).toBe('open-app-signin');
|
||||
expect(parsed.searchParams.get('payload')).toBe(JSON.stringify(payload));
|
||||
expect(parsed.searchParams.get('server')).toBe('https://app.affine.local');
|
||||
});
|
||||
|
||||
test('buildOpenAppUrlRoute', () => {
|
||||
const urlToOpen = 'affine://authentication?method=oauth&payload=%7B%7D';
|
||||
const route = buildOpenAppUrlRoute(urlToOpen);
|
||||
|
||||
const parsed = new URL(route, 'https://app.affine.local');
|
||||
expect(parsed.pathname).toBe('/open-app/url');
|
||||
expect(parsed.searchParams.get('url')).toBe(urlToOpen);
|
||||
});
|
||||
|
||||
test('normalizeOpenAppSignInNextParam', () => {
|
||||
expect(
|
||||
normalizeOpenAppSignInNextParam(
|
||||
'/workspace/123',
|
||||
'https://app.affine.local'
|
||||
)
|
||||
).toBe('/workspace/123');
|
||||
|
||||
expect(
|
||||
normalizeOpenAppSignInNextParam(
|
||||
'https://app.affine.local/workspace/123?foo=1#bar',
|
||||
'https://app.affine.local'
|
||||
)
|
||||
).toBe('/workspace/123?foo=1#bar');
|
||||
|
||||
expect(
|
||||
normalizeOpenAppSignInNextParam(
|
||||
'https://evil.example/workspace/123',
|
||||
'https://app.affine.local'
|
||||
)
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
normalizeOpenAppSignInNextParam(
|
||||
'//evil.example/workspace/123',
|
||||
'https://app.affine.local'
|
||||
)
|
||||
).toBeUndefined();
|
||||
|
||||
expect(
|
||||
normalizeOpenAppSignInNextParam(
|
||||
'/redirect-proxy?redirect_uri=https://evil.example',
|
||||
'https://app.affine.local'
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
@@ -1,8 +1,84 @@
|
||||
import { channelToScheme } from '@affine/core/utils';
|
||||
import { channelToScheme } from '@affine/core/utils/channel';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
const logger = new DebugLogger('open-in-app');
|
||||
|
||||
export type AuthenticationMethod = 'magic-link' | 'oauth' | 'open-app-signin';
|
||||
|
||||
export function buildAuthenticationDeepLink(options: {
|
||||
scheme: string;
|
||||
method: AuthenticationMethod;
|
||||
payload: unknown;
|
||||
server?: string;
|
||||
}) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('method', options.method);
|
||||
params.set('payload', JSON.stringify(options.payload));
|
||||
if (options.server) {
|
||||
params.set('server', options.server);
|
||||
}
|
||||
|
||||
return `${options.scheme}://authentication?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function buildOpenAppUrlRoute(urlToOpen: string) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('url', urlToOpen);
|
||||
return `/open-app/url?${params.toString()}`;
|
||||
}
|
||||
|
||||
function isAllowedOpenAppSignInNext(next: string) {
|
||||
if (next === '/') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (next.startsWith('/workspace')) {
|
||||
const boundary = next.charAt('/workspace'.length);
|
||||
return (
|
||||
boundary === '' ||
|
||||
boundary === '/' ||
|
||||
boundary === '?' ||
|
||||
boundary === '#'
|
||||
);
|
||||
}
|
||||
|
||||
return next.startsWith('/share/');
|
||||
}
|
||||
|
||||
export function normalizeOpenAppSignInNextParam(
|
||||
next: string | null,
|
||||
currentOrigin: string
|
||||
) {
|
||||
if (!next) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow protocol-relative urls like `//evil.example`.
|
||||
if (next.startsWith('//')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(next, currentOrigin);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow navigation within current origin.
|
||||
if (parsed.origin !== currentOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
||||
|
||||
if (!isAllowedOpenAppSignInNext(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// return an AFFiNE app's url to be opened in desktop app
|
||||
export const getOpenUrlInDesktopAppLink = (
|
||||
url: string,
|
||||
|
||||
Reference in New Issue
Block a user