Description:
// server.js
import express from 'express';
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
import apiRouter from './routes/api.js';
import session from 'express-session';
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
// NEW: Import Google API libraries
import { google } from 'googleapis';
import { OAuth2Client } from 'google-auth-library'; // For managing OAuth tokens
// Load environment variables from .env file
dotenv.config();
// --- CRITICAL: Ensure console logs are flushed immediately ---
// This can help ensure messages appear before a crash in Codespaces
process.stdout.uncork();
process.stderr.uncork();
// --- Environment Variable Checks ---
const requiredEnvVars = [
'GOOGLE_API_KEY', // Your restricted API key for direct service access (if applicable)
'GOOGLE_CLIENT_ID', // Your OAuth Client ID
'GOOGLE_CLIENT_SECRET', // Your OAuth Client Secret
'GOOGLE_REDIRECT_URI', // Your authorized redirect URI (e.g., https://ubiquitous-bassoon-jjg6wq44p9jr3q7j7-3001.app.github.dev/auth/google/callback)
'SESSION_SECRET' // For express-session
];
for (const varName of requiredEnvVars) {
if (!process.env[varName]) {
console.error(`FATAL ERROR: Environment variable ${varName} is not set. Please check your .env file or Codespaces secrets.`);
process.exit(1); // Exit with a failure code
}
}
// --- Catch unhandled errors ---
process.on('uncaughtException', (err, origin) => {
console.error('FATAL UNCAUGHT EXCEPTION:', err);
console.error('Exception origin:', origin);
process.exit(1);
});
// --- Log process exit/termination ---
process.on('exit', (code) => {
console.log(`Server process exited with code: ${code}`);
});
process.on('SIGTERM', () => {
console.log('Server process received SIGTERM signal. Shutting down gracefully...');
process.exit(0);
});
// Setup for ES module __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = process.env.PORT || 3001;
// --- Google OAuth2Client Setup ---
// This client is used to initiate the OAuth flow and refresh tokens.
const oauth2Client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);
// In-memory user database (for demonstration purposes, replace with persistent DB in production)
const users = {};
// --- Passport.js Configuration ---
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_REDIRECT_URI, // Use the environment variable
passReqToCallback: true // Allows us to access req in the callback
},
async (request, accessToken, refreshToken, profile, done) => {
// Save tokens and profile info. In a real app, save to a DB.
// For demonstration, we'll store basic info and tokens (NOT SECURE FOR PRODUCTION)
users[profile.id] = {
id: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
photo: profile.photos[0].value,
accessToken: accessToken, // Store for making API calls on behalf of the user
refreshToken: refreshToken, // Store for refreshing access tokens
};
console.log("Google profile received: " + profile.displayName);
console.log("Access Token (store securely!): " + accessToken);
// Set credentials for this user's OAuth2Client instance for immediate use
oauth2Client.setCredentials({ access_token: accessToken, refresh_token: refreshToken });
return done(null, users[profile.id]);
}
));
// REVISED: Fix for 'ReferenceError: id is not defined'
passport.serializeUser((user, done) => {
done(null, user.id); // Corrected from 'users[id]' to 'user.id'
});
passport.deserializeUser((id, done) => {
// In a real app, retrieve user from database.
done(null, users[id]);
});
// --- Middleware Setup ---
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: { secure: app.get('env') === 'production' } // 'true' for HTTPS in prod, 'false' for http in dev (Codespaces)
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Serve static files from 'public' and 'node_modules'
app.use(express.static(path.join(__dirname, 'public')));
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
// --- Routes ---
app.get('/auth/google',
passport.authenticate('google', { scope: [
'profile',
'email',
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/contacts.readonly', // Uses People API internally
'https://www.googleapis.com/auth/photoslibrary.readonly'
] })); // Include all necessary scopes here
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/' }),
(req, res) => {
console.log('Successfully authenticated! User:', req.user.name);
// Redirect to the main application page or a dashboard
res.redirect('/');
}
);
app.get('/logout', (req, res, next) => {
req.logout(err => {
if (err) { return next(err); }
// Clear tokens from in-memory store if applicable (for demo purposes)
if (req.user && users[req.user.id]) {
delete users[req.user.id].accessToken;
delete users[req.user.id].refreshToken;
}
res.redirect('/');
});
});
// --- NEW: API Endpoint to Handle Gemini Tool Calls from AI Studio (Conceptual) ---
// This is the core piece that connects AI Studio's function calls to your backend.
// In a real application, this would be a secure endpoint that:
// 1. Receives the tool call from AI Studio (after Gemini decides to call a function).
// 2. Executes the corresponding function using the Google APIs.
// 3. Returns the result back to AI Studio (which then feeds it back to Gemini).
app.post('/api/gemini-tool-call', async (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized: User not logged in.' });
}
const { toolName, args } = req.body; // Expecting toolName and args from AI Studio's output
console.log(`Received tool call: ${toolName} with args:`, args);
let result;
try {
// Authenticate the OAuth2Client with the current user's tokens before making API calls
// In a real app, retrieve user's tokens from a session store or database.
const currentUser = users[req.user.id]; // Access token from in-memory users object
if (!currentUser || !currentUser.accessToken) {
throw new Error('User access token not found for API call.');
}
oauth2Client.setCredentials({
access_token: currentUser.accessToken,
refresh_token: currentUser.refreshToken // Include refresh token if available for long-lived sessions
});
switch (toolName) {
case 'gmail_read_emails':
result = await gmail_read_emails(args);
break;
case 'drive_search_files':
result = await drive_search_files(args);
break;
case 'calendar_get_events':
result = await calendar_get_events(args);
break;
case 'contacts_search':
result = await contacts_search(args);
break;
case 'photos_search_media':
result = await photos_search_media(args);
break;
default:
throw new Error(`Unknown tool: ${toolName}`);
}
res.json({ success: true, data: result });
} catch (error) {
console.error(`Error executing tool ${toolName}:`, error.message, error.stack);
res.status(500).json({ success: false, error: error.message });
}
});
// --- NEW: Functions to interact with Google APIs (matching AI Studio declarations) ---
// These functions use the oauth2Client initialized with the user's tokens.
async function gmail_read_emails(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Gmail API.');
const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
const queryParts = [];
if (args.subject) queryParts.push(`subject:(${args.subject})`);
if (args.sender) queryParts.push(`from:(${args.sender})`);
if (args.label) queryParts.push(`label:(${args.label})`);
if (args.keywords) queryParts.push(args.keywords); // General keywords are not 'subject' or 'from'
const q = queryParts.join(' ');
console.log(`Calling Gmail API with query: "${q}", maxResults: ${args.maxResults}`);
const res = await gmail.users.messages.list({
userId: 'me',
q: q,
maxResults: args.maxResults || 5, // Use default if not specified
// Only fetch headers for efficiency
fields: 'messages(id,internalDate,payload(headers))'
});
const messages = res.data.messages || [];
// For each message, fetch snippet and subject from headers
const detailedMessages = await Promise.all(messages.map(async (message) => {
const msg = await gmail.users.messages.get({ userId: 'me', id: message.id, format: 'metadata', fields: 'snippet,payload(headers)' });
const headers = msg.data.payload.headers;
const subject = headers.find(header => header.name === 'Subject')?.value;
const from = headers.find(header => header.name === 'From')?.value;
return {
id: message.id,
subject: subject,
from: from,
snippet: msg.data.snippet
};
}));
return detailedMessages;
}
async function drive_search_files(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Drive API.');
const drive = google.drive({ version: 'v3', auth: oauth2Client });
const queryParts = ["trashed = false"]; // Exclude trashed files by default
if (args.keywords) queryParts.push(`(name contains '${args.keywords}' or fullText contains '${args.keywords}')`);
if (args.fileType) queryParts.push(`mimeType contains 'application/vnd.google-apps.${args.fileType}'`); // Map generic types to MIME types
if (args.folderName) {
// This is simplified. Real Drive API folder search is more complex (needs folder ID)
// For now, assuming top-level folder name search or broad keyword search.
console.warn("Drive API folderName search is simplified. Consider using folder IDs for precision.");
queryParts.push(`'${args.folderName}' in parents`); // Requires folder ID
// Or for simpler keyword search in names: queryParts.push(`name contains '${args.folderName}' and mimeType = 'application/vnd.google-apps.folder'`);
}
const q = queryParts.join(' and ');
console.log(`Calling Drive API with query: "${q}", maxResults: ${args.maxResults}`);
const res = await drive.files.list({
q: q,
pageSize: args.maxResults || 5,
fields: 'files(id, name, mimeType, webContentLink, parents)',
});
return res.data.files;
}
async function calendar_get_events(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Calendar API.');
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
const timeMin = args.timeMin ? new Date(args.timeMin).toISOString() : (new Date()).toISOString();
const timeMax = args.timeMax ? new Date(args.timeMax).toISOString() : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to next 7 days
console.log(`Calling Calendar API from ${timeMin} to ${timeMax} with keywords: "${args.keywords}", maxResults: ${args.maxResults}`);
const res = await calendar.events.list({
calendarId: 'primary',
timeMin: timeMin,
timeMax: timeMax,
q: args.keywords,
maxResults: args.maxResults || 5,
singleEvents: true,
orderBy: 'startTime',
});
return res.data.items;
}
async function contacts_search(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for People API.');
const people = google.people({ version: 'v1', auth: oauth2Client });
console.log(`Calling People API with query: "${args.query}", maxResults: ${args.maxResults}`);
const res = await people.people.connections.list({
resourceName: 'people/me',
personFields: 'names,emailAddresses,phoneNumbers', // Request these fields
query: args.query, // Search by name or email
pageSize: args.maxResults || 5,
});
const connections = res.data.connections || [];
return connections.map(person => ({
displayName: person.names && person.names.length > 0 ? person.names[0].displayName : 'N/A',
email: person.emailAddresses && person.emailAddresses.length > 0 ? person.emailAddresses[0].value : 'N/A',
phone: person.phoneNumbers && person.phoneNumbers.length > 0 ? person.phoneNumbers[0].value : 'N/A',
}));
}
async function photos_search_media(args) {
if (!oauth2Client.credentials.access_token) throw new Error('Access token not available for Photos Library API.');
const photoslibrary = google.photoslibrary({ version: 'v1', auth: oauth2Client });
const filters = {};
if (args.keywords) {
console.warn("Photos Library API keyword search is highly simplified. Complex keyword mapping needed.");
}
if (args.startDate && args.endDate) {
filters.dateFilter = {
ranges: [{
startDate: { year: parseInt(args.startDate.substring(0,4)), month: parseInt(args.startDate.substring(5,7)), day: parseInt(args.startDate.substring(8,10)) },
endDate: { year: parseInt(args.endDate.substring(0,4)), month: parseInt(args.endDate.substring(5,7)), day: parseInt(args.endDate.substring(8,10)) }
}]
};
}
console.log(`Calling Photos Library API with filters:`, filters, `maxResults: ${args.maxResults}`);
const res = await photoslibrary.mediaItems.search({
filters: filters,
pageSize: args.maxResults || 5,
});
const mediaItems = res.data.mediaItems || [];
return mediaItems.map(item => ({
id: item.id,
filename: item.filename,
baseUrl: item.baseUrl,
mediaMetadata: item.mediaMetadata,
}));
}
// --- API Router for other custom endpoints ---
app.use('/api', apiRouter);
// --- Start Server ---
app.listen(PORT, () => {
console.log(`🚀 Server is running on http://localhost:${PORT}`);
});
Status: Published Priority: 0.0
Target: Comments: URLs: Images:
]]>