super-productivity/electron/plugin-oauth.ts
Johannes Millan 02bc3e88e3 feat(two-way-sync): add plugin OAuth, two-way field sync, and remote issue deletion
Add OAuth support for plugins with PKCE, token persistence in local-only
IndexedDB, and Electron/web redirect handling. Extend two-way sync with
dueDay, dueWithTime, and timeEstimate field mappings including mutually
exclusive field clearing. Support remote issue deletion on task delete
and remote deletion detection during polling.

Add Google Calendar plugin (disabled from bundled builds pending legal
review) as a development reference for the plugin OAuth and two-way
sync APIs.

Additional fixes:
- Restore accidentally deleted focus-mode translation keys
- Remove full Task[] from deleteTasks action to prevent op-log bloat
- Add dueDay/deadlineDay format validation guards (#6908)
- Fix Android reminder alarm cancel intent matching
- Validate dueDay format to prevent false overdue from corrupted data
- Fix PKCE test expectation after utility consolidation
2026-03-22 13:02:41 +01:00

86 lines
2.3 KiB
TypeScript

import { BrowserWindow, ipcMain } from 'electron';
import { IPC } from './shared-with-frontend/ipc-events.const';
import { log } from 'electron-log/main';
const OAUTH_REDIRECT_PREFIX = 'super-productivity://oauth';
export const initPluginOAuth = (mainWin: BrowserWindow): void => {
ipcMain.on(IPC.PLUGIN_OAUTH_START, (_ev: unknown, { url }: { url: string }) => {
log('Plugin OAuth: Opening auth window');
const authWin = new BrowserWindow({
width: 600,
height: 700,
parent: mainWin,
modal: true,
webPreferences: { nodeIntegration: false, contextIsolation: true },
});
let handled = false;
const closeAuthWin = (): void => {
if (!authWin.isDestroyed()) {
authWin.close();
}
};
const handleRedirectUrl = (redirectUrl: string): boolean => {
if (!redirectUrl.startsWith(OAUTH_REDIRECT_PREFIX)) {
return false;
}
if (handled) {
return true;
}
handled = true;
const parsed = new URL(redirectUrl);
const code = parsed.searchParams.get('code');
const error = parsed.searchParams.get('error');
const state = parsed.searchParams.get('state');
mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, { code, error, state });
closeAuthWin();
return true;
};
authWin.webContents.on('will-redirect', (details) => {
if (handleRedirectUrl(details.url)) {
details.preventDefault();
}
});
authWin.webContents.on('will-navigate', (details) => {
if (handleRedirectUrl(details.url)) {
details.preventDefault();
}
});
authWin.on('closed', () => {
if (!handled) {
mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, {
error: 'window_closed',
});
}
});
// Validate URL protocol before loading to prevent file:// or javascript: abuse
try {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
log('Plugin OAuth: Rejected non-https auth URL:', parsed.protocol);
mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, {
error: 'invalid_auth_url',
});
closeAuthWin();
return;
}
} catch {
mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, {
error: 'invalid_auth_url',
});
closeAuthWin();
return;
}
authWin.loadURL(url);
});
};