import os import sys import locale import logging from flask import Flask, send_from_directory, request from flask_cors import CORS from flask_jwt_extended import JWTManager # Import for job handling import uuid from concurrent.futures import ThreadPoolExecutor # Configure logging for APScheduler - only show essential logs logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logging.getLogger('apscheduler').setLevel(logging.WARNING) # Use relative import for the Config class to work with Hugging Face Spaces from backend.config import Config from backend.utils.database import init_supabase from backend.utils.cookies import setup_secure_cookies, configure_jwt_with_cookies # APScheduler imports from backend.scheduler.apscheduler_service import APSchedulerService def setup_unicode_environment(): """Setup Unicode environment for proper character handling.""" try: # Set environment variables for UTF-8 support os.environ['PYTHONIOENCODING'] = 'utf-8' os.environ['PYTHONUTF8'] = '1' # Set locale to UTF-8 if available try: locale.setlocale(locale.LC_ALL, 'C.UTF-8') except locale.Error: try: locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') except locale.Error: try: locale.setlocale(locale.LC_ALL, '') except locale.Error: pass # Set stdout/stderr encoding to UTF-8 if possible if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace') sys.stderr.reconfigure(encoding='utf-8', errors='replace') # Log to app logger instead of print if 'app' in globals(): app.logger.info("Unicode environment setup completed") except Exception as e: if 'app' in globals(): app.logger.warning(f"Unicode setup failed: {str(e)}") def create_app(): """Create and configure the Flask application.""" # Setup Unicode environment first setup_unicode_environment() app = Flask(__name__, static_folder='../frontend/dist') app.config.from_object(Config) # Disable strict slashes to prevent redirects app.url_map.strict_slashes = False # Initialize CORS with specific configuration CORS(app, resources={ r"/api/*": { "origins": [ "http://localhost:3000", "http://localhost:5000", "http://127.0.0.1:3000", "http://127.0.0.1:5000", "http://192.168.1.4:3000", "https://zelyanoth-lin-cbfcff2.hf.space" ], "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization", "X-Requested-With"], "supports_credentials": True, "max_age": 86400 # 24 hours } }) # Add CORS headers for all routes @app.after_request def add_cors_headers(response): """Add CORS headers to all responses.""" # Get the origin from the request origin = request.headers.get('Origin', '') # Check if the origin is in our allowed list allowed_origins = [ "http://localhost:3000", "http://localhost:5000", "http://127.0.0.1:3000", "http://127.0.0.1:5000", "http://192.168.1.4:3000", "https://zelyanoth-lin-cbfcff2.hf.space" ] if origin in allowed_origins: response.headers.add('Access-Control-Allow-Origin', origin) response.headers.add('Access-Control-Allow-Credentials', 'true') response.headers.add('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With') return response # Setup secure cookies app = setup_secure_cookies(app) # Initialize JWT with cookie support jwt = configure_jwt_with_cookies(app) # Initialize Supabase client app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY']) # Initialize a simple in-memory job store for tracking async tasks # In production, you'd use a database or Redis for this app.job_store = {} # Initialize a ThreadPoolExecutor for running background tasks # In production, you'd use a proper task scheduler like APScheduler app.executor = ThreadPoolExecutor(max_workers=4) # Initialize APScheduler if app.config.get('SCHEDULER_ENABLED', True): try: from backend.scheduler.apscheduler_service import APSchedulerService scheduler = APSchedulerService(app) app.scheduler = scheduler app.logger.info("APScheduler initialized successfully") # Verify APScheduler initialization if hasattr(app, 'scheduler') and app.scheduler.scheduler is not None: app.logger.info("✅ APScheduler initialized successfully") app.logger.info(f"📊 Current jobs: {len(app.scheduler.scheduler.get_jobs())}") app.logger.info("🔄 Schedule loading job added (runs every 5 minutes)") else: app.logger.warning("⚠️ APScheduler initialization failed") except Exception as e: app.logger.error(f"Failed to initialize APScheduler: {str(e)}") import traceback app.logger.error(traceback.format_exc()) # Register blueprints from backend.api.auth import auth_bp from backend.api.sources import sources_bp from backend.api.accounts import accounts_bp from backend.api.posts import posts_bp from backend.api.schedules import schedules_bp app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(sources_bp, url_prefix='/api/sources') app.register_blueprint(accounts_bp, url_prefix='/api/accounts') app.register_blueprint(posts_bp, url_prefix='/api/posts') app.register_blueprint(schedules_bp, url_prefix='/api/schedules') # Serve frontend static files @app.route('/', defaults={'path': ''}) @app.route('/') def serve_frontend(path): # If the path is a file (has a dot), try to serve it from dist if path != "" and os.path.exists(os.path.join(app.static_folder, path)): return send_from_directory(app.static_folder, path) # For API routes, return 404 elif path.startswith('api/'): return {'error': 'Not found'}, 404 # Otherwise, serve index.html for SPA routing else: return send_from_directory(app.static_folder, 'index.html') # Health check endpoint @app.route('/health') def health_check(): return {'status': 'healthy', 'message': 'Lin backend is running'}, 200 # Add database connection check endpoint @app.route('/api/health') def api_health_check(): """Enhanced health check that includes database connection.""" try: from backend.utils.database import check_database_connection db_connected = check_database_connection(app.supabase) return { 'status': 'healthy' if db_connected else 'degraded', 'database': 'connected' if db_connected else 'disconnected', 'message': 'Lin backend is running' if db_connected else 'Database connection issues' }, 200 if db_connected else 503 except Exception as e: return { 'status': 'unhealthy', 'database': 'error', 'message': f'Health check failed: {str(e)}' }, 503 # Add OAuth callback handler route @app.route('/auth/callback') def handle_auth_callback(): """Handle OAuth callback from social networks.""" try: # Parse URL parameters from urllib.parse import parse_qs, urlparse url = request.url parsed_url = urlparse(url) query_params = parse_qs(parsed_url.query) code = query_params.get('code', [None])[0] state = query_params.get('state', [None])[0] error = query_params.get('error', [None])[0] app.logger.info(f"🔗 [OAuth] Direct callback handler triggered") app.logger.info(f"🔗 [OAuth] URL: {url}") app.logger.info(f"🔗 [OAuth] Code: {code[:20] + '...' if code else None}") app.logger.info(f"🔗 [OAuth] State: {state}") app.logger.info(f"🔗 [OAuth] Error: {error}") if error: app.logger.error(f"🔗 [OAuth] OAuth error: {error}") # Redirect to frontend with error parameter from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error={error}&from=linkedin" return redirect(redirect_url) if not code or not state: app.logger.error(f"🔗 [OAuth] Missing required parameters") # Redirect to frontend with error from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=missing_params&from=linkedin" return redirect(redirect_url) # Get the JWT token from cookies token = request.cookies.get('access_token') if not token: app.logger.error(f"🔗 [OAuth] No token found in cookies") # Redirect to frontend with error from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=no_token&from=linkedin" return redirect(redirect_url) # Verify JWT and get user identity try: from flask_jwt_extended import decode_token user_data = decode_token(token) user_id = user_data['sub'] app.logger.info(f"🔗 [OAuth] Processing OAuth for user: {user_id}") except Exception as jwt_error: app.logger.error(f"🔗 [OAuth] JWT verification failed: {str(jwt_error)}") from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=jwt_failed&from=linkedin" return redirect(redirect_url) # Process the OAuth flow directly from backend.services.linkedin_service import LinkedInService linkedin_service = LinkedInService() # Exchange code for access token app.logger.info("🔗 [OAuth] Exchanging code for access token...") try: token_response = linkedin_service.get_access_token(code) access_token = token_response['access_token'] app.logger.info(f"🔗 [OAuth] Token exchange successful. Token length: {len(access_token)}") except Exception as token_error: app.logger.error(f"🔗 [OAuth] Token exchange failed: {str(token_error)}") from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=token_exchange_failed&from=linkedin" return redirect(redirect_url) # Get user info app.logger.info("🔗 [OAuth] Fetching user info...") try: user_info = linkedin_service.get_user_info(access_token) app.logger.info(f"🔗 [OAuth] User info fetched: {user_info}") except Exception as user_info_error: app.logger.error(f"🔗 [OAuth] User info fetch failed: {str(user_info_error)}") from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=user_info_failed&from=linkedin" return redirect(redirect_url) # Prepare account data for insertion account_data = { "social_network": "LinkedIn", "account_name": user_info.get('name', 'LinkedIn Account'), "id_utilisateur": user_id, "token": access_token, "sub": user_info.get('sub'), "given_name": user_info.get('given_name'), "family_name": user_info.get('family_name'), "picture": user_info.get('picture') } app.logger.info(f"🔗 [OAuth] Prepared account data: {account_data}") # Store account info in Supabase app.logger.info("🔗 [OAuth] Inserting account into database...") try: response = ( app.supabase .table("Social_network") .insert(account_data) .execute() ) # DEBUG: Log database response app.logger.info(f"🔗 [OAuth] Database response: {response}") app.logger.info(f"🔗 [OAuth] Response data: {response.data}") app.logger.info(f"🔗 [OAuth] Response error: {getattr(response, 'error', None)}") if response.data: app.logger.info(f"🔗 [OAuth] Account linked successfully for user: {user_id}") # Redirect to frontend with success from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?oauth_success=true&account_linked=true&from=linkedin" return redirect(redirect_url) else: app.logger.error(f"🔗 [OAuth] No data returned from database insertion for user: {user_id}") from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=database_insert_failed&from=linkedin" return redirect(redirect_url) except Exception as db_error: app.logger.error(f"🔗 [OAuth] Database insertion failed: {str(db_error)}") app.logger.error(f"🔗 [OAuth] Database error type: {type(db_error)}") from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=database_error&from=linkedin" return redirect(redirect_url) except Exception as e: app.logger.error(f"🔗 [OAuth] Callback handler error: {str(e)}") import traceback app.logger.error(f"🔗 [OAuth] Traceback: {traceback.format_exc()}") # Redirect to frontend with error from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error=server_error&from=linkedin" return redirect(redirect_url) return app if __name__ == '__main__': app = create_app() app.run( host='0.0.0.0', port=int(os.environ.get('PORT', 5000)), debug=app.config['DEBUG'] )