import { google } from 'googleapis';
import { GoogleAuth } from 'google-auth-library';
import { GOOGLE_SHEETS_SCOPES } from '../config/constants.js';
let authClient: GoogleAuth | null = null;
let sheetsClient: any = null;
export async function getAuthClient(): Promise<GoogleAuth> {
if (!authClient) {
const options: any = {
scopes: GOOGLE_SHEETS_SCOPES,
};
if (process.env.GOOGLE_PROJECT_ID) {
options.projectId = process.env.GOOGLE_PROJECT_ID;
}
// Priority 1: Use file-based authentication if available
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
options.keyFilename = process.env.GOOGLE_APPLICATION_CREDENTIALS;
}
// Priority 2: Use JSON string authentication as fallback
else if (process.env.GOOGLE_SERVICE_ACCOUNT_KEY) {
try {
const credentials = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY);
options.credentials = credentials;
// Extract project ID from credentials if not explicitly set
if (!options.projectId && credentials.project_id) {
options.projectId = credentials.project_id;
}
} catch (error) {
throw new Error(
'Failed to parse GOOGLE_SERVICE_ACCOUNT_KEY: Invalid JSON format. ' +
'Please ensure the environment variable contains valid JSON.'
);
}
}
// Priority 3: Use private key + email authentication
else if (process.env.GOOGLE_PRIVATE_KEY && process.env.GOOGLE_CLIENT_EMAIL) {
const credentials = {
type: 'service_account',
private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'),
client_email: process.env.GOOGLE_CLIENT_EMAIL,
};
options.credentials = credentials;
}
authClient = new GoogleAuth(options);
}
return authClient;
}
export async function getAuthenticatedClient() {
if (!sheetsClient) {
const auth = await getAuthClient();
const authClient = await auth.getClient();
sheetsClient = google.sheets({
version: 'v4',
auth: authClient as any,
});
}
return sheetsClient;
}
export function validateAuth(): void {
// Check if at least one authentication method is provided
const hasFileAuth = !!process.env.GOOGLE_APPLICATION_CREDENTIALS;
const hasJsonAuth = !!process.env.GOOGLE_SERVICE_ACCOUNT_KEY;
const hasPrivateKeyAuth = !!process.env.GOOGLE_PRIVATE_KEY && !!process.env.GOOGLE_CLIENT_EMAIL;
if (!hasFileAuth && !hasJsonAuth && !hasPrivateKeyAuth) {
throw new Error(
'No authentication method provided. Please set one of:\n' +
'- GOOGLE_APPLICATION_CREDENTIALS to the path of your service account key file\n' +
'- GOOGLE_SERVICE_ACCOUNT_KEY to the JSON string of your service account credentials\n' +
'- GOOGLE_PRIVATE_KEY and GOOGLE_CLIENT_EMAIL for direct private key authentication'
);
}
// If using private key authentication, validate both fields are present
if (!hasFileAuth && !hasJsonAuth && hasPrivateKeyAuth) {
if (!process.env.GOOGLE_PRIVATE_KEY) {
throw new Error('GOOGLE_PRIVATE_KEY is required when using private key authentication');
}
if (!process.env.GOOGLE_CLIENT_EMAIL) {
throw new Error('GOOGLE_CLIENT_EMAIL is required when using private key authentication');
}
// Validate private key format
const privateKey = process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n');
if (!privateKey.includes('BEGIN PRIVATE KEY') || !privateKey.includes('END PRIVATE KEY')) {
throw new Error(
'GOOGLE_PRIVATE_KEY appears to be invalid. ' +
'It should start with -----BEGIN PRIVATE KEY----- and end with -----END PRIVATE KEY-----'
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(process.env.GOOGLE_CLIENT_EMAIL)) {
throw new Error(
'GOOGLE_CLIENT_EMAIL appears to be invalid. ' +
'It should be a valid email address (e.g., your-service-account@your-project.iam.gserviceaccount.com)'
);
}
}
// If using JSON authentication, validate it can be parsed
if (!hasFileAuth && hasJsonAuth) {
try {
const credentials = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_KEY!);
// Validate required fields in the service account JSON
if (!credentials.type || credentials.type !== 'service_account') {
throw new Error('Invalid service account: type must be "service_account"');
}
if (!credentials.private_key) {
throw new Error('Invalid service account: missing private_key');
}
if (!credentials.client_email) {
throw new Error('Invalid service account: missing client_email');
}
// Extract project ID from credentials if GOOGLE_PROJECT_ID is not set
if (!process.env.GOOGLE_PROJECT_ID && credentials.project_id) {
process.env.GOOGLE_PROJECT_ID = credentials.project_id;
}
} catch (error: any) {
if (error instanceof SyntaxError) {
throw new Error(
'GOOGLE_SERVICE_ACCOUNT_KEY contains invalid JSON. ' +
'Please ensure it is a valid JSON string.'
);
}
throw error;
}
}
// Validate project ID is available (optional for private key auth as it's not always needed)
if (!process.env.GOOGLE_PROJECT_ID && !hasPrivateKeyAuth) {
throw new Error(
'GOOGLE_PROJECT_ID environment variable is not set. ' +
'Please set it to your Google Cloud project ID, or ensure it is included in your service account credentials.'
);
}
}