credential-manager.ts•4.55 kB
import fs from 'fs/promises';
import path from 'path';
import { AuthCredentials } from '../types/auth.js';
/**
* Secure credential management for Google Drive authentication
* Handles loading, validation, and secure storage of credentials
*/
export class CredentialManager {
private credentialsPath: string;
private credentials: AuthCredentials | null = null;
constructor(credentialsPath: string = './config/credentials.json') {
this.credentialsPath = credentialsPath;
}
/**
* Load credentials from file or environment variables
*/
async loadCredentials(): Promise<AuthCredentials> {
// Try environment variables first (more secure for production)
const envCredentials = this.loadFromEnvironment();
if (envCredentials) {
this.credentials = envCredentials;
return envCredentials;
}
// Fallback to file-based credentials
try {
const credentialsData = await fs.readFile(this.credentialsPath, 'utf-8');
const fileCredentials = JSON.parse(credentialsData);
this.validateCredentials(fileCredentials);
this.credentials = fileCredentials;
return fileCredentials;
} catch (error) {
throw new Error(`Failed to load credentials: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Load credentials from environment variables
*/
private loadFromEnvironment(): AuthCredentials | null {
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
const redirectUri = process.env.GOOGLE_REDIRECT_URI;
if (!clientId || !clientSecret || !redirectUri) {
return null;
}
const credentials: AuthCredentials = {
clientId,
clientSecret,
redirectUri
};
// Only add optional tokens if they exist
if (process.env.GOOGLE_REFRESH_TOKEN) {
credentials.refreshToken = process.env.GOOGLE_REFRESH_TOKEN;
}
if (process.env.GOOGLE_ACCESS_TOKEN) {
credentials.accessToken = process.env.GOOGLE_ACCESS_TOKEN;
}
return credentials;
}
/**
* Validate credentials structure and required fields
*/
private validateCredentials(credentials: any): void {
if (!credentials || typeof credentials !== 'object') {
throw new Error('Invalid credentials format');
}
const required = ['clientId', 'clientSecret', 'redirectUri'];
const missing = required.filter(field => !credentials[field]);
if (missing.length > 0) {
throw new Error(`Missing required credential fields: ${missing.join(', ')}`);
}
// Validate format of fields
if (typeof credentials.clientId !== 'string' || !credentials.clientId.trim()) {
throw new Error('Invalid clientId format');
}
if (typeof credentials.clientSecret !== 'string' || !credentials.clientSecret.trim()) {
throw new Error('Invalid clientSecret format');
}
if (typeof credentials.redirectUri !== 'string' || !this.isValidUrl(credentials.redirectUri)) {
throw new Error('Invalid redirectUri format');
}
}
/**
* Validate URL format
*/
private isValidUrl(url: string): boolean {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Get current credentials
*/
getCredentials(): AuthCredentials {
if (!this.credentials) {
throw new Error('Credentials not loaded. Call loadCredentials() first.');
}
return this.credentials;
}
/**
* Create sample credentials file for setup
*/
async createSampleCredentialsFile(): Promise<void> {
const sampleCredentials = {
clientId: "your-google-client-id.apps.googleusercontent.com",
clientSecret: "your-google-client-secret",
redirectUri: "http://localhost:8080/callback",
refreshToken: "optional-refresh-token",
accessToken: "optional-access-token"
};
const credentialsDir = path.dirname(this.credentialsPath);
await fs.mkdir(credentialsDir, { recursive: true });
await fs.writeFile(
this.credentialsPath,
JSON.stringify(sampleCredentials, null, 2),
{ mode: 0o600 } // Restrict file permissions
);
console.log(`Sample credentials file created at: ${this.credentialsPath}`);
console.log('Please update it with your actual Google OAuth credentials.');
}
/**
* Check if credentials file exists
*/
async credentialsExist(): Promise<boolean> {
try {
await fs.access(this.credentialsPath);
return true;
} catch {
return false;
}
}
}