mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-29 00:22:56 +03:00
refactor: remove hacky email login (#4075)
Co-authored-by: Alex Yang <himself65@outlook.com>
This commit is contained in:
parent
f99a7a5ecd
commit
a2e4ef904b
@ -1,13 +0,0 @@
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
|
||||
export function buildCallbackUrl(callbackUrl: string) {
|
||||
const params: string[][] = [];
|
||||
if (isDesktop && window.appInfo.schema) {
|
||||
params.push(['schema', window.appInfo.schema]);
|
||||
}
|
||||
const query =
|
||||
params.length > 0
|
||||
? '?' + params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
|
||||
: '';
|
||||
return callbackUrl + query;
|
||||
}
|
@ -83,8 +83,8 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
marginTop: 30,
|
||||
}}
|
||||
icon={<GoogleDuotoneIcon />}
|
||||
onClick={useCallback(async () => {
|
||||
await signInWithGoogle();
|
||||
onClick={useCallback(() => {
|
||||
signInWithGoogle();
|
||||
}, [signInWithGoogle])}
|
||||
>
|
||||
{t['Continue with Google']()}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type { Notification } from '@affine/component/notification-center/index.jotai';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { type SignInResponse } from 'next-auth/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { signInCloud } from '../../../utils/cloud-utils';
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
|
||||
const COUNT_DOWN_TIME = 60;
|
||||
const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
|
||||
@ -77,7 +75,7 @@ export const useAuth = ({ onNoAccess }: { onNoAccess: () => void }) => {
|
||||
|
||||
const res = await signInCloud('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signIn'),
|
||||
callbackUrl: '/auth/signIn',
|
||||
redirect: false,
|
||||
}).catch(console.error);
|
||||
|
||||
@ -100,7 +98,7 @@ export const useAuth = ({ onNoAccess }: { onNoAccess: () => void }) => {
|
||||
|
||||
const res = await signInCloud('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signUp'),
|
||||
callbackUrl: '/auth/signUp',
|
||||
redirect: false,
|
||||
}).catch(console.error);
|
||||
|
||||
@ -114,16 +112,7 @@ export const useAuth = ({ onNoAccess }: { onNoAccess: () => void }) => {
|
||||
);
|
||||
|
||||
const signInWithGoogle = useCallback(() => {
|
||||
if (isDesktop) {
|
||||
open(
|
||||
`/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
|
||||
'/open-app/oauth-jwt'
|
||||
)}`,
|
||||
'_target'
|
||||
);
|
||||
} else {
|
||||
signInCloud('google').catch(console.error);
|
||||
}
|
||||
signInCloud('google').catch(console.error);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
@ -76,8 +76,10 @@ const OpenAppImpl = ({ urlToOpen, channel }: OpenAppProps) => {
|
||||
if (!urlToOpen || lastOpened === urlToOpen || !autoOpen) {
|
||||
return;
|
||||
}
|
||||
lastOpened = urlToOpen;
|
||||
open(urlToOpen, '_blank');
|
||||
setTimeout(() => {
|
||||
lastOpened = urlToOpen;
|
||||
open(urlToOpen, '_blank');
|
||||
}, 1000);
|
||||
}, [urlToOpen, autoOpen]);
|
||||
|
||||
if (!urlToOpen) {
|
||||
|
@ -164,7 +164,7 @@ export const DesktopLoginModal = (): ReactElement => {
|
||||
|
||||
useEffect(() => {
|
||||
return window.events?.ui.onFinishLogin(({ success, email }) => {
|
||||
if (email !== signingEmail) {
|
||||
if (email && email !== signingEmail) {
|
||||
return;
|
||||
}
|
||||
setSigningEmail(undefined);
|
||||
|
@ -1,15 +1,36 @@
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { refreshRootMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signIn, signOut } from 'next-auth/react';
|
||||
import { startTransition } from 'react';
|
||||
|
||||
export const signInCloud: typeof signIn = async (...args) => {
|
||||
return signIn(...args).then(result => {
|
||||
// do not refresh root metadata,
|
||||
// because the session won't change in this callback
|
||||
return result;
|
||||
});
|
||||
export const signInCloud: typeof signIn = async (provider, ...rest) => {
|
||||
if (isDesktop) {
|
||||
if (provider === 'google') {
|
||||
open(
|
||||
`/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
|
||||
'/open-app/oauth-jwt'
|
||||
)}`,
|
||||
'_target'
|
||||
);
|
||||
return;
|
||||
} else if (provider === 'email') {
|
||||
const [options, ...tail] = rest;
|
||||
return signIn(
|
||||
provider,
|
||||
{
|
||||
...options,
|
||||
callbackUrl: buildCallbackUrl('/open-app/oauth-jwt'),
|
||||
},
|
||||
...tail
|
||||
);
|
||||
} else {
|
||||
throw new Error('Unsupported provider');
|
||||
}
|
||||
} else {
|
||||
return signIn(provider, ...rest);
|
||||
}
|
||||
};
|
||||
|
||||
export const signOutCloud: typeof signOut = async (...args) => {
|
||||
@ -22,3 +43,15 @@ export const signOutCloud: typeof signOut = async (...args) => {
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
export function buildCallbackUrl(callbackUrl: string) {
|
||||
const params: string[][] = [];
|
||||
if (isDesktop && window.appInfo.schema) {
|
||||
params.push(['schema', window.appInfo.schema]);
|
||||
}
|
||||
const query =
|
||||
params.length > 0
|
||||
? '?' + params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
|
||||
: '';
|
||||
return callbackUrl + query;
|
||||
}
|
||||
|
@ -58,55 +58,11 @@ async function handleAffineUrl(url: string) {
|
||||
logger.info('handle affine schema action', urlObj.hostname);
|
||||
// handle more actions here
|
||||
// hostname is the action name
|
||||
if (urlObj.hostname === 'sign-in') {
|
||||
const urlToOpen = urlObj.search.slice(1);
|
||||
if (urlToOpen) {
|
||||
await handleSignIn(urlToOpen);
|
||||
}
|
||||
} else if (urlObj.hostname === 'oauth-jwt') {
|
||||
if (urlObj.hostname === 'oauth-jwt') {
|
||||
await handleOauthJwt(url);
|
||||
}
|
||||
}
|
||||
|
||||
// todo: move to another place?
|
||||
async function handleSignIn(url: string) {
|
||||
if (url) {
|
||||
try {
|
||||
const mainWindow = await restoreOrCreateWindow();
|
||||
mainWindow.show();
|
||||
const urlObj = new URL(url);
|
||||
const email = urlObj.searchParams.get('email');
|
||||
|
||||
if (!email) {
|
||||
logger.error('no email in url', url);
|
||||
return;
|
||||
}
|
||||
|
||||
uiSubjects.onStartLogin.next({
|
||||
email,
|
||||
});
|
||||
const window = await handleOpenUrlInHiddenWindow(url);
|
||||
logger.info('opened url in hidden window', window.webContents.getURL());
|
||||
// check path
|
||||
// - if path === /auth/{signIn,signUp}, we know sign in succeeded
|
||||
// - if path === expired, we know sign in failed
|
||||
const finalUrl = new URL(window.webContents.getURL());
|
||||
console.log('final url', finalUrl);
|
||||
// hack: wait for the hidden window to send broadcast message to the main window
|
||||
// that's how next-auth works for cross-tab communication
|
||||
setTimeout(() => {
|
||||
window.destroy();
|
||||
}, 3000);
|
||||
uiSubjects.onFinishLogin.next({
|
||||
success: ['/auth/signIn', '/auth/signUp'].includes(finalUrl.pathname),
|
||||
email,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('failed to open url in popup', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOauthJwt(url: string) {
|
||||
if (url) {
|
||||
try {
|
||||
@ -114,6 +70,7 @@ async function handleOauthJwt(url: string) {
|
||||
mainWindow.show();
|
||||
const urlObj = new URL(url);
|
||||
const token = urlObj.searchParams.get('token');
|
||||
const mainOrigin = new URL(mainWindow.webContents.getURL()).origin;
|
||||
|
||||
if (!token) {
|
||||
logger.error('no token in url', url);
|
||||
@ -122,14 +79,22 @@ async function handleOauthJwt(url: string) {
|
||||
|
||||
// set token to cookie
|
||||
await setCookie({
|
||||
url: new URL(mainWindow.webContents.getURL()).origin,
|
||||
url: mainOrigin,
|
||||
httpOnly: true,
|
||||
value: token,
|
||||
name: 'next-auth.session-token',
|
||||
});
|
||||
|
||||
// force reload app
|
||||
mainWindow.webContents.reload();
|
||||
// hacks to refresh auth state in the main window
|
||||
const window = await handleOpenUrlInHiddenWindow(
|
||||
mainOrigin + '/auth/signIn'
|
||||
);
|
||||
uiSubjects.onFinishLogin.next({
|
||||
success: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
window.destroy();
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
logger.error('failed to open url in popup', e);
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { isMacOS, isWindows } from '../shared/utils';
|
||||
import { CLOUD_BASE_URL } from './config';
|
||||
import { getExposedMeta } from './exposed';
|
||||
import { ensureHelperProcess } from './helper-process';
|
||||
import { logger } from './logger';
|
||||
@ -115,7 +114,7 @@ async function createWindow() {
|
||||
/**
|
||||
* URL for main window.
|
||||
*/
|
||||
const pageUrl = CLOUD_BASE_URL; // see protocol.ts
|
||||
const pageUrl = process.env.DEV_SERVER_URL || 'file://.'; // see protocol.ts
|
||||
|
||||
logger.info('loading page at', pageUrl);
|
||||
|
||||
|
@ -2,8 +2,6 @@ import { net, protocol, session } from 'electron';
|
||||
import { join } from 'path';
|
||||
|
||||
import { CLOUD_BASE_URL } from './config';
|
||||
import { setCookie } from './main-window';
|
||||
import { simpleGet } from './utils';
|
||||
|
||||
const NETWORK_REQUESTS = ['/api', '/ws', '/socket.io', '/graphql'];
|
||||
const webStaticDir = join(__dirname, '../resources/web-static');
|
||||
@ -16,38 +14,16 @@ async function handleHttpRequest(request: Request) {
|
||||
const clonedRequest = Object.assign(request.clone(), {
|
||||
bypassCustomProtocolHandlers: true,
|
||||
});
|
||||
const { pathname, origin } = new URL(request.url);
|
||||
if (
|
||||
!origin.startsWith(CLOUD_BASE_URL) ||
|
||||
isNetworkResource(pathname) ||
|
||||
process.env.DEV_SERVER_URL // when debugging locally
|
||||
) {
|
||||
// note: I don't find a good way to get over with 302 redirect
|
||||
// by default in net.fetch, or don't know if there is a way to
|
||||
// bypass http request handling to browser instead ...
|
||||
if (pathname.startsWith('/api/auth/callback')) {
|
||||
const originResponse = await simpleGet(request.url);
|
||||
// hack: use window.webContents.session.cookies to set cookies
|
||||
// since return set-cookie header in response doesn't work here
|
||||
for (const [, cookie] of originResponse.headers.filter(
|
||||
p => p[0] === 'set-cookie'
|
||||
)) {
|
||||
await setCookie(origin, cookie);
|
||||
}
|
||||
return new Response(originResponse.body, {
|
||||
headers: originResponse.headers,
|
||||
status: originResponse.statusCode,
|
||||
});
|
||||
} else {
|
||||
// just pass through (proxy)
|
||||
return net.fetch(request.url, clonedRequest);
|
||||
}
|
||||
const urlObject = new URL(request.url);
|
||||
if (isNetworkResource(urlObject.pathname)) {
|
||||
// just pass through (proxy)
|
||||
return net.fetch(CLOUD_BASE_URL + urlObject.pathname, clonedRequest);
|
||||
} else {
|
||||
// this will be file types (in the web-static folder)
|
||||
let filepath = '';
|
||||
// if is a file type, load the file in resources
|
||||
if (pathname.split('/').at(-1)?.includes('.')) {
|
||||
filepath = join(webStaticDir, decodeURIComponent(pathname));
|
||||
if (urlObject.pathname.split('/').at(-1)?.includes('.')) {
|
||||
filepath = join(webStaticDir, decodeURIComponent(urlObject.pathname));
|
||||
} else {
|
||||
// else, fallback to load the index.html instead
|
||||
filepath = join(webStaticDir, 'index.html');
|
||||
@ -57,11 +33,7 @@ async function handleHttpRequest(request: Request) {
|
||||
}
|
||||
|
||||
export function registerProtocol() {
|
||||
protocol.handle('http', request => {
|
||||
return handleHttpRequest(request);
|
||||
});
|
||||
|
||||
protocol.handle('https', request => {
|
||||
protocol.handle('file', request => {
|
||||
return handleHttpRequest(request);
|
||||
});
|
||||
|
||||
|
@ -6,14 +6,14 @@ import { uiSubjects } from './subject';
|
||||
*/
|
||||
export const uiEvents = {
|
||||
onFinishLogin: (
|
||||
fn: (result: { success: boolean; email: string }) => void
|
||||
fn: (result: { success: boolean; email?: string }) => void
|
||||
) => {
|
||||
const sub = uiSubjects.onFinishLogin.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onStartLogin: (fn: (opts: { email: string }) => void) => {
|
||||
onStartLogin: (fn: (opts: { email?: string }) => void) => {
|
||||
const sub = uiSubjects.onStartLogin.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export const uiSubjects = {
|
||||
onStartLogin: new Subject<{ email: string }>(),
|
||||
onFinishLogin: new Subject<{ success: boolean; email: string }>(),
|
||||
onStartLogin: new Subject<{ email?: string }>(),
|
||||
onFinishLogin: new Subject<{ success: boolean; email?: string }>(),
|
||||
};
|
||||
|
@ -20,23 +20,6 @@ import { getUtcTimestamp, UserClaim } from './service';
|
||||
|
||||
export const NextAuthOptionsProvide = Symbol('NextAuthOptions');
|
||||
|
||||
function getSchemaFromCallbackUrl(origin: string, callbackUrl: string) {
|
||||
const { searchParams } = new URL(callbackUrl, origin);
|
||||
return searchParams.has('schema') ? searchParams.get('schema') : null;
|
||||
}
|
||||
|
||||
function wrapUrlWithOpenApp(
|
||||
origin: string,
|
||||
url: string,
|
||||
schema: string | null
|
||||
) {
|
||||
if (schema) {
|
||||
const urlWithSchema = `${schema}://sign-in?${url}`;
|
||||
return `${origin}/open-app?url=${encodeURIComponent(urlWithSchema)}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
||||
provide: NextAuthOptionsProvide,
|
||||
useFactory(config: Config, prisma: PrismaService, mailer: MailService) {
|
||||
@ -88,17 +71,12 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
|
||||
from: config.auth.email.sender,
|
||||
async sendVerificationRequest(params: SendVerificationRequestParams) {
|
||||
const { identifier, url, provider } = params;
|
||||
const { searchParams, origin } = new URL(url);
|
||||
const { searchParams } = new URL(url);
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '';
|
||||
if (!callbackUrl) {
|
||||
throw new Error('callbackUrl is not set');
|
||||
}
|
||||
|
||||
const schema = getSchemaFromCallbackUrl(origin, callbackUrl);
|
||||
const wrappedUrl = wrapUrlWithOpenApp(origin, url, schema);
|
||||
|
||||
// hack: check if link is opened via desktop
|
||||
const result = await mailer.sendSignInEmail(wrappedUrl, {
|
||||
const result = await mailer.sendSignInEmail(url, {
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
});
|
||||
|
@ -266,9 +266,9 @@ export interface WorkspaceEvents {
|
||||
}
|
||||
|
||||
export interface UIEvents {
|
||||
onStartLogin: (fn: (options: { email: string }) => void) => () => void;
|
||||
onStartLogin: (fn: (options: { email?: string }) => void) => () => void;
|
||||
onFinishLogin: (
|
||||
fn: (result: { success: boolean; email: string }) => void
|
||||
fn: (result: { success: boolean; email?: string }) => void
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user