import { createRouter } from '../../create-router'; import { FORBIDDEN, BAD_REQUEST } from '../../custom-errors'; import { getServerIntegration, upsertServerIntegration, getUserByEmail, getUserIntegration, upsertUserIntegration, deleteUserIntegration, getDockPaths, setDockPaths, } from 'officerdb'; const GOOGLE_SCOPES = [ 'https://mail.google.com/', 'https://www.googleapis.com/auth/calendar.readonly', 'https://www.googleapis.com/auth/userinfo.email', ]; type GoogleConfig = { clientId: string; clientSecret: string }; export const readGoogleConfig = async (): Promise => { const integration = await getServerIntegration('google'); if (!integration) return null; const config = integration.config as Record; if (!config.clientId || !config.clientSecret) return null; return config as unknown as GoogleConfig; }; export const integrationsRouter = createRouter(); integrationsRouter.get('/', async (ctx) => { return ctx.json([]); }); // --- Enterprise: Apify config (Super Admin only) --- type ApifyConfig = { apiToken: string }; export const readApifyConfig = async (): Promise => { const integration = await getServerIntegration('apify'); if (!integration) return null; const config = integration.config as Record; if (!config.apiToken) return null; return config as unknown as ApifyConfig; }; integrationsRouter.get('/apify/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') throw FORBIDDEN(); return ctx.json(await readApifyConfig()); }); integrationsRouter.put('/apify/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') throw FORBIDDEN(); const body = ctx.get('body') as { apiToken?: string }; const config = { apiToken: body.apiToken ?? '' }; await upsertServerIntegration('apify', config); return ctx.json(config); }); integrationsRouter.get('/apify/status', async (ctx) => { const config = await readApifyConfig(); return ctx.json({ configured: !!config?.apiToken }); }); // --- Enterprise: Google OAuth config (Super Admin only) --- integrationsRouter.get('/google/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') throw FORBIDDEN(); return ctx.json(await readGoogleConfig()); }); integrationsRouter.put('/google/config', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') throw FORBIDDEN(); const body = ctx.get('body') as { clientId?: string; clientSecret?: string }; const config = { clientId: body.clientId ?? '', clientSecret: body.clientSecret ?? '' }; await upsertServerIntegration('google', config); return ctx.json(config); }); integrationsRouter.get('/google/verify', async (ctx) => { const user = ctx.get('user'); if (user.role !== 'Super Admin') throw FORBIDDEN(); const config = await readGoogleConfig(); if (!config?.clientId || !config?.clientSecret) { return ctx.json({ valid: false, error: 'Missing credentials' }); } const res = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: config.clientId, client_secret: config.clientSecret, code: 'invalid_code', redirect_uri: 'https://localhost', grant_type: 'authorization_code', }), }); const body = await res.json(); const valid = body.error === 'invalid_grant' || body.error === 'redirect_uri_mismatch'; return ctx.json({ valid, error: valid ? null : body.error_description ?? body.error }); }); // --- Personal: Google account connection status --- integrationsRouter.get('/google/status', async (ctx) => { const user = ctx.get('user'); const config = await readGoogleConfig(); const connection = await getUserIntegration(user.id, 'google'); const connConfig = connection?.config as Record | undefined; return ctx.json({ configured: !!(config?.clientId && config?.clientSecret), connected: !!connConfig?.accessToken, email: connConfig?.email ?? null, picture: connConfig?.picture ?? null, hasAppPassword: !!connConfig?.imapAppPassword, }); }); integrationsRouter.put('/google/app-password', async (ctx) => { const user = ctx.get('user'); const body = ctx.get('body') as { appPassword?: string }; if (!body.appPassword) throw BAD_REQUEST('Missing appPassword'); const connection = await getUserIntegration(user.id, 'google'); const connConfig = (connection?.config as Record) ?? {}; await upsertUserIntegration({ userId: user.id, provider: 'google', serverIntegrationId: connection?.serverIntegrationId ?? undefined, config: { ...connConfig, imapAppPassword: body.appPassword }, }); return ctx.json({ ok: true }); }); integrationsRouter.delete('/google/connection', async (ctx) => { const user = ctx.get('user'); const connection = await getUserIntegration(user.id, 'google'); const connConfig = connection?.config as Record | undefined; // Revoke token at Google so the old grant is fully removed const token = (connConfig?.refreshToken as string) || (connConfig?.accessToken as string); if (token) { fetch(`https://oauth2.googleapis.com/revoke?token=${token}`, { method: 'POST' }).catch(() => {}); } await deleteUserIntegration(user.id, 'google'); return ctx.json({ ok: true }); }); // --- OAuth flow: authorize (protected — user must be logged in) --- integrationsRouter.get('/google/authorize', async (ctx) => { const config = await readGoogleConfig(); if (!config?.clientId || !config?.clientSecret) { throw BAD_REQUEST('Google OAuth not configured'); } const email = ctx.get('user').email; const origin = ctx.req.query('origin'); if (!origin) throw BAD_REQUEST('Missing origin parameter'); const redirectUri = `${origin}/api/integrations/google/callback`; const state = Buffer.from(JSON.stringify({ email, redirectUri })).toString('base64url'); const params = new URLSearchParams({ client_id: config.clientId, redirect_uri: redirectUri, response_type: 'code', scope: GOOGLE_SCOPES.join(' '), access_type: 'offline', prompt: 'consent', include_granted_scopes: 'false', state, }); return ctx.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`); }); // --- OAuth callback (public — called by Google, exported for hono.ts) --- export const googleCallbackHandler = async (ctx: any) => { const code = ctx.req.query('code'); const stateParam = ctx.req.query('state'); const error = ctx.req.query('error'); if (error || !code || !stateParam) { return ctx.redirect('/settings/integrations?google=error'); } let email: string; let redirectUri: string; try { const parsed = JSON.parse(Buffer.from(stateParam, 'base64url').toString()); email = parsed.email; redirectUri = parsed.redirectUri; } catch { return ctx.redirect('/settings/integrations?google=error'); } const config = await readGoogleConfig(); if (!config?.clientId || !config?.clientSecret) { return ctx.redirect('/settings/integrations?google=error'); } const tokenResponse = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ code, client_id: config.clientId, client_secret: config.clientSecret, redirect_uri: redirectUri, grant_type: 'authorization_code', }), }); if (!tokenResponse.ok) { console.error('Google token exchange failed:', await tokenResponse.text()); return ctx.redirect('/settings/integrations?google=error'); } const tokens = await tokenResponse.json(); const userinfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); let googleEmail = email; let picture: string | null = null; if (userinfoResponse.ok) { const userinfo = await userinfoResponse.json(); googleEmail = userinfo.email ?? email; picture = userinfo.picture ?? null; } const dbUser = await getUserByEmail(email); if (!dbUser) { return ctx.redirect('/settings/integrations?google=error'); } const serverIntegration = await getServerIntegration('google'); await upsertUserIntegration({ userId: dbUser.id, provider: 'google', serverIntegrationId: serverIntegration?.id, config: { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000, email: googleEmail, picture, scope: tokens.scope, }, }); // Auto-add /email to dock try { const existing = await getDockPaths(dbUser.id); const paths = existing ?? ['/', '/files', '/automation', '/projects', '/dashboards', '/chat']; if (!paths.includes('/email')) { paths.push('/email'); await setDockPaths(dbUser.id, paths); } } catch { // Non-critical — don't block the OAuth flow } return ctx.redirect('/settings/integrations?google=success'); };