/** * Facebook OAuth Service * Handles OAuth 2.0 Authorization Code Flow for Facebook */ import axios from 'axios'; import { createHash, randomBytes } from 'crypto'; import { humanApprovalService, ApprovalRiskLevel } from './HumanApprovalService.js'; interface FacebookTokenResponse { access_token: string; token_type: string; expires_in: number; } interface FacebookUserProfile { id: string; name: string; email?: string; picture?: { data: { url: string; }; }; } interface FacebookPost { id: string; message?: string; created_time: string; full_picture?: string; permalink_url?: string; } interface FacebookPhoto { id: string; images: Array<{ source: string; width: number; height: number }>; created_time: string; name?: string; link?: string; } export class FacebookOAuthService { private appId: string; private appSecret: string; private redirectUri: string; private graphApiVersion = 'v18.0'; private tokens: Map = new Map(); constructor() { this.appId = process.env.FACEBOOK_APP_ID || ''; this.appSecret = process.env.FACEBOOK_APP_SECRET || ''; this.redirectUri = process.env.FACEBOOK_REDIRECT_URI || 'http://localhost:3001/api/auth/facebook/callback'; if (!this.appId || !this.appSecret) { console.warn('⚠️ Facebook OAuth credentials not configured'); } } /** * Generate OAuth authorization URL */ getAuthorizationUrl(state?: string): string { const scopes = [ 'public_profile', 'email', 'user_posts', 'user_photos', ]; const params = new URLSearchParams({ client_id: this.appId, redirect_uri: this.redirectUri, scope: scopes.join(','), response_type: 'code', state: state || this.generateState(), }); return `https://www.facebook.com/${this.graphApiVersion}/dialog/oauth?${params.toString()}`; } /** * Exchange authorization code for access token */ async exchangeCodeForToken(code: string): Promise<{ accessToken: string; userId: string }> { try { const params = new URLSearchParams({ client_id: this.appId, client_secret: this.appSecret, redirect_uri: this.redirectUri, code, }); const response = await axios.get( `https://graph.facebook.com/${this.graphApiVersion}/oauth/access_token?${params.toString()}` ); const { access_token, expires_in } = response.data; // Get user profile to obtain user ID const profile = await this.getUserProfile(access_token); // Store token with expiration const expiresAt = Date.now() + (expires_in * 1000); this.tokens.set(profile.id, { accessToken: access_token, expiresAt, userId: profile.id, }); console.log(`✅ Facebook token obtained for user ${profile.id}`); return { accessToken: access_token, userId: profile.id, }; } catch (error: any) { console.error('❌ Facebook token exchange failed:', error.response?.data || error.message); throw new Error(`Facebook OAuth failed: ${error.response?.data?.error?.message || error.message}`); } } /** * Get user profile */ async getUserProfile(accessToken: string): Promise { try { const response = await axios.get( `https://graph.facebook.com/${this.graphApiVersion}/me`, { params: { access_token: accessToken, fields: 'id,name,email,picture', }, } ); return response.data; } catch (error: any) { throw new Error(`Failed to fetch user profile: ${error.response?.data?.error?.message || error.message}`); } } /** * Get user's posts */ async getUserPosts(userId: string, limit: number = 25): Promise { const tokenData = this.tokens.get(userId); if (!tokenData) { throw new Error('No access token found for user. Please authenticate first.'); } if (Date.now() > tokenData.expiresAt) { throw new Error('Access token expired. Please re-authenticate.'); } try { const response = await axios.get( `https://graph.facebook.com/${this.graphApiVersion}/${userId}/posts`, { params: { access_token: tokenData.accessToken, fields: 'id,message,created_time,full_picture,permalink_url', limit, }, } ); return response.data.data || []; } catch (error: any) { console.error('❌ Failed to fetch posts:', error.response?.data); throw new Error(`Failed to fetch posts: ${error.response?.data?.error?.message || error.message}`); } } /** * Get specific photo */ async getPhoto(photoId: string, userId: string): Promise { const tokenData = this.tokens.get(userId); if (!tokenData) { throw new Error('No access token found for user. Please authenticate first.'); } if (Date.now() > tokenData.expiresAt) { throw new Error('Access token expired. Please re-authenticate.'); } try { const response = await axios.get( `https://graph.facebook.com/${this.graphApiVersion}/${photoId}`, { params: { access_token: tokenData.accessToken, fields: 'id,images,created_time,name,link', }, } ); return response.data; } catch (error: any) { console.error('❌ Failed to fetch photo:', error.response?.data); throw new Error(`Failed to fetch photo: ${error.response?.data?.error?.message || error.message}`); } } /** * Download and analyze photo */ async downloadAndAnalyzePhoto(photoId: string, userId: string): Promise<{ photo: FacebookPhoto; imageBuffer: Buffer; analysis?: any; }> { const photo = await this.getPhoto(photoId, userId); // Get highest resolution image const largestImage = photo.images.reduce((prev, current) => (current.width > prev.width) ? current : prev ); // Download image const imageResponse = await axios.get(largestImage.source, { responseType: 'arraybuffer', }); const imageBuffer = Buffer.from(imageResponse.data); console.log(`📸 Downloaded photo ${photoId} (${largestImage.width}x${largestImage.height})`); return { photo, imageBuffer, // TODO: Add Gemini Vision API analysis here }; } /** * Ingest photo into knowledge base * 🔐 REQUIRES HUMAN APPROVAL - No data can be ingested without explicit permission */ async ingestPhoto(photoId: string, userId: string, approvedBy?: string): Promise<{ success: boolean; photoId: string; vectorId?: string; graphNodeId?: string; approvalRequestId?: string; }> { try { // Step 1: Download and analyze photo first (read-only) const { photo, imageBuffer } = await this.downloadAndAnalyzePhoto(photoId, userId); // Step 2: REQUEST APPROVAL before ingesting const approvalRequestId = await humanApprovalService.requestApproval( 'facebook.ingest.photo', `Ingest Facebook photo ${photoId} into knowledge base\nPhoto: ${photo.name || 'Untitled'}\nSize: ${(imageBuffer.length / 1024).toFixed(2)} KB`, ApprovalRiskLevel.MEDIUM, `FacebookOAuthService (user: ${userId})`, { photoId, userId, photoName: photo.name, photoUrl: photo.link, imageSize: imageBuffer.length, }, 600000 // 10 minute expiry ); console.log(`⏳ [WAITING FOR APPROVAL] Photo ingestion request ID: ${approvalRequestId}`); // Step 3: WAIT for human approval (blocks here) try { await humanApprovalService.waitForApproval(approvalRequestId, 600000); } catch (approvalError: any) { console.error(`🚫 [APPROVAL DENIED] ${approvalError.message}`); throw new Error(`Photo ingestion not approved: ${approvalError.message}`); } console.log(`✅ [APPROVED] Proceeding with photo ingestion`); // Step 4: Only NOW proceed with actual ingestion // TODO: Integrate with KnowledgeAcquisition service // 1. Analyze image with Gemini Vision // 2. Extract entities with NER // 3. Store in Neo4j graph // 4. Create vector embeddings // 5. Store in pgvector console.log(`✅ Photo ${photoId} ingested into knowledge base`); return { success: true, photoId, vectorId: 'placeholder', // From pgvector graphNodeId: 'placeholder', // From Neo4j approvalRequestId, }; } catch (error: any) { console.error(`❌ Failed to ingest photo ${photoId}:`, error.message); throw error; } } /** * Get stored token for user */ getAccessToken(userId: string): string | null { const tokenData = this.tokens.get(userId); if (!tokenData || Date.now() > tokenData.expiresAt) { return null; } return tokenData.accessToken; } /** * Check if user is authenticated */ isAuthenticated(userId: string): boolean { const tokenData = this.tokens.get(userId); return tokenData !== undefined && Date.now() < tokenData.expiresAt; } /** * Revoke access token */ revokeToken(userId: string): void { this.tokens.delete(userId); console.log(`🔒 Token revoked for user ${userId}`); } /** * Generate random state for CSRF protection */ private generateState(): string { return randomBytes(32).toString('hex'); } /** * Verify state parameter */ verifyState(receivedState: string, expectedState: string): boolean { return receivedState === expectedState; } } // Singleton instance export const facebookOAuth = new FacebookOAuthService();