godcrm/backend/services/google-calendar/auth.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

177 lines
5.5 KiB
JavaScript

/**
* Google Calendar — Authentication & Status
*
* getAuthUrl, handleCallback, getOAuth2Client, getConnectionStatus, disconnectCalendar
*/
import { google } from 'googleapis';
import axios from 'axios';
import { apiLogger } from '../../utils/logger.js';
import {
loadConfig, encrypt, decrypt,
getUserTokens, setUserTokens, removeUserTokens,
} from './helpers.js';
export function getAuthUrl(userId, redirectUri) {
const config = loadConfig();
if (!config) {
throw new Error('Google OAuth config not available');
}
// Use the config redirectUri directly (should be the registered login OAuth redirect)
// The /auth/google/callback handler will detect "calendar:" state prefix and route accordingly
const calendarRedirect = redirectUri || config.redirectUri;
const oauth2Client = new google.auth.OAuth2(
config.clientId,
config.clientSecret,
calendarRedirect
);
const url = oauth2Client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: [
'https://www.googleapis.com/auth/calendar',
'https://www.googleapis.com/auth/calendar.events',
],
// Prefix state with "calendar:" so the shared callback can route to calendar handler
state: `calendar:${userId}`,
});
return url;
}
export async function handleCallback(code, redirectUri, userId) {
const config = loadConfig();
if (!config) {
throw new Error('Google OAuth config not available');
}
// Use the provided redirect_uri or fall back to config (the registered login OAuth redirect)
// IMPORTANT: redirect_uri must match exactly what was used in getAuthUrl()
const calendarRedirect = redirectUri || config.redirectUri;
// Exchange authorization code for tokens
const response = await axios.post('https://oauth2.googleapis.com/token', {
code,
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: calendarRedirect,
grant_type: 'authorization_code',
});
const { access_token, refresh_token, expires_in } = response.data;
const tokenExpiry = new Date(Date.now() + (expires_in || 3600) * 1000).toISOString();
setUserTokens(userId, {
refresh_token: refresh_token ? encrypt(refresh_token) : null,
access_token,
token_expiry: tokenExpiry,
calendars: ['primary'], // Will be populated with all calendars on first sync
syncAllCalendars: true, // Flag to sync ALL accessible calendars
});
apiLogger.info(`Google Calendar connected for user ${userId}`);
// Fetch full calendar list and save IDs
try {
const oauth2Client = new google.auth.OAuth2();
oauth2Client.setCredentials({ access_token });
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const res = await calendar.calendarList.list();
const calIds = (res.data.items || []).map(c => c.id);
if (calIds.length > 0) {
setUserTokens(userId, {
...getUserTokens(userId),
calendars: calIds,
});
apiLogger.info(`Saved ${calIds.length} calendar IDs for user ${userId}`);
}
} catch (err) {
apiLogger.warn(`Could not fetch calendar list during connect for user ${userId}`, err);
}
return { connected: true };
}
export async function getOAuth2Client(userId) {
const config = loadConfig();
if (!config) {
throw new Error('Google OAuth config not available');
}
const userTokens = getUserTokens(userId);
if (!userTokens) {
throw new Error(`No Google Calendar tokens found for user ${userId}`);
}
const oauth2Client = new google.auth.OAuth2(
config.clientId,
config.clientSecret
);
let accessToken = userTokens.access_token;
const refreshToken = userTokens.refresh_token ? decrypt(userTokens.refresh_token) : null;
// Check if access token is expired
const isExpired = userTokens.token_expiry && new Date(userTokens.token_expiry) <= new Date();
if (isExpired && refreshToken) {
apiLogger.info(`Refreshing expired access token for user ${userId}`);
try {
const response = await axios.post('https://oauth2.googleapis.com/token', {
client_id: config.clientId,
client_secret: config.clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token',
});
accessToken = response.data.access_token;
const newExpiry = new Date(Date.now() + (response.data.expires_in || 3600) * 1000).toISOString();
// Update stored tokens with new access token
setUserTokens(userId, {
...userTokens,
access_token: accessToken,
token_expiry: newExpiry,
});
} catch (err) {
apiLogger.error(`Failed to refresh access token for user ${userId}`, err);
throw new Error('Failed to refresh Google access token. Please reconnect.');
}
}
oauth2Client.setCredentials({
access_token: accessToken,
refresh_token: refreshToken,
});
return oauth2Client;
}
export async function getConnectionStatus(userId) {
const userTokens = getUserTokens(userId);
if (!userTokens) {
return { connected: false };
}
const hasRefreshToken = !!userTokens.refresh_token;
const hasAccessToken = !!userTokens.access_token;
const isExpired = userTokens.token_expiry && new Date(userTokens.token_expiry) <= new Date();
return {
connected: hasRefreshToken && hasAccessToken,
hasRefreshToken,
hasAccessToken,
isExpired: !!isExpired,
calendars: userTokens.calendars || [],
tokenExpiry: userTokens.token_expiry || null,
};
}
export function disconnectCalendar(userId) {
removeUserTokens(userId);
apiLogger.info(`Google Calendar disconnected for user ${userId}`);
return { disconnected: true };
}