import os import sys import locale import logging from logging.handlers import RotatingFileHandler from flask_limiter import Limiter from flask_limiter.util import get_remote_address # Add the project root to the Python path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 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 with rotating file handler and proper formatting def setup_logging(): """Setup comprehensive logging configuration.""" # Create logs directory if it doesn't exist log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs') os.makedirs(log_dir, exist_ok=True) # Configure root logger logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s' ) # Create a rotating file handler file_handler = RotatingFileHandler( os.path.join(log_dir, 'app.log'), maxBytes=1024 * 1024 * 10, # 10 MB backupCount=5 ) file_handler.setLevel(logging.INFO) file_handler.setFormatter(logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s' )) # Add the file handler to the root logger logging.getLogger().addHandler(file_handler) # Set specific loggers to WARNING to reduce noise logging.getLogger('apscheduler').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) logging.getLogger('werkzeug').setLevel(logging.WARNING) setup_logging() # Use relative import for the Config class to work with Hugging Face Spaces try: from backend.config import Config except ValueError as e: print(f"Configuration error: {e}") sys.exit(1) 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) # Initialize rate limiting limiter = Limiter( get_remote_address, app=app, default_limits=["200 per day", "50 per hour"] ) # Disable strict slashes to prevent redirects app.url_map.strict_slashes = False # Initialize CORS with specific configuration - only for API routes 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 additional CORS headers for non-API routes specifically needed for OAuth callbacks @app.after_request def add_additional_cors_headers(response): """Add additional CORS headers where needed.""" # 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" ] # Add CORS headers specifically for OAuth callback routes if request.endpoint == 'handle_auth_callback' or request.path == '/auth/callback': if origin in allowed_origins: response.headers.set('Access-Control-Allow-Origin', origin) response.headers.set('Access-Control-Allow-Credentials', 'true') response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') response.headers.set('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 ContentService try: from backend.services.content_service import ContentService app.content_service = ContentService(hugging_key=app.config.get('HUGGING_KEY')) app.logger.info("ContentService initialized successfully") except Exception as e: app.logger.error(f"Failed to initialize ContentService: {str(e)}") import traceback app.logger.error(traceback.format_exc()) # 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') # Add rate limiting to specific authentication routes after blueprint registration limiter.limit("5 per minute")(app.view_functions['auth.register']) limiter.limit("5 per minute")(app.view_functions['auth.login']) limiter.limit("10 per minute")(app.view_functions['auth.forgot_password']) # 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 helper functions for OAuth callback to reduce duplication def create_oauth_redirect(error_code, from_source='linkedin'): """Helper function to create OAuth redirect with error.""" from flask import redirect redirect_url = f"{request.host_url.rstrip('/')}?error={error_code}&from={from_source}" return redirect(redirect_url) def handle_oauth_error(message, error_code, from_source='linkedin'): """Helper function to handle OAuth errors.""" app.logger.error(f"🔗 [OAuth] {message}") return create_oauth_redirect(error_code, from_source) def fetch_linkedin_data(code): """Helper function to fetch LinkedIn user data from code.""" from backend.services.linkedin_service import LinkedInService linkedin_service = LinkedInService() # Exchange code for access token app.logger.info("🔗 [OAuth] Exchanging code for access token...") 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)}") # Get user info app.logger.info("🔗 [OAuth] Fetching user info...") user_info = linkedin_service.get_user_info(access_token) app.logger.info(f"🔗 [OAuth] User info fetched: {user_info}") return access_token, user_info # Add OAuth callback handler route @app.route('/auth/callback') @limiter.limit("10 per minute") 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: return handle_oauth_error(f"OAuth error: {error}", error, 'linkedin') if not code or not state: return handle_oauth_error("Missing required parameters", 'missing_params', 'linkedin') # Get the JWT token from cookies token = request.cookies.get('access_token') if not token: return handle_oauth_error("No token found in cookies", 'no_token', 'linkedin') # 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: return handle_oauth_error(f"JWT verification failed: {str(jwt_error)}", 'jwt_failed', 'linkedin') # Process the OAuth flow directly try: access_token, user_info = fetch_linkedin_data(code) except Exception as token_error: app.logger.error(f"🔗 [OAuth] Token exchange failed: {str(token_error)}") return create_oauth_redirect('token_exchange_failed', 'linkedin') # 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}") return create_oauth_redirect('database_insert_failed', 'linkedin') 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)}") return create_oauth_redirect('database_error', 'linkedin') 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 return create_oauth_redirect('server_error', 'linkedin') 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'] )