Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions backend/controllers/googleCalendarController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
const { google } = require("googleapis");
const jwt = require("jsonwebtoken");
const User = require("../models/User");

const getOAuth2Client = () => {
const redirectUri =
process.env.GOOGLE_REDIRECT_URI ||
"http://localhost:5000/api/google-calendar/callback";

return new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
redirectUri,
);
};

/**
* @desc Generate Google OAuth URL for calendar connection
* @route GET /api/google-calendar/connect
* @access Private
*/
const connectCalendar = async (req, res) => {
try {
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
return res.status(500).json({
success: false,
message: "Google OAuth is not configured on the server",
});
}

const oauth2Client = getOAuth2Client();

// Create a signed JWT state containing the user ID to prevent CSRF in OAuth callback
const stateToken = jwt.sign(
{ userId: req.user._id.toString(), action: "google_calendar_connect" },
process.env.JWT_SECRET,
{ expiresIn: "15m" },
);

const authUrl = oauth2Client.generateAuthUrl({
access_type: "offline",
prompt: "consent",
scope: [
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/userinfo.email",
],
state: stateToken,
});

return res.status(200).json({
success: true,
authUrl,
});
} catch (error) {
console.error("Connect Google Calendar error:", error);
return res.status(500).json({
success: false,
message: "Failed to initiate Google Calendar connection",
});
}
};

/**
* @desc OAuth callback route executed by Google redirect
* @route GET /api/google-calendar/callback
* @access Public (verifies signed state token)
*/
const handleCallback = async (req, res) => {
const frontendOrigin =
process.env.FRONTEND_ORIGIN?.trim() || "http://localhost:5173";

try {
const { code, state, error: oauthError } = req.query;

if (oauthError || !code || !state) {
return res.redirect(
`${frontendOrigin}/interview-prep?error=calendar_connection_failed`,
);
}

let decoded;
try {
decoded = jwt.verify(state, process.env.JWT_SECRET);
} catch {
return res.redirect(
`${frontendOrigin}/interview-prep?error=invalid_state`,
);
}

if (!decoded?.userId) {
return res.redirect(
`${frontendOrigin}/interview-prep?error=invalid_user`,
);
}

const user = await User.findById(decoded.userId);
if (!user) {
return res.redirect(
`${frontendOrigin}/interview-prep?error=user_not_found`,
);
}

const oauth2Client = getOAuth2Client();
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);

let email = user.email;
try {
const oauth2 = google.oauth2({ version: "v2", auth: oauth2Client });
const userInfo = await oauth2.userinfo.get();
if (userInfo.data?.email) {
email = userInfo.data.email;
}
} catch (err) {
console.warn("Could not retrieve Google user email:", err.message);
}

const refreshToken =
tokens.refresh_token || user.googleCalendar?.refreshToken || null;

user.googleCalendar = {
connected: true,
refreshToken,
email,
connectedAt: new Date(),
};

await user.save();

return res.redirect(
`${frontendOrigin}/interview-prep?google_calendar=connected`,
);
} catch (error) {
console.error("Google Calendar callback error:", error);
return res.redirect(
`${frontendOrigin}/interview-prep?error=calendar_connection_error`,
);
}
};

/**
* @desc Get Google Calendar connection status for the authenticated user
* @route GET /api/google-calendar/status
* @access Private
*/
const getCalendarStatus = async (req, res) => {
try {
const isConnected = !!(
req.user.googleCalendar?.connected &&
req.user.googleCalendar?.refreshToken
);

return res.status(200).json({
success: true,
connected: isConnected,
email: isConnected ? req.user.googleCalendar?.email || req.user.email : null,
});
} catch (error) {
console.error("Get calendar status error:", error);
return res.status(500).json({
success: false,
message: "Failed to load Google Calendar status",
});
}
};

/**
* @desc Create/sync events to the authenticated user's Google Calendar
* @route POST /api/google-calendar/events & POST /api/google-calendar/sync-events
* @access Private
*/
const syncCalendarEvents = async (req, res) => {
try {
const user = req.user;
if (!user.googleCalendar?.connected || !user.googleCalendar?.refreshToken) {
return res.status(400).json({
success: false,
message: "Google Calendar is not connected. Please connect your account first.",
});
}

const oauth2Client = getOAuth2Client();
oauth2Client.setCredentials({
refresh_token: user.googleCalendar.refreshToken,
});

const calendar = google.calendar({ version: "v3", auth: oauth2Client });

const payload = req.body || {};
const eventsList = Array.isArray(payload.events)
? payload.events
: [payload];

const createdEvents = [];

for (const evt of eventsList) {
const summary = evt.title || evt.summary || "Interview Practice Session";
const description =
evt.description || "PrepPilot interview practice session";

const startDateTime = evt.startTime || evt.start || new Date().toISOString();
const endDateTime =
evt.endTime ||
evt.end ||
new Date(new Date(startDateTime).getTime() + 60 * 60 * 1000).toISOString();

const reminderMinutes =
typeof evt.reminderMinutes === "number" ? evt.reminderMinutes : 15;

const eventResource = {
summary,
description,
start: { dateTime: new Date(startDateTime).toISOString() },
end: { dateTime: new Date(endDateTime).toISOString() },
reminders: {
useDefault: false,
overrides: [{ method: "popup", minutes: reminderMinutes }],
},
};

const response = await calendar.events.insert({
calendarId: "primary",
requestBody: eventResource,
});

createdEvents.push(response.data);
}

return res.status(200).json({
success: true,
message: "Google Calendar event synced",
count: createdEvents.length,
eventId: createdEvents[0]?.id || null,
});
} catch (error) {
console.error("Sync Google Calendar events error:", error?.message || error);
return res.status(500).json({
success: false,
message: "Failed to sync Google Calendar event",
error: "A server error occurred",
});
}
};

module.exports = {
connectCalendar,
handleCallback,
getCalendarStatus,
syncCalendarEvents,
};
11 changes: 11 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ const UserSchema = new mongoose.Schema(
isEmailVerified: { type: Boolean, default: false },
emailVerificationToken: { type: String, default: null },
emailVerificationExpires: { type: Date, default: null },

// Google Calendar Connection
googleCalendar: {
connected: { type: Boolean, default: false },
refreshToken: { type: String, default: null },
email: { type: String, default: null },
connectedAt: { type: Date, default: null },
},
},
{ timestamps: true }
);
Expand Down Expand Up @@ -95,6 +103,9 @@ UserSchema.set("toJSON", {
delete ret.emailVerificationToken;
delete ret.emailVerificationExpires;
delete ret.tokenVersion;
if (ret.googleCalendar) {
delete ret.googleCalendar.refreshToken;
}
return ret;
},
});
Expand Down
39 changes: 13 additions & 26 deletions backend/routes/googleCalendarRoutes.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,27 @@
const express = require("express");

const router = express.Router();
const { protect } = require("../middlewares/authMiddleware");
const {
connectCalendar,
handleCallback,
getCalendarStatus,
syncCalendarEvents,
} = require("../controllers/googleCalendarController");

// GET /api/google-calendar/connect
router.get("/connect", (req, res) => {
res.json({
success: true,
message: "Google Calendar connect route is working",
});
});
router.get("/connect", protect, connectCalendar);

// GET /api/google-calendar/callback
router.get("/callback", (req, res) => {
res.json({
success: true,
message: "Google Calendar callback route is working",
});
});
router.get("/callback", handleCallback);

// GET /api/google-calendar/status
router.get("/status", (req, res) => {
res.json({
success: true,
connected: false,
});
});
router.get("/status", protect, getCalendarStatus);

// POST /api/google-calendar/events
router.post("/events", (req, res) => {
const { events } = req.body;
router.post("/events", protect, syncCalendarEvents);

res.json({
success: true,
message: "Calendar events received successfully",
count: Array.isArray(events) ? events.length : 0,
});
});
// POST /api/google-calendar/sync-events
router.post("/sync-events", protect, syncCalendarEvents);

module.exports = router;
14 changes: 1 addition & 13 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,6 @@ process.on("unhandledRejection", (reason, promise) => {
process.exit(1);
});

app.use("/api/books", generalLimiter, booksRoutes);
app.use("/api/jobs", jobRoutes);

app.use("/api/google-calendar", googleCalendarRoutes);


app.use(
"/api/google-calendar",
generalLimiter,
googleCalendarRoutes
);


app.use("/api/google-calendar", generalLimiter, googleCalendarRoutes);
app.use("/api/courses", generalLimiter, coursesRoutes);

Loading
Loading