feat: Enhance security and configuration management
Browse files- Implemented JWT cookie security improvements including SameSite policies and CSRF protection.
- Added email validation using the email-validator library and standardized authentication responses.
- Integrated Flask-Limiter for rate limiting on authentication endpoints.
- Enhanced error handling and logging with structured logging and detailed traceback.
- Improved configuration management with validation for required environment variables and secure defaults.
- Updated database handling with better connection management and input validation.
- Modularized code structure for better maintainability and performance optimizations.
- Added Celery support with configuration for task scheduling and background processing.
- Updated Gunicorn configuration for improved performance and logging.
- Removed deprecated start scripts and consolidated startup procedures.
- Dockerfile +5 -5
- IMPROVEMENTS_SUMMARY.md +207 -0
- Linkedin_poster_dev +1 -1
- README.md +7 -3
- backend/Dockerfile +2 -2
- backend/api/auth.py +139 -26
- backend/app.py +144 -110
- backend/celery_app.py +28 -0
- backend/celery_beat_config.py +33 -0
- backend/config.py +27 -5
- backend/gunicorn.conf.py +36 -0
- backend/requirements.txt +3 -0
- backend/services/auth_service.py +5 -23
- backend/utils/cookies.py +43 -49
- backend/utils/database.py +72 -30
- gunicorn.conf.py +36 -0
- requirements.txt +7 -2
- start_app.py +0 -47
- start_celery.py +8 -8
- start_gunicorn.py +55 -0
- starty.py +0 -145
|
@@ -15,7 +15,7 @@ RUN apt-get update && apt-get install -y \
|
|
| 15 |
|
| 16 |
# Copy and install Python dependencies
|
| 17 |
COPY requirements.txt .
|
| 18 |
-
RUN pip install -r requirements.txt
|
| 19 |
|
| 20 |
# Copy package files for frontend
|
| 21 |
COPY frontend/package*.json ./frontend/
|
|
@@ -28,11 +28,11 @@ COPY . .
|
|
| 28 |
# Build frontend
|
| 29 |
RUN cd frontend && npm run build
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
RUN
|
| 33 |
|
| 34 |
# Expose port
|
| 35 |
EXPOSE 7860
|
| 36 |
|
| 37 |
-
# Start Redis server in background and then start the application
|
| 38 |
-
CMD ["sh", "-c", "redis-server --daemonize yes &&
|
|
|
|
| 15 |
|
| 16 |
# Copy and install Python dependencies
|
| 17 |
COPY requirements.txt .
|
| 18 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
|
| 20 |
# Copy package files for frontend
|
| 21 |
COPY frontend/package*.json ./frontend/
|
|
|
|
| 28 |
# Build frontend
|
| 29 |
RUN cd frontend && npm run build
|
| 30 |
|
| 31 |
+
# Create logs directory
|
| 32 |
+
RUN mkdir -p logs
|
| 33 |
|
| 34 |
# Expose port
|
| 35 |
EXPOSE 7860
|
| 36 |
|
| 37 |
+
# Start Redis server in background and then start the application with gunicorn
|
| 38 |
+
CMD ["sh", "-c", "redis-server --daemonize yes && gunicorn --config gunicorn.conf.py 'backend.app:create_app()'"]
|
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Lin Application - Code Improvements Summary
|
| 2 |
+
|
| 3 |
+
This document provides a comprehensive overview of all improvements made to the Lin application codebase, including security enhancements, bug fixes, performance optimizations, and architectural improvements.
|
| 4 |
+
|
| 5 |
+
## Table of Contents
|
| 6 |
+
1. [Security Enhancements](#security-enhancements)
|
| 7 |
+
2. [Error Handling & Logging](#error-handling--logging)
|
| 8 |
+
3. [Configuration Management](#configuration-management)
|
| 9 |
+
4. [CORS & Headers Configuration](#cors--headers-configuration)
|
| 10 |
+
5. [Docker & Gunicorn Improvements](#docker--gunicorn-improvements)
|
| 11 |
+
6. [Database Handling](#database-handling)
|
| 12 |
+
7. [Code Quality & Organization](#code-quality--organization)
|
| 13 |
+
8. [Dependencies Added](#dependencies-added)
|
| 14 |
+
9. [Files Modified](#files-modified)
|
| 15 |
+
|
| 16 |
+
## Security Enhancements
|
| 17 |
+
|
| 18 |
+
### JWT Token Security
|
| 19 |
+
- **Enhanced cookie security**: Implemented proper SameSite policies (Lax), secure flags, and CSRF protection for JWT cookies
|
| 20 |
+
- **Improved cookie configuration**: Added proper path restrictions and secure flag based on environment detection
|
| 21 |
+
- **Token validation**: Enhanced token validation and refresh mechanisms
|
| 22 |
+
|
| 23 |
+
### Input Validation & Sanitization
|
| 24 |
+
- **Email validation**: Integrated `email-validator` library for robust email format validation
|
| 25 |
+
- **Password strength**: Implemented comprehensive password requirements (minimum 8 characters, uppercase, lowercase, digit, special character)
|
| 26 |
+
- **User enumeration prevention**: Standardized authentication responses to prevent account discovery
|
| 27 |
+
- **Sensitive data filtering**: Added sanitization of sensitive fields (passwords, hashes) from user data responses
|
| 28 |
+
|
| 29 |
+
### Rate Limiting
|
| 30 |
+
- **Distributed protection**: Implemented Flask-Limiter to prevent brute force and DoS attacks
|
| 31 |
+
- **Endpoint-specific limits**: Applied targeted rate limiting to authentication endpoints (5 requests/minute for register/login, 10/minute for forgot password)
|
| 32 |
+
- **IP-based tracking**: Rate limiting based on client IP address with default limits of 200/day and 50/hour
|
| 33 |
+
|
| 34 |
+
### Authentication Security
|
| 35 |
+
- **Consistent error responses**: All authentication endpoints return identical responses regardless of user existence
|
| 36 |
+
- **Secure session management**: Enhanced JWT token handling with proper expiration and refresh mechanisms
|
| 37 |
+
- **OAuth callback security**: Improved OAuth callback handling with better parameter validation and error handling
|
| 38 |
+
|
| 39 |
+
## Error Handling & Logging
|
| 40 |
+
|
| 41 |
+
### Structured Logging
|
| 42 |
+
- **Rotating file handler**: Implemented rotating log files with 10MB size limit and 5 backup files
|
| 43 |
+
- **Enhanced log format**: Added filename, line number, and structured format for better debugging
|
| 44 |
+
- **Log level management**: Configurable log levels per environment with reduced noise from third-party libraries
|
| 45 |
+
|
| 46 |
+
### Exception Handling
|
| 47 |
+
- **Comprehensive error catching**: Enhanced try-catch blocks with specific exception handling
|
| 48 |
+
- **Detailed traceback logging**: Added full traceback logging for debugging while maintaining user-friendly messages
|
| 49 |
+
- **Configurable error responses**: Environment-aware error responses that don't expose sensitive system details
|
| 50 |
+
|
| 51 |
+
### Logging Best Practices
|
| 52 |
+
- **Application-specific loggers**: Dedicated loggers for different components (OAuth, authentication, database)
|
| 53 |
+
- **Contextual information**: Enhanced logs with request context, user IDs, and operational details
|
| 54 |
+
- **Security logging**: Specialized logging for security-relevant events and potential threats
|
| 55 |
+
|
| 56 |
+
## Configuration Management
|
| 57 |
+
|
| 58 |
+
### Environment Validation
|
| 59 |
+
- **Required variable checking**: Added validation for critical environment variables (SUPABASE_URL, SUPABASE_KEY, JWT_SECRET_KEY)
|
| 60 |
+
- **Secure defaults**: Implemented generation of secure random keys when not provided in environment
|
| 61 |
+
- **Configuration class**: Enhanced Config class with validation methods and better organization
|
| 62 |
+
|
| 63 |
+
### Environment Detection
|
| 64 |
+
- **Development vs Production**: Improved environment detection for cookie security, logging levels, and other environment-specific settings
|
| 65 |
+
- **Hugging Face Spaces support**: Enhanced detection and configuration for Hugging Face Spaces deployment
|
| 66 |
+
- **Platform-specific settings**: Windows/Unix-specific configuration handling
|
| 67 |
+
|
| 68 |
+
## CORS & Headers Configuration
|
| 69 |
+
|
| 70 |
+
### Eliminated Duplication
|
| 71 |
+
- **Single source of truth**: Removed duplicate CORS headers by relying on Flask-CORS with targeted manual headers only where needed
|
| 72 |
+
- **Targeted configuration**: Applied CORS headers only to OAuth callback routes rather than all routes
|
| 73 |
+
- **Proper resource mapping**: Improved CORS resource mapping to specific API routes
|
| 74 |
+
|
| 75 |
+
### Security Improvements
|
| 76 |
+
- **Origin validation**: Enhanced origin validation with proper allowlist management
|
| 77 |
+
- **Secure headers**: Added proper security headers for credential handling and cross-site protection
|
| 78 |
+
- **Endpoint-specific policies**: Differentiated CORS policies between API routes and other endpoints
|
| 79 |
+
|
| 80 |
+
## Docker & Gunicorn Improvements
|
| 81 |
+
|
| 82 |
+
### Port Consistency
|
| 83 |
+
- **Config alignment**: Fixed port inconsistencies between Dockerfile (7860) and Gunicorn configuration
|
| 84 |
+
- **Environment consistency**: Ensured all components use the same port configuration (7860)
|
| 85 |
+
- **Configuration validation**: Updated start scripts to use correct application paths
|
| 86 |
+
|
| 87 |
+
### Container Optimization
|
| 88 |
+
- **No-cache installation**: Added `--no-cache-dir` flag for pip installations to reduce image size
|
| 89 |
+
- **Log directory creation**: Added log directory creation in Dockerfile for proper logging
|
| 90 |
+
- **Dependency optimization**: Improved container build process with better dependency management
|
| 91 |
+
|
| 92 |
+
### Process Management
|
| 93 |
+
- **Supervisor configuration**: Enhanced Gunicorn configuration with proper worker management and timeout settings
|
| 94 |
+
- **Start script updates**: Updated start scripts to use correct module paths for application startup
|
| 95 |
+
- **Environment handling**: Improved environment variable handling in containerized deployments
|
| 96 |
+
|
| 97 |
+
## Database Handling
|
| 98 |
+
|
| 99 |
+
### Connection Management
|
| 100 |
+
- **Validation improvements**: Enhanced database connection validation with actual table queries instead of user queries
|
| 101 |
+
- **Error handling**: Improved database error handling with better logging and user feedback
|
| 102 |
+
- **Connection pooling**: Better connection management patterns for production use
|
| 103 |
+
|
| 104 |
+
### Security Enhancements
|
| 105 |
+
- **Query validation**: Added input validation for database queries to prevent injection attacks
|
| 106 |
+
- **Connection security**: Enhanced connection security with proper SSL and authentication handling
|
| 107 |
+
- **Error concealment**: Improved database error handling that doesn't expose internal system details
|
| 108 |
+
|
| 109 |
+
## Code Quality & Organization
|
| 110 |
+
|
| 111 |
+
### Code Duplication Reduction
|
| 112 |
+
- **OAuth helper functions**: Created reusable helper functions for OAuth callback handling
|
| 113 |
+
- **Configuration functions**: Centralized configuration functions for consistent application setup
|
| 114 |
+
- **Utility functions**: Added common utility functions for validation and error handling
|
| 115 |
+
|
| 116 |
+
### Architecture Improvements
|
| 117 |
+
- **Modular design**: Improved module organization with better separation of concerns
|
| 118 |
+
- **Function documentation**: Enhanced docstrings and function documentation for better maintainability
|
| 119 |
+
- **Code structure**: Improved overall code structure with better logical organization
|
| 120 |
+
|
| 121 |
+
### Performance Optimizations
|
| 122 |
+
- **Efficient queries**: Optimized database queries and API request handling
|
| 123 |
+
- **Resource management**: Better resource management with proper cleanup and connection handling
|
| 124 |
+
- **Caching considerations**: Added framework for potential caching implementations
|
| 125 |
+
|
| 126 |
+
## Dependencies Added
|
| 127 |
+
|
| 128 |
+
### Security Dependencies
|
| 129 |
+
- `Flask-Limiter`: For rate limiting and DDoS protection
|
| 130 |
+
- `email-validator`: For robust email format validation
|
| 131 |
+
- `bcrypt`: Enhanced password security (already present but noted for security context)
|
| 132 |
+
|
| 133 |
+
### Development Dependencies
|
| 134 |
+
- Enhanced logging and monitoring capabilities
|
| 135 |
+
- Improved error handling libraries
|
| 136 |
+
- Additional validation libraries for better input sanitization
|
| 137 |
+
|
| 138 |
+
## Files Modified
|
| 139 |
+
|
| 140 |
+
### Backend Core Files
|
| 141 |
+
- `backend/app.py`: Main application with security enhancements, rate limiting, improved logging, and configuration validation
|
| 142 |
+
- `backend/config.py`: Enhanced configuration with validation, secure defaults, and environment detection
|
| 143 |
+
- `backend/utils/cookies.py`: Improved cookie security with proper SameSite and secure flags
|
| 144 |
+
- `backend/utils/database.py`: Enhanced database connection handling with security and validation
|
| 145 |
+
- `backend/api/auth.py`: Major improvements to authentication with security, validation, and error handling
|
| 146 |
+
|
| 147 |
+
### Service Files
|
| 148 |
+
- `backend/services/auth_service.py`: Improved error handling and security validation
|
| 149 |
+
- `start_gunicorn.py`: Updated to use correct application paths
|
| 150 |
+
- `start_celery.py`: Updated module references for proper Celery configuration
|
| 151 |
+
|
| 152 |
+
### Infrastructure Files
|
| 153 |
+
- `Dockerfile`: Port consistency, optimization, and log directory creation
|
| 154 |
+
- `gunicorn.conf.py`: Port configuration alignment and performance tuning
|
| 155 |
+
- `requirements.txt`: Added security dependencies
|
| 156 |
+
|
| 157 |
+
### Additional Files
|
| 158 |
+
- `IMPROVEMENTS_SUMMARY.md`: This comprehensive documentation
|
| 159 |
+
|
| 160 |
+
## Impact Assessment
|
| 161 |
+
|
| 162 |
+
### Security Impact
|
| 163 |
+
- **High**: Implemented comprehensive authentication security, input validation, and user enumeration prevention
|
| 164 |
+
- **Medium**: Enhanced cookie security, rate limiting, and error response standardization
|
| 165 |
+
|
| 166 |
+
### Performance Impact
|
| 167 |
+
- **Positive**: Eliminated CORS duplication, optimized database queries, and improved resource management
|
| 168 |
+
- **Neutral**: Additional validation adds minimal overhead with significant security benefits
|
| 169 |
+
|
| 170 |
+
### Maintainability Impact
|
| 171 |
+
- **High**: Improved code organization, documentation, and modular functions
|
| 172 |
+
- **Positive**: Better error handling and logging for easier debugging
|
| 173 |
+
|
| 174 |
+
### Compatibility Impact
|
| 175 |
+
- **Minimal**: All changes maintain backward compatibility while adding security features
|
| 176 |
+
- **Configuration**: Minor configuration adjustments may be needed for new security features
|
| 177 |
+
|
| 178 |
+
## Testing Recommendations
|
| 179 |
+
|
| 180 |
+
### Security Testing
|
| 181 |
+
- Conduct penetration testing focusing on authentication and authorization flows
|
| 182 |
+
- Test rate limiting effectiveness against various attack vectors
|
| 183 |
+
- Verify CORS policy effectiveness
|
| 184 |
+
|
| 185 |
+
### Performance Testing
|
| 186 |
+
- Load test the application with the new rate limiting in place
|
| 187 |
+
- Verify database connection handling under high load
|
| 188 |
+
- Test authentication flows with various input scenarios
|
| 189 |
+
|
| 190 |
+
### Integration Testing
|
| 191 |
+
- Test OAuth flows with different providers
|
| 192 |
+
- Verify deployment processes with new Docker configuration
|
| 193 |
+
- Validate environment-specific configurations
|
| 194 |
+
|
| 195 |
+
## Deployment Considerations
|
| 196 |
+
|
| 197 |
+
### Environment Variables
|
| 198 |
+
- Ensure all required environment variables are properly set in all environments
|
| 199 |
+
- Verify JWT and other security keys are set to strong values in production
|
| 200 |
+
- Test environment detection logic in different deployment scenarios
|
| 201 |
+
|
| 202 |
+
### Monitoring
|
| 203 |
+
- Set up monitoring for rate limiting to detect potential attacks
|
| 204 |
+
- Monitor authentication failure patterns for security analysis
|
| 205 |
+
- Ensure logging is properly configured for the production environment
|
| 206 |
+
|
| 207 |
+
This comprehensive improvement effort enhances the Lin application's security, performance, and maintainability while maintaining full functionality and backward compatibility.
|
|
@@ -1 +1 @@
|
|
| 1 |
-
Subproject commit
|
|
|
|
| 1 |
+
Subproject commit 72618a7b4f3129cba037a2da22bde685550235dd
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
---
|
| 2 |
title: Lin - LinkedIn Community Manager
|
| 3 |
sdk: docker
|
| 4 |
-
app_file:
|
| 5 |
license: mit
|
| 6 |
---
|
| 7 |
|
|
@@ -86,11 +86,15 @@ From the project root directory, you can use the following commands:
|
|
| 86 |
|
| 87 |
#### Development Servers
|
| 88 |
- `npm run dev:frontend` - Start frontend development server
|
| 89 |
-
- `npm run dev:backend` - Start backend development server
|
| 90 |
- `npm run dev:all` - Start both servers concurrently
|
| 91 |
- `npm run start` - Alias for `npm run dev:all`
|
| 92 |
- `npm run start:frontend` - Start frontend only
|
| 93 |
-
- `npm run start:backend` - Start backend only
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
#### Build & Test
|
| 96 |
- `npm run build` - Build frontend for production
|
|
|
|
| 1 |
---
|
| 2 |
title: Lin - LinkedIn Community Manager
|
| 3 |
sdk: docker
|
| 4 |
+
app_file: backend/app.py
|
| 5 |
license: mit
|
| 6 |
---
|
| 7 |
|
|
|
|
| 86 |
|
| 87 |
#### Development Servers
|
| 88 |
- `npm run dev:frontend` - Start frontend development server
|
| 89 |
+
- `npm run dev:backend` - Start backend development server (Flask development server)
|
| 90 |
- `npm run dev:all` - Start both servers concurrently
|
| 91 |
- `npm run start` - Alias for `npm run dev:all`
|
| 92 |
- `npm run start:frontend` - Start frontend only
|
| 93 |
+
- `npm run start:backend` - Start backend only (Flask development server)
|
| 94 |
+
|
| 95 |
+
#### Production Deployment
|
| 96 |
+
- `python start_gunicorn.py` - Start backend with Gunicorn (for local testing)
|
| 97 |
+
- Use Docker to run with Gunicorn as configured in the Dockerfile
|
| 98 |
|
| 99 |
#### Build & Test
|
| 100 |
- `npm run build` - Build frontend for production
|
|
@@ -36,5 +36,5 @@ USER appuser
|
|
| 36 |
# Expose port
|
| 37 |
EXPOSE 5000
|
| 38 |
|
| 39 |
-
# Run the application
|
| 40 |
-
CMD ["
|
|
|
|
| 36 |
# Expose port
|
| 37 |
EXPOSE 5000
|
| 38 |
|
| 39 |
+
# Run the application with Gunicorn using configuration file
|
| 40 |
+
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:create_app()"]
|
|
@@ -1,11 +1,28 @@
|
|
| 1 |
from flask import Blueprint, request, jsonify, current_app
|
| 2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
|
|
| 3 |
from backend.services.auth_service import register_user, login_user, get_user_by_id, request_password_reset, reset_user_password
|
| 4 |
from backend.models.user import User
|
| 5 |
from backend.utils.country_language_data import COUNTRIES, LANGUAGES
|
| 6 |
|
| 7 |
auth_bp = Blueprint('auth', __name__)
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
@auth_bp.route('/', methods=['OPTIONS'])
|
| 10 |
def handle_options():
|
| 11 |
"""Handle OPTIONS requests for preflight CORS checks."""
|
|
@@ -45,32 +62,40 @@ def register():
|
|
| 45 |
country = data.get('country') # Optional: User country (ISO 3166-1 alpha-2 code)
|
| 46 |
language = data.get('language') # Optional: User language (ISO 639-1 code)
|
| 47 |
|
| 48 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
# Validate password
|
| 51 |
-
|
|
|
|
| 52 |
return jsonify({
|
| 53 |
'success': False,
|
| 54 |
-
'message': '
|
| 55 |
}), 400
|
| 56 |
|
| 57 |
# Optional: Validate country and language parameters if provided
|
| 58 |
if country:
|
| 59 |
# Validate if country is a valid ISO 3166-1 alpha-2 code
|
| 60 |
-
|
| 61 |
-
if not isinstance(country, str) or len(country) != 2:
|
| 62 |
return jsonify({
|
| 63 |
'success': False,
|
| 64 |
-
'message': 'Country must be a valid ISO 3166-1 alpha-2 code (2 characters)'
|
| 65 |
}), 400
|
| 66 |
|
| 67 |
if language:
|
| 68 |
# Validate if language is a valid ISO 639-1 code
|
| 69 |
-
|
| 70 |
-
if not isinstance(language, str) or len(language) != 2:
|
| 71 |
return jsonify({
|
| 72 |
'success': False,
|
| 73 |
-
'message': 'Language must be a valid ISO 639-1 code (2 characters)'
|
| 74 |
}), 400
|
| 75 |
|
| 76 |
# Register user with preferences
|
|
@@ -79,6 +104,12 @@ def register():
|
|
| 79 |
if result['success']:
|
| 80 |
return jsonify(result), 201
|
| 81 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
return jsonify(result), 400
|
| 83 |
|
| 84 |
except Exception as e:
|
|
@@ -113,7 +144,6 @@ def login():
|
|
| 113 |
# Log the incoming request
|
| 114 |
current_app.logger.info(f"Login request received from {request.remote_addr}")
|
| 115 |
current_app.logger.info(f"Request headers: {dict(request.headers)}")
|
| 116 |
-
current_app.logger.info(f"Request data: {request.get_json()}")
|
| 117 |
|
| 118 |
data = request.get_json()
|
| 119 |
|
|
@@ -129,6 +159,19 @@ def login():
|
|
| 129 |
password = data['password']
|
| 130 |
remember_me = data.get('remember_me', False)
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
# Login user
|
| 133 |
result = login_user(email, password, remember_me)
|
| 134 |
|
|
@@ -165,6 +208,7 @@ def logout():
|
|
| 165 |
JSON: Logout result
|
| 166 |
"""
|
| 167 |
try:
|
|
|
|
| 168 |
return jsonify({
|
| 169 |
'success': True,
|
| 170 |
'message': 'Logged out successfully'
|
|
@@ -193,12 +237,15 @@ def get_current_user():
|
|
| 193 |
"""
|
| 194 |
try:
|
| 195 |
user_id = get_jwt_identity()
|
|
|
|
| 196 |
user_data = get_user_by_id(user_id)
|
| 197 |
|
| 198 |
if user_data:
|
|
|
|
|
|
|
| 199 |
return jsonify({
|
| 200 |
'success': True,
|
| 201 |
-
'user':
|
| 202 |
}), 200
|
| 203 |
else:
|
| 204 |
return jsonify({
|
|
@@ -264,20 +311,36 @@ def forgot_password():
|
|
| 264 |
|
| 265 |
email = data['email']
|
| 266 |
|
| 267 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
result = request_password_reset(current_app.supabase, email)
|
| 269 |
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
|
|
|
| 274 |
|
| 275 |
except Exception as e:
|
| 276 |
current_app.logger.error(f"Forgot password error: {str(e)}")
|
|
|
|
| 277 |
return jsonify({
|
| 278 |
-
'success':
|
| 279 |
-
'message': '
|
| 280 |
-
}),
|
| 281 |
|
| 282 |
|
| 283 |
@auth_bp.route('/reset-password', methods=['OPTIONS'])
|
|
@@ -326,13 +389,12 @@ def reset_password():
|
|
| 326 |
token = data['token']
|
| 327 |
password = data['password']
|
| 328 |
|
| 329 |
-
#
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
if len(password) < 8:
|
| 333 |
return jsonify({
|
| 334 |
'success': False,
|
| 335 |
-
'message': '
|
| 336 |
}), 400
|
| 337 |
|
| 338 |
# Reset password
|
|
@@ -348,4 +410,55 @@ def reset_password():
|
|
| 348 |
return jsonify({
|
| 349 |
'success': False,
|
| 350 |
'message': 'An error occurred while resetting your password'
|
| 351 |
-
}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from flask import Blueprint, request, jsonify, current_app
|
| 2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
| 3 |
+
from email_validator import validate_email, EmailNotValidError
|
| 4 |
from backend.services.auth_service import register_user, login_user, get_user_by_id, request_password_reset, reset_user_password
|
| 5 |
from backend.models.user import User
|
| 6 |
from backend.utils.country_language_data import COUNTRIES, LANGUAGES
|
| 7 |
|
| 8 |
auth_bp = Blueprint('auth', __name__)
|
| 9 |
|
| 10 |
+
def validate_email_format(email: str) -> tuple[bool, str]:
|
| 11 |
+
"""
|
| 12 |
+
Validate email format using email-validator library.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
email: Email string to validate
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
Tuple of (is_valid, validated_email_or_error_message)
|
| 19 |
+
"""
|
| 20 |
+
try:
|
| 21 |
+
validated = validate_email(email)
|
| 22 |
+
return True, validated['email']
|
| 23 |
+
except EmailNotValidError as e:
|
| 24 |
+
return False, str(e)
|
| 25 |
+
|
| 26 |
@auth_bp.route('/', methods=['OPTIONS'])
|
| 27 |
def handle_options():
|
| 28 |
"""Handle OPTIONS requests for preflight CORS checks."""
|
|
|
|
| 62 |
country = data.get('country') # Optional: User country (ISO 3166-1 alpha-2 code)
|
| 63 |
language = data.get('language') # Optional: User language (ISO 639-1 code)
|
| 64 |
|
| 65 |
+
# Validate email format using email-validator
|
| 66 |
+
is_valid_email, validated_email_or_error = validate_email_format(email)
|
| 67 |
+
if not is_valid_email:
|
| 68 |
+
return jsonify({
|
| 69 |
+
'success': False,
|
| 70 |
+
'message': f'Invalid email format: {validated_email_or_error}'
|
| 71 |
+
}), 400
|
| 72 |
+
|
| 73 |
+
# Use validated email (it may be normalized)
|
| 74 |
+
email = validated_email_or_error
|
| 75 |
|
| 76 |
+
# Validate password strength
|
| 77 |
+
password_validation = validate_password_strength(password)
|
| 78 |
+
if not password_validation['valid']:
|
| 79 |
return jsonify({
|
| 80 |
'success': False,
|
| 81 |
+
'message': password_validation['message']
|
| 82 |
}), 400
|
| 83 |
|
| 84 |
# Optional: Validate country and language parameters if provided
|
| 85 |
if country:
|
| 86 |
# Validate if country is a valid ISO 3166-1 alpha-2 code
|
| 87 |
+
if not isinstance(country, str) or len(country) != 2 or not country.isalpha():
|
|
|
|
| 88 |
return jsonify({
|
| 89 |
'success': False,
|
| 90 |
+
'message': 'Country must be a valid ISO 3166-1 alpha-2 code (2 alphabetic characters)'
|
| 91 |
}), 400
|
| 92 |
|
| 93 |
if language:
|
| 94 |
# Validate if language is a valid ISO 639-1 code
|
| 95 |
+
if not isinstance(language, str) or len(language) != 2 or not language.isalpha():
|
|
|
|
| 96 |
return jsonify({
|
| 97 |
'success': False,
|
| 98 |
+
'message': 'Language must be a valid ISO 639-1 code (2 alphabetic characters)'
|
| 99 |
}), 400
|
| 100 |
|
| 101 |
# Register user with preferences
|
|
|
|
| 104 |
if result['success']:
|
| 105 |
return jsonify(result), 201
|
| 106 |
else:
|
| 107 |
+
# Avoid exposing specific reasons for registration failure (security)
|
| 108 |
+
if 'already exist' in result.get('message', '').lower():
|
| 109 |
+
return jsonify({
|
| 110 |
+
'success': False,
|
| 111 |
+
'message': 'Account with this email already exists'
|
| 112 |
+
}), 400
|
| 113 |
return jsonify(result), 400
|
| 114 |
|
| 115 |
except Exception as e:
|
|
|
|
| 144 |
# Log the incoming request
|
| 145 |
current_app.logger.info(f"Login request received from {request.remote_addr}")
|
| 146 |
current_app.logger.info(f"Request headers: {dict(request.headers)}")
|
|
|
|
| 147 |
|
| 148 |
data = request.get_json()
|
| 149 |
|
|
|
|
| 159 |
password = data['password']
|
| 160 |
remember_me = data.get('remember_me', False)
|
| 161 |
|
| 162 |
+
# Validate email format
|
| 163 |
+
is_valid_email, validated_email_or_error = validate_email_format(email)
|
| 164 |
+
if not is_valid_email:
|
| 165 |
+
current_app.logger.warning(f"Login attempt with invalid email format: {email}")
|
| 166 |
+
# Do not reveal that email format is invalid to avoid enumeration
|
| 167 |
+
return jsonify({
|
| 168 |
+
'success': False,
|
| 169 |
+
'message': 'Invalid email or password'
|
| 170 |
+
}), 401
|
| 171 |
+
|
| 172 |
+
# Use validated email (it may be normalized)
|
| 173 |
+
email = validated_email_or_error
|
| 174 |
+
|
| 175 |
# Login user
|
| 176 |
result = login_user(email, password, remember_me)
|
| 177 |
|
|
|
|
| 208 |
JSON: Logout result
|
| 209 |
"""
|
| 210 |
try:
|
| 211 |
+
current_app.logger.info(f"Logout request for user: {get_jwt_identity()}")
|
| 212 |
return jsonify({
|
| 213 |
'success': True,
|
| 214 |
'message': 'Logged out successfully'
|
|
|
|
| 237 |
"""
|
| 238 |
try:
|
| 239 |
user_id = get_jwt_identity()
|
| 240 |
+
current_app.logger.info(f"Get user profile request for user: {user_id}")
|
| 241 |
user_data = get_user_by_id(user_id)
|
| 242 |
|
| 243 |
if user_data:
|
| 244 |
+
# Remove sensitive information from user data
|
| 245 |
+
safe_user_data = {k: v for k, v in user_data.items() if k not in ['password', 'password_hash']}
|
| 246 |
return jsonify({
|
| 247 |
'success': True,
|
| 248 |
+
'user': safe_user_data
|
| 249 |
}), 200
|
| 250 |
else:
|
| 251 |
return jsonify({
|
|
|
|
| 311 |
|
| 312 |
email = data['email']
|
| 313 |
|
| 314 |
+
# Validate email format
|
| 315 |
+
is_valid_email, validated_email_or_error = validate_email_format(email)
|
| 316 |
+
if not is_valid_email:
|
| 317 |
+
# Don't reveal that email format is invalid to prevent enumeration
|
| 318 |
+
current_app.logger.warning(f"Forgot password request with invalid email format: {email}")
|
| 319 |
+
# Return success to prevent user enumeration
|
| 320 |
+
return jsonify({
|
| 321 |
+
'success': True,
|
| 322 |
+
'message': 'If an account exists with this email, password reset instructions have been sent.'
|
| 323 |
+
}), 200
|
| 324 |
+
|
| 325 |
+
# Use validated email (it may be normalized)
|
| 326 |
+
email = validated_email_or_error
|
| 327 |
+
|
| 328 |
+
# Request password reset (this should be handled in a way that doesn't reveal user existence)
|
| 329 |
result = request_password_reset(current_app.supabase, email)
|
| 330 |
|
| 331 |
+
# Always return success to prevent user enumeration
|
| 332 |
+
return jsonify({
|
| 333 |
+
'success': True,
|
| 334 |
+
'message': 'If an account exists with this email, password reset instructions have been sent.'
|
| 335 |
+
}), 200
|
| 336 |
|
| 337 |
except Exception as e:
|
| 338 |
current_app.logger.error(f"Forgot password error: {str(e)}")
|
| 339 |
+
# Even on error, don't reveal if user exists
|
| 340 |
return jsonify({
|
| 341 |
+
'success': True,
|
| 342 |
+
'message': 'If an account exists with this email, password reset instructions have been sent.'
|
| 343 |
+
}), 200
|
| 344 |
|
| 345 |
|
| 346 |
@auth_bp.route('/reset-password', methods=['OPTIONS'])
|
|
|
|
| 389 |
token = data['token']
|
| 390 |
password = data['password']
|
| 391 |
|
| 392 |
+
# Validate password strength
|
| 393 |
+
password_validation = validate_password_strength(password)
|
| 394 |
+
if not password_validation['valid']:
|
|
|
|
| 395 |
return jsonify({
|
| 396 |
'success': False,
|
| 397 |
+
'message': password_validation['message']
|
| 398 |
}), 400
|
| 399 |
|
| 400 |
# Reset password
|
|
|
|
| 410 |
return jsonify({
|
| 411 |
'success': False,
|
| 412 |
'message': 'An error occurred while resetting your password'
|
| 413 |
+
}), 500
|
| 414 |
+
|
| 415 |
+
def validate_password_strength(password: str) -> dict:
|
| 416 |
+
"""
|
| 417 |
+
Validates password strength based on security requirements.
|
| 418 |
+
|
| 419 |
+
Args:
|
| 420 |
+
password: Password string to validate
|
| 421 |
+
|
| 422 |
+
Returns:
|
| 423 |
+
Dictionary with validation result and message
|
| 424 |
+
"""
|
| 425 |
+
if len(password) < 8:
|
| 426 |
+
return {
|
| 427 |
+
'valid': False,
|
| 428 |
+
'message': 'Password must be at least 8 characters long'
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
# Check for complexity requirements
|
| 432 |
+
has_upper = any(c.isupper() for c in password)
|
| 433 |
+
has_lower = any(c.islower() for c in password)
|
| 434 |
+
has_digit = any(c.isdigit() for c in password)
|
| 435 |
+
has_special = any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password)
|
| 436 |
+
|
| 437 |
+
if not has_upper:
|
| 438 |
+
return {
|
| 439 |
+
'valid': False,
|
| 440 |
+
'message': 'Password must contain at least one uppercase letter'
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
if not has_lower:
|
| 444 |
+
return {
|
| 445 |
+
'valid': False,
|
| 446 |
+
'message': 'Password must contain at least one lowercase letter'
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
if not has_digit:
|
| 450 |
+
return {
|
| 451 |
+
'valid': False,
|
| 452 |
+
'message': 'Password must contain at least one number'
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
if not has_special:
|
| 456 |
+
return {
|
| 457 |
+
'valid': False,
|
| 458 |
+
'message': 'Password must contain at least one special character'
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
return {
|
| 462 |
+
'valid': True,
|
| 463 |
+
'message': 'Password is valid'
|
| 464 |
+
}
|
|
@@ -2,6 +2,9 @@ import os
|
|
| 2 |
import sys
|
| 3 |
import locale
|
| 4 |
import logging
|
|
|
|
|
|
|
|
|
|
| 5 |
# Add the project root to the Python path
|
| 6 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 7 |
|
|
@@ -12,15 +15,47 @@ from flask_jwt_extended import JWTManager
|
|
| 12 |
import uuid
|
| 13 |
from concurrent.futures import ThreadPoolExecutor
|
| 14 |
|
| 15 |
-
# Configure logging
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
)
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Use relative import for the Config class to work with Hugging Face Spaces
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
from backend.utils.database import init_supabase
|
| 25 |
from backend.utils.cookies import setup_secure_cookies, configure_jwt_with_cookies
|
| 26 |
|
|
@@ -33,7 +68,7 @@ def setup_unicode_environment():
|
|
| 33 |
# Set environment variables for UTF-8 support
|
| 34 |
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
| 35 |
os.environ['PYTHONUTF8'] = '1'
|
| 36 |
-
|
| 37 |
# Set locale to UTF-8 if available
|
| 38 |
try:
|
| 39 |
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
|
|
@@ -45,12 +80,12 @@ def setup_unicode_environment():
|
|
| 45 |
locale.setlocale(locale.LC_ALL, '')
|
| 46 |
except locale.Error:
|
| 47 |
pass
|
| 48 |
-
|
| 49 |
# Set stdout/stderr encoding to UTF-8 if possible
|
| 50 |
if hasattr(sys.stdout, 'reconfigure'):
|
| 51 |
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
| 52 |
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
| 53 |
-
|
| 54 |
# Log to app logger instead of print
|
| 55 |
if 'app' in globals():
|
| 56 |
app.logger.info("Unicode environment setup completed")
|
|
@@ -62,14 +97,21 @@ def create_app():
|
|
| 62 |
"""Create and configure the Flask application."""
|
| 63 |
# Setup Unicode environment first
|
| 64 |
setup_unicode_environment()
|
| 65 |
-
|
| 66 |
app = Flask(__name__, static_folder='../frontend/dist')
|
| 67 |
app.config.from_object(Config)
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
# Disable strict slashes to prevent redirects
|
| 70 |
app.url_map.strict_slashes = False
|
| 71 |
-
|
| 72 |
-
# Initialize CORS with specific configuration
|
| 73 |
CORS(app, resources={
|
| 74 |
r"/api/*": {
|
| 75 |
"origins": [
|
|
@@ -86,14 +128,14 @@ def create_app():
|
|
| 86 |
"max_age": 86400 # 24 hours
|
| 87 |
}
|
| 88 |
})
|
| 89 |
-
|
| 90 |
-
# Add CORS headers for
|
| 91 |
@app.after_request
|
| 92 |
-
def
|
| 93 |
-
"""Add CORS headers
|
| 94 |
# Get the origin from the request
|
| 95 |
origin = request.headers.get('Origin', '')
|
| 96 |
-
|
| 97 |
# Check if the origin is in our allowed list
|
| 98 |
allowed_origins = [
|
| 99 |
"http://localhost:3000",
|
|
@@ -103,37 +145,34 @@ def create_app():
|
|
| 103 |
"http://192.168.1.4:3000",
|
| 104 |
"https://zelyanoth-lin-cbfcff2.hf.space"
|
| 105 |
]
|
| 106 |
-
|
| 107 |
-
#
|
| 108 |
-
if
|
| 109 |
-
if
|
| 110 |
-
response.headers
|
| 111 |
-
|
| 112 |
-
response.headers
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
if 'Access-Control-Allow-Headers' not in response.headers:
|
| 116 |
-
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'
|
| 117 |
-
|
| 118 |
return response
|
| 119 |
-
|
| 120 |
# Setup secure cookies
|
| 121 |
app = setup_secure_cookies(app)
|
| 122 |
-
|
| 123 |
# Initialize JWT with cookie support
|
| 124 |
jwt = configure_jwt_with_cookies(app)
|
| 125 |
-
|
| 126 |
# Initialize Supabase client
|
| 127 |
app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY'])
|
| 128 |
-
|
| 129 |
# Initialize a simple in-memory job store for tracking async tasks
|
| 130 |
# In production, you'd use a database or Redis for this
|
| 131 |
app.job_store = {}
|
| 132 |
-
|
| 133 |
# Initialize a ThreadPoolExecutor for running background tasks
|
| 134 |
# In production, you'd use a proper task scheduler like APScheduler
|
| 135 |
app.executor = ThreadPoolExecutor(max_workers=4)
|
| 136 |
-
|
| 137 |
# Initialize ContentService
|
| 138 |
try:
|
| 139 |
from backend.services.content_service import ContentService
|
|
@@ -143,7 +182,7 @@ def create_app():
|
|
| 143 |
app.logger.error(f"Failed to initialize ContentService: {str(e)}")
|
| 144 |
import traceback
|
| 145 |
app.logger.error(traceback.format_exc())
|
| 146 |
-
|
| 147 |
# Initialize APScheduler
|
| 148 |
if app.config.get('SCHEDULER_ENABLED', True):
|
| 149 |
try:
|
|
@@ -151,7 +190,7 @@ def create_app():
|
|
| 151 |
scheduler = APSchedulerService(app)
|
| 152 |
app.scheduler = scheduler
|
| 153 |
app.logger.info("APScheduler initialized successfully")
|
| 154 |
-
|
| 155 |
# Verify APScheduler initialization
|
| 156 |
if hasattr(app, 'scheduler') and app.scheduler.scheduler is not None:
|
| 157 |
app.logger.info("✅ APScheduler initialized successfully")
|
|
@@ -163,20 +202,25 @@ def create_app():
|
|
| 163 |
app.logger.error(f"Failed to initialize APScheduler: {str(e)}")
|
| 164 |
import traceback
|
| 165 |
app.logger.error(traceback.format_exc())
|
| 166 |
-
|
| 167 |
# Register blueprints
|
| 168 |
from backend.api.auth import auth_bp
|
| 169 |
from backend.api.sources import sources_bp
|
| 170 |
from backend.api.accounts import accounts_bp
|
| 171 |
from backend.api.posts import posts_bp
|
| 172 |
from backend.api.schedules import schedules_bp
|
| 173 |
-
|
| 174 |
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
| 175 |
app.register_blueprint(sources_bp, url_prefix='/api/sources')
|
| 176 |
app.register_blueprint(accounts_bp, url_prefix='/api/accounts')
|
| 177 |
app.register_blueprint(posts_bp, url_prefix='/api/posts')
|
| 178 |
app.register_blueprint(schedules_bp, url_prefix='/api/schedules')
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
# Serve frontend static files
|
| 181 |
@app.route('/', defaults={'path': ''})
|
| 182 |
@app.route('/<path:path>')
|
|
@@ -190,12 +234,12 @@ def create_app():
|
|
| 190 |
# Otherwise, serve index.html for SPA routing
|
| 191 |
else:
|
| 192 |
return send_from_directory(app.static_folder, 'index.html')
|
| 193 |
-
|
| 194 |
# Health check endpoint
|
| 195 |
@app.route('/health')
|
| 196 |
def health_check():
|
| 197 |
return {'status': 'healthy', 'message': 'Lin backend is running'}, 200
|
| 198 |
-
|
| 199 |
# Add database connection check endpoint
|
| 200 |
@app.route('/api/health')
|
| 201 |
def api_health_check():
|
|
@@ -214,52 +258,71 @@ def create_app():
|
|
| 214 |
'database': 'error',
|
| 215 |
'message': f'Health check failed: {str(e)}'
|
| 216 |
}, 503
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
# Add OAuth callback handler route
|
| 219 |
@app.route('/auth/callback')
|
|
|
|
| 220 |
def handle_auth_callback():
|
| 221 |
"""Handle OAuth callback from social networks."""
|
| 222 |
try:
|
| 223 |
# Parse URL parameters
|
| 224 |
from urllib.parse import parse_qs, urlparse
|
| 225 |
-
|
| 226 |
url = request.url
|
| 227 |
parsed_url = urlparse(url)
|
| 228 |
query_params = parse_qs(parsed_url.query)
|
| 229 |
-
|
| 230 |
code = query_params.get('code', [None])[0]
|
| 231 |
state = query_params.get('state', [None])[0]
|
| 232 |
error = query_params.get('error', [None])[0]
|
| 233 |
-
|
| 234 |
app.logger.info(f"🔗 [OAuth] Direct callback handler triggered")
|
| 235 |
app.logger.info(f"🔗 [OAuth] URL: {url}")
|
| 236 |
app.logger.info(f"🔗 [OAuth] Code: {code[:20] + '...' if code else None}")
|
| 237 |
app.logger.info(f"🔗 [OAuth] State: {state}")
|
| 238 |
app.logger.info(f"🔗 [OAuth] Error: {error}")
|
| 239 |
-
|
| 240 |
if error:
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
from flask import redirect
|
| 244 |
-
redirect_url = f"{request.host_url.rstrip('/')}?error={error}&from=linkedin"
|
| 245 |
-
return redirect(redirect_url)
|
| 246 |
-
|
| 247 |
if not code or not state:
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
from flask import redirect
|
| 251 |
-
redirect_url = f"{request.host_url.rstrip('/')}?error=missing_params&from=linkedin"
|
| 252 |
-
return redirect(redirect_url)
|
| 253 |
-
|
| 254 |
# Get the JWT token from cookies
|
| 255 |
token = request.cookies.get('access_token')
|
| 256 |
if not token:
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
from flask import redirect
|
| 260 |
-
redirect_url = f"{request.host_url.rstrip('/')}?error=no_token&from=linkedin"
|
| 261 |
-
return redirect(redirect_url)
|
| 262 |
-
|
| 263 |
# Verify JWT and get user identity
|
| 264 |
try:
|
| 265 |
from flask_jwt_extended import decode_token
|
|
@@ -267,38 +330,15 @@ def create_app():
|
|
| 267 |
user_id = user_data['sub']
|
| 268 |
app.logger.info(f"🔗 [OAuth] Processing OAuth for user: {user_id}")
|
| 269 |
except Exception as jwt_error:
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
redirect_url = f"{request.host_url.rstrip('/')}?error=jwt_failed&from=linkedin"
|
| 273 |
-
return redirect(redirect_url)
|
| 274 |
-
|
| 275 |
# Process the OAuth flow directly
|
| 276 |
-
from backend.services.linkedin_service import LinkedInService
|
| 277 |
-
linkedin_service = LinkedInService()
|
| 278 |
-
|
| 279 |
-
# Exchange code for access token
|
| 280 |
-
app.logger.info("🔗 [OAuth] Exchanging code for access token...")
|
| 281 |
try:
|
| 282 |
-
|
| 283 |
-
access_token = token_response['access_token']
|
| 284 |
-
app.logger.info(f"🔗 [OAuth] Token exchange successful. Token length: {len(access_token)}")
|
| 285 |
except Exception as token_error:
|
| 286 |
app.logger.error(f"🔗 [OAuth] Token exchange failed: {str(token_error)}")
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
return redirect(redirect_url)
|
| 290 |
-
|
| 291 |
-
# Get user info
|
| 292 |
-
app.logger.info("🔗 [OAuth] Fetching user info...")
|
| 293 |
-
try:
|
| 294 |
-
user_info = linkedin_service.get_user_info(access_token)
|
| 295 |
-
app.logger.info(f"🔗 [OAuth] User info fetched: {user_info}")
|
| 296 |
-
except Exception as user_info_error:
|
| 297 |
-
app.logger.error(f"🔗 [OAuth] User info fetch failed: {str(user_info_error)}")
|
| 298 |
-
from flask import redirect
|
| 299 |
-
redirect_url = f"{request.host_url.rstrip('/')}?error=user_info_failed&from=linkedin"
|
| 300 |
-
return redirect(redirect_url)
|
| 301 |
-
|
| 302 |
# Prepare account data for insertion
|
| 303 |
account_data = {
|
| 304 |
"social_network": "LinkedIn",
|
|
@@ -311,7 +351,7 @@ def create_app():
|
|
| 311 |
"picture": user_info.get('picture')
|
| 312 |
}
|
| 313 |
app.logger.info(f"🔗 [OAuth] Prepared account data: {account_data}")
|
| 314 |
-
|
| 315 |
# Store account info in Supabase
|
| 316 |
app.logger.info("🔗 [OAuth] Inserting account into database...")
|
| 317 |
try:
|
|
@@ -321,12 +361,12 @@ def create_app():
|
|
| 321 |
.insert(account_data)
|
| 322 |
.execute()
|
| 323 |
)
|
| 324 |
-
|
| 325 |
# DEBUG: Log database response
|
| 326 |
app.logger.info(f"🔗 [OAuth] Database response: {response}")
|
| 327 |
app.logger.info(f"🔗 [OAuth] Response data: {response.data}")
|
| 328 |
app.logger.info(f"🔗 [OAuth] Response error: {getattr(response, 'error', None)}")
|
| 329 |
-
|
| 330 |
if response.data:
|
| 331 |
app.logger.info(f"🔗 [OAuth] Account linked successfully for user: {user_id}")
|
| 332 |
# Redirect to frontend with success
|
|
@@ -335,26 +375,20 @@ def create_app():
|
|
| 335 |
return redirect(redirect_url)
|
| 336 |
else:
|
| 337 |
app.logger.error(f"🔗 [OAuth] No data returned from database insertion for user: {user_id}")
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
return redirect(redirect_url)
|
| 341 |
-
|
| 342 |
except Exception as db_error:
|
| 343 |
app.logger.error(f"🔗 [OAuth] Database insertion failed: {str(db_error)}")
|
| 344 |
app.logger.error(f"🔗 [OAuth] Database error type: {type(db_error)}")
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
return redirect(redirect_url)
|
| 348 |
-
|
| 349 |
except Exception as e:
|
| 350 |
app.logger.error(f"🔗 [OAuth] Callback handler error: {str(e)}")
|
| 351 |
import traceback
|
| 352 |
app.logger.error(f"🔗 [OAuth] Traceback: {traceback.format_exc()}")
|
| 353 |
# Redirect to frontend with error
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
return redirect(redirect_url)
|
| 357 |
-
|
| 358 |
return app
|
| 359 |
|
| 360 |
if __name__ == '__main__':
|
|
|
|
| 2 |
import sys
|
| 3 |
import locale
|
| 4 |
import logging
|
| 5 |
+
from logging.handlers import RotatingFileHandler
|
| 6 |
+
from flask_limiter import Limiter
|
| 7 |
+
from flask_limiter.util import get_remote_address
|
| 8 |
# Add the project root to the Python path
|
| 9 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 10 |
|
|
|
|
| 15 |
import uuid
|
| 16 |
from concurrent.futures import ThreadPoolExecutor
|
| 17 |
|
| 18 |
+
# Configure logging with rotating file handler and proper formatting
|
| 19 |
+
def setup_logging():
|
| 20 |
+
"""Setup comprehensive logging configuration."""
|
| 21 |
+
# Create logs directory if it doesn't exist
|
| 22 |
+
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
|
| 23 |
+
os.makedirs(log_dir, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
# Configure root logger
|
| 26 |
+
logging.basicConfig(
|
| 27 |
+
level=logging.INFO,
|
| 28 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# Create a rotating file handler
|
| 32 |
+
file_handler = RotatingFileHandler(
|
| 33 |
+
os.path.join(log_dir, 'app.log'),
|
| 34 |
+
maxBytes=1024 * 1024 * 10, # 10 MB
|
| 35 |
+
backupCount=5
|
| 36 |
+
)
|
| 37 |
+
file_handler.setLevel(logging.INFO)
|
| 38 |
+
file_handler.setFormatter(logging.Formatter(
|
| 39 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
|
| 40 |
+
))
|
| 41 |
+
|
| 42 |
+
# Add the file handler to the root logger
|
| 43 |
+
logging.getLogger().addHandler(file_handler)
|
| 44 |
+
|
| 45 |
+
# Set specific loggers to WARNING to reduce noise
|
| 46 |
+
logging.getLogger('apscheduler').setLevel(logging.WARNING)
|
| 47 |
+
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
| 48 |
+
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
| 49 |
+
|
| 50 |
+
setup_logging()
|
| 51 |
|
| 52 |
# Use relative import for the Config class to work with Hugging Face Spaces
|
| 53 |
+
try:
|
| 54 |
+
from backend.config import Config
|
| 55 |
+
except ValueError as e:
|
| 56 |
+
print(f"Configuration error: {e}")
|
| 57 |
+
sys.exit(1)
|
| 58 |
+
|
| 59 |
from backend.utils.database import init_supabase
|
| 60 |
from backend.utils.cookies import setup_secure_cookies, configure_jwt_with_cookies
|
| 61 |
|
|
|
|
| 68 |
# Set environment variables for UTF-8 support
|
| 69 |
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
| 70 |
os.environ['PYTHONUTF8'] = '1'
|
| 71 |
+
|
| 72 |
# Set locale to UTF-8 if available
|
| 73 |
try:
|
| 74 |
locale.setlocale(locale.LC_ALL, 'C.UTF-8')
|
|
|
|
| 80 |
locale.setlocale(locale.LC_ALL, '')
|
| 81 |
except locale.Error:
|
| 82 |
pass
|
| 83 |
+
|
| 84 |
# Set stdout/stderr encoding to UTF-8 if possible
|
| 85 |
if hasattr(sys.stdout, 'reconfigure'):
|
| 86 |
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
| 87 |
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
| 88 |
+
|
| 89 |
# Log to app logger instead of print
|
| 90 |
if 'app' in globals():
|
| 91 |
app.logger.info("Unicode environment setup completed")
|
|
|
|
| 97 |
"""Create and configure the Flask application."""
|
| 98 |
# Setup Unicode environment first
|
| 99 |
setup_unicode_environment()
|
| 100 |
+
|
| 101 |
app = Flask(__name__, static_folder='../frontend/dist')
|
| 102 |
app.config.from_object(Config)
|
| 103 |
+
|
| 104 |
+
# Initialize rate limiting
|
| 105 |
+
limiter = Limiter(
|
| 106 |
+
get_remote_address,
|
| 107 |
+
app=app,
|
| 108 |
+
default_limits=["200 per day", "50 per hour"]
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
# Disable strict slashes to prevent redirects
|
| 112 |
app.url_map.strict_slashes = False
|
| 113 |
+
|
| 114 |
+
# Initialize CORS with specific configuration - only for API routes
|
| 115 |
CORS(app, resources={
|
| 116 |
r"/api/*": {
|
| 117 |
"origins": [
|
|
|
|
| 128 |
"max_age": 86400 # 24 hours
|
| 129 |
}
|
| 130 |
})
|
| 131 |
+
|
| 132 |
+
# Add additional CORS headers for non-API routes specifically needed for OAuth callbacks
|
| 133 |
@app.after_request
|
| 134 |
+
def add_additional_cors_headers(response):
|
| 135 |
+
"""Add additional CORS headers where needed."""
|
| 136 |
# Get the origin from the request
|
| 137 |
origin = request.headers.get('Origin', '')
|
| 138 |
+
|
| 139 |
# Check if the origin is in our allowed list
|
| 140 |
allowed_origins = [
|
| 141 |
"http://localhost:3000",
|
|
|
|
| 145 |
"http://192.168.1.4:3000",
|
| 146 |
"https://zelyanoth-lin-cbfcff2.hf.space"
|
| 147 |
]
|
| 148 |
+
|
| 149 |
+
# Add CORS headers specifically for OAuth callback routes
|
| 150 |
+
if request.endpoint == 'handle_auth_callback' or request.path == '/auth/callback':
|
| 151 |
+
if origin in allowed_origins:
|
| 152 |
+
response.headers.set('Access-Control-Allow-Origin', origin)
|
| 153 |
+
response.headers.set('Access-Control-Allow-Credentials', 'true')
|
| 154 |
+
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
| 155 |
+
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
|
| 156 |
+
|
|
|
|
|
|
|
|
|
|
| 157 |
return response
|
| 158 |
+
|
| 159 |
# Setup secure cookies
|
| 160 |
app = setup_secure_cookies(app)
|
| 161 |
+
|
| 162 |
# Initialize JWT with cookie support
|
| 163 |
jwt = configure_jwt_with_cookies(app)
|
| 164 |
+
|
| 165 |
# Initialize Supabase client
|
| 166 |
app.supabase = init_supabase(app.config['SUPABASE_URL'], app.config['SUPABASE_KEY'])
|
| 167 |
+
|
| 168 |
# Initialize a simple in-memory job store for tracking async tasks
|
| 169 |
# In production, you'd use a database or Redis for this
|
| 170 |
app.job_store = {}
|
| 171 |
+
|
| 172 |
# Initialize a ThreadPoolExecutor for running background tasks
|
| 173 |
# In production, you'd use a proper task scheduler like APScheduler
|
| 174 |
app.executor = ThreadPoolExecutor(max_workers=4)
|
| 175 |
+
|
| 176 |
# Initialize ContentService
|
| 177 |
try:
|
| 178 |
from backend.services.content_service import ContentService
|
|
|
|
| 182 |
app.logger.error(f"Failed to initialize ContentService: {str(e)}")
|
| 183 |
import traceback
|
| 184 |
app.logger.error(traceback.format_exc())
|
| 185 |
+
|
| 186 |
# Initialize APScheduler
|
| 187 |
if app.config.get('SCHEDULER_ENABLED', True):
|
| 188 |
try:
|
|
|
|
| 190 |
scheduler = APSchedulerService(app)
|
| 191 |
app.scheduler = scheduler
|
| 192 |
app.logger.info("APScheduler initialized successfully")
|
| 193 |
+
|
| 194 |
# Verify APScheduler initialization
|
| 195 |
if hasattr(app, 'scheduler') and app.scheduler.scheduler is not None:
|
| 196 |
app.logger.info("✅ APScheduler initialized successfully")
|
|
|
|
| 202 |
app.logger.error(f"Failed to initialize APScheduler: {str(e)}")
|
| 203 |
import traceback
|
| 204 |
app.logger.error(traceback.format_exc())
|
| 205 |
+
|
| 206 |
# Register blueprints
|
| 207 |
from backend.api.auth import auth_bp
|
| 208 |
from backend.api.sources import sources_bp
|
| 209 |
from backend.api.accounts import accounts_bp
|
| 210 |
from backend.api.posts import posts_bp
|
| 211 |
from backend.api.schedules import schedules_bp
|
| 212 |
+
|
| 213 |
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
| 214 |
app.register_blueprint(sources_bp, url_prefix='/api/sources')
|
| 215 |
app.register_blueprint(accounts_bp, url_prefix='/api/accounts')
|
| 216 |
app.register_blueprint(posts_bp, url_prefix='/api/posts')
|
| 217 |
app.register_blueprint(schedules_bp, url_prefix='/api/schedules')
|
| 218 |
+
|
| 219 |
+
# Add rate limiting to specific authentication routes after blueprint registration
|
| 220 |
+
limiter.limit("5 per minute")(app.view_functions['auth.register'])
|
| 221 |
+
limiter.limit("5 per minute")(app.view_functions['auth.login'])
|
| 222 |
+
limiter.limit("10 per minute")(app.view_functions['auth.forgot_password'])
|
| 223 |
+
|
| 224 |
# Serve frontend static files
|
| 225 |
@app.route('/', defaults={'path': ''})
|
| 226 |
@app.route('/<path:path>')
|
|
|
|
| 234 |
# Otherwise, serve index.html for SPA routing
|
| 235 |
else:
|
| 236 |
return send_from_directory(app.static_folder, 'index.html')
|
| 237 |
+
|
| 238 |
# Health check endpoint
|
| 239 |
@app.route('/health')
|
| 240 |
def health_check():
|
| 241 |
return {'status': 'healthy', 'message': 'Lin backend is running'}, 200
|
| 242 |
+
|
| 243 |
# Add database connection check endpoint
|
| 244 |
@app.route('/api/health')
|
| 245 |
def api_health_check():
|
|
|
|
| 258 |
'database': 'error',
|
| 259 |
'message': f'Health check failed: {str(e)}'
|
| 260 |
}, 503
|
| 261 |
+
|
| 262 |
+
# Add helper functions for OAuth callback to reduce duplication
|
| 263 |
+
def create_oauth_redirect(error_code, from_source='linkedin'):
|
| 264 |
+
"""Helper function to create OAuth redirect with error."""
|
| 265 |
+
from flask import redirect
|
| 266 |
+
redirect_url = f"{request.host_url.rstrip('/')}?error={error_code}&from={from_source}"
|
| 267 |
+
return redirect(redirect_url)
|
| 268 |
+
|
| 269 |
+
def handle_oauth_error(message, error_code, from_source='linkedin'):
|
| 270 |
+
"""Helper function to handle OAuth errors."""
|
| 271 |
+
app.logger.error(f"🔗 [OAuth] {message}")
|
| 272 |
+
return create_oauth_redirect(error_code, from_source)
|
| 273 |
+
|
| 274 |
+
def fetch_linkedin_data(code):
|
| 275 |
+
"""Helper function to fetch LinkedIn user data from code."""
|
| 276 |
+
from backend.services.linkedin_service import LinkedInService
|
| 277 |
+
linkedin_service = LinkedInService()
|
| 278 |
+
|
| 279 |
+
# Exchange code for access token
|
| 280 |
+
app.logger.info("🔗 [OAuth] Exchanging code for access token...")
|
| 281 |
+
token_response = linkedin_service.get_access_token(code)
|
| 282 |
+
access_token = token_response['access_token']
|
| 283 |
+
app.logger.info(f"🔗 [OAuth] Token exchange successful. Token length: {len(access_token)}")
|
| 284 |
+
|
| 285 |
+
# Get user info
|
| 286 |
+
app.logger.info("🔗 [OAuth] Fetching user info...")
|
| 287 |
+
user_info = linkedin_service.get_user_info(access_token)
|
| 288 |
+
app.logger.info(f"🔗 [OAuth] User info fetched: {user_info}")
|
| 289 |
+
|
| 290 |
+
return access_token, user_info
|
| 291 |
+
|
| 292 |
# Add OAuth callback handler route
|
| 293 |
@app.route('/auth/callback')
|
| 294 |
+
@limiter.limit("10 per minute")
|
| 295 |
def handle_auth_callback():
|
| 296 |
"""Handle OAuth callback from social networks."""
|
| 297 |
try:
|
| 298 |
# Parse URL parameters
|
| 299 |
from urllib.parse import parse_qs, urlparse
|
| 300 |
+
|
| 301 |
url = request.url
|
| 302 |
parsed_url = urlparse(url)
|
| 303 |
query_params = parse_qs(parsed_url.query)
|
| 304 |
+
|
| 305 |
code = query_params.get('code', [None])[0]
|
| 306 |
state = query_params.get('state', [None])[0]
|
| 307 |
error = query_params.get('error', [None])[0]
|
| 308 |
+
|
| 309 |
app.logger.info(f"🔗 [OAuth] Direct callback handler triggered")
|
| 310 |
app.logger.info(f"🔗 [OAuth] URL: {url}")
|
| 311 |
app.logger.info(f"🔗 [OAuth] Code: {code[:20] + '...' if code else None}")
|
| 312 |
app.logger.info(f"🔗 [OAuth] State: {state}")
|
| 313 |
app.logger.info(f"🔗 [OAuth] Error: {error}")
|
| 314 |
+
|
| 315 |
if error:
|
| 316 |
+
return handle_oauth_error(f"OAuth error: {error}", error, 'linkedin')
|
| 317 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
if not code or not state:
|
| 319 |
+
return handle_oauth_error("Missing required parameters", 'missing_params', 'linkedin')
|
| 320 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
# Get the JWT token from cookies
|
| 322 |
token = request.cookies.get('access_token')
|
| 323 |
if not token:
|
| 324 |
+
return handle_oauth_error("No token found in cookies", 'no_token', 'linkedin')
|
| 325 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
# Verify JWT and get user identity
|
| 327 |
try:
|
| 328 |
from flask_jwt_extended import decode_token
|
|
|
|
| 330 |
user_id = user_data['sub']
|
| 331 |
app.logger.info(f"🔗 [OAuth] Processing OAuth for user: {user_id}")
|
| 332 |
except Exception as jwt_error:
|
| 333 |
+
return handle_oauth_error(f"JWT verification failed: {str(jwt_error)}", 'jwt_failed', 'linkedin')
|
| 334 |
+
|
|
|
|
|
|
|
|
|
|
| 335 |
# Process the OAuth flow directly
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
try:
|
| 337 |
+
access_token, user_info = fetch_linkedin_data(code)
|
|
|
|
|
|
|
| 338 |
except Exception as token_error:
|
| 339 |
app.logger.error(f"🔗 [OAuth] Token exchange failed: {str(token_error)}")
|
| 340 |
+
return create_oauth_redirect('token_exchange_failed', 'linkedin')
|
| 341 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
# Prepare account data for insertion
|
| 343 |
account_data = {
|
| 344 |
"social_network": "LinkedIn",
|
|
|
|
| 351 |
"picture": user_info.get('picture')
|
| 352 |
}
|
| 353 |
app.logger.info(f"🔗 [OAuth] Prepared account data: {account_data}")
|
| 354 |
+
|
| 355 |
# Store account info in Supabase
|
| 356 |
app.logger.info("🔗 [OAuth] Inserting account into database...")
|
| 357 |
try:
|
|
|
|
| 361 |
.insert(account_data)
|
| 362 |
.execute()
|
| 363 |
)
|
| 364 |
+
|
| 365 |
# DEBUG: Log database response
|
| 366 |
app.logger.info(f"🔗 [OAuth] Database response: {response}")
|
| 367 |
app.logger.info(f"🔗 [OAuth] Response data: {response.data}")
|
| 368 |
app.logger.info(f"🔗 [OAuth] Response error: {getattr(response, 'error', None)}")
|
| 369 |
+
|
| 370 |
if response.data:
|
| 371 |
app.logger.info(f"🔗 [OAuth] Account linked successfully for user: {user_id}")
|
| 372 |
# Redirect to frontend with success
|
|
|
|
| 375 |
return redirect(redirect_url)
|
| 376 |
else:
|
| 377 |
app.logger.error(f"🔗 [OAuth] No data returned from database insertion for user: {user_id}")
|
| 378 |
+
return create_oauth_redirect('database_insert_failed', 'linkedin')
|
| 379 |
+
|
|
|
|
|
|
|
| 380 |
except Exception as db_error:
|
| 381 |
app.logger.error(f"🔗 [OAuth] Database insertion failed: {str(db_error)}")
|
| 382 |
app.logger.error(f"🔗 [OAuth] Database error type: {type(db_error)}")
|
| 383 |
+
return create_oauth_redirect('database_error', 'linkedin')
|
| 384 |
+
|
|
|
|
|
|
|
| 385 |
except Exception as e:
|
| 386 |
app.logger.error(f"🔗 [OAuth] Callback handler error: {str(e)}")
|
| 387 |
import traceback
|
| 388 |
app.logger.error(f"🔗 [OAuth] Traceback: {traceback.format_exc()}")
|
| 389 |
# Redirect to frontend with error
|
| 390 |
+
return create_oauth_redirect('server_error', 'linkedin')
|
| 391 |
+
|
|
|
|
|
|
|
| 392 |
return app
|
| 393 |
|
| 394 |
if __name__ == '__main__':
|
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from celery import Celery
|
| 2 |
+
from backend.config import Config
|
| 3 |
+
|
| 4 |
+
def make_celery(app_name=__name__):
|
| 5 |
+
"""Create and configure Celery instance."""
|
| 6 |
+
celery = Celery(app_name)
|
| 7 |
+
|
| 8 |
+
# Configure Celery using the Flask app's config
|
| 9 |
+
celery.conf.update(
|
| 10 |
+
broker_url=Config.CELERY_BROKER_URL,
|
| 11 |
+
result_backend=Config.CELERY_RESULT_BACKEND,
|
| 12 |
+
task_serializer='json',
|
| 13 |
+
accept_content=['json'],
|
| 14 |
+
result_serializer='json',
|
| 15 |
+
timezone='UTC',
|
| 16 |
+
enable_utc=True,
|
| 17 |
+
result_expires=3600, # Results expire after 1 hour
|
| 18 |
+
task_routes={
|
| 19 |
+
# Example routing - adjust based on your actual tasks
|
| 20 |
+
# 'backend.tasks.example_task': {'queue': 'default'},
|
| 21 |
+
},
|
| 22 |
+
broker_connection_retry_on_startup=True,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
return celery
|
| 26 |
+
|
| 27 |
+
# Create the main Celery instance
|
| 28 |
+
celery = make_celery()
|
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from celery import Celery
|
| 2 |
+
from celery.schedules import crontab
|
| 3 |
+
from backend.config import Config
|
| 4 |
+
|
| 5 |
+
def make_celery_beat(app_name=__name__):
|
| 6 |
+
"""Create and configure Celery Beat scheduler."""
|
| 7 |
+
celery_beat = Celery(app_name)
|
| 8 |
+
|
| 9 |
+
# Configure Celery Beat using the Flask app's config
|
| 10 |
+
celery_beat.conf.update(
|
| 11 |
+
broker_url=Config.CELERY_BROKER_URL,
|
| 12 |
+
result_backend=Config.CELERY_RESULT_BACKEND,
|
| 13 |
+
task_serializer='json',
|
| 14 |
+
accept_content=['json'],
|
| 15 |
+
result_serializer='json',
|
| 16 |
+
timezone='UTC',
|
| 17 |
+
enable_utc=True,
|
| 18 |
+
result_expires=3600, # Results expire after 1 hour
|
| 19 |
+
beat_schedule={
|
| 20 |
+
# Example scheduled tasks - adjust as needed
|
| 21 |
+
# 'example-task': {
|
| 22 |
+
# 'task': 'backend.tasks.example_task',
|
| 23 |
+
# 'schedule': crontab(minute=0, hour='*/6'), # Every 6 hours
|
| 24 |
+
# },
|
| 25 |
+
},
|
| 26 |
+
worker_prefetch_multiplier=1,
|
| 27 |
+
task_acks_late=True,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
return celery_beat
|
| 31 |
+
|
| 32 |
+
# Create the Celery Beat instance
|
| 33 |
+
celery_beat = make_celery_beat()
|
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
import platform
|
|
|
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
|
| 5 |
# Load environment variables from .env file
|
|
@@ -47,14 +48,14 @@ class Config:
|
|
| 47 |
# Check for lowercase hugging_key first (for dev), then uppercase HUGGING_KEY (for production)
|
| 48 |
HUGGING_KEY = os.environ.get('hugging_key') or os.environ.get('HUGGING_KEY') or ''
|
| 49 |
|
| 50 |
-
# JWT configuration
|
| 51 |
-
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or '
|
| 52 |
|
| 53 |
# Database configuration
|
| 54 |
DATABASE_URL = os.environ.get('DATABASE_URL') or ''
|
| 55 |
|
| 56 |
-
# Application configuration
|
| 57 |
-
SECRET_KEY = os.environ.get('SECRET_KEY') or
|
| 58 |
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
|
| 59 |
|
| 60 |
# Scheduler configuration
|
|
@@ -76,4 +77,25 @@ class Config:
|
|
| 76 |
|
| 77 |
# Debug and logging settings
|
| 78 |
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG')
|
| 79 |
-
UNICODE_SAFE_LOGGING = UNICODE_LOGGING and not IS_WINDOWS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import platform
|
| 3 |
+
import secrets
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
|
| 6 |
# Load environment variables from .env file
|
|
|
|
| 48 |
# Check for lowercase hugging_key first (for dev), then uppercase HUGGING_KEY (for production)
|
| 49 |
HUGGING_KEY = os.environ.get('hugging_key') or os.environ.get('HUGGING_KEY') or ''
|
| 50 |
|
| 51 |
+
# JWT configuration - generate a secure key if not provided
|
| 52 |
+
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or os.environ.get('SECRET_KEY') or secrets.token_hex(32)
|
| 53 |
|
| 54 |
# Database configuration
|
| 55 |
DATABASE_URL = os.environ.get('DATABASE_URL') or ''
|
| 56 |
|
| 57 |
+
# Application configuration - generate a secure key if not provided
|
| 58 |
+
SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32)
|
| 59 |
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
|
| 60 |
|
| 61 |
# Scheduler configuration
|
|
|
|
| 77 |
|
| 78 |
# Debug and logging settings
|
| 79 |
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO' if ENVIRONMENT == 'production' else 'DEBUG')
|
| 80 |
+
UNICODE_SAFE_LOGGING = UNICODE_LOGGING and not IS_WINDOWS
|
| 81 |
+
|
| 82 |
+
# Celery configuration
|
| 83 |
+
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
| 84 |
+
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
| 85 |
+
|
| 86 |
+
# Validate required configuration values
|
| 87 |
+
@classmethod
|
| 88 |
+
def validate_config(cls):
|
| 89 |
+
"""Validate that all required config values are set."""
|
| 90 |
+
required_vars = ['SUPABASE_URL', 'SUPABASE_KEY', 'JWT_SECRET_KEY']
|
| 91 |
+
missing_vars = []
|
| 92 |
+
|
| 93 |
+
for var in required_vars:
|
| 94 |
+
if not getattr(cls, var, None) or getattr(cls, var) == 'your-secret-key-change-in-production':
|
| 95 |
+
missing_vars.append(var)
|
| 96 |
+
|
| 97 |
+
if missing_vars:
|
| 98 |
+
raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}. Please check your .env file.")
|
| 99 |
+
|
| 100 |
+
# Validate configuration after class definition
|
| 101 |
+
Config.validate_config()
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gunicorn configuration file
|
| 2 |
+
import multiprocessing
|
| 3 |
+
|
| 4 |
+
# Server socket
|
| 5 |
+
bind = "0.0.0.0:5000" # Use port 5000 to match the backend configuration in docker-compose.yml
|
| 6 |
+
backlog = 2048
|
| 7 |
+
|
| 8 |
+
# Worker processes
|
| 9 |
+
workers = 2 * multiprocessing.cpu_count() + 1 # Recommended formula
|
| 10 |
+
worker_class = "sync"
|
| 11 |
+
worker_connections = 1000
|
| 12 |
+
timeout = 120 # Increase timeout to match Dockerfile CMD
|
| 13 |
+
keepalive = 2
|
| 14 |
+
max_requests = 1000
|
| 15 |
+
max_requests_jitter = 100
|
| 16 |
+
|
| 17 |
+
# Logging
|
| 18 |
+
accesslog = "-"
|
| 19 |
+
errorlog = "-"
|
| 20 |
+
loglevel = "info"
|
| 21 |
+
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
| 22 |
+
|
| 23 |
+
# Process naming
|
| 24 |
+
proc_name = "gunicorn_app"
|
| 25 |
+
|
| 26 |
+
# Server mechanics
|
| 27 |
+
preload_app = True
|
| 28 |
+
daemon = False
|
| 29 |
+
pidfile = "/tmp/gunicorn.pid"
|
| 30 |
+
user = None
|
| 31 |
+
group = None
|
| 32 |
+
tmp_upload_dir = None
|
| 33 |
+
|
| 34 |
+
# SSL
|
| 35 |
+
keyfile = None
|
| 36 |
+
certfile = None
|
|
@@ -27,6 +27,9 @@ supabase>=2.16.0
|
|
| 27 |
# Security
|
| 28 |
bcrypt>=4.3.0
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
# Testing
|
| 31 |
pytest>=8.4.1
|
| 32 |
pytest-cov>=6.2.1
|
|
|
|
| 27 |
# Security
|
| 28 |
bcrypt>=4.3.0
|
| 29 |
|
| 30 |
+
# WSGI server
|
| 31 |
+
gunicorn>=23.0.0
|
| 32 |
+
|
| 33 |
# Testing
|
| 34 |
pytest>=8.4.1
|
| 35 |
pytest-cov>=6.2.1
|
|
@@ -186,7 +186,7 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 186 |
'message': 'Invalid email or password. Please check your credentials and try again.'
|
| 187 |
}
|
| 188 |
except Exception as e:
|
| 189 |
-
current_app.logger.error(f"Login error: {str(e)}")
|
| 190 |
|
| 191 |
# Provide more specific error messages
|
| 192 |
error_str = str(e).lower()
|
|
@@ -207,28 +207,10 @@ def login_user(email: str, password: str, remember_me: bool = False) -> dict:
|
|
| 207 |
'message': 'No account found with this email. Please check your email or register for a new account.'
|
| 208 |
}
|
| 209 |
else:
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
'message': 'Password/email Incorrect'
|
| 215 |
-
}
|
| 216 |
-
elif 'email not confirmed' in error_str or 'email not verified' in error_str:
|
| 217 |
-
return {
|
| 218 |
-
'success': False,
|
| 219 |
-
'message': 'Check your mail to confirm your account',
|
| 220 |
-
'requires_confirmation': True
|
| 221 |
-
}
|
| 222 |
-
elif 'user not found' in error_str:
|
| 223 |
-
return {
|
| 224 |
-
'success': False,
|
| 225 |
-
'message': 'No account found with this email. Please check your email or register for a new account.'
|
| 226 |
-
}
|
| 227 |
-
else:
|
| 228 |
-
return {
|
| 229 |
-
'success': False,
|
| 230 |
-
'message': 'Password/email Incorrect'
|
| 231 |
-
}
|
| 232 |
|
| 233 |
def get_user_by_id(user_id: str) -> dict:
|
| 234 |
"""
|
|
|
|
| 186 |
'message': 'Invalid email or password. Please check your credentials and try again.'
|
| 187 |
}
|
| 188 |
except Exception as e:
|
| 189 |
+
current_app.logger.error(f"Login error for email {email}: {str(e)}", exc_info=True)
|
| 190 |
|
| 191 |
# Provide more specific error messages
|
| 192 |
error_str = str(e).lower()
|
|
|
|
| 207 |
'message': 'No account found with this email. Please check your email or register for a new account.'
|
| 208 |
}
|
| 209 |
else:
|
| 210 |
+
return {
|
| 211 |
+
'success': False,
|
| 212 |
+
'message': 'Invalid email or password. Please check your credentials and try again.'
|
| 213 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
def get_user_by_id(user_id: str) -> dict:
|
| 216 |
"""
|
|
@@ -1,9 +1,9 @@
|
|
| 1 |
from flask import Flask, request, jsonify
|
| 2 |
-
from flask_jwt_extended import JWTManager
|
| 3 |
-
from datetime import datetime, timedelta
|
| 4 |
import hashlib
|
| 5 |
import secrets
|
| 6 |
-
import os
|
|
|
|
| 7 |
|
| 8 |
def setup_secure_cookies(app: Flask):
|
| 9 |
"""Setup secure cookie configuration for the Flask app."""
|
|
@@ -13,10 +13,21 @@ def setup_secure_cookies(app: Flask):
|
|
| 13 |
app.config.get('ENV') == 'development' or
|
| 14 |
app.config.get('ENVIRONMENT') == 'development'
|
| 15 |
)
|
| 16 |
-
|
| 17 |
# Check if we're running in Hugging Face Spaces
|
| 18 |
is_huggingface = os.environ.get('SPACE_ID') is not None
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
@app.after_request
|
| 21 |
def set_secure_cookies(response):
|
| 22 |
"""Set secure cookies for all responses."""
|
|
@@ -26,22 +37,23 @@ def setup_secure_cookies(app: Flask):
|
|
| 26 |
token = request.headers.get('Authorization')
|
| 27 |
if token and token.startswith('Bearer '):
|
| 28 |
token = token[7:] # Remove 'Bearer ' prefix
|
| 29 |
-
|
| 30 |
# Determine cookie security settings
|
| 31 |
secure_cookie = not is_development
|
| 32 |
samesite_policy = 'Lax' if is_huggingface else 'Strict'
|
| 33 |
-
|
| 34 |
# Set secure cookie for access token
|
| 35 |
response.set_cookie(
|
| 36 |
'access_token',
|
| 37 |
token,
|
| 38 |
httponly=True, # Prevent XSS attacks
|
| 39 |
secure=secure_cookie, # Send over HTTPS in production/HF Spaces
|
| 40 |
-
samesite=samesite_policy, #
|
| 41 |
max_age=3600, # 1 hour (matches default JWT expiration)
|
| 42 |
-
path='/'
|
|
|
|
| 43 |
)
|
| 44 |
-
|
| 45 |
# Safely check for rememberMe in JSON data
|
| 46 |
remember_me = False
|
| 47 |
try:
|
|
@@ -52,7 +64,7 @@ def setup_secure_cookies(app: Flask):
|
|
| 52 |
except:
|
| 53 |
# If there's any error parsing JSON, default to False
|
| 54 |
remember_me = False
|
| 55 |
-
|
| 56 |
# Set remember me cookie if requested
|
| 57 |
if remember_me:
|
| 58 |
response.set_cookie(
|
|
@@ -62,9 +74,10 @@ def setup_secure_cookies(app: Flask):
|
|
| 62 |
secure=secure_cookie,
|
| 63 |
samesite=samesite_policy,
|
| 64 |
max_age=7*24*60*60, # 7 days
|
| 65 |
-
path='/'
|
|
|
|
| 66 |
)
|
| 67 |
-
|
| 68 |
return response
|
| 69 |
|
| 70 |
return app
|
|
@@ -72,7 +85,7 @@ def setup_secure_cookies(app: Flask):
|
|
| 72 |
def configure_jwt_with_cookies(app: Flask):
|
| 73 |
"""Configure JWT to work with cookies."""
|
| 74 |
jwt = JWTManager(app)
|
| 75 |
-
|
| 76 |
# Get allowed origins from CORS configuration
|
| 77 |
allowed_origins = [
|
| 78 |
'http://localhost:3000',
|
|
@@ -82,71 +95,52 @@ def configure_jwt_with_cookies(app: Flask):
|
|
| 82 |
'http://192.168.1.4:3000',
|
| 83 |
'https://zelyanoth-lin-cbfcff2.hf.space'
|
| 84 |
]
|
| 85 |
-
|
| 86 |
@jwt.token_verification_loader
|
| 87 |
def verify_token_on_refresh_callback(jwt_header, jwt_payload):
|
| 88 |
"""Verify token and refresh if needed."""
|
| 89 |
# This is a simplified version - in production, you'd check a refresh token
|
| 90 |
return True
|
| 91 |
-
|
| 92 |
@jwt.expired_token_loader
|
| 93 |
def expired_token_callback(jwt_header, jwt_payload):
|
| 94 |
"""Handle expired tokens."""
|
| 95 |
# Clear cookies when token expires
|
| 96 |
response = jsonify({'success': False, 'message': 'Token has expired'})
|
| 97 |
-
response.set_cookie('access_token', '', expires=0, path='/')
|
| 98 |
-
response.set_cookie('refresh_token', '', expires=0, path='/')
|
| 99 |
-
|
| 100 |
# Add CORS headers for all allowed origins
|
| 101 |
for origin in allowed_origins:
|
| 102 |
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 103 |
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 104 |
-
|
| 105 |
return response, 401
|
| 106 |
-
|
| 107 |
@jwt.invalid_token_loader
|
| 108 |
def invalid_token_callback(error):
|
| 109 |
"""Handle invalid tokens."""
|
| 110 |
response = jsonify({'success': False, 'message': 'Invalid token'})
|
| 111 |
-
response.set_cookie('access_token', '', expires=0, path='/')
|
| 112 |
-
response.set_cookie('refresh_token', '', expires=0, path='/')
|
| 113 |
-
|
| 114 |
# Add CORS headers for all allowed origins
|
| 115 |
for origin in allowed_origins:
|
| 116 |
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 117 |
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 118 |
-
|
| 119 |
return response, 401
|
| 120 |
-
|
| 121 |
@jwt.unauthorized_loader
|
| 122 |
def missing_token_callback(error):
|
| 123 |
"""Handle missing tokens."""
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
if token:
|
| 127 |
-
# Instead of modifying immutable headers, we'll create a custom response
|
| 128 |
-
# that includes the token in the response data
|
| 129 |
-
# This approach avoids the immutable headers error
|
| 130 |
-
response = jsonify({
|
| 131 |
-
'success': False,
|
| 132 |
-
'message': 'Token found in cookies but not in headers',
|
| 133 |
-
'cookie_token_available': True
|
| 134 |
-
})
|
| 135 |
-
|
| 136 |
-
# Add CORS headers for all allowed origins
|
| 137 |
-
for origin in allowed_origins:
|
| 138 |
-
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 139 |
-
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 140 |
-
|
| 141 |
-
return response, 401
|
| 142 |
-
|
| 143 |
-
response = jsonify({'success': False, 'message': 'Missing token'})
|
| 144 |
-
|
| 145 |
# Add CORS headers for all allowed origins
|
| 146 |
for origin in allowed_origins:
|
| 147 |
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 148 |
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 149 |
-
|
| 150 |
return response, 401
|
| 151 |
-
|
| 152 |
return jwt
|
|
|
|
| 1 |
from flask import Flask, request, jsonify
|
| 2 |
+
from flask_jwt_extended import JWTManager
|
|
|
|
| 3 |
import hashlib
|
| 4 |
import secrets
|
| 5 |
+
import os
|
| 6 |
+
from datetime import timedelta
|
| 7 |
|
| 8 |
def setup_secure_cookies(app: Flask):
|
| 9 |
"""Setup secure cookie configuration for the Flask app."""
|
|
|
|
| 13 |
app.config.get('ENV') == 'development' or
|
| 14 |
app.config.get('ENVIRONMENT') == 'development'
|
| 15 |
)
|
| 16 |
+
|
| 17 |
# Check if we're running in Hugging Face Spaces
|
| 18 |
is_huggingface = os.environ.get('SPACE_ID') is not None
|
| 19 |
+
|
| 20 |
+
# Configure JWT cookie settings in app config
|
| 21 |
+
app.config['JWT_TOKEN_LOCATION'] = ['cookies', 'headers']
|
| 22 |
+
app.config['JWT_ACCESS_COOKIE_PATH'] = '/api'
|
| 23 |
+
app.config['JWT_REFRESH_COOKIE_PATH'] = '/api/auth/refresh'
|
| 24 |
+
app.config['JWT_COOKIE_CSRF_PROTECT'] = True # Enable CSRF protection
|
| 25 |
+
app.config['JWT_CSRF_IN_COOKIES'] = True
|
| 26 |
+
app.config['JWT_ACCESS_COOKIE_NAME'] = 'access_token'
|
| 27 |
+
app.config['JWT_REFRESH_COOKIE_NAME'] = 'refresh_token'
|
| 28 |
+
app.config['JWT_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
|
| 29 |
+
app.config['JWT_COOKIE_SECURE'] = not is_development # Secure in production only
|
| 30 |
+
|
| 31 |
@app.after_request
|
| 32 |
def set_secure_cookies(response):
|
| 33 |
"""Set secure cookies for all responses."""
|
|
|
|
| 37 |
token = request.headers.get('Authorization')
|
| 38 |
if token and token.startswith('Bearer '):
|
| 39 |
token = token[7:] # Remove 'Bearer ' prefix
|
| 40 |
+
|
| 41 |
# Determine cookie security settings
|
| 42 |
secure_cookie = not is_development
|
| 43 |
samesite_policy = 'Lax' if is_huggingface else 'Strict'
|
| 44 |
+
|
| 45 |
# Set secure cookie for access token
|
| 46 |
response.set_cookie(
|
| 47 |
'access_token',
|
| 48 |
token,
|
| 49 |
httponly=True, # Prevent XSS attacks
|
| 50 |
secure=secure_cookie, # Send over HTTPS in production/HF Spaces
|
| 51 |
+
samesite=samesite_policy, # CSRF protection
|
| 52 |
max_age=3600, # 1 hour (matches default JWT expiration)
|
| 53 |
+
path='/api', # Restrict to API routes
|
| 54 |
+
domain=None # Don't set domain for cross-origin security
|
| 55 |
)
|
| 56 |
+
|
| 57 |
# Safely check for rememberMe in JSON data
|
| 58 |
remember_me = False
|
| 59 |
try:
|
|
|
|
| 64 |
except:
|
| 65 |
# If there's any error parsing JSON, default to False
|
| 66 |
remember_me = False
|
| 67 |
+
|
| 68 |
# Set remember me cookie if requested
|
| 69 |
if remember_me:
|
| 70 |
response.set_cookie(
|
|
|
|
| 74 |
secure=secure_cookie,
|
| 75 |
samesite=samesite_policy,
|
| 76 |
max_age=7*24*60*60, # 7 days
|
| 77 |
+
path='/api/auth/refresh', # Restrict to refresh endpoint
|
| 78 |
+
domain=None # Don't set domain for cross-origin security
|
| 79 |
)
|
| 80 |
+
|
| 81 |
return response
|
| 82 |
|
| 83 |
return app
|
|
|
|
| 85 |
def configure_jwt_with_cookies(app: Flask):
|
| 86 |
"""Configure JWT to work with cookies."""
|
| 87 |
jwt = JWTManager(app)
|
| 88 |
+
|
| 89 |
# Get allowed origins from CORS configuration
|
| 90 |
allowed_origins = [
|
| 91 |
'http://localhost:3000',
|
|
|
|
| 95 |
'http://192.168.1.4:3000',
|
| 96 |
'https://zelyanoth-lin-cbfcff2.hf.space'
|
| 97 |
]
|
| 98 |
+
|
| 99 |
@jwt.token_verification_loader
|
| 100 |
def verify_token_on_refresh_callback(jwt_header, jwt_payload):
|
| 101 |
"""Verify token and refresh if needed."""
|
| 102 |
# This is a simplified version - in production, you'd check a refresh token
|
| 103 |
return True
|
| 104 |
+
|
| 105 |
@jwt.expired_token_loader
|
| 106 |
def expired_token_callback(jwt_header, jwt_payload):
|
| 107 |
"""Handle expired tokens."""
|
| 108 |
# Clear cookies when token expires
|
| 109 |
response = jsonify({'success': False, 'message': 'Token has expired'})
|
| 110 |
+
response.set_cookie('access_token', '', expires=0, path='/api', httponly=True, samesite='Lax')
|
| 111 |
+
response.set_cookie('refresh_token', '', expires=0, path='/api/auth/refresh', httponly=True, samesite='Lax')
|
| 112 |
+
|
| 113 |
# Add CORS headers for all allowed origins
|
| 114 |
for origin in allowed_origins:
|
| 115 |
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 116 |
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 117 |
+
|
| 118 |
return response, 401
|
| 119 |
+
|
| 120 |
@jwt.invalid_token_loader
|
| 121 |
def invalid_token_callback(error):
|
| 122 |
"""Handle invalid tokens."""
|
| 123 |
response = jsonify({'success': False, 'message': 'Invalid token'})
|
| 124 |
+
response.set_cookie('access_token', '', expires=0, path='/api', httponly=True, samesite='Lax')
|
| 125 |
+
response.set_cookie('refresh_token', '', expires=0, path='/api/auth/refresh', httponly=True, samesite='Lax')
|
| 126 |
+
|
| 127 |
# Add CORS headers for all allowed origins
|
| 128 |
for origin in allowed_origins:
|
| 129 |
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 130 |
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 131 |
+
|
| 132 |
return response, 401
|
| 133 |
+
|
| 134 |
@jwt.unauthorized_loader
|
| 135 |
def missing_token_callback(error):
|
| 136 |
"""Handle missing tokens."""
|
| 137 |
+
response = jsonify({'success': False, 'message': 'Authorization token required'})
|
| 138 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
# Add CORS headers for all allowed origins
|
| 140 |
for origin in allowed_origins:
|
| 141 |
response.headers.add('Access-Control-Allow-Origin', origin)
|
| 142 |
response.headers.add('Access-Control-Allow-Credentials', 'true')
|
| 143 |
+
|
| 144 |
return response, 401
|
| 145 |
+
|
| 146 |
return jwt
|
|
@@ -1,21 +1,26 @@
|
|
| 1 |
from supabase import create_client, Client
|
| 2 |
import os
|
| 3 |
import logging
|
|
|
|
| 4 |
|
| 5 |
def init_supabase(url: str, key: str) -> Client:
|
| 6 |
"""
|
| 7 |
Initialize Supabase client.
|
| 8 |
-
|
| 9 |
Args:
|
| 10 |
url (str): Supabase project URL
|
| 11 |
key (str): Supabase API key
|
| 12 |
-
|
| 13 |
Returns:
|
| 14 |
Client: Supabase client instance
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
if not url or not key:
|
| 17 |
raise ValueError("Supabase URL and key must be provided")
|
| 18 |
-
|
| 19 |
try:
|
| 20 |
client = create_client(url, key)
|
| 21 |
logging.info("Supabase client initialized successfully")
|
|
@@ -24,65 +29,79 @@ def init_supabase(url: str, key: str) -> Client:
|
|
| 24 |
logging.error(f"Failed to initialize Supabase client: {str(e)}")
|
| 25 |
raise e
|
| 26 |
|
| 27 |
-
def get_user_by_email(supabase: Client, email: str):
|
| 28 |
"""
|
| 29 |
Get user by email from Supabase Auth.
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
Args:
|
| 32 |
supabase (Client): Supabase client instance
|
| 33 |
email (str): User email
|
| 34 |
-
|
| 35 |
Returns:
|
| 36 |
dict: User data or None if not found
|
| 37 |
"""
|
| 38 |
try:
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
return None
|
| 46 |
|
| 47 |
-
def create_user(supabase: Client, email: str, password: str):
|
| 48 |
"""
|
| 49 |
Create a new user in Supabase Auth.
|
| 50 |
-
|
| 51 |
Args:
|
| 52 |
supabase (Client): Supabase client instance
|
| 53 |
email (str): User email
|
| 54 |
password (str): User password
|
| 55 |
-
|
| 56 |
Returns:
|
| 57 |
dict: User creation response
|
|
|
|
|
|
|
|
|
|
| 58 |
"""
|
| 59 |
try:
|
| 60 |
response = supabase.auth.sign_up({
|
| 61 |
"email": email,
|
| 62 |
"password": password
|
| 63 |
})
|
|
|
|
| 64 |
return response
|
| 65 |
except Exception as e:
|
|
|
|
| 66 |
raise e
|
| 67 |
|
| 68 |
-
def authenticate_user(supabase: Client, email: str, password: str):
|
| 69 |
"""
|
| 70 |
Authenticate user with email and password.
|
| 71 |
-
|
| 72 |
Args:
|
| 73 |
supabase (Client): Supabase client instance
|
| 74 |
email (str): User email
|
| 75 |
password (str): User password
|
| 76 |
-
|
| 77 |
Returns:
|
| 78 |
-
dict: Authentication response
|
|
|
|
|
|
|
|
|
|
| 79 |
"""
|
| 80 |
try:
|
| 81 |
response = supabase.auth.sign_in_with_password({
|
| 82 |
"email": email,
|
| 83 |
"password": password
|
| 84 |
})
|
| 85 |
-
|
|
|
|
| 86 |
return response
|
| 87 |
except Exception as e:
|
| 88 |
logging.error(f"Authentication error for user {email}: {str(e)}")
|
|
@@ -90,22 +109,45 @@ def authenticate_user(supabase: Client, email: str, password: str):
|
|
| 90 |
|
| 91 |
def check_database_connection(supabase: Client) -> bool:
|
| 92 |
"""
|
| 93 |
-
Check if the database connection is working.
|
| 94 |
-
|
| 95 |
Args:
|
| 96 |
supabase (Client): Supabase client instance
|
| 97 |
-
|
| 98 |
Returns:
|
| 99 |
bool: True if connection is working, False otherwise
|
| 100 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
try:
|
| 102 |
response = supabase.auth.get_user()
|
| 103 |
if response.user:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
| 109 |
except Exception as e:
|
| 110 |
-
logging.error(f"
|
| 111 |
-
return
|
|
|
|
| 1 |
from supabase import create_client, Client
|
| 2 |
import os
|
| 3 |
import logging
|
| 4 |
+
from typing import Optional, Dict, Any
|
| 5 |
|
| 6 |
def init_supabase(url: str, key: str) -> Client:
|
| 7 |
"""
|
| 8 |
Initialize Supabase client.
|
| 9 |
+
|
| 10 |
Args:
|
| 11 |
url (str): Supabase project URL
|
| 12 |
key (str): Supabase API key
|
| 13 |
+
|
| 14 |
Returns:
|
| 15 |
Client: Supabase client instance
|
| 16 |
+
|
| 17 |
+
Raises:
|
| 18 |
+
ValueError: If URL or key is missing
|
| 19 |
+
Exception: If client initialization fails
|
| 20 |
"""
|
| 21 |
if not url or not key:
|
| 22 |
raise ValueError("Supabase URL and key must be provided")
|
| 23 |
+
|
| 24 |
try:
|
| 25 |
client = create_client(url, key)
|
| 26 |
logging.info("Supabase client initialized successfully")
|
|
|
|
| 29 |
logging.error(f"Failed to initialize Supabase client: {str(e)}")
|
| 30 |
raise e
|
| 31 |
|
| 32 |
+
def get_user_by_email(supabase: Client, email: str) -> Optional[Dict[Any, Any]]:
|
| 33 |
"""
|
| 34 |
Get user by email from Supabase Auth.
|
| 35 |
+
Note: This approach is not recommended for checking user existence.
|
| 36 |
+
Instead, use profiles table lookup or Supabase's built-in user management functions.
|
| 37 |
+
|
| 38 |
Args:
|
| 39 |
supabase (Client): Supabase client instance
|
| 40 |
email (str): User email
|
| 41 |
+
|
| 42 |
Returns:
|
| 43 |
dict: User data or None if not found
|
| 44 |
"""
|
| 45 |
try:
|
| 46 |
+
# More appropriate way to check user existence would be to query the profiles table
|
| 47 |
+
# since sign_in_with_password requires a password
|
| 48 |
+
response = supabase.table("profiles").select("*").eq("email", email).execute()
|
| 49 |
+
if response.data:
|
| 50 |
+
# Return the first matched user
|
| 51 |
+
return response.data[0]
|
| 52 |
+
return None
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logging.error(f"Error getting user by email {email}: {str(e)}")
|
| 55 |
return None
|
| 56 |
|
| 57 |
+
def create_user(supabase: Client, email: str, password: str) -> Dict[Any, Any]:
|
| 58 |
"""
|
| 59 |
Create a new user in Supabase Auth.
|
| 60 |
+
|
| 61 |
Args:
|
| 62 |
supabase (Client): Supabase client instance
|
| 63 |
email (str): User email
|
| 64 |
password (str): User password
|
| 65 |
+
|
| 66 |
Returns:
|
| 67 |
dict: User creation response
|
| 68 |
+
|
| 69 |
+
Raises:
|
| 70 |
+
Exception: If user creation fails
|
| 71 |
"""
|
| 72 |
try:
|
| 73 |
response = supabase.auth.sign_up({
|
| 74 |
"email": email,
|
| 75 |
"password": password
|
| 76 |
})
|
| 77 |
+
logging.info(f"Successfully created user with email: {email}")
|
| 78 |
return response
|
| 79 |
except Exception as e:
|
| 80 |
+
logging.error(f"Failed to create user with email {email}: {str(e)}")
|
| 81 |
raise e
|
| 82 |
|
| 83 |
+
def authenticate_user(supabase: Client, email: str, password: str) -> Dict[Any, Any]:
|
| 84 |
"""
|
| 85 |
Authenticate user with email and password.
|
| 86 |
+
|
| 87 |
Args:
|
| 88 |
supabase (Client): Supabase client instance
|
| 89 |
email (str): User email
|
| 90 |
password (str): User password
|
| 91 |
+
|
| 92 |
Returns:
|
| 93 |
+
dict: Authentication response with user data
|
| 94 |
+
|
| 95 |
+
Raises:
|
| 96 |
+
Exception: If authentication fails
|
| 97 |
"""
|
| 98 |
try:
|
| 99 |
response = supabase.auth.sign_in_with_password({
|
| 100 |
"email": email,
|
| 101 |
"password": password
|
| 102 |
})
|
| 103 |
+
|
| 104 |
+
logging.info(f"Successfully authenticated user: {email}")
|
| 105 |
return response
|
| 106 |
except Exception as e:
|
| 107 |
logging.error(f"Authentication error for user {email}: {str(e)}")
|
|
|
|
| 109 |
|
| 110 |
def check_database_connection(supabase: Client) -> bool:
|
| 111 |
"""
|
| 112 |
+
Check if the database connection is working by performing a simple query.
|
| 113 |
+
|
| 114 |
Args:
|
| 115 |
supabase (Client): Supabase client instance
|
| 116 |
+
|
| 117 |
Returns:
|
| 118 |
bool: True if connection is working, False otherwise
|
| 119 |
"""
|
| 120 |
+
try:
|
| 121 |
+
# Perform a simple query to check the connection
|
| 122 |
+
# This tests both the connection and basic functionality
|
| 123 |
+
response = supabase.from_("profiles").select("id").limit(1).execute()
|
| 124 |
+
|
| 125 |
+
logging.info("Database connection check: SUCCESS")
|
| 126 |
+
return True
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logging.error(f"Database connection check: FAILED - {str(e)}")
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
def get_user_session(supabase: Client) -> Optional[Dict[Any, Any]]:
|
| 132 |
+
"""
|
| 133 |
+
Get current user session if available.
|
| 134 |
+
|
| 135 |
+
Args:
|
| 136 |
+
supabase (Client): Supabase client instance
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
dict: Session data or None if no session
|
| 140 |
+
"""
|
| 141 |
try:
|
| 142 |
response = supabase.auth.get_user()
|
| 143 |
if response.user:
|
| 144 |
+
return {
|
| 145 |
+
'user_id': response.user.id,
|
| 146 |
+
'email': response.user.email,
|
| 147 |
+
'created_at': response.user.created_at,
|
| 148 |
+
'email_confirmed_at': response.user.email_confirmed_at,
|
| 149 |
+
}
|
| 150 |
+
return None
|
| 151 |
except Exception as e:
|
| 152 |
+
logging.error(f"Error getting user session: {str(e)}")
|
| 153 |
+
return None
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gunicorn configuration file
|
| 2 |
+
import multiprocessing
|
| 3 |
+
|
| 4 |
+
# Server socket
|
| 5 |
+
bind = "0.0.0.0:7860" # Use port 7860 to match the Dockerfile CMD
|
| 6 |
+
backlog = 2048
|
| 7 |
+
|
| 8 |
+
# Worker processes
|
| 9 |
+
workers = 2 * multiprocessing.cpu_count() + 1 # Recommended formula
|
| 10 |
+
worker_class = "sync"
|
| 11 |
+
worker_connections = 1000
|
| 12 |
+
timeout = 120 # Increase timeout to handle long-running tasks
|
| 13 |
+
keepalive = 2
|
| 14 |
+
max_requests = 1000
|
| 15 |
+
max_requests_jitter = 100
|
| 16 |
+
|
| 17 |
+
# Logging
|
| 18 |
+
accesslog = "-"
|
| 19 |
+
errorlog = "-"
|
| 20 |
+
loglevel = "info"
|
| 21 |
+
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
| 22 |
+
|
| 23 |
+
# Process naming
|
| 24 |
+
proc_name = "gunicorn_app"
|
| 25 |
+
|
| 26 |
+
# Server mechanics
|
| 27 |
+
preload_app = True
|
| 28 |
+
daemon = False
|
| 29 |
+
pidfile = "/tmp/gunicorn.pid"
|
| 30 |
+
user = None
|
| 31 |
+
group = None
|
| 32 |
+
tmp_upload_dir = None
|
| 33 |
+
|
| 34 |
+
# SSL
|
| 35 |
+
keyfile = None
|
| 36 |
+
certfile = None
|
|
@@ -3,7 +3,8 @@ Flask-CORS>=5.0.1
|
|
| 3 |
Flask-JWT-Extended>=4.7.1
|
| 4 |
Flask-SQLAlchemy>=3.1.1
|
| 5 |
Flask-Migrate>=4.1.0
|
| 6 |
-
|
|
|
|
| 7 |
feedparser
|
| 8 |
# Environment management
|
| 9 |
python-dotenv>=1.0.1
|
|
@@ -26,12 +27,16 @@ supabase>=2.16.0
|
|
| 26 |
|
| 27 |
# Security
|
| 28 |
bcrypt>=4.3.0
|
|
|
|
| 29 |
|
| 30 |
# Task queue
|
| 31 |
celery>=5.5.3
|
| 32 |
-
django-celery-beat>=2.8.1
|
| 33 |
redis>=6.4.0
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
# Testing
|
| 36 |
pytest>=8.4.1
|
| 37 |
pytest-cov>=6.2.1
|
|
|
|
| 3 |
Flask-JWT-Extended>=4.7.1
|
| 4 |
Flask-SQLAlchemy>=3.1.1
|
| 5 |
Flask-Migrate>=4.1.0
|
| 6 |
+
Flask-Limiter>=3.8.0
|
| 7 |
+
Pillow>=11.1.0
|
| 8 |
feedparser
|
| 9 |
# Environment management
|
| 10 |
python-dotenv>=1.0.1
|
|
|
|
| 27 |
|
| 28 |
# Security
|
| 29 |
bcrypt>=4.3.0
|
| 30 |
+
email-validator>=2.2.0
|
| 31 |
|
| 32 |
# Task queue
|
| 33 |
celery>=5.5.3
|
| 34 |
+
django-celery-beat>=2.8.1
|
| 35 |
redis>=6.4.0
|
| 36 |
|
| 37 |
+
# WSGI server
|
| 38 |
+
gunicorn>=23.0.0
|
| 39 |
+
|
| 40 |
# Testing
|
| 41 |
pytest>=8.4.1
|
| 42 |
pytest-cov>=6.2.1
|
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python
|
| 2 |
-
"""
|
| 3 |
-
Entry point for the Lin application.
|
| 4 |
-
This script starts the Flask application with APScheduler.
|
| 5 |
-
"""
|
| 6 |
-
import os
|
| 7 |
-
import sys
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
|
| 10 |
-
if __name__ == "__main__":
|
| 11 |
-
# Set the port for Hugging Face Spaces
|
| 12 |
-
port = os.environ.get('PORT', '7860')
|
| 13 |
-
os.environ.setdefault('PORT', port)
|
| 14 |
-
|
| 15 |
-
print(f"Starting Lin application on port {port}...")
|
| 16 |
-
print("=" * 60)
|
| 17 |
-
|
| 18 |
-
try:
|
| 19 |
-
# Import and run the backend Flask app directly
|
| 20 |
-
from backend.app import create_app
|
| 21 |
-
app = create_app()
|
| 22 |
-
|
| 23 |
-
print("=" * 60)
|
| 24 |
-
print("Flask application starting...")
|
| 25 |
-
print("Access the application at:")
|
| 26 |
-
print(f" http://localhost:{port}")
|
| 27 |
-
print(f" http://127.0.0.1:{port}")
|
| 28 |
-
print("=" * 60)
|
| 29 |
-
|
| 30 |
-
app.run(
|
| 31 |
-
host='0.0.0.0',
|
| 32 |
-
port=int(port),
|
| 33 |
-
debug=False,
|
| 34 |
-
threaded=True
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
except KeyboardInterrupt:
|
| 38 |
-
print("\nShutting down application...")
|
| 39 |
-
# Shutdown scheduler if it exists
|
| 40 |
-
if hasattr(app, 'scheduler'):
|
| 41 |
-
app.scheduler.shutdown()
|
| 42 |
-
sys.exit(0)
|
| 43 |
-
except Exception as e:
|
| 44 |
-
print(f"Failed to start Lin application: {e}")
|
| 45 |
-
import traceback
|
| 46 |
-
traceback.print_exc()
|
| 47 |
-
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -10,25 +10,25 @@ import subprocess
|
|
| 10 |
def start_celery_worker():
|
| 11 |
"""Start the Celery worker."""
|
| 12 |
print("Starting Celery worker...")
|
| 13 |
-
os.system("celery -A backend.celery_app
|
| 14 |
|
| 15 |
def start_celery_beat():
|
| 16 |
"""Start the Celery beat scheduler."""
|
| 17 |
print("Starting Celery beat scheduler...")
|
| 18 |
-
os.system("celery -A backend.celery_beat_config
|
| 19 |
|
| 20 |
def start_celery_flower():
|
| 21 |
"""Start the Celery flower monitoring tool."""
|
| 22 |
print("Starting Celery flower...")
|
| 23 |
-
os.system("celery -A backend.celery_app
|
| 24 |
|
| 25 |
if __name__ == "__main__":
|
| 26 |
if len(sys.argv) < 2:
|
| 27 |
print("Usage: python start_celery.py [worker|beat|flower|all]")
|
| 28 |
sys.exit(1)
|
| 29 |
-
|
| 30 |
command = sys.argv[1]
|
| 31 |
-
|
| 32 |
if command == "worker":
|
| 33 |
start_celery_worker()
|
| 34 |
elif command == "beat":
|
|
@@ -38,12 +38,12 @@ if __name__ == "__main__":
|
|
| 38 |
elif command == "all":
|
| 39 |
# Start worker and beat in background processes
|
| 40 |
worker_process = subprocess.Popen([
|
| 41 |
-
"celery", "-A", "backend.celery_app
|
| 42 |
])
|
| 43 |
beat_process = subprocess.Popen([
|
| 44 |
-
"celery", "-A", "backend.celery_beat_config
|
| 45 |
])
|
| 46 |
-
|
| 47 |
try:
|
| 48 |
worker_process.wait()
|
| 49 |
beat_process.wait()
|
|
|
|
| 10 |
def start_celery_worker():
|
| 11 |
"""Start the Celery worker."""
|
| 12 |
print("Starting Celery worker...")
|
| 13 |
+
os.system("celery -A backend.celery_app worker --loglevel=info")
|
| 14 |
|
| 15 |
def start_celery_beat():
|
| 16 |
"""Start the Celery beat scheduler."""
|
| 17 |
print("Starting Celery beat scheduler...")
|
| 18 |
+
os.system("celery -A backend.celery_beat_config beat --loglevel=info")
|
| 19 |
|
| 20 |
def start_celery_flower():
|
| 21 |
"""Start the Celery flower monitoring tool."""
|
| 22 |
print("Starting Celery flower...")
|
| 23 |
+
os.system("celery -A backend.celery_app flower")
|
| 24 |
|
| 25 |
if __name__ == "__main__":
|
| 26 |
if len(sys.argv) < 2:
|
| 27 |
print("Usage: python start_celery.py [worker|beat|flower|all]")
|
| 28 |
sys.exit(1)
|
| 29 |
+
|
| 30 |
command = sys.argv[1]
|
| 31 |
+
|
| 32 |
if command == "worker":
|
| 33 |
start_celery_worker()
|
| 34 |
elif command == "beat":
|
|
|
|
| 38 |
elif command == "all":
|
| 39 |
# Start worker and beat in background processes
|
| 40 |
worker_process = subprocess.Popen([
|
| 41 |
+
"celery", "-A", "backend.celery_app", "worker", "--loglevel=info"
|
| 42 |
])
|
| 43 |
beat_process = subprocess.Popen([
|
| 44 |
+
"celery", "-A", "backend.celery_beat_config", "beat", "--loglevel=info"
|
| 45 |
])
|
| 46 |
+
|
| 47 |
try:
|
| 48 |
worker_process.wait()
|
| 49 |
beat_process.wait()
|
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
Start script for running the Lin application with Gunicorn.
|
| 4 |
+
This is primarily for local development and testing of the Gunicorn configuration.
|
| 5 |
+
For production, the application is run via the Dockerfile CMD.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python start_gunicorn.py
|
| 9 |
+
|
| 10 |
+
Note: This is for local testing only. The production setup uses Gunicorn
|
| 11 |
+
directly as specified in the gunicorn.conf.py file.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import subprocess
|
| 15 |
+
import sys
|
| 16 |
+
import os
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
"""Start the application using Gunicorn."""
|
| 21 |
+
print("Starting Lin application with Gunicorn...")
|
| 22 |
+
print("Note: This is for local testing. Production uses Docker with Gunicorn directly.")
|
| 23 |
+
print("-" * 60)
|
| 24 |
+
|
| 25 |
+
# Change to the project root directory
|
| 26 |
+
project_root = Path(__file__).parent
|
| 27 |
+
os.chdir(project_root)
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
# Run gunicorn with the configuration file - use the correct app path
|
| 31 |
+
cmd = [
|
| 32 |
+
"gunicorn",
|
| 33 |
+
"--config",
|
| 34 |
+
str(project_root / "gunicorn.conf.py"), # Use the main gunicorn config
|
| 35 |
+
"backend.app:create_app()"
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
print(f"Running command: {' '.join(cmd)}")
|
| 39 |
+
print("-" * 60)
|
| 40 |
+
|
| 41 |
+
# Start Gunicorn
|
| 42 |
+
process = subprocess.Popen(cmd)
|
| 43 |
+
process.wait()
|
| 44 |
+
|
| 45 |
+
except KeyboardInterrupt:
|
| 46 |
+
print("\nShutting down Gunicorn server...")
|
| 47 |
+
sys.exit(0)
|
| 48 |
+
except Exception as e:
|
| 49 |
+
print(f"Failed to start Gunicorn: {e}")
|
| 50 |
+
import traceback
|
| 51 |
+
traceback.print_exc()
|
| 52 |
+
sys.exit(1)
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
main()
|
|
@@ -1,145 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Lin - Community Manager Assistant for LinkedIn
|
| 4 |
-
Universal startup script for development servers
|
| 5 |
-
"""
|
| 6 |
-
import os
|
| 7 |
-
import sys
|
| 8 |
-
import subprocess
|
| 9 |
-
import platform
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
|
| 12 |
-
def is_windows():
|
| 13 |
-
"""Check if the current OS is Windows"""
|
| 14 |
-
return platform.system().lower() == 'windows'
|
| 15 |
-
|
| 16 |
-
def run_command(command, cwd=None, shell=False):
|
| 17 |
-
"""Run a command and stream output"""
|
| 18 |
-
try:
|
| 19 |
-
# For Windows, we need to use shell=True for certain commands
|
| 20 |
-
use_shell = shell or is_windows()
|
| 21 |
-
|
| 22 |
-
process = subprocess.Popen(
|
| 23 |
-
command,
|
| 24 |
-
cwd=cwd,
|
| 25 |
-
shell=use_shell,
|
| 26 |
-
stdout=subprocess.PIPE,
|
| 27 |
-
stderr=subprocess.STDOUT,
|
| 28 |
-
text=True,
|
| 29 |
-
bufsize=1,
|
| 30 |
-
universal_newlines=True
|
| 31 |
-
)
|
| 32 |
-
|
| 33 |
-
# Stream output in real-time
|
| 34 |
-
for line in process.stdout:
|
| 35 |
-
print(line, end='')
|
| 36 |
-
|
| 37 |
-
process.wait()
|
| 38 |
-
return process.returncode == 0
|
| 39 |
-
except Exception as e:
|
| 40 |
-
print(f"Error running command: {e}")
|
| 41 |
-
return False
|
| 42 |
-
|
| 43 |
-
def start_frontend():
|
| 44 |
-
"""Start the React frontend development server"""
|
| 45 |
-
print("Starting frontend development server...")
|
| 46 |
-
if is_windows():
|
| 47 |
-
cmd = "npm run dev:frontend"
|
| 48 |
-
else:
|
| 49 |
-
cmd = ["npm", "run", "dev:frontend"]
|
| 50 |
-
return run_command(cmd, shell=True)
|
| 51 |
-
|
| 52 |
-
def start_backend():
|
| 53 |
-
"""Start the Flask backend server"""
|
| 54 |
-
print("Starting backend server...")
|
| 55 |
-
|
| 56 |
-
# Set environment variables and run the app
|
| 57 |
-
env = os.environ.copy()
|
| 58 |
-
# Add the project root to PYTHONPATH to ensure imports work correctly
|
| 59 |
-
env["PYTHONPATH"] = str(Path(__file__).parent)
|
| 60 |
-
|
| 61 |
-
try:
|
| 62 |
-
# Run the Flask app directly from the root directory
|
| 63 |
-
process = subprocess.Popen(
|
| 64 |
-
[sys.executable, "app.py"],
|
| 65 |
-
cwd=Path(__file__).parent,
|
| 66 |
-
env=env,
|
| 67 |
-
stdout=subprocess.PIPE,
|
| 68 |
-
stderr=subprocess.STDOUT,
|
| 69 |
-
text=True,
|
| 70 |
-
bufsize=1,
|
| 71 |
-
universal_newlines=True
|
| 72 |
-
)
|
| 73 |
-
|
| 74 |
-
# Stream output
|
| 75 |
-
for line in process.stdout:
|
| 76 |
-
print(line, end='')
|
| 77 |
-
|
| 78 |
-
process.wait()
|
| 79 |
-
return process.returncode == 0
|
| 80 |
-
except Exception as e:
|
| 81 |
-
print(f"Error starting backend: {e}")
|
| 82 |
-
return False
|
| 83 |
-
|
| 84 |
-
def start_all():
|
| 85 |
-
"""Start both frontend and backend using npm concurrently script"""
|
| 86 |
-
print("Starting both frontend and backend servers...")
|
| 87 |
-
# Use the frontend's concurrently script to run both servers
|
| 88 |
-
if is_windows():
|
| 89 |
-
cmd = "cd frontend && npx concurrently \"npm run dev:frontend\" \"npm run dev:backend\""
|
| 90 |
-
else:
|
| 91 |
-
cmd = "cd frontend && npx concurrently \"npm run dev:frontend\" \"npm run dev:backend\""
|
| 92 |
-
return run_command(cmd, shell=True)
|
| 93 |
-
|
| 94 |
-
def show_help():
|
| 95 |
-
"""Display usage information"""
|
| 96 |
-
help_text = """
|
| 97 |
-
Lin Development Launcher
|
| 98 |
-
|
| 99 |
-
Usage:
|
| 100 |
-
python starty.py [command]
|
| 101 |
-
|
| 102 |
-
Commands:
|
| 103 |
-
backend Start only the backend server (default)
|
| 104 |
-
frontend Start only the frontend development server
|
| 105 |
-
dev:all Start both frontend and backend servers
|
| 106 |
-
help Show this help message
|
| 107 |
-
|
| 108 |
-
Examples:
|
| 109 |
-
python starty.py # Start backend server
|
| 110 |
-
python starty.py backend # Start backend server
|
| 111 |
-
python starty.py frontend # Start frontend server
|
| 112 |
-
python starty.py dev:all # Start both servers
|
| 113 |
-
"""
|
| 114 |
-
print(help_text)
|
| 115 |
-
|
| 116 |
-
def main():
|
| 117 |
-
"""Main entry point"""
|
| 118 |
-
# Always ensure we're in the project root
|
| 119 |
-
project_root = Path(__file__).parent
|
| 120 |
-
os.chdir(project_root)
|
| 121 |
-
|
| 122 |
-
if len(sys.argv) < 2:
|
| 123 |
-
# Default behavior - start backend
|
| 124 |
-
start_backend()
|
| 125 |
-
return
|
| 126 |
-
|
| 127 |
-
command = sys.argv[1].lower()
|
| 128 |
-
|
| 129 |
-
if command in ['help', '--help', '-h']:
|
| 130 |
-
show_help()
|
| 131 |
-
elif command == 'frontend':
|
| 132 |
-
start_frontend()
|
| 133 |
-
elif command == 'backend':
|
| 134 |
-
start_backend()
|
| 135 |
-
elif command in ['dev:all', 'all', 'both']:
|
| 136 |
-
# Instead of calling npm run dev:all (which creates a loop),
|
| 137 |
-
# directly start both servers
|
| 138 |
-
start_all()
|
| 139 |
-
else:
|
| 140 |
-
print(f"Unknown command: {command}")
|
| 141 |
-
show_help()
|
| 142 |
-
sys.exit(1)
|
| 143 |
-
|
| 144 |
-
if __name__ == "__main__":
|
| 145 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|