auth-middleware.ts•5.29 kB
import { AuthManager } from '../types/auth.js';
/**
* Authentication middleware for Google Drive API calls
* Ensures requests are authenticated and handles token refresh
*/
export class AuthMiddleware {
private authManager: AuthManager;
private maxRetries: number;
constructor(authManager: AuthManager, maxRetries: number = 3) {
this.authManager = authManager;
this.maxRetries = maxRetries;
}
/**
* Execute an authenticated API call with automatic retry on auth failure
*/
async executeWithAuth<T>(
apiCall: () => Promise<T>,
context: string = 'API call'
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
// Ensure we're authenticated before making the call
if (!this.authManager.isAuthenticated()) {
const authSuccess = await this.authManager.authenticate();
if (!authSuccess) {
throw new Error('Authentication failed');
}
}
// Execute the API call
return await apiCall();
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
// Check if this is an authentication error
if (this.isAuthError(error)) {
console.warn(`Authentication error on attempt ${attempt} for ${context}:`, lastError.message);
// Try to refresh token if not on last attempt
if (attempt < this.maxRetries) {
const refreshSuccess = await this.authManager.refreshToken();
if (refreshSuccess) {
console.log(`Token refreshed successfully, retrying ${context}`);
continue;
} else {
console.error(`Token refresh failed for ${context}`);
}
}
} else {
// Non-auth error, don't retry
throw lastError;
}
}
}
// All retries exhausted
throw new AuthenticationError(
`Authentication failed after ${this.maxRetries} attempts for ${context}`,
lastError
);
}
/**
* Validate current authentication status
*/
async validateAuth(): Promise<AuthValidationResult> {
try {
if (!this.authManager.isAuthenticated()) {
return {
isValid: false,
error: 'Not authenticated',
needsReauth: true
};
}
// Try to get access token to verify it's available
const token = this.authManager.getAccessToken();
if (!token) {
return {
isValid: false,
error: 'No access token available',
needsReauth: true
};
}
return {
isValid: true,
error: null,
needsReauth: false
};
} catch (error) {
return {
isValid: false,
error: error instanceof Error ? error.message : 'Unknown error',
needsReauth: true
};
}
}
/**
* Check if an error is authentication-related
*/
private isAuthError(error: any): boolean {
if (!error) return false;
// Check error message patterns
const errorMessage = error.message?.toLowerCase() || '';
const authErrorPatterns = [
'unauthorized',
'invalid_token',
'token_expired',
'authentication',
'invalid credentials',
'access denied',
'401'
];
if (authErrorPatterns.some(pattern => errorMessage.includes(pattern))) {
return true;
}
// Check HTTP status codes
if (error.status === 401 || error.code === 401) {
return true;
}
// Check Google API specific error codes
if (error.code === 'UNAUTHENTICATED' || error.code === 'PERMISSION_DENIED') {
return true;
}
return false;
}
/**
* Create a wrapper function that automatically handles authentication
*/
createAuthenticatedWrapper<TArgs extends any[], TReturn>(
fn: (...args: TArgs) => Promise<TReturn>,
context?: string
): (...args: TArgs) => Promise<TReturn> {
return async (...args: TArgs): Promise<TReturn> => {
return this.executeWithAuth(
() => fn(...args),
context || fn.name || 'wrapped function'
);
};
}
}
/**
* Custom error class for authentication failures
*/
export class AuthenticationError extends Error {
public readonly originalError: Error | null;
constructor(message: string, originalError: Error | null = null) {
super(message);
this.name = 'AuthenticationError';
this.originalError = originalError;
}
}
/**
* Result of authentication validation
*/
export interface AuthValidationResult {
isValid: boolean;
error: string | null;
needsReauth: boolean;
}
/**
* Utility function to create authenticated API client wrapper
*/
export function createAuthenticatedClient<T>(
client: T,
authMiddleware: AuthMiddleware
): T {
const wrapper = {} as T;
// Wrap all methods of the client with authentication
for (const key in client) {
const value = client[key];
if (typeof value === 'function') {
(wrapper as any)[key] = authMiddleware.createAuthenticatedWrapper(
value.bind(client),
`${String(key)} method`
);
} else {
(wrapper as any)[key] = value;
}
}
return wrapper;
}