127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { google } from 'googleapis';
|
|
import { PrismaClient } from '@prisma/client';
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
// Google Calendar OAuth callback endpoint
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const session = await getServerSession();
|
|
const { searchParams } = new URL(request.url);
|
|
const code = searchParams.get('code');
|
|
const state = searchParams.get('state'); // User email passed from start route
|
|
|
|
if (!code) {
|
|
return NextResponse.redirect(new URL('/tasks?error=oauth_code_missing', request.url));
|
|
}
|
|
|
|
// Get user from session or state parameter
|
|
const userEmail = session?.user?.email || state;
|
|
|
|
if (!userEmail) {
|
|
return NextResponse.redirect(new URL('/auth/login?error=session_expired', request.url));
|
|
}
|
|
|
|
// Find user in database
|
|
const user = await prisma.user.findUnique({
|
|
where: { email: userEmail }
|
|
});
|
|
|
|
if (!user) {
|
|
return NextResponse.redirect(new URL('/auth/login?error=user_not_found', request.url));
|
|
}
|
|
|
|
// Initialize Google OAuth client
|
|
const clientId = process.env.GOOGLE_CLIENT_ID || '';
|
|
const clientSecret = process.env.GOOGLE_CLIENT_SECRET || '';
|
|
const redirectUri = process.env.GOOGLE_REDIRECT_URI || `${process.env.NEXTAUTH_URL}/api/calendar/google/oauth`;
|
|
|
|
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
|
|
|
// Exchange authorization code for access token
|
|
const { tokens } = await oauth2Client.getToken(code);
|
|
oauth2Client.setCredentials(tokens);
|
|
|
|
// Fetch user calendars to store in connection settings
|
|
// Dynamic import to avoid circular dep issues in some envs, or just standard import?
|
|
// Standard import is better but we are inside function scope to check diff.
|
|
// I'll assume the import is added at top level or I force it here if possible.
|
|
// I will add import at top level in separate chunk if needed?
|
|
// Replace whole file content is unsafe. I'll use multi-replace.
|
|
|
|
// We need getUserCalendars. I'll use require or assume import added.
|
|
// Actually, I'll allow ReplaceFileContent to manage imports? No.
|
|
// I'll use multi_replace to add import AND update logic.
|
|
|
|
// WAIT, better approach: Just implement the fetch logic here locally to avoid import issues or dependency on lib if it changes.
|
|
// But duplicate code is bad.
|
|
// I'll add the import at the top.
|
|
|
|
// Logic:
|
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
const response = await calendar.calendarList.list();
|
|
const remoteCalendars = response.data.items?.map((item: any) => ({
|
|
id: item.id,
|
|
title: item.summary,
|
|
isPrimary: item.primary,
|
|
backgroundColor: item.backgroundColor, // Store calendar color for event fallback
|
|
selected: true // Default newly found to true
|
|
})) || [];
|
|
|
|
// Check if connection already exists
|
|
const existingConnection = await prisma.calendarConnection.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
provider: 'google'
|
|
}
|
|
});
|
|
|
|
let finalCalendars = remoteCalendars;
|
|
|
|
if (existingConnection) {
|
|
// Merge with existing selection
|
|
if (existingConnection.calendars && Array.isArray(existingConnection.calendars)) {
|
|
const existingList = existingConnection.calendars as any[];
|
|
finalCalendars = remoteCalendars.map(remote => {
|
|
const match = existingList.find(e => e.id === remote.id);
|
|
return {
|
|
...remote,
|
|
selected: match ? match.selected : true // Preserve selection
|
|
};
|
|
});
|
|
}
|
|
|
|
// Update existing connection
|
|
await prisma.calendarConnection.update({
|
|
where: { id: existingConnection.id },
|
|
data: {
|
|
accessToken: tokens.access_token || '',
|
|
refreshToken: tokens.refresh_token || existingConnection.refreshToken,
|
|
expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : null,
|
|
calendars: finalCalendars, // Store calendars
|
|
updatedAt: new Date()
|
|
}
|
|
});
|
|
} else {
|
|
// Create new connection
|
|
await prisma.calendarConnection.create({
|
|
data: {
|
|
userId: user.id,
|
|
provider: 'google',
|
|
accessToken: tokens.access_token || '',
|
|
refreshToken: tokens.refresh_token || null,
|
|
expiresAt: tokens.expiry_date ? new Date(tokens.expiry_date) : null,
|
|
calendars: finalCalendars, // Store calendars
|
|
}
|
|
});
|
|
}
|
|
|
|
// Redirect to tasks page with success message
|
|
return NextResponse.redirect(new URL('/tasks?calendar=connected', request.url));
|
|
} catch (error) {
|
|
console.error('Google OAuth error:', error);
|
|
return NextResponse.redirect(new URL('/tasks?error=oauth_failed', request.url));
|
|
}
|
|
} |