widgetdc-cortex / apps /backend /src /services /FacebookOAuthService.ts
Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* 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();