mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat: support google cloud login in client (#1822)
Co-authored-by: Himself65 <himself65@outlook.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
32
apps/electron/layers/main/src/app-state/google-auth.ts
Normal file
32
apps/electron/layers/main/src/app-state/google-auth.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { RequestInit } from 'undici';
|
||||
import { fetch, ProxyAgent } from 'undici';
|
||||
|
||||
const redirectUri = 'https://affine.pro/client/auth-callback';
|
||||
|
||||
export const oauthEndpoint = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.AFFINE_GOOGLE_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&scope=openid https://www.googleapis.com/auth/userinfo.email profile&access_type=offline`;
|
||||
|
||||
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
export const exchangeToken = async (code: string) => {
|
||||
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
||||
const proxyAgent = httpProxy ? new ProxyAgent(httpProxy) : undefined;
|
||||
|
||||
const postData = {
|
||||
code,
|
||||
client_id: process.env.AFFINE_GOOGLE_CLIENT_ID || '',
|
||||
client_secret: process.env.AFFINE_GOOGLE_CLIENT_SECRET || '',
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
};
|
||||
const requestOptions: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(postData).toString(),
|
||||
dispatcher: proxyAgent,
|
||||
};
|
||||
return fetch(tokenEndpoint, requestOptions).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
@@ -2,13 +2,19 @@ import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { Storage } from '@affine/octobase-node';
|
||||
import { app, shell } from 'electron';
|
||||
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
|
||||
import fs from 'fs-extra';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { exchangeToken, oauthEndpoint } from './google-auth';
|
||||
|
||||
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
|
||||
|
||||
fs.ensureDirSync(AFFINE_ROOT);
|
||||
|
||||
const logger = console;
|
||||
|
||||
// todo: rethink this
|
||||
export const appState = {
|
||||
storage: new Storage(path.join(AFFINE_ROOT, 'test.db')),
|
||||
@@ -21,6 +27,7 @@ export const registerHandlers = () => {
|
||||
|
||||
ipcMain.handle('ui:theme-change', async (_, theme) => {
|
||||
nativeTheme.themeSource = theme;
|
||||
logger.info('theme change', theme);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
|
||||
@@ -30,5 +37,38 @@ export const registerHandlers = () => {
|
||||
// hide window buttons when sidebar is not visible
|
||||
w.setWindowButtonVisibility(visible);
|
||||
});
|
||||
logger.info('sidebar visibility change', visible);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:google-sign-in', async () => {
|
||||
logger.info('starting google sign in ...');
|
||||
shell.openExternal(oauthEndpoint);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const handleOpenUrl = async (_: any, url: string) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find(
|
||||
w => !w.isDestroyed()
|
||||
);
|
||||
const urlObj = parse(url.replace('??', '?'), true);
|
||||
if (!mainWindow || !url.startsWith('affine://')) return;
|
||||
const token = (await exchangeToken(urlObj.query['code'] as string)) as {
|
||||
id_token: string;
|
||||
};
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
resolve(token.id_token);
|
||||
logger.info('google sign in', token);
|
||||
};
|
||||
|
||||
app.on('open-url', handleOpenUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
}, 60000);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('main:env-update', async (_, env, value) => {
|
||||
process.env[env] = value;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import './security-restrictions';
|
||||
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import { registerHandlers } from './app-state';
|
||||
import { restoreOrCreateWindow } from './main-window';
|
||||
import { registerProtocol } from './protocol';
|
||||
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient('affine', process.execPath, [
|
||||
path.resolve(process.argv[1]),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient('affine');
|
||||
}
|
||||
/**
|
||||
* Prevent multiple instances
|
||||
*/
|
||||
@@ -15,7 +25,13 @@ if (!isSingleInstance) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.on('second-instance', restoreOrCreateWindow);
|
||||
app.on('second-instance', (event, argv) => {
|
||||
restoreOrCreateWindow();
|
||||
});
|
||||
|
||||
app.on('open-url', async (_, url) => {
|
||||
// todo: handle `affine://...` urls
|
||||
});
|
||||
|
||||
/**
|
||||
* Disable Hardware Acceleration for more power-save
|
||||
@@ -45,7 +61,6 @@ app
|
||||
.then(registerHandlers)
|
||||
.then(restoreOrCreateWindow)
|
||||
.catch(e => console.error('Failed create window:', e));
|
||||
|
||||
/**
|
||||
* Check new app version in production mode only
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,14 @@ app.on('web-contents-created', (_, contents) => {
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
||||
*/
|
||||
contents.on('will-navigate', (event, url) => {
|
||||
if (
|
||||
(process.env.DEV_SERVER_URL &&
|
||||
url.startsWith(process.env.DEV_SERVER_URL)) ||
|
||||
url.startsWith('affine://') ||
|
||||
url.startsWith('file://.')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Prevent navigation
|
||||
event.preventDefault();
|
||||
shell.openExternal(url).catch(console.error);
|
||||
|
||||
2
apps/electron/layers/preload/preload.d.ts
vendored
2
apps/electron/layers/preload/preload.d.ts
vendored
@@ -7,6 +7,6 @@ interface Window {
|
||||
*
|
||||
* @see https://github.com/cawa-93/dts-for-context-bridge
|
||||
*/
|
||||
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; };
|
||||
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; googleSignIn: () => Promise<string>; updateEnv: (env: string, value: string) => void; };
|
||||
readonly appInfo: { electron: boolean; isMacOS: boolean; };
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { isMacOS } from '../../utils';
|
||||
*
|
||||
* @see https://github.com/cawa-93/dts-for-context-bridge
|
||||
*/
|
||||
|
||||
contextBridge.exposeInMainWorld('apis', {
|
||||
workspaceSync: (id: string) => ipcRenderer.invoke('octo:workspace-sync', id),
|
||||
// ui
|
||||
@@ -30,6 +29,18 @@ contextBridge.exposeInMainWorld('apis', {
|
||||
|
||||
onSidebarVisibilityChange: (visible: boolean) =>
|
||||
ipcRenderer.invoke('ui:sidebar-visibility-change', visible),
|
||||
|
||||
/**
|
||||
* Try sign in using Google and return a Google IDToken
|
||||
*/
|
||||
googleSignIn: (): Promise<string> => ipcRenderer.invoke('ui:google-sign-in'),
|
||||
|
||||
/**
|
||||
* Secret backdoor to update environment variables in main process
|
||||
*/
|
||||
updateEnv: (env: string, value: string) => {
|
||||
ipcRenderer.invoke('main:env-update', env, value);
|
||||
},
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('appInfo', {
|
||||
|
||||
Reference in New Issue
Block a user