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:
Horus
2023-04-12 02:42:36 +08:00
committed by GitHub
parent 024c469a2c
commit c0669359ed
17 changed files with 252 additions and 42 deletions

View 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();
});
};

View File

@@ -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;
});
};

View File

@@ -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
*/

View File

@@ -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);

View File

@@ -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; };
}

View File

@@ -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', {