diff --git a/backend/main.py b/backend/main.py index c030c67..1b7dc9d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -110,16 +110,89 @@ def check_docker_availability(): # ✅ ENHANCED: Flask app configuration with your .env variables app = Flask(__name__) + +class MissingSecretError(ValueError): + """Raised when a required secret is not set in environment variables.""" + pass + +def get_required_secret(env_var: str, description: str) -> str: + """ + Get required secret from environment. + + Args: + env_var: Name of the environment variable + description: Human-readable description of the secret + + Returns: + The secret value from the environment + + Raises: + MissingSecretError: If the environment variable is not set + """ + value = os.getenv(env_var) + if not value: + raise MissingSecretError(f"{description} ({env_var}) must be set in environment variables for security. Do not use default values for secrets.") + return value + +def get_dev_fallback_secret(name: str) -> str: + """ + Generate a persistent random secret for development use only. + + Stores the secret in a file in the system temp directory to persist across restarts. + Files are created with restrictive permissions (0600) to limit access. + + Args: + name: Unique identifier for this secret (used in filename) + + Returns: + A 64-character hex string (32 bytes of randomness) + + Security Note: + These secrets are stored in temp files and should only be used for development. + In production, always set proper secrets via environment variables. + """ + import tempfile + import stat + secret_file = os.path.join(tempfile.gettempdir(), f'.openlearnx_dev_{name}') + try: + if os.path.exists(secret_file): + with open(secret_file, 'r') as f: + return f.read().strip() + except Exception: + pass + # Generate new secret and persist it + new_secret = os.urandom(32).hex() + try: + with open(secret_file, 'w') as f: + f.write(new_secret) + # Set restrictive permissions (owner read/write only) + os.chmod(secret_file, stat.S_IRUSR | stat.S_IWUSR) + except Exception: + pass # If we can't persist, just return the generated secret + return new_secret + +# Validate required secrets at startup +try: + _secret_key = get_required_secret('SECRET_KEY', 'Flask secret key') + _jwt_secret_key = get_required_secret('JWT_SECRET_KEY', 'JWT secret key') + _admin_token = get_required_secret('ADMIN_TOKEN', 'Admin authentication token') +except MissingSecretError as e: + print(f"⚠️ SECURITY WARNING: {e}") + print("⚠️ Using persistent development secrets. Set proper secrets in production!") + _secret_key = os.getenv('SECRET_KEY') or get_dev_fallback_secret('secret_key') + _jwt_secret_key = os.getenv('JWT_SECRET_KEY') or get_dev_fallback_secret('jwt_secret_key') + _admin_token = os.getenv('ADMIN_TOKEN') or get_dev_fallback_secret('admin_token') + app.config.update( - SECRET_KEY=os.getenv('SECRET_KEY', 'your-super-secret-key-change-this-in-production-openlearnx-2024'), + SECRET_KEY=_secret_key, MONGODB_URI=os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'), WEB3_PROVIDER_URL=os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545'), CONTRACT_ADDRESS=os.getenv('CONTRACT_ADDRESS', '0x739f0aCef964f87Bc7974D972a811f8417d74B4C'), DEPLOYER_PRIVATE_KEY=os.getenv('DEPLOYER_PRIVATE_KEY'), MINTER_PRIVATE_KEY=os.getenv('MINTER_PRIVATE_KEY'), - ADMIN_TOKEN=os.getenv('ADMIN_TOKEN', 'admin-secret-key'), + ADMIN_TOKEN=_admin_token, # ✅ JWT Configuration from your .env - JWT_SECRET_KEY=os.getenv('JWT_SECRET_KEY', 'openlearnx-jwt-secret-key-change-in-production'), + JWT_SECRET_KEY=_jwt_secret_key, JWT_ACCESS_TOKEN_EXPIRES=timedelta(hours=int(os.getenv('JWT_EXPIRATION_HOURS', 168))), # ✅ IPFS Configuration from your .env IPFS_GATEWAY=os.getenv('IPFS_GATEWAY', 'https://ipfs.infura.io:5001'), diff --git a/backend/routes/admin.py b/backend/routes/admin.py index ca82ad0..d74505a 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -29,15 +29,12 @@ def admin_required(f): return jsonify({"error": "Invalid authorization format"}), 401 token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None - print(f"Extracted token: '{token}'") - # Check environment variable first, then fallback to default + # Check environment variable - no fallback for security expected_token = os.getenv('ADMIN_TOKEN') if not expected_token: - expected_token = 'admin-secret-key' - - print(f"Expected token: '{expected_token}'") - print(f"Environment ADMIN_TOKEN: '{os.getenv('ADMIN_TOKEN')}'") + print("❌ ADMIN_TOKEN environment variable not set") + return jsonify({"error": "Server configuration error: ADMIN_TOKEN not configured"}), 500 # Strip any whitespace from both tokens if token and expected_token: diff --git a/backend/routes/auth.py b/backend/routes/auth.py index bd7d18d..957157c 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -16,8 +16,33 @@ mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') client = MongoClient(mongo_uri) db = client.openlearnx -# JWT secret -JWT_SECRET = os.getenv('JWT_SECRET', 'your-secret-key-here') +# JWT secret - must be set via environment variable +JWT_SECRET = os.getenv('JWT_SECRET') +if not JWT_SECRET: + import warnings + import tempfile + import stat + import secrets as secrets_module + warnings.warn("JWT_SECRET environment variable not set. Using persistent dev secret.", UserWarning) + + def _generate_and_store_secret(): + """Generate a random secret and store it with restrictive permissions.""" + return secrets_module.token_hex(32) + + # Use persistent file-based secret for development to avoid invalidating tokens on restart + _secret_file = os.path.join(tempfile.gettempdir(), '.openlearnx_dev_jwt_secret_auth') + try: + if os.path.exists(_secret_file): + with open(_secret_file, 'r') as f: + JWT_SECRET = f.read().strip() + if not JWT_SECRET: + JWT_SECRET = _generate_and_store_secret() + with open(_secret_file, 'w') as f: + f.write(JWT_SECRET) + # Set restrictive permissions (owner read/write only) + os.chmod(_secret_file, stat.S_IRUSR | stat.S_IWUSR) + except Exception: + JWT_SECRET = _generate_and_store_secret() @bp.route('/nonce', methods=['POST', 'OPTIONS']) def get_nonce(): diff --git a/frontend/app/admin/login/page.tsx b/frontend/app/admin/login/page.tsx index 303ea23..73a9ca5 100644 --- a/frontend/app/admin/login/page.tsx +++ b/frontend/app/admin/login/page.tsx @@ -15,9 +15,9 @@ export default function AdminLogin() { // Check if already authenticated const checkExistingAuth = async () => { const token = localStorage.getItem('admin_token') - if (token === 'admin-secret-key') { + if (token) { try { - // Verify token with API + // Verify token with API - no hardcoded secret check const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { headers: { 'Authorization': `Bearer ${token}` } }) diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx index b3e2e9b..5dc3004 100644 --- a/frontend/app/admin/page.tsx +++ b/frontend/app/admin/page.tsx @@ -62,6 +62,26 @@ export default function AdminDashboard() { const router = useRouter() // Authentication logic + // Helper function to get admin token from localStorage + const getAdminToken = (): string | null => { + if (typeof window !== 'undefined') { + return localStorage.getItem('admin_token') + } + return null + } + + // Helper function to get authorization headers + const getAuthHeaders = (): Record => { + const token = getAdminToken() + const headers: Record = { + 'Content-Type': 'application/json' + } + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + return headers + } + useEffect(() => { setIsClient(true) @@ -69,31 +89,28 @@ export default function AdminDashboard() { try { await new Promise(resolve => setTimeout(resolve, 500)) - const token = localStorage.getItem('admin_token') + const token = getAdminToken() if (!token) { router.push('/admin/login') return } - if (token === 'admin-secret-key') { - try { - const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { - headers: { 'Authorization': `Bearer ${token}` } - }) - - if (response.ok) { - setIsAuthenticated(true) - fetchData() - } else { - localStorage.removeItem('admin_token') - router.push('/admin/login') - } - } catch (apiError) { + // Verify token with API - no hardcoded secret check + try { + const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (response.ok) { setIsAuthenticated(true) fetchData() + } else { + localStorage.removeItem('admin_token') + router.push('/admin/login') } - } else { + } catch (apiError) { + // If API is unavailable, don't allow access without verification localStorage.removeItem('admin_token') router.push('/admin/login') } @@ -114,10 +131,7 @@ export default function AdminDashboard() { const fetchCourses = async () => { try { const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { - headers: { - 'Authorization': 'Bearer admin-secret-key', - 'Content-Type': 'application/json' - } + headers: getAuthHeaders() }) if (!response.ok) { @@ -141,7 +155,7 @@ export default function AdminDashboard() { const fetchStats = async () => { try { const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', { - headers: { 'Authorization': 'Bearer admin-secret-key' } + headers: getAuthHeaders() }) if (response.ok) { const data = await response.json() @@ -161,10 +175,7 @@ export default function AdminDashboard() { console.log('🔍 Fetching modules for course:', courseId) // Debug log const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, { - headers: { - 'Authorization': 'Bearer admin-secret-key', - 'Content-Type': 'application/json' - } + headers: getAuthHeaders() }) console.log('🔍 Modules response status:', response.status) // Debug log @@ -209,10 +220,7 @@ export default function AdminDashboard() { console.log('🔍 Fetching lessons for module:', moduleId) // Debug log const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, { - headers: { - 'Authorization': 'Bearer admin-secret-key', - 'Content-Type': 'application/json' - } + headers: getAuthHeaders() }) console.log('🔍 Lessons response status:', response.status) // Debug log @@ -253,10 +261,7 @@ export default function AdminDashboard() { try { const response = await fetch('http://127.0.0.1:5000/api/admin/courses', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer admin-secret-key' - }, + headers: getAuthHeaders(), body: JSON.stringify(formData) }) @@ -277,10 +282,7 @@ export default function AdminDashboard() { try { const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, { method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer admin-secret-key' - }, + headers: getAuthHeaders(), body: JSON.stringify(formData) }) @@ -301,7 +303,7 @@ export default function AdminDashboard() { try { const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, { method: 'DELETE', - headers: { 'Authorization': 'Bearer admin-secret-key' } + headers: getAuthHeaders() }) if (response.ok) { @@ -333,10 +335,7 @@ export default function AdminDashboard() { const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${selectedCourse?.id}/modules`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer admin-secret-key' - }, + headers: getAuthHeaders(), body: JSON.stringify(formData) }) @@ -358,10 +357,7 @@ export default function AdminDashboard() { const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer admin-secret-key' - }, + headers: getAuthHeaders(), body: JSON.stringify(formData) }) diff --git a/frontend/app/courses/[courseId]/page.tsx b/frontend/app/courses/[courseId]/page.tsx index 30c1898..3de18ad 100644 --- a/frontend/app/courses/[courseId]/page.tsx +++ b/frontend/app/courses/[courseId]/page.tsx @@ -108,37 +108,20 @@ export default function CoursePage() { let modulesData = null let modulesResponse = null + // Use public endpoint for course page (not admin endpoint) try { - modulesResponse = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, { + modulesResponse = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/modules`, { headers: { - 'Authorization': 'Bearer admin-secret-key', 'Content-Type': 'application/json' } }) if (modulesResponse.ok) { modulesData = await modulesResponse.json() - console.log('✅ Modules loaded from admin endpoint:', modulesData) - } - } catch (adminError) { - console.log('⚠️ Admin endpoint failed, trying public endpoint') - } - - if (!modulesData || !modulesResponse?.ok) { - try { - modulesResponse = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/modules`, { - headers: { - 'Content-Type': 'application/json' - } - }) - - if (modulesResponse.ok) { - modulesData = await modulesResponse.json() - console.log('✅ Modules loaded from public endpoint:', modulesData) - } - } catch (publicError) { - console.error('❌ Both module endpoints failed') + console.log('✅ Modules loaded from public endpoint:', modulesData) } + } catch (publicError) { + console.error('❌ Module endpoint failed') } if (modulesData) { @@ -185,21 +168,13 @@ export default function CoursePage() { try { console.log('🔍 Fetching lessons for module:', module.id) - let lessonsResponse = await fetch(`http://127.0.0.1:5000/api/admin/modules/${module.id}/lessons`, { + // Use public endpoint for course page (not admin endpoint) + const lessonsResponse = await fetch(`http://127.0.0.1:5000/api/modules/${module.id}/lessons`, { headers: { - 'Authorization': 'Bearer admin-secret-key', 'Content-Type': 'application/json' } }) - if (!lessonsResponse.ok) { - lessonsResponse = await fetch(`http://127.0.0.1:5000/api/modules/${module.id}/lessons`, { - headers: { - 'Content-Type': 'application/json' - } - }) - } - if (lessonsResponse.ok) { const lessonData = await lessonsResponse.json() console.log(`✅ Lessons loaded for module ${module.id}:`, lessonData)