From e33aa35f7ed0e8b75e2d2fbe230b74eafa29eeee Mon Sep 17 00:00:00 2001 From: forehalo Date: Tue, 3 Sep 2024 09:03:43 +0000 Subject: [PATCH] refactor(core): auth (#7999) closes AF-753 AF-1227 --- .../auth-components/onboarding-page.tsx | 2 + .../core/src/components/affine/auth/oauth.tsx | 20 +--- .../src/components/affine/auth/sign-in.tsx | 39 +++---- .../src/components/affine/auth/style.css.ts | 8 ++ .../core/src/modules/cloud/services/auth.ts | 100 +++++++++--------- .../core/src/modules/cloud/stores/auth.ts | 23 ++-- .../frontend/core/src/pages/magic-link.tsx | 15 +++ .../core/src/pages/oauth-callback.tsx | 74 +++++++++++++ packages/frontend/core/src/pages/open-app.tsx | 31 +++--- packages/frontend/core/src/pages/redirect.tsx | 1 + packages/frontend/core/src/router.tsx | 22 ++-- packages/frontend/core/src/utils/index.ts | 1 + packages/frontend/core/src/utils/url.ts | 33 ++++++ packages/frontend/electron/renderer/app.tsx | 2 + .../frontend/electron/src/main/deep-link.ts | 55 +++------- .../src/main/windows-manager/main-window.ts | 13 ++- .../electron/src/preload/electron-api.ts | 12 ++- tools/cli/src/webpack/config.ts | 1 - 18 files changed, 286 insertions(+), 166 deletions(-) create mode 100644 packages/frontend/core/src/pages/oauth-callback.tsx create mode 100644 packages/frontend/core/src/utils/url.ts diff --git a/packages/frontend/component/src/components/auth-components/onboarding-page.tsx b/packages/frontend/component/src/components/auth-components/onboarding-page.tsx index 75c16f350..82444b3b5 100644 --- a/packages/frontend/component/src/components/auth-components/onboarding-page.tsx +++ b/packages/frontend/component/src/components/auth-components/onboarding-page.tsx @@ -126,6 +126,8 @@ export const OnboardingPage = ({ return null; } + // deprecated + // TODO(@forehalo): remove if (callbackUrl?.startsWith('/open-app/signin-redirect')) { const url = new URL(callbackUrl, window.location.origin); url.searchParams.set('next', 'onboarding'); diff --git a/packages/frontend/core/src/components/affine/auth/oauth.tsx b/packages/frontend/core/src/components/affine/auth/oauth.tsx index a0b78de0e..5fbc09fb5 100644 --- a/packages/frontend/core/src/components/affine/auth/oauth.tsx +++ b/packages/frontend/core/src/components/affine/auth/oauth.tsx @@ -30,7 +30,7 @@ const OAuthProviderMap: Record< }, }; -export function OAuth({ redirectUri }: { redirectUri?: string | null }) { +export function OAuth() { const serverConfig = useService(ServerConfigService).serverConfig; const oauth = useLiveData(serverConfig.features$.map(r => r?.oauth)); const oauthProviders = useLiveData( @@ -47,21 +47,11 @@ export function OAuth({ redirectUri }: { redirectUri?: string | null }) { } return oauthProviders?.map(provider => ( - + )); } -function OAuthProvider({ - provider, - redirectUri, -}: { - provider: OAuthProviderType; - redirectUri?: string | null; -}) { +function OAuthProvider({ provider }: { provider: OAuthProviderType }) { const { icon } = OAuthProviderMap[provider]; const authService = useService(AuthService); const [isConnecting, setIsConnecting] = useState(false); @@ -69,7 +59,7 @@ function OAuthProvider({ const onClick = useAsyncCallback(async () => { try { setIsConnecting(true); - await authService.signInOauth(provider, redirectUri); + await authService.signInOauth(provider); } catch (err) { console.error(err); notify.error({ title: 'Failed to sign in, please try again.' }); @@ -77,7 +67,7 @@ function OAuthProvider({ setIsConnecting(false); track.$.$.auth.oauth({ provider }); } - }, [authService, provider, redirectUri]); + }, [authService, provider]); return ( - ) : null} + ) : ( + + )} + + {errorMsg &&
{errorMsg}
}
{/*prettier-ignore*/} diff --git a/packages/frontend/core/src/components/affine/auth/style.css.ts b/packages/frontend/core/src/components/affine/auth/style.css.ts index cd7695528..fdbe8d55b 100644 --- a/packages/frontend/core/src/components/affine/auth/style.css.ts +++ b/packages/frontend/core/src/components/affine/auth/style.css.ts @@ -14,6 +14,14 @@ export const authMessage = style({ fontSize: cssVar('fontXs'), lineHeight: 1.5, }); + +export const errorMessage = style({ + marginTop: '30px', + color: cssVar('textHighlightForegroundRed'), + fontSize: cssVar('fontXs'), + lineHeight: 1.5, +}); + globalStyle(`${authMessage} a`, { color: cssVar('linkColor'), }); diff --git a/packages/frontend/core/src/modules/cloud/services/auth.ts b/packages/frontend/core/src/modules/cloud/services/auth.ts index 9fea3730c..f4ee13e2b 100644 --- a/packages/frontend/core/src/modules/cloud/services/auth.ts +++ b/packages/frontend/core/src/modules/cloud/services/auth.ts @@ -1,4 +1,5 @@ import { AIProvider } from '@affine/core/blocksuite/presets/ai'; +import { buildAppUrl, popupWindow } from '@affine/core/utils'; import { apis, appInfo } from '@affine/electron-api'; import type { OAuthProviderType } from '@affine/graphql'; import { @@ -80,72 +81,82 @@ export class AuthService extends Service { async sendEmailMagicLink( email: string, verifyToken: string, - challenge?: string, - redirectUri?: string | null + challenge?: string ) { const searchParams = new URLSearchParams(); if (challenge) { searchParams.set('challenge', challenge); } searchParams.set('token', verifyToken); - const redirect = environment.isDesktop - ? this.buildRedirectUri('/open-app/signin-redirect') - : (redirectUri ?? location.href); - searchParams.set('redirect_uri', redirect.toString()); const res = await this.fetchService.fetch( '/api/auth/sign-in?' + searchParams.toString(), { method: 'POST', - body: JSON.stringify({ email }), + body: JSON.stringify({ + email, + // we call it [callbackUrl] instead of [redirect_uri] + // to make it clear the url is used to finish the sign-in process instead of redirect after signed-in + callbackUrl: buildAppUrl('/magic-link', { + desktop: environment.isDesktop, + openInHiddenWindow: true, + redirectFromWeb: true, + }), + }), headers: { 'content-type': 'application/json', }, } ); - if (!res?.ok) { + if (!res.ok) { throw new Error('Failed to send email'); } } - async signInOauth(provider: OAuthProviderType, redirectUri?: string | null) { + async signInOauth(provider: OAuthProviderType) { + const res = await this.fetchService.fetch('/api/oauth/preflight', { + method: 'POST', + body: JSON.stringify({ provider }), + headers: { + 'content-type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`Failed to sign in with ${provider}`); + } + + let { url } = await res.json(); + + // change `state=xxx` to `state={state:xxx,native:true}` + // so we could know the callback should be redirect to native app + const oauthUrl = new URL(url); + oauthUrl.searchParams.set( + 'state', + JSON.stringify({ + state: oauthUrl.searchParams.get('state'), + client: environment.isDesktop ? appInfo?.schema : 'web', + }) + ); + url = oauthUrl.toString(); + if (environment.isDesktop) { - await apis?.ui.openExternal( - `${ - runtimeConfig.serverUrlPrefix - }/desktop-signin?provider=${provider}&redirect_uri=${this.buildRedirectUri( - '/open-app/signin-redirect' - )}` - ); + await apis?.ui.openExternal(url); } else { - location.href = `${ - runtimeConfig.serverUrlPrefix - }/oauth/login?provider=${provider}&redirect_uri=${encodeURIComponent( - redirectUri ?? location.pathname - )}`; + popupWindow(url); } return; } async signInPassword(credential: { email: string; password: string }) { - const searchParams = new URLSearchParams(); - const redirectUri = new URL(location.href); - if (environment.isDesktop) { - redirectUri.pathname = this.buildRedirectUri('/open-app/signin-redirect'); - } - searchParams.set('redirect_uri', redirectUri.toString()); - - const res = await this.fetchService.fetch( - '/api/auth/sign-in?' + searchParams.toString(), - { - method: 'POST', - body: JSON.stringify(credential), - headers: { - 'content-type': 'application/json', - }, - } - ); + const res = await this.fetchService.fetch('/api/auth/sign-in', { + method: 'POST', + body: JSON.stringify(credential), + headers: { + 'content-type': 'application/json', + }, + }); if (!res.ok) { throw new Error('Failed to sign in'); } @@ -158,19 +169,6 @@ export class AuthService extends Service { this.session.revalidate(); } - private buildRedirectUri(callbackUrl: string) { - const params: string[][] = []; - if (environment.isDesktop && appInfo?.schema) { - params.push(['schema', appInfo.schema]); - } - const query = - params.length > 0 - ? '?' + - params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&') - : ''; - return callbackUrl + query; - } - checkUserByEmail(email: string) { return this.store.checkUserByEmail(email); } diff --git a/packages/frontend/core/src/modules/cloud/stores/auth.ts b/packages/frontend/core/src/modules/cloud/stores/auth.ts index 535fb1778..06e860d70 100644 --- a/packages/frontend/core/src/modules/cloud/stores/auth.ts +++ b/packages/frontend/core/src/modules/cloud/stores/auth.ts @@ -1,5 +1,4 @@ import { - getUserQuery, removeAvatarMutation, updateUserProfileMutation, uploadAvatarMutation, @@ -81,15 +80,23 @@ export class AuthStore extends Store { } async checkUserByEmail(email: string) { - const data = await this.gqlService.gql({ - query: getUserQuery, - variables: { - email, + const res = await this.fetchService.fetch('/api/auth/preflight', { + method: 'POST', + body: JSON.stringify({ email }), + headers: { + 'content-type': 'application/json', }, }); - return { - isExist: !!data.user, - hasPassword: !!data.user?.hasPassword, + + if (!res.ok) { + throw new Error(`Failed to check user by email: ${email}`); + } + + const data = (await res.json()) as { + registered: boolean; + hasPassword: boolean; }; + + return data; } } diff --git a/packages/frontend/core/src/pages/magic-link.tsx b/packages/frontend/core/src/pages/magic-link.tsx index 6f833c425..ff36e5952 100644 --- a/packages/frontend/core/src/pages/magic-link.tsx +++ b/packages/frontend/core/src/pages/magic-link.tsx @@ -1,5 +1,9 @@ +import { useLiveData, useService } from '@toeverything/infra'; +import { useEffect } from 'react'; import { type LoaderFunction, redirect } from 'react-router-dom'; +import { AuthService } from '../modules/cloud'; + export const loader: LoaderFunction = async ({ request }) => { const url = new URL(request.url); const queries = url.searchParams; @@ -35,6 +39,17 @@ export const loader: LoaderFunction = async ({ request }) => { }; export const Component = () => { + const service = useService(AuthService); + const user = useLiveData(service.session.account$); + useEffect(() => { + service.session.revalidate(); + }, [service]); + + // TODO(@pengx17): window.close() in electron hidden window will close main window as well + if (!environment.isDesktop && user) { + window.close(); + } + // TODO(@eyhn): loading ui return null; }; diff --git a/packages/frontend/core/src/pages/oauth-callback.tsx b/packages/frontend/core/src/pages/oauth-callback.tsx new file mode 100644 index 000000000..fb996f405 --- /dev/null +++ b/packages/frontend/core/src/pages/oauth-callback.tsx @@ -0,0 +1,74 @@ +import { useLiveData, useService } from '@toeverything/infra'; +import { useEffect } from 'react'; +import { type LoaderFunction, redirect } from 'react-router-dom'; + +import { AuthService } from '../modules/cloud'; + +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') ?? '{}'; + + let error: string | undefined; + try { + const { state, client } = JSON.parse(stateStr); + stateStr = state; + + // bypass code & state to redirect_uri + if (!environment.isDesktop && client && client !== 'web') { + url.searchParams.set('state', JSON.stringify({ state })); + return redirect( + `/open-app/url?url=${encodeURIComponent(`${client}://${url.pathname}${url.search}`)}&hidden=true` + ); + } + } catch { + error = 'Invalid oauth callback parameters'; + } + + const res = await fetch('/api/oauth/callback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code, state: stateStr }), + }); + + if (!res.ok) { + try { + const { message } = await res.json(); + error = message; + } catch { + error = 'failed to verify sign-in token'; + } + } + + if (error) { + // TODO(@pengx17): in desktop app, the callback page will be opened in a hidden window + // how could we tell the main window to show the error message? + return redirect(`/signIn?error=${encodeURIComponent(error)}`); + } else { + const body = await res.json(); + /* @deprecated handle for old client */ + if (body.redirect_uri) { + return redirect(body.redirect_uri); + } + } + + return null; +}; + +export const Component = () => { + const service = useService(AuthService); + const user = useLiveData(service.session.account$); + useEffect(() => { + service.session.revalidate(); + }, [service]); + + // TODO(@pengx17): window.close() in electron hidden window will close main window as well + if (!environment.isDesktop && user) { + window.close(); + } + + return null; +}; diff --git a/packages/frontend/core/src/pages/open-app.tsx b/packages/frontend/core/src/pages/open-app.tsx index ff84e73a5..3276a4f03 100644 --- a/packages/frontend/core/src/pages/open-app.tsx +++ b/packages/frontend/core/src/pages/open-app.tsx @@ -148,24 +148,31 @@ const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => { const OpenUrl = () => { const [params] = useSearchParams(); - const urlToOpen = useMemo(() => params.get('url'), [params]); - const channel = useMemo(() => { - const urlObj = new URL(urlToOpen || ''); - const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', '')); - return schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine']; - }, [urlToOpen]); + const urlToOpen = params.get('url'); + params.delete('url'); - return ; + const urlObj = new URL(urlToOpen || ''); + const maybeSchema = appSchemas.safeParse(urlObj.protocol.replace(':', '')); + const channel = + schemaToChanel[maybeSchema.success ? maybeSchema.data : 'affine']; + + params.forEach((v, k) => { + urlObj.searchParams.set(k, v); + }); + + return ; }; +/** + * @deprecated + */ const OpenOAuthJwt = () => { const { currentUser } = useLoaderData() as LoaderData; const [params] = useSearchParams(); - const schema = useMemo(() => { - const maybeSchema = appSchemas.safeParse(params.get('schema')); - return maybeSchema.success ? maybeSchema.data : 'affine'; - }, [params]); - const next = useMemo(() => params.get('next'), [params]); + + const maybeSchema = appSchemas.safeParse(params.get('schema')); + const schema = maybeSchema.success ? maybeSchema.data : 'affine'; + const next = params.get('next'); const channel = schemaToChanel[schema as Schema]; if (!currentUser || !currentUser?.token?.sessionToken) { diff --git a/packages/frontend/core/src/pages/redirect.tsx b/packages/frontend/core/src/pages/redirect.tsx index d4869f475..fad78a794 100644 --- a/packages/frontend/core/src/pages/redirect.tsx +++ b/packages/frontend/core/src/pages/redirect.tsx @@ -2,6 +2,7 @@ import { DebugLogger } from '@affine/debug'; import { type LoaderFunction, Navigate, useLoaderData } from 'react-router-dom'; const trustedDomain = [ + 'google.com', 'stripe.com', 'github.com', 'twitter.com', diff --git a/packages/frontend/core/src/router.tsx b/packages/frontend/core/src/router.tsx index 3fe3d4567..37dbd7de0 100644 --- a/packages/frontend/core/src/router.tsx +++ b/packages/frontend/core/src/router.tsx @@ -74,10 +74,6 @@ export const topLevelRoutes = [ path: '/magic-link', lazy: () => import('./pages/magic-link'), }, - { - path: '/open-app/:action', - lazy: () => import('./pages/open-app'), - }, { path: '/upgrade-success', lazy: () => import('./pages/upgrade-success'), @@ -86,10 +82,6 @@ export const topLevelRoutes = [ path: '/ai-upgrade-success', lazy: () => import('./pages/ai-upgrade-success'), }, - { - path: '/desktop-signin', - lazy: () => import('./pages/desktop-signin'), - }, { path: '/onboarding', lazy: () => import('./pages/onboarding'), @@ -118,6 +110,20 @@ export const topLevelRoutes = [ path: '/template/import', lazy: () => import('./pages/import-template'), }, + { + path: '/oauth/callback', + lazy: () => import('./pages/oauth-callback'), + }, + { + path: '/open-app/:action', + lazy: () => import('./pages/open-app'), + }, + // deprecated, keep for old client compatibility + // TODO(@forehalo): remove + { + path: '/desktop-signin', + lazy: () => import('./pages/desktop-signin'), + }, { path: '*', lazy: () => import('./pages/404'), diff --git a/packages/frontend/core/src/utils/index.ts b/packages/frontend/core/src/utils/index.ts index c83da23b3..2311d8c22 100644 --- a/packages/frontend/core/src/utils/index.ts +++ b/packages/frontend/core/src/utils/index.ts @@ -6,3 +6,4 @@ export * from './popup'; export * from './string2color'; export * from './toast'; export * from './unflatten-object'; +export * from './url'; diff --git a/packages/frontend/core/src/utils/url.ts b/packages/frontend/core/src/utils/url.ts new file mode 100644 index 000000000..b916efefe --- /dev/null +++ b/packages/frontend/core/src/utils/url.ts @@ -0,0 +1,33 @@ +import { appInfo } from '@affine/electron-api'; + +interface AppUrlOptions { + desktop?: boolean | string; + openInHiddenWindow?: boolean; + redirectFromWeb?: boolean; +} + +export function buildAppUrl(path: string, opts: AppUrlOptions = {}) { + // TODO(@EYHN): should use server base url + const webBase = runtimeConfig.serverUrlPrefix; + // TODO(@pengx17): how could we know the corresponding app schema in web environment + if (opts.desktop && appInfo?.schema) { + const urlCtor = new URL(path, webBase); + + if (opts.openInHiddenWindow) { + urlCtor.searchParams.set('hidden', 'true'); + } + + const url = `${appInfo.schema}://${urlCtor.pathname}${urlCtor.search}`; + + if (opts.redirectFromWeb) { + const redirect_uri = new URL('/open-app/url', webBase); + redirect_uri.searchParams.set('url', url); + + return redirect_uri.toString(); + } + + return url; + } else { + return new URL(path, webBase).toString(); + } +} diff --git a/packages/frontend/electron/renderer/app.tsx b/packages/frontend/electron/renderer/app.tsx index f3a68287c..4b0cad8fd 100644 --- a/packages/frontend/electron/renderer/app.tsx +++ b/packages/frontend/electron/renderer/app.tsx @@ -39,6 +39,8 @@ const desktopWhiteList = [ '/upgrade-success', '/ai-upgrade-success', '/share', + '/oauth', + '/magic-link', ]; if ( !environment.isDesktop && diff --git a/packages/frontend/electron/src/main/deep-link.ts b/packages/frontend/electron/src/main/deep-link.ts index f4d7a8f63..b49fa3c9b 100644 --- a/packages/frontend/electron/src/main/deep-link.ts +++ b/packages/frontend/electron/src/main/deep-link.ts @@ -2,13 +2,13 @@ import path from 'node:path'; import type { App } from 'electron'; -import { buildType, CLOUD_BASE_URL, isDev } from './config'; +import { buildType, isDev } from './config'; import { mainWindowOrigin } from './constants'; import { logger } from './logger'; import { getMainWindow, - handleOpenUrlInHiddenWindow, - setCookie, + openUrlInHiddenWindow, + openUrlInMainWindow, } from './windows-manager'; let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`; @@ -61,51 +61,28 @@ async function handleAffineUrl(url: string) { logger.info('open affine url', url); const urlObj = new URL(url); logger.info('handle affine schema action', urlObj.hostname); - // handle more actions here - // hostname is the action name - if (urlObj.hostname === 'signin-redirect') { - await handleOauthJwt(url); - } + if (urlObj.hostname === 'bring-to-front') { const mainWindow = await getMainWindow(); if (mainWindow) { mainWindow.show(); } + } else { + await openUrl(urlObj); } } -async function handleOauthJwt(url: string) { - const mainWindow = await getMainWindow(); - if (url && mainWindow) { - try { - mainWindow.show(); - const urlObj = new URL(url); - const token = urlObj.searchParams.get('token'); +async function openUrl(urlObj: URL) { + const params = urlObj.searchParams; - if (!token) { - logger.error('no token in url', url); - return; - } + const openInHiddenWindow = params.get('hidden'); + params.delete('hidden'); - // set token to cookie - await setCookie({ - url: CLOUD_BASE_URL, - httpOnly: true, - value: token, - secure: true, - name: 'affine_session', - expirationDate: Math.floor( - Date.now() / 1000 + - 3600 * - 24 * - 399 /* as long as possible, cookie max expires is 400 days */ - ), - }); - - // hacks to refresh auth state in the main window - await handleOpenUrlInHiddenWindow(mainWindowOrigin + '/auth/signIn'); - } catch (e) { - logger.error('failed to open url in popup', e); - } + const url = mainWindowOrigin + urlObj.pathname + '?' + params.toString(); + if (!openInHiddenWindow) { + await openUrlInHiddenWindow(url); + } else { + // TODO(@pengx17): somehow the page won't load the url passed, help needed + await openUrlInMainWindow(url); } } diff --git a/packages/frontend/electron/src/main/windows-manager/main-window.ts b/packages/frontend/electron/src/main/windows-manager/main-window.ts index 65eff862d..00ee021f1 100644 --- a/packages/frontend/electron/src/main/windows-manager/main-window.ts +++ b/packages/frontend/electron/src/main/windows-manager/main-window.ts @@ -229,16 +229,15 @@ export async function showMainWindow() { /** * Open a URL in a hidden window. - * This is useful for opening a URL in the background without user interaction for *authentication*. */ -export async function handleOpenUrlInHiddenWindow(url: string) { +export async function openUrlInHiddenWindow(url: string) { const win = new BrowserWindow({ width: 1200, height: 600, webPreferences: { preload: join(__dirname, './preload.js'), }, - show: false, + show: environment.isDebug, }); win.on('close', e => { e.preventDefault(); @@ -250,3 +249,11 @@ export async function handleOpenUrlInHiddenWindow(url: string) { await win.loadURL(url); return win; } + +export async function openUrlInMainWindow(url: string) { + const mainWindow = await getMainWindow(); + if (mainWindow) { + mainWindow.show(); + await mainWindow.loadURL(url); + } +} diff --git a/packages/frontend/electron/src/preload/electron-api.ts b/packages/frontend/electron/src/preload/electron-api.ts index 9299ff4d5..6b47e3693 100644 --- a/packages/frontend/electron/src/preload/electron-api.ts +++ b/packages/frontend/electron/src/preload/electron-api.ts @@ -29,12 +29,20 @@ export function getElectronAPIs() { }; } +type Schema = + | 'affine' + | 'affine-canary' + | 'affine-beta' + | 'affine-internal' + | 'affine-dev'; + // todo: remove duplicated codes const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']); const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase(); const buildType = ReleaseTypeSchema.parse(envBuildType); const isDev = process.env.NODE_ENV === 'development'; -let schema = buildType === 'stable' ? 'affine' : `affine-${envBuildType}`; +let schema = + buildType === 'stable' ? 'affine' : (`affine-${envBuildType}` as Schema); schema = isDev ? 'affine-dev' : schema; export const appInfo = { @@ -45,7 +53,7 @@ export const appInfo = { viewId: process.argv.find(arg => arg.startsWith('--view-id='))?.split('=')[1] ?? 'unknown', - schema: `${schema}`, + schema, }; function getMainAPIs() { diff --git a/tools/cli/src/webpack/config.ts b/tools/cli/src/webpack/config.ts index e11d67a37..6b69c67e6 100644 --- a/tools/cli/src/webpack/config.ts +++ b/tools/cli/src/webpack/config.ts @@ -412,7 +412,6 @@ export const createConfiguration: ( { context: '/api', target: 'http://localhost:3010' }, { context: '/socket.io', target: 'http://localhost:3010', ws: true }, { context: '/graphql', target: 'http://localhost:3010' }, - { context: '/oauth', target: 'http://localhost:3010' }, ], } as DevServerConfiguration, } satisfies webpack.Configuration;