mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 11:09:01 +08:00
refactor(core): use Route component
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||
import { router } from '@affine/core/desktop/router';
|
||||
import { Router } from '@affine/core/desktop/router';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
||||
import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
|
||||
import { setupEffects } from './effects';
|
||||
import { DesktopThemeSync } from './theme-sync';
|
||||
@@ -41,7 +41,9 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DesktopThemeSync />
|
||||
<RouterProvider router={router} />
|
||||
<BrowserRouter basename={environment.subPath}>
|
||||
<Router />
|
||||
</BrowserRouter>
|
||||
{environment.isWindows && (
|
||||
<div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}>
|
||||
<WindowsAppControls />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { router } from '@affine/core/desktop/router';
|
||||
import { Router } from '@affine/core/desktop/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { LifecycleService } from '@affine/core/modules/lifecycle';
|
||||
@@ -17,7 +17,7 @@ import { CacheProvider } from '@emotion/react';
|
||||
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
||||
import { OpClient } from '@toeverything/infra/op';
|
||||
import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
|
||||
const cache = createEmotionCache();
|
||||
|
||||
@@ -85,7 +85,9 @@ export function App() {
|
||||
<CacheProvider value={cache}>
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<RouterProvider router={router} />
|
||||
<BrowserRouter basename={environment.subPath}>
|
||||
<Router />
|
||||
</BrowserRouter>
|
||||
</AffineContext>
|
||||
</I18nProvider>
|
||||
</CacheProvider>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/nbstore": "workspace:*",
|
||||
"@affine/routes": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||
import { toDocSearchParams } from '@affine/core/modules/navigation';
|
||||
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { FACTORIES } from '@affine/routes';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
@@ -52,7 +53,7 @@ export function useNavigateHelper() {
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}`, {
|
||||
return navigate(FACTORIES.workspace.doc({ workspaceId, docId: pageId }), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -74,15 +75,18 @@ export function useNavigateHelper() {
|
||||
refreshKey: nanoid(),
|
||||
});
|
||||
const query = search?.size ? `?${search.toString()}` : '';
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}${query}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
return navigate(
|
||||
FACTORIES.workspace.doc({ workspaceId, docId: pageId }) + query,
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToCollections = useCallback(
|
||||
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(`/workspace/${workspaceId}/collection`, {
|
||||
return navigate(FACTORIES.workspace.collections({ workspaceId }), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -90,7 +94,7 @@ export function useNavigateHelper() {
|
||||
);
|
||||
const jumpToTags = useCallback(
|
||||
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(`/workspace/${workspaceId}/tag`, {
|
||||
return navigate(FACTORIES.workspace.tags({ workspaceId }), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -102,7 +106,7 @@ export function useNavigateHelper() {
|
||||
tagId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/workspace/${workspaceId}/tag/${tagId}`, {
|
||||
return navigate(FACTORIES.workspace.tags.tag({ workspaceId, tagId }), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -114,16 +118,22 @@ export function useNavigateHelper() {
|
||||
collectionId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/workspace/${workspaceId}/collection/${collectionId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
return navigate(
|
||||
FACTORIES.workspace.collections.collection({
|
||||
workspaceId,
|
||||
collectionId,
|
||||
}),
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const jumpToAll = useCallback(
|
||||
(workspaceId: string, logic?: RouteLogic) => {
|
||||
return navigate(`/workspace/${workspaceId}/all`, {
|
||||
return navigate(FACTORIES.workspace.all({ workspaceId }), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -144,7 +154,7 @@ export function useNavigateHelper() {
|
||||
|
||||
const jumpTo404 = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/404', {
|
||||
return navigate(FACTORIES.notFound(), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -152,7 +162,7 @@ export function useNavigateHelper() {
|
||||
);
|
||||
const jumpToExpired = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/expired', {
|
||||
return navigate(FACTORIES.expired(), {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -176,7 +186,7 @@ export function useNavigateHelper() {
|
||||
}
|
||||
|
||||
return navigate(
|
||||
'/sign-in' +
|
||||
FACTORIES.signIn() +
|
||||
(searchParams.toString() ? '?' + searchParams.toString() : ''),
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
@@ -196,7 +206,7 @@ export function useNavigateHelper() {
|
||||
}
|
||||
|
||||
const encodedUrl = encodeURIComponent(deeplink);
|
||||
return navigate(`/open-app/url?url=${encodedUrl}`);
|
||||
return navigate(FACTORIES.openApp({ action: `url?url=${encodedUrl}` }));
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
@@ -204,7 +214,8 @@ export function useNavigateHelper() {
|
||||
const jumpToImportTemplate = useCallback(
|
||||
(name: string, snapshotUrl: string) => {
|
||||
return navigate(
|
||||
`/template/import?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
|
||||
FACTORIES.template.import() +
|
||||
`?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
@@ -221,7 +232,8 @@ export function useNavigateHelper() {
|
||||
searchParams.set('tab', tab);
|
||||
}
|
||||
return navigate(
|
||||
`/workspace/${workspaceId}/settings?${searchParams.toString()}`,
|
||||
FACTORIES.workspace.settings({ workspaceId }) +
|
||||
(searchParams.toString() ? `?${searchParams.toString()}` : ''),
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
|
||||
@@ -14,9 +14,7 @@ import {
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router';
|
||||
import { redirect, useParams, useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
|
||||
import { useMutation } from '../../../components/hooks/use-mutation';
|
||||
import {
|
||||
@@ -28,18 +26,6 @@ import { AppContainer } from '../../components/app-container';
|
||||
import { ConfirmChangeEmail } from './confirm-change-email';
|
||||
import { ConfirmVerifiedEmail } from './email-verified-email';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'onboarding',
|
||||
'setPassword',
|
||||
'signIn',
|
||||
'changePassword',
|
||||
'signUp',
|
||||
'changeEmail',
|
||||
'confirm-change-email',
|
||||
'subscription-redirect',
|
||||
'verify-email',
|
||||
]);
|
||||
|
||||
export const Component = () => {
|
||||
const authService = useService(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
@@ -159,14 +145,3 @@ export const Component = () => {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
if (!args.params.authType) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (!authTypeSchema.safeParse(args.params.authType).success) {
|
||||
return redirect('/404');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,57 +1,19 @@
|
||||
import { useAsyncNavigate } from '@affine/core/utils/use-async-navigate';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { type LoaderFunction, redirect, useLoaderData } from 'react-router';
|
||||
import { useLoaderData } from 'react-router';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { supportedClient } from './common';
|
||||
|
||||
interface LoaderData {
|
||||
export interface MagicLinkLoaderData {
|
||||
token: string;
|
||||
email: string;
|
||||
redirectUri: string | null;
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const params = url.searchParams;
|
||||
const client = params.get('client');
|
||||
const email = params.get('email');
|
||||
const token = params.get('token');
|
||||
const redirectUri = params.get('redirect_uri');
|
||||
|
||||
if (!email || !token) {
|
||||
return redirect('/sign-in?error=Invalid magic link');
|
||||
}
|
||||
|
||||
const payload: LoaderData = {
|
||||
email,
|
||||
token,
|
||||
redirectUri,
|
||||
};
|
||||
|
||||
if (!client || client === 'web') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const clientCheckResult = supportedClient.safeParse(client);
|
||||
if (!clientCheckResult.success) {
|
||||
return redirect('/sign-in?error=Invalid callback parameters');
|
||||
}
|
||||
|
||||
const authParams = new URLSearchParams();
|
||||
authParams.set('method', 'magic-link');
|
||||
authParams.set('payload', JSON.stringify(payload));
|
||||
|
||||
return redirect(
|
||||
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
// TODO(@eyhn): loading ui
|
||||
const auth = useService(AuthService);
|
||||
const data = useLoaderData() as LoaderData;
|
||||
const data = useLoaderData() as MagicLinkLoaderData;
|
||||
|
||||
const nav = useAsyncNavigate();
|
||||
|
||||
|
||||
@@ -1,62 +1,19 @@
|
||||
import { useAsyncNavigate } from '@affine/core/utils/use-async-navigate';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { type LoaderFunction, redirect, useLoaderData } from 'react-router';
|
||||
import { useLoaderData } from 'react-router';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
import { supportedClient } from './common';
|
||||
|
||||
interface LoaderData {
|
||||
export interface OAuthCallbackLoaderData {
|
||||
state: string;
|
||||
code: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const queries = url.searchParams;
|
||||
const code = queries.get('code');
|
||||
let stateStr = queries.get('state') ?? '{}';
|
||||
|
||||
if (!code || !stateStr) {
|
||||
return redirect('/sign-in?error=Invalid oauth callback parameters');
|
||||
}
|
||||
|
||||
try {
|
||||
const { state, client, provider } = JSON.parse(stateStr);
|
||||
stateStr = state;
|
||||
|
||||
const payload: LoaderData = {
|
||||
state,
|
||||
code,
|
||||
provider,
|
||||
};
|
||||
|
||||
if (!client || client === 'web') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const clientCheckResult = supportedClient.safeParse(client);
|
||||
if (!clientCheckResult.success) {
|
||||
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);
|
||||
|
||||
return redirect(
|
||||
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||
);
|
||||
} catch {
|
||||
return redirect('/sign-in?error=Invalid oauth callback parameters');
|
||||
}
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const auth = useService(AuthService);
|
||||
const data = useLoaderData() as LoaderData;
|
||||
const data = useLoaderData() as OAuthCallbackLoaderData;
|
||||
|
||||
// loader data from useLoaderData is not reactive, so that we can safely
|
||||
// assume the effect below is only triggered once
|
||||
|
||||
@@ -1,61 +1,19 @@
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { useAsyncNavigate } from '@affine/core/utils';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import type { OAuthProviderType } from '@affine/graphql';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
import { type LoaderFunction, redirect, useLoaderData } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
import { useLoaderData } from 'react-router';
|
||||
|
||||
import { supportedClient } from './common';
|
||||
|
||||
const supportedProvider = z.nativeEnum(OAuthProviderType);
|
||||
|
||||
const oauthParameters = z.object({
|
||||
provider: supportedProvider,
|
||||
client: supportedClient,
|
||||
redirectUri: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
interface LoaderData {
|
||||
interface OAuthLoginLoaderData {
|
||||
provider: OAuthProviderType;
|
||||
client: string;
|
||||
redirectUri?: string;
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const provider = searchParams.get('provider');
|
||||
const client = searchParams.get('client') ?? 'web';
|
||||
const redirectUri = searchParams.get('redirect_uri');
|
||||
|
||||
// sign out first, web only
|
||||
if (client === 'web') {
|
||||
await fetch('/api/auth/sign-out');
|
||||
}
|
||||
|
||||
const paramsParseResult = oauthParameters.safeParse({
|
||||
provider,
|
||||
client,
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
if (paramsParseResult.success) {
|
||||
return {
|
||||
provider,
|
||||
client,
|
||||
redirectUri,
|
||||
};
|
||||
}
|
||||
|
||||
return redirect(
|
||||
`/sign-in?error=${encodeURIComponent(`Invalid oauth parameters`)}`
|
||||
);
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const auth = useService(AuthService);
|
||||
const data = useLoaderData() as LoaderData;
|
||||
const data = useLoaderData() as OAuthLoginLoaderData;
|
||||
|
||||
const nav = useAsyncNavigate();
|
||||
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { useServiceOptional } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
import { Onboarding } from '../../../components/affine/onboarding/onboarding';
|
||||
import { appConfigStorage } from '../../../components/hooks/use-app-config-storage';
|
||||
|
||||
/**
|
||||
* /onboarding page
|
||||
*
|
||||
* only for electron
|
||||
*/
|
||||
export const loader = () => {
|
||||
if (!BUILD_CONFIG.isElectron && !appConfigStorage.get('onBoarding')) {
|
||||
// onboarding is off, redirect to index
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const desktopApi = useServiceOptional(DesktopApiService);
|
||||
|
||||
@@ -1,53 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { type LoaderFunction, Navigate, useLoaderData } from 'react-router';
|
||||
|
||||
const trustedDomain = [
|
||||
'google.com',
|
||||
'stripe.com',
|
||||
'github.com',
|
||||
'twitter.com',
|
||||
'discord.gg',
|
||||
'youtube.com',
|
||||
't.me',
|
||||
'reddit.com',
|
||||
'affine.pro',
|
||||
];
|
||||
|
||||
const logger = new DebugLogger('redirect_proxy');
|
||||
|
||||
/**
|
||||
* /redirect-proxy page
|
||||
*
|
||||
* only for web
|
||||
*/
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const redirectUri = searchParams.get('redirect_uri');
|
||||
|
||||
if (!redirectUri) {
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const target = new URL(redirectUri);
|
||||
|
||||
if (
|
||||
target.hostname === window.location.hostname ||
|
||||
trustedDomain.some(domain =>
|
||||
new RegExp(`.?${domain}$`).test(target.hostname)
|
||||
)
|
||||
) {
|
||||
location.href = redirectUri;
|
||||
return { allow: true };
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Failed to parse redirect uri', e);
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
return { allow: true };
|
||||
};
|
||||
import { Navigate, useLoaderData } from 'react-router';
|
||||
|
||||
export const Component = () => {
|
||||
const { allow } = useLoaderData() as { allow: boolean };
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import { FACTORIES } from '@affine/routes';
|
||||
import type { LoaderFunction } from 'react-router';
|
||||
import { redirect } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { appConfigStorage } from '../components/hooks/use-app-config-storage';
|
||||
import { supportedClient } from './pages/auth/common';
|
||||
import type { MagicLinkLoaderData } from './pages/auth/magic-link';
|
||||
import type { OAuthCallbackLoaderData } from './pages/auth/oauth-callback';
|
||||
|
||||
const trustedDomain = [
|
||||
'google.com',
|
||||
'stripe.com',
|
||||
'github.com',
|
||||
'twitter.com',
|
||||
'discord.gg',
|
||||
'youtube.com',
|
||||
't.me',
|
||||
'reddit.com',
|
||||
'affine.pro',
|
||||
];
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'onboarding',
|
||||
'setPassword',
|
||||
'signIn',
|
||||
'changePassword',
|
||||
'signUp',
|
||||
'changeEmail',
|
||||
'confirm-change-email',
|
||||
'subscription-redirect',
|
||||
'verify-email',
|
||||
]);
|
||||
|
||||
const supportedProvider = z.nativeEnum(OAuthProviderType);
|
||||
|
||||
const oauthParameters = z.object({
|
||||
provider: supportedProvider,
|
||||
client: supportedClient,
|
||||
redirectUri: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
const redirectLogger = new DebugLogger('redirect_proxy');
|
||||
|
||||
/**
|
||||
* /onboarding page
|
||||
*
|
||||
* only for electron
|
||||
*/
|
||||
export const onboardingLoader = async () => {
|
||||
if (!BUILD_CONFIG.isElectron && !appConfigStorage.get('onBoarding')) {
|
||||
// onboarding is off, redirect to index
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* /redirect-proxy page
|
||||
*
|
||||
* only for web
|
||||
*/
|
||||
export const redirectLoader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const redirectUri = searchParams.get('redirect_uri');
|
||||
|
||||
if (!redirectUri) {
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const target = new URL(redirectUri);
|
||||
|
||||
if (
|
||||
target.hostname === window.location.hostname ||
|
||||
trustedDomain.some(domain =>
|
||||
new RegExp(`.?${domain}$`).test(target.hostname)
|
||||
)
|
||||
) {
|
||||
location.href = redirectUri;
|
||||
return { allow: true };
|
||||
}
|
||||
} catch (e) {
|
||||
redirectLogger.error('Failed to parse redirect uri', e);
|
||||
return { allow: false };
|
||||
}
|
||||
|
||||
return { allow: true };
|
||||
};
|
||||
|
||||
export const authLoader: LoaderFunction = async args => {
|
||||
if (!args.params.authType) {
|
||||
return redirect(FACTORIES.notFound());
|
||||
}
|
||||
if (!authTypeSchema.safeParse(args.params.authType).success) {
|
||||
return redirect(FACTORIES.notFound());
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const magicLinkLoader: LoaderFunction = ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const params = url.searchParams;
|
||||
const client = params.get('client');
|
||||
const email = params.get('email');
|
||||
const token = params.get('token');
|
||||
const redirectUri = params.get('redirect_uri');
|
||||
|
||||
if (!email || !token) {
|
||||
return redirect(FACTORIES.signIn() + '?error=Invalid magic link');
|
||||
}
|
||||
|
||||
const payload: MagicLinkLoaderData = {
|
||||
email,
|
||||
token,
|
||||
redirectUri,
|
||||
};
|
||||
|
||||
if (!client || client === 'web') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const clientCheckResult = supportedClient.safeParse(client);
|
||||
if (!clientCheckResult.success) {
|
||||
return redirect(FACTORIES.signIn() + '?error=Invalid callback parameters');
|
||||
}
|
||||
|
||||
const authParams = new URLSearchParams();
|
||||
authParams.set('method', 'magic-link');
|
||||
authParams.set('payload', JSON.stringify(payload));
|
||||
|
||||
return redirect(
|
||||
`${FACTORIES.openApp({ action: 'url' })}?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||
);
|
||||
};
|
||||
|
||||
export const oauthLoginLoader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const provider = searchParams.get('provider');
|
||||
const client = searchParams.get('client') ?? 'web';
|
||||
const redirectUri = searchParams.get('redirect_uri');
|
||||
|
||||
// sign out first, web only
|
||||
if (client === 'web') {
|
||||
await fetch('/api/auth/sign-out');
|
||||
}
|
||||
|
||||
const paramsParseResult = oauthParameters.safeParse({
|
||||
provider,
|
||||
client,
|
||||
redirectUri,
|
||||
});
|
||||
|
||||
if (paramsParseResult.success) {
|
||||
return {
|
||||
provider,
|
||||
client,
|
||||
redirectUri,
|
||||
};
|
||||
}
|
||||
|
||||
return redirect(
|
||||
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth parameters`)}`
|
||||
);
|
||||
};
|
||||
|
||||
export const oauthCallbackLoader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const queries = url.searchParams;
|
||||
const code = queries.get('code');
|
||||
let stateStr = queries.get('state') ?? '{}';
|
||||
|
||||
if (!code || !stateStr) {
|
||||
return redirect(
|
||||
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth callback parameters`)}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const { state, client, provider } = JSON.parse(stateStr);
|
||||
stateStr = state;
|
||||
|
||||
const payload: OAuthCallbackLoaderData = {
|
||||
state,
|
||||
code,
|
||||
provider,
|
||||
};
|
||||
|
||||
if (!client || client === 'web') {
|
||||
return payload;
|
||||
}
|
||||
|
||||
const clientCheckResult = supportedClient.safeParse(client);
|
||||
if (!clientCheckResult.success) {
|
||||
return redirect(
|
||||
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth callback parameters`)}`
|
||||
);
|
||||
}
|
||||
|
||||
const authParams = new URLSearchParams();
|
||||
authParams.set('method', 'oauth');
|
||||
authParams.set('payload', JSON.stringify(payload));
|
||||
authParams.set('server', location.origin);
|
||||
|
||||
return redirect(
|
||||
`${FACTORIES.openApp({ action: 'url' })}?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||
);
|
||||
} catch {
|
||||
return redirect(
|
||||
`${FACTORIES.signIn()}?error=${encodeURIComponent(`Invalid oauth callback parameters`)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,16 +1,26 @@
|
||||
import { wrapCreateBrowserRouterV7 } from '@sentry/react';
|
||||
import { FACTORIES, lazy, RELATIVE_ROUTES } from '@affine/routes';
|
||||
import { withSentryReactRouterV7Routing } from '@sentry/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { RouteObject } from 'react-router';
|
||||
import type { Params } from 'react-router';
|
||||
import {
|
||||
createBrowserRouter as reactRouterCreateBrowserRouter,
|
||||
redirect,
|
||||
Route,
|
||||
Routes as ReactRouterRoutes,
|
||||
useNavigate,
|
||||
} from 'react-router';
|
||||
|
||||
import { AffineErrorComponent } from '../components/affine/affine-error-boundary/affine-error-fallback';
|
||||
import { NavigateContext } from '../components/hooks/use-navigate-helper';
|
||||
import { AppContainer } from './components/app-container';
|
||||
import { RootWrapper } from './pages/root';
|
||||
|
||||
import {
|
||||
authLoader,
|
||||
magicLinkLoader,
|
||||
oauthCallbackLoader,
|
||||
oauthLoginLoader,
|
||||
onboardingLoader,
|
||||
redirectLoader,
|
||||
} from './router-loader';
|
||||
export function RootRouter() {
|
||||
const navigate = useNavigate();
|
||||
const [ready, setReady] = useState(false);
|
||||
@@ -27,176 +37,194 @@ export function RootRouter() {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const topLevelRoutes = [
|
||||
{
|
||||
element: <RootRouter />,
|
||||
errorElement: <AffineErrorComponent />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
lazy: async () => await import('./pages/index'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/*',
|
||||
lazy: async () => await import('./pages/workspace/index'),
|
||||
},
|
||||
{
|
||||
path: '/share/:workspaceId/:pageId',
|
||||
loader: ({ params }) => {
|
||||
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
lazy: async () => await import('./pages/404'),
|
||||
},
|
||||
{
|
||||
path: '/expired',
|
||||
lazy: async () => await import('./pages/expired'),
|
||||
},
|
||||
{
|
||||
path: '/invite/:inviteId',
|
||||
lazy: async () => await import('./pages/invite'),
|
||||
},
|
||||
{
|
||||
path: '/upgrade-success',
|
||||
lazy: async () => await import('./pages/upgrade-success'),
|
||||
},
|
||||
{
|
||||
path: '/upgrade-success/team',
|
||||
lazy: async () => await import('./pages/upgrade-success/team'),
|
||||
},
|
||||
{
|
||||
path: '/upgrade-success/self-hosted-team',
|
||||
lazy: async () =>
|
||||
await import('./pages/upgrade-success/self-host-team'),
|
||||
},
|
||||
{
|
||||
path: '/ai-upgrade-success',
|
||||
lazy: async () => await import('./pages/ai-upgrade-success'),
|
||||
},
|
||||
{
|
||||
path: '/onboarding',
|
||||
lazy: async () => await import('./pages/onboarding'),
|
||||
},
|
||||
{
|
||||
path: '/redirect-proxy',
|
||||
lazy: async () => await import('./pages/redirect'),
|
||||
},
|
||||
{
|
||||
path: '/subscribe',
|
||||
lazy: async () => await import('./pages/subscribe'),
|
||||
},
|
||||
{
|
||||
path: '/upgrade-to-team',
|
||||
lazy: async () => await import('./pages/upgrade-to-team'),
|
||||
},
|
||||
{
|
||||
path: '/try-cloud',
|
||||
loader: () => {
|
||||
return redirect(
|
||||
`/sign-in?redirect_uri=${encodeURIComponent('/?initCloud=true')}`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/theme-editor',
|
||||
lazy: async () => await import('./pages/theme-editor'),
|
||||
},
|
||||
{
|
||||
path: '/clipper/import',
|
||||
lazy: async () => await import('./pages/import-clipper'),
|
||||
},
|
||||
{
|
||||
path: '/template/import',
|
||||
lazy: async () => await import('./pages/import-template'),
|
||||
},
|
||||
{
|
||||
path: '/template/preview',
|
||||
loader: ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const workspaceId = url.searchParams.get('workspaceId');
|
||||
const docId = url.searchParams.get('docId');
|
||||
const templateName = url.searchParams.get('name');
|
||||
const templateMode = url.searchParams.get('mode');
|
||||
const snapshotUrl = url.searchParams.get('snapshotUrl');
|
||||
|
||||
return redirect(
|
||||
`/workspace/${workspaceId}/${docId}?${new URLSearchParams({
|
||||
isTemplate: 'true',
|
||||
templateName: templateName ?? '',
|
||||
snapshotUrl: snapshotUrl ?? '',
|
||||
mode: templateMode ?? 'page',
|
||||
}).toString()}`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/auth/:authType',
|
||||
lazy: async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/auth'),
|
||||
},
|
||||
{
|
||||
path: '/sign-In',
|
||||
lazy: async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/magic-link',
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ './pages/auth/magic-link'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/oauth/login',
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ './pages/auth/oauth-login'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/oauth/callback',
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ './pages/auth/oauth-callback'
|
||||
),
|
||||
},
|
||||
// deprecated, keep for old client compatibility
|
||||
// TODO(@forehalo): remove
|
||||
{
|
||||
path: '/desktop-signin',
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ './pages/auth/oauth-login'
|
||||
),
|
||||
},
|
||||
// deprecated, keep for old client compatibility
|
||||
// use '/sign-in'
|
||||
// TODO(@forehalo): remove
|
||||
{
|
||||
path: '/signIn',
|
||||
lazy: async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/open-app/:action',
|
||||
lazy: async () => await import('./pages/open-app'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: async () => await import('./pages/404'),
|
||||
},
|
||||
],
|
||||
},
|
||||
] satisfies [RouteObject, ...RouteObject[]];
|
||||
|
||||
const createBrowserRouter = wrapCreateBrowserRouterV7(
|
||||
reactRouterCreateBrowserRouter
|
||||
export const Index = lazy(async () => await import('./pages/index'));
|
||||
export const Workspace = lazy(
|
||||
async () => await import('./pages/workspace/index')
|
||||
);
|
||||
export const router = (
|
||||
window.SENTRY_RELEASE ? createBrowserRouter : reactRouterCreateBrowserRouter
|
||||
)(topLevelRoutes, {
|
||||
basename: environment.subPath,
|
||||
});
|
||||
export const NotFound = lazy(async () => await import('./pages/404'));
|
||||
export const Expired = lazy(async () => await import('./pages/expired'));
|
||||
export const Invite = lazy(async () => await import('./pages/invite'));
|
||||
export const UpgradeSuccess = lazy(
|
||||
async () => await import('./pages/upgrade-success')
|
||||
);
|
||||
export const UpgradeSuccessTeam = lazy(
|
||||
async () => await import('./pages/upgrade-success/team')
|
||||
);
|
||||
export const UpgradeSuccessSelfHostedTeam = lazy(
|
||||
async () => await import('./pages/upgrade-success/self-host-team')
|
||||
);
|
||||
export const AIUpgradeSuccess = lazy(
|
||||
async () => await import('./pages/ai-upgrade-success')
|
||||
);
|
||||
export const Subscribe = lazy(async () => await import('./pages/subscribe'));
|
||||
export const UpgradeToTeam = lazy(
|
||||
async () => await import('./pages/upgrade-to-team')
|
||||
);
|
||||
export const ThemeEditor = lazy(
|
||||
async () => await import('./pages/theme-editor')
|
||||
);
|
||||
export const ImportClipper = lazy(
|
||||
async () => await import('./pages/import-clipper')
|
||||
);
|
||||
export const ImportTemplate = lazy(
|
||||
async () => await import('./pages/import-template')
|
||||
);
|
||||
export const OpenApp = lazy(async () => await import('./pages/open-app'));
|
||||
export const Onboarding = lazy(async () => await import('./pages/onboarding'));
|
||||
export const Redirect = lazy(async () => await import('./pages/redirect'));
|
||||
export const Auth = lazy(
|
||||
async () => await import(/* webpackChunkName: "auth" */ './pages/auth/auth')
|
||||
);
|
||||
export const SignIn = lazy(
|
||||
async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/sign-in')
|
||||
);
|
||||
export const MagicLink = lazy(
|
||||
async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/magic-link')
|
||||
);
|
||||
export const OAuthLogin = lazy(
|
||||
async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/oauth-login')
|
||||
);
|
||||
export const OAuthCallback = lazy(
|
||||
async () =>
|
||||
await import(/* webpackChunkName: "auth" */ './pages/auth/oauth-callback')
|
||||
);
|
||||
|
||||
// Define routes using JSX syntax for better type checking
|
||||
export const routes = (
|
||||
<Route
|
||||
element={<RootRouter />}
|
||||
errorElement={<AffineErrorComponent />}
|
||||
hydrateFallbackElement={<AppContainer fallback />}
|
||||
>
|
||||
<Route path={RELATIVE_ROUTES.index}>
|
||||
<Route index element={<Index />} />
|
||||
<Route path={RELATIVE_ROUTES.workspace.index} element={<Workspace />} />
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.share}
|
||||
loader={async ({ params }: { params: Params<string> }) => {
|
||||
return redirect(
|
||||
FACTORIES.workspace.doc({
|
||||
workspaceId: params.workspaceId ?? '',
|
||||
docId: params.pageId ?? '',
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path={RELATIVE_ROUTES.notFound} element={<NotFound />} />
|
||||
<Route path={RELATIVE_ROUTES.expired} element={<Expired />} />
|
||||
<Route path={RELATIVE_ROUTES.invite} element={<Invite />} />
|
||||
<Route path={RELATIVE_ROUTES.upgradeSuccess.index}>
|
||||
<Route index element={<UpgradeSuccess />} />
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.upgradeSuccess.team}
|
||||
element={<UpgradeSuccessTeam />}
|
||||
/>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.upgradeSuccess.selfHostTeam}
|
||||
element={<UpgradeSuccessSelfHostedTeam />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.aiUpgradeSuccess}
|
||||
element={<AIUpgradeSuccess />}
|
||||
/>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.onboarding}
|
||||
element={<Onboarding />}
|
||||
loader={onboardingLoader}
|
||||
/>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.redirect}
|
||||
element={<Redirect />}
|
||||
loader={redirectLoader}
|
||||
/>
|
||||
<Route path={RELATIVE_ROUTES.subscribe} element={<Subscribe />} />
|
||||
<Route path={RELATIVE_ROUTES.upgradeToTeam} element={<UpgradeToTeam />} />
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.tryCloud}
|
||||
loader={async () => {
|
||||
return redirect(
|
||||
FACTORIES.signIn() +
|
||||
`?redirect_uri=${encodeURIComponent('/?initCloud=true')}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path={RELATIVE_ROUTES.themeEditor} element={<ThemeEditor />} />
|
||||
<Route path="clipper/import" element={<ImportClipper />} />
|
||||
<Route path={RELATIVE_ROUTES.template.index} element={<ImportTemplate />}>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.template.import}
|
||||
element={<ImportTemplate />}
|
||||
/>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.template.preview}
|
||||
loader={async ({ request }: { request: Request }) => {
|
||||
const url = new URL(request.url);
|
||||
const workspaceId = url.searchParams.get('workspaceId');
|
||||
const docId = url.searchParams.get('docId');
|
||||
const templateName = url.searchParams.get('name');
|
||||
const templateMode = url.searchParams.get('mode');
|
||||
const snapshotUrl = url.searchParams.get('snapshotUrl');
|
||||
|
||||
return redirect(
|
||||
FACTORIES.workspace.doc({
|
||||
workspaceId: workspaceId ?? '',
|
||||
docId: docId ?? '',
|
||||
}) +
|
||||
`?${new URLSearchParams({
|
||||
isTemplate: 'true',
|
||||
templateName: templateName ?? '',
|
||||
snapshotUrl: snapshotUrl ?? '',
|
||||
mode: templateMode ?? 'page',
|
||||
}).toString()}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.auth}
|
||||
element={<Auth />}
|
||||
loader={authLoader}
|
||||
/>
|
||||
<Route path={RELATIVE_ROUTES.signIn} element={<SignIn />} />
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.magicLink}
|
||||
element={<MagicLink />}
|
||||
loader={magicLinkLoader}
|
||||
/>
|
||||
<Route path={RELATIVE_ROUTES.oauth.index} element={<NotFound />}>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.oauth.login}
|
||||
element={<OAuthLogin />}
|
||||
loader={oauthLoginLoader}
|
||||
/>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.oauth.callback}
|
||||
element={<OAuthCallback />}
|
||||
loader={oauthCallbackLoader}
|
||||
/>
|
||||
</Route>
|
||||
<Route path={RELATIVE_ROUTES.openApp} element={<OpenApp />} />
|
||||
{/* deprecated, keep for old client compatibility */}
|
||||
{/* TODO(@forehalo): remove */}
|
||||
<Route path="desktop-signin" element={<OAuthLogin />} />
|
||||
{/* deprecated, keep for old client compatibility */}
|
||||
{/* use '/sign-in' */}
|
||||
{/* TODO(@forehalo): remove */}
|
||||
<Route path="signIn" element={<SignIn />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Route>
|
||||
);
|
||||
|
||||
// Apply Sentry wrapper to ReactRouterRoutes if needed
|
||||
const Routes = window.SENTRY_RELEASE
|
||||
? withSentryReactRouterV7Routing(ReactRouterRoutes)
|
||||
: ReactRouterRoutes;
|
||||
|
||||
// Export Router component - will be wrapped by BrowserRouter in app.tsx
|
||||
export const Router = () => <Routes>{routes}</Routes>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { ROUTES } from '@affine/routes';
|
||||
import { wrapCreateBrowserRouterV7 } from '@sentry/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { RouteObject } from 'react-router';
|
||||
@@ -34,58 +35,60 @@ export const topLevelRoutes = [
|
||||
hydrateFallbackElement: <AppFallback />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
path: ROUTES.index,
|
||||
lazy: async () => await import('./pages/index'),
|
||||
},
|
||||
{
|
||||
path: '/workspace/:workspaceId/*',
|
||||
path: `${ROUTES.workspace.index}/*`,
|
||||
lazy: async () => await import('./pages/workspace/index'),
|
||||
},
|
||||
{
|
||||
path: '/share/:workspaceId/:pageId',
|
||||
path: ROUTES.share,
|
||||
loader: async ({ params }) => {
|
||||
return redirect(`/workspace/${params.workspaceId}/${params.pageId}`);
|
||||
return redirect(
|
||||
`/workspaces/${params.workspaceId}/docs/${params.pageId}`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
path: ROUTES.notFound,
|
||||
lazy: async () => await import('./pages/404'),
|
||||
},
|
||||
{
|
||||
path: '/auth/:authType',
|
||||
path: ROUTES.auth,
|
||||
lazy: async () => await import('./pages/auth'),
|
||||
},
|
||||
{
|
||||
path: '/sign-in',
|
||||
path: ROUTES.signIn,
|
||||
lazy: async () => await import('./pages/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/magic-link',
|
||||
path: ROUTES.magicLink,
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/magic-link'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/oauth/login',
|
||||
path: ROUTES.oauth.login,
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-login'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/oauth/callback',
|
||||
path: ROUTES.oauth.callback,
|
||||
lazy: async () =>
|
||||
await import(
|
||||
/* webpackChunkName: "auth" */ '@affine/core/desktop/pages/auth/oauth-callback'
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/redirect-proxy',
|
||||
path: ROUTES.redirect,
|
||||
lazy: async () => await import('@affine/core/desktop/pages/redirect'),
|
||||
},
|
||||
{
|
||||
path: '/open-app/:action',
|
||||
path: ROUTES.openApp,
|
||||
lazy: async () => await import('@affine/core/desktop/pages/open-app'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,70 @@
|
||||
"$schema": "./schema.json",
|
||||
"route": "/",
|
||||
"children": {
|
||||
"workspace": {
|
||||
"route": "workspaces/:workspaceId",
|
||||
"children": {
|
||||
"all": "all",
|
||||
"trash": "trash",
|
||||
"doc": {
|
||||
"route": "docs/:docId",
|
||||
"children": {
|
||||
"attachment": "attachment/:attachmentId"
|
||||
}
|
||||
},
|
||||
"journals": "journals",
|
||||
"collections": {
|
||||
"route": "collections",
|
||||
"children": {
|
||||
"collection": ":collectionId"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"route": "tags",
|
||||
"children": {
|
||||
"tag": ":tagId"
|
||||
}
|
||||
},
|
||||
"settings": "settings"
|
||||
}
|
||||
},
|
||||
"share": "share/:workspaceId/:pageId",
|
||||
"expired": "expired",
|
||||
"invite": "invite/:inviteId",
|
||||
"payment": "payment/:plan/success",
|
||||
"onboarding": "onboarding",
|
||||
"redirect": "redirect",
|
||||
"subscribe": "subscribe",
|
||||
"upgradeToTeam": "upgrade-to-team",
|
||||
"upgradeSuccess": {
|
||||
"route": "upgrade-success",
|
||||
"children": {
|
||||
"team": "team",
|
||||
"selfHostTeam": "self-host-team"
|
||||
}
|
||||
},
|
||||
"aiUpgradeSuccess": "ai-upgrade-success",
|
||||
"tryCloud": "try-cloud",
|
||||
"themeEditor": "theme-editor",
|
||||
"template": {
|
||||
"route": "template",
|
||||
"children": {
|
||||
"import": "import",
|
||||
"preview": "preview"
|
||||
}
|
||||
},
|
||||
"auth": "auth/:authType",
|
||||
"signIn": "sign-in",
|
||||
"magicLink": "magic-link",
|
||||
"oauth": {
|
||||
"route": "oauth",
|
||||
"children": {
|
||||
"login": "login",
|
||||
"callback": "callback"
|
||||
}
|
||||
},
|
||||
"openApp": "open-app/:action",
|
||||
"notFound": "404",
|
||||
"admin": {
|
||||
"route": "admin",
|
||||
"children": {
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
// #region Path Parameter Types
|
||||
export interface RouteParamsTypes {
|
||||
workspace: {
|
||||
index: { workspaceId: string };
|
||||
all: { workspaceId: string };
|
||||
trash: { workspaceId: string };
|
||||
doc: {
|
||||
index: { workspaceId: string; docId: string };
|
||||
attachment: { workspaceId: string; docId: string; attachmentId: string };
|
||||
};
|
||||
journals: { workspaceId: string };
|
||||
collections: {
|
||||
index: { workspaceId: string };
|
||||
collection: { workspaceId: string; collectionId: string };
|
||||
};
|
||||
tags: {
|
||||
index: { workspaceId: string };
|
||||
tag: { workspaceId: string; tagId: string };
|
||||
};
|
||||
settings: { workspaceId: string };
|
||||
};
|
||||
share: { workspaceId: string; pageId: string };
|
||||
invite: { inviteId: string };
|
||||
payment: { plan: string };
|
||||
auth: { authType: string };
|
||||
openApp: { action: string };
|
||||
admin: { settings: { module: { module: string } } };
|
||||
}
|
||||
// #endregion
|
||||
@@ -7,6 +31,57 @@ export interface RouteParamsTypes {
|
||||
// #region Absolute Paths
|
||||
export const ROUTES = {
|
||||
index: '/',
|
||||
workspace: {
|
||||
index: '/workspaces/:workspaceId',
|
||||
all: '/workspaces/:workspaceId/all',
|
||||
trash: '/workspaces/:workspaceId/trash',
|
||||
doc: {
|
||||
index: '/workspaces/:workspaceId/docs/:docId',
|
||||
attachment:
|
||||
'/workspaces/:workspaceId/docs/:docId/attachment/:attachmentId',
|
||||
},
|
||||
journals: '/workspaces/:workspaceId/journals',
|
||||
collections: {
|
||||
index: '/workspaces/:workspaceId/collections',
|
||||
collection: '/workspaces/:workspaceId/collections/:collectionId',
|
||||
},
|
||||
tags: {
|
||||
index: '/workspaces/:workspaceId/tags',
|
||||
tag: '/workspaces/:workspaceId/tags/:tagId',
|
||||
},
|
||||
settings: '/workspaces/:workspaceId/settings',
|
||||
},
|
||||
share: '/share/:workspaceId/:pageId',
|
||||
expired: '/expired',
|
||||
invite: '/invite/:inviteId',
|
||||
payment: '/payment/:plan/success',
|
||||
onboarding: '/onboarding',
|
||||
redirect: '/redirect',
|
||||
subscribe: '/subscribe',
|
||||
upgradeToTeam: '/upgrade-to-team',
|
||||
upgradeSuccess: {
|
||||
index: '/upgrade-success',
|
||||
team: '/upgrade-success/team',
|
||||
selfHostTeam: '/upgrade-success/self-host-team',
|
||||
},
|
||||
aiUpgradeSuccess: '/ai-upgrade-success',
|
||||
tryCloud: '/try-cloud',
|
||||
themeEditor: '/theme-editor',
|
||||
template: {
|
||||
index: '/template',
|
||||
import: '/template/import',
|
||||
preview: '/template/preview',
|
||||
},
|
||||
auth: '/auth/:authType',
|
||||
signIn: '/sign-in',
|
||||
magicLink: '/magic-link',
|
||||
oauth: {
|
||||
index: '/oauth',
|
||||
login: '/oauth/login',
|
||||
callback: '/oauth/callback',
|
||||
},
|
||||
openApp: '/open-app/:action',
|
||||
notFound: '/404',
|
||||
admin: {
|
||||
index: '/admin',
|
||||
auth: '/admin/auth',
|
||||
@@ -23,6 +98,39 @@ export const ROUTES = {
|
||||
// #region Relative Paths
|
||||
export const RELATIVE_ROUTES = {
|
||||
index: '/',
|
||||
workspace: {
|
||||
index: 'workspaces/:workspaceId',
|
||||
all: 'all',
|
||||
trash: 'trash',
|
||||
doc: { index: 'docs/:docId', attachment: 'attachment/:attachmentId' },
|
||||
journals: 'journals',
|
||||
collections: { index: 'collections', collection: ':collectionId' },
|
||||
tags: { index: 'tags', tag: ':tagId' },
|
||||
settings: 'settings',
|
||||
},
|
||||
share: 'share/:workspaceId/:pageId',
|
||||
expired: 'expired',
|
||||
invite: 'invite/:inviteId',
|
||||
payment: 'payment/:plan/success',
|
||||
onboarding: 'onboarding',
|
||||
redirect: 'redirect',
|
||||
subscribe: 'subscribe',
|
||||
upgradeToTeam: 'upgrade-to-team',
|
||||
upgradeSuccess: {
|
||||
index: 'upgrade-success',
|
||||
team: 'team',
|
||||
selfHostTeam: 'self-host-team',
|
||||
},
|
||||
aiUpgradeSuccess: 'ai-upgrade-success',
|
||||
tryCloud: 'try-cloud',
|
||||
themeEditor: 'theme-editor',
|
||||
template: { index: 'template', import: 'import', preview: 'preview' },
|
||||
auth: 'auth/:authType',
|
||||
signIn: 'sign-in',
|
||||
magicLink: 'magic-link',
|
||||
oauth: { index: 'oauth', login: 'login', callback: 'callback' },
|
||||
openApp: 'open-app/:action',
|
||||
notFound: '404',
|
||||
admin: {
|
||||
index: 'admin',
|
||||
auth: 'auth',
|
||||
@@ -38,6 +146,63 @@ export const RELATIVE_ROUTES = {
|
||||
|
||||
// #region Path Factories
|
||||
const home = () => '/';
|
||||
const workspace = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}`;
|
||||
workspace.all = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/all`;
|
||||
workspace.trash = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/trash`;
|
||||
const workspace_doc = (params: { workspaceId: string; docId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/docs/${params.docId}`;
|
||||
workspace_doc.attachment = (params: {
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
attachmentId: string;
|
||||
}) =>
|
||||
`/workspaces/${params.workspaceId}/docs/${params.docId}/attachment/${params.attachmentId}`;
|
||||
workspace.doc = workspace_doc;
|
||||
workspace.journals = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/journals`;
|
||||
const workspace_collections = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/collections`;
|
||||
workspace_collections.collection = (params: {
|
||||
workspaceId: string;
|
||||
collectionId: string;
|
||||
}) => `/workspaces/${params.workspaceId}/collections/${params.collectionId}`;
|
||||
workspace.collections = workspace_collections;
|
||||
const workspace_tags = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/tags`;
|
||||
workspace_tags.tag = (params: { workspaceId: string; tagId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/tags/${params.tagId}`;
|
||||
workspace.tags = workspace_tags;
|
||||
workspace.settings = (params: { workspaceId: string }) =>
|
||||
`/workspaces/${params.workspaceId}/settings`;
|
||||
const share = (params: { workspaceId: string; pageId: string }) =>
|
||||
`/share/${params.workspaceId}/${params.pageId}`;
|
||||
const expired = () => '/expired';
|
||||
const invite = (params: { inviteId: string }) => `/invite/${params.inviteId}`;
|
||||
const payment = (params: { plan: string }) => `/payment/${params.plan}/success`;
|
||||
const onboarding = () => '/onboarding';
|
||||
const redirect = () => '/redirect';
|
||||
const subscribe = () => '/subscribe';
|
||||
const upgradeToTeam = () => '/upgrade-to-team';
|
||||
const upgradeSuccess = () => '/upgrade-success';
|
||||
upgradeSuccess.team = () => '/upgrade-success/team';
|
||||
upgradeSuccess.selfHostTeam = () => '/upgrade-success/self-host-team';
|
||||
const aiUpgradeSuccess = () => '/ai-upgrade-success';
|
||||
const tryCloud = () => '/try-cloud';
|
||||
const themeEditor = () => '/theme-editor';
|
||||
const template = () => '/template';
|
||||
template.import = () => '/template/import';
|
||||
template.preview = () => '/template/preview';
|
||||
const auth = (params: { authType: string }) => `/auth/${params.authType}`;
|
||||
const signIn = () => '/sign-in';
|
||||
const magicLink = () => '/magic-link';
|
||||
const oauth = () => '/oauth';
|
||||
oauth.login = () => '/oauth/login';
|
||||
oauth.callback = () => '/oauth/callback';
|
||||
const openApp = (params: { action: string }) => `/open-app/${params.action}`;
|
||||
const notFound = () => '/404';
|
||||
const admin = () => '/admin';
|
||||
admin.auth = () => '/admin/auth';
|
||||
admin.setup = () => '/admin/setup';
|
||||
@@ -49,5 +214,28 @@ admin_settings.module = (params: { module: string }) =>
|
||||
admin.settings = admin_settings;
|
||||
admin.about = () => '/admin/about';
|
||||
admin.notFound = () => '/admin/404';
|
||||
export const FACTORIES = { admin, home };
|
||||
export const FACTORIES = {
|
||||
workspace,
|
||||
share,
|
||||
expired,
|
||||
invite,
|
||||
payment,
|
||||
onboarding,
|
||||
redirect,
|
||||
subscribe,
|
||||
upgradeToTeam,
|
||||
upgradeSuccess,
|
||||
aiUpgradeSuccess,
|
||||
tryCloud,
|
||||
themeEditor,
|
||||
template,
|
||||
auth,
|
||||
signIn,
|
||||
magicLink,
|
||||
oauth,
|
||||
openApp,
|
||||
notFound,
|
||||
admin,
|
||||
home,
|
||||
};
|
||||
// #endregion
|
||||
|
||||
Reference in New Issue
Block a user