import { OAuth2Client } from 'google-auth-library';
import { googleConfig } from '../config';
import { DatabaseManager } from '../database';
import { AuthManager } from './AuthManager';
import { CalendarProvider, User, ConnectedCalendar } from '../types';
export interface OAuthState {
userId?: string;
redirectUrl?: string;
state: string;
}
export interface OAuthResult {
success: boolean;
user?: User;
tokens?: any;
error?: string;
}
export class GoogleOAuthHandler {
private oauth2Client: OAuth2Client;
private pendingStates: Map<string, OAuthState> = new Map();
constructor(
private database: DatabaseManager,
private authManager: AuthManager
) {
this.oauth2Client = new OAuth2Client(
googleConfig.clientId,
googleConfig.clientSecret,
googleConfig.redirectUri
);
}
// Generate OAuth authorization URL
generateAuthUrl(userId?: string, redirectUrl?: string): { url: string; state: string } {
const state = this.generateStateParameter();
// Store state for verification
this.pendingStates.set(state, {
userId,
redirectUrl,
state
});
// Clean up old states (older than 10 minutes)
this.cleanupOldStates();
const url = this.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: googleConfig.scopes,
state,
prompt: 'consent' // Force consent to get refresh token
});
return { url, state };
}
// Handle OAuth callback
async handleCallback(code: string, state: string): Promise<OAuthResult> {
try {
// Verify state parameter
const pendingState = this.pendingStates.get(state);
if (!pendingState) {
return { success: false, error: 'Invalid state parameter' };
}
// Clean up used state
this.pendingStates.delete(state);
// Exchange code for tokens
const { tokens } = await this.oauth2Client.getToken(code);
if (!tokens.access_token) {
return { success: false, error: 'No access token received' };
}
// Set credentials to get user info
this.oauth2Client.setCredentials(tokens);
// Get user information from Google
const userInfo = await this.getUserInfo();
if (!userInfo.success || !userInfo.email) {
return { success: false, error: 'Failed to get user information' };
}
// Find or create user
let user = await this.database.getUserByEmail(userInfo.email);
if (!user) {
// Create new user
user = await this.database.createUser({
email: userInfo.email,
displayName: userInfo.name,
timezone: 'UTC', // Default, user can change later
preferences: {
defaultCalendarId: 'primary',
workingHours: {
monday: { startTime: '09:00', endTime: '17:00' },
tuesday: { startTime: '09:00', endTime: '17:00' },
wednesday: { startTime: '09:00', endTime: '17:00' },
thursday: { startTime: '09:00', endTime: '17:00' },
friday: { startTime: '09:00', endTime: '17:00' }
},
defaultEventDuration: 60,
bufferTime: 15,
autoDeclineConflicts: false,
reminderDefaults: {
defaultReminders: [15], // 15 minutes before
emailReminders: true,
pushReminders: false
}
}
});
}
// Add or update connected calendar
const connectedCalendar: Omit<ConnectedCalendar, 'calendarId'> = {
calendarId: 'primary', // Will be set by addConnectedCalendar
provider: CalendarProvider.GOOGLE,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || undefined,
tokenExpiry: tokens.expiry_date ? new Date(tokens.expiry_date) : new Date(Date.now() + 3600000),
isDefault: true,
syncEnabled: true
};
try {
await this.database.addConnectedCalendar(user.id, connectedCalendar);
} catch (error) {
// If calendar already exists, update it
await this.database.updateConnectedCalendar(user.id, 'primary', {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || undefined,
tokenExpiry: tokens.expiry_date ? new Date(tokens.expiry_date) : new Date(Date.now() + 3600000)
});
}
// Create auth session
const authTokens = await this.authManager.createSession(user);
return {
success: true,
user,
tokens: authTokens
};
} catch (error) {
console.error('OAuth callback error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'OAuth authentication failed'
};
}
}
// Refresh Google OAuth tokens
async refreshTokens(userId: string): Promise<{ success: boolean; error?: string }> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: 'User not found' };
}
const googleCalendar = user.connectedCalendars.find(
cal => cal.provider === CalendarProvider.GOOGLE && cal.isDefault
);
if (!googleCalendar || !googleCalendar.refreshToken) {
return { success: false, error: 'No Google refresh token found' };
}
// Set up OAuth client with refresh token
this.oauth2Client.setCredentials({
refresh_token: googleCalendar.refreshToken
});
// Refresh the token
const { credentials } = await this.oauth2Client.refreshAccessToken();
if (!credentials.access_token) {
return { success: false, error: 'Failed to refresh access token' };
}
// Update stored tokens
await this.database.updateConnectedCalendar(userId, googleCalendar.calendarId, {
accessToken: credentials.access_token,
refreshToken: credentials.refresh_token || googleCalendar.refreshToken,
tokenExpiry: credentials.expiry_date ? new Date(credentials.expiry_date) : new Date(Date.now() + 3600000)
});
return { success: true };
} catch (error) {
console.error('Token refresh error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Token refresh failed'
};
}
}
// Revoke Google OAuth tokens
async revokeTokens(userId: string): Promise<{ success: boolean; error?: string }> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: 'User not found' };
}
const googleCalendar = user.connectedCalendars.find(
cal => cal.provider === CalendarProvider.GOOGLE && cal.isDefault
);
if (!googleCalendar) {
return { success: false, error: 'No Google calendar connection found' };
}
// Revoke the token with Google
if (googleCalendar.accessToken) {
await this.oauth2Client.revokeToken(googleCalendar.accessToken);
}
// Remove from database
// Note: This would require additional database method
// await this.database.removeConnectedCalendar(userId, googleCalendar.calendarId);
return { success: true };
} catch (error) {
console.error('Token revocation error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Token revocation failed'
};
}
}
// Get user info from Google
private async getUserInfo(): Promise<{ success: boolean; email?: string; name?: string; error?: string }> {
try {
const oauth2 = google.oauth2({ version: 'v2', auth: this.oauth2Client });
const response = await oauth2.userinfo.get();
return {
success: true,
email: response.data.email || undefined,
name: response.data.name || undefined
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get user info'
};
}
}
// Generate secure state parameter
private generateStateParameter(): string {
return require('crypto').randomBytes(32).toString('hex');
}
// Clean up old pending states
private cleanupOldStates(): void {
const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
for (const [state, stateData] of this.pendingStates.entries()) {
// This is a simple cleanup - in production you'd want to store timestamps
if (this.pendingStates.size > 100) { // Arbitrary cleanup threshold
this.pendingStates.delete(state);
}
}
}
// Check if user has valid Google Calendar connection
async hasValidConnection(userId: string): Promise<boolean> {
try {
const user = await this.database.getUser(userId);
if (!user) return false;
const googleCalendar = user.connectedCalendars.find(
cal => cal.provider === CalendarProvider.GOOGLE && cal.isDefault
);
if (!googleCalendar) return false;
// Check if token is expired
if (googleCalendar.tokenExpiry && googleCalendar.tokenExpiry < new Date()) {
// Try to refresh
const refreshResult = await this.refreshTokens(userId);
return refreshResult.success;
}
return true;
} catch (error) {
console.error('Connection check error:', error);
return false;
}
}
}
// Import required Google API
const { google } = require('googleapis');