Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
177 lines
5.5 KiB
JavaScript
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 };
|
|
}
|