mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-27 02:44:49 +03:00
parent
8b0afd6eeb
commit
e33aa35f7e
@ -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');
|
||||
|
@ -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 => (
|
||||
<OAuthProvider
|
||||
key={provider}
|
||||
provider={provider}
|
||||
redirectUri={redirectUri}
|
||||
/>
|
||||
<OAuthProvider key={provider} provider={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 (
|
||||
<Button
|
||||
|
@ -38,6 +38,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
const { openModal } = useAtomValue(authAtom);
|
||||
const errorMsg = searchParams.get('error');
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setInterval(() => {
|
||||
@ -65,32 +66,22 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
|
||||
setAuthEmail(email);
|
||||
try {
|
||||
const { hasPassword, isExist: isUserExist } =
|
||||
const { hasPassword, registered } =
|
||||
await authService.checkUserByEmail(email);
|
||||
|
||||
if (verifyToken) {
|
||||
if (isUserExist) {
|
||||
if (registered) {
|
||||
// provider password sign-in if user has by default
|
||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||
if (hasPassword) {
|
||||
setAuthState('signInWithPassword');
|
||||
} else {
|
||||
track.$.$.auth.signIn();
|
||||
await authService.sendEmailMagicLink(
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
searchParams.get('redirect_uri')
|
||||
);
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
}
|
||||
} else {
|
||||
await authService.sendEmailMagicLink(
|
||||
email,
|
||||
verifyToken,
|
||||
challenge,
|
||||
searchParams.get('redirect_uri')
|
||||
);
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
track.$.$.auth.signUp();
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
}
|
||||
@ -105,15 +96,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
}
|
||||
|
||||
setIsMutating(false);
|
||||
}, [
|
||||
authService,
|
||||
challenge,
|
||||
email,
|
||||
searchParams,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
verifyToken,
|
||||
]);
|
||||
}, [authService, challenge, email, setAuthEmail, setAuthState, verifyToken]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -122,7 +105,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
subTitle={t['com.affine.brand.affineCloud']()}
|
||||
/>
|
||||
|
||||
<OAuth redirectUri={searchParams.get('redirect_uri')} />
|
||||
<OAuth />
|
||||
|
||||
<div className={style.authModalContent}>
|
||||
<AuthInput
|
||||
@ -142,8 +125,6 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
onEnter={onContinue}
|
||||
/>
|
||||
|
||||
{verifyToken ? null : <Captcha />}
|
||||
|
||||
{verifyToken ? (
|
||||
<Button
|
||||
style={{ width: '100%' }}
|
||||
@ -157,7 +138,11 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
>
|
||||
{t['com.affine.auth.sign.email.continue']()}
|
||||
</Button>
|
||||
) : null}
|
||||
) : (
|
||||
<Captcha />
|
||||
)}
|
||||
|
||||
{errorMsg && <div className={style.errorMessage}>{errorMsg}</div>}
|
||||
|
||||
<div className={style.authMessage}>
|
||||
{/*prettier-ignore*/}
|
||||
|
@ -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'),
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
74
packages/frontend/core/src/pages/oauth-callback.tsx
Normal file
74
packages/frontend/core/src/pages/oauth-callback.tsx
Normal file
@ -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;
|
||||
};
|
@ -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 <OpenAppImpl urlToOpen={urlToOpen} channel={channel} />;
|
||||
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 <OpenAppImpl urlToOpen={urlObj.toString()} channel={channel} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
|
@ -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',
|
||||
|
@ -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'),
|
||||
|
@ -6,3 +6,4 @@ export * from './popup';
|
||||
export * from './string2color';
|
||||
export * from './toast';
|
||||
export * from './unflatten-object';
|
||||
export * from './url';
|
||||
|
33
packages/frontend/core/src/utils/url.ts
Normal file
33
packages/frontend/core/src/utils/url.ts
Normal file
@ -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();
|
||||
}
|
||||
}
|
@ -39,6 +39,8 @@ const desktopWhiteList = [
|
||||
'/upgrade-success',
|
||||
'/ai-upgrade-success',
|
||||
'/share',
|
||||
'/oauth',
|
||||
'/magic-link',
|
||||
];
|
||||
if (
|
||||
!environment.isDesktop &&
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user