Spaces:
Paused
Paused
| /** | |
| * 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<string, { accessToken: string; expiresAt: number; userId: string }> = 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<FacebookTokenResponse>( | |
| `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<FacebookUserProfile> { | |
| try { | |
| const response = await axios.get<FacebookUserProfile>( | |
| `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<FacebookPost[]> { | |
| 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<FacebookPhoto> { | |
| 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<FacebookPhoto>( | |
| `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(); | |