""" auth.py – Lightweight authentication micro-service ---------------------------------------------------- • Exposes /auth/verify_user (login check) • Optional /auth/register_user for sign-up • Uses MySQL, bcrypt, and Pydantic • No Streamlit, cookies, or front-end logic """ from fastapi import FastAPI, Request, Response, Cookie, HTTPException, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.responses import HTMLResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.security import OAuth2PasswordRequestForm from jose import jwt, JWTError from pydantic import BaseModel from dotenv import load_dotenv import os import time import requests import os import pymysql import bcrypt import sys import importlib.util from services.api.db.token_utils import create_token, decode_token from sqlalchemy.orm import Session from contextlib import asynccontextmanager # Add parent directory to path to enable imports sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # Define lifespan context manager for startup/shutdown events @asynccontextmanager async def lifespan(app: FastAPI): # Startup print("Starting up FastAPI application...") # Start background tasks from upload endpoints try: # Import and start upload cleanup task from services.api.upload_endpoints import start_cleanup_task cleanup_task = start_cleanup_task() print("Upload cleanup task started successfully") except Exception as e: print(f"Failed to start upload cleanup task: {e}") yield # Shutdown print("Shutting down FastAPI application...") # Any cleanup can be done here # Create single FastAPI instance with lifespan app = FastAPI(lifespan=lifespan) # Configure CORS with all needed origins app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:8503", "http://localhost:3000", "http://127.0.0.1:3000", "https://tlong-ds.github.io", "https://tlong-ds.github.io/thelearninghouse/", "https://*.hf.space", "https://*.huggingface.co" ], allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], expose_headers=["*"] ) # Import API endpoints try: from services.api.api_endpoints import router as api_router except ImportError: # Try a different import strategy for direct module execution spec = importlib.util.spec_from_file_location( "api_endpoints", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "api_endpoints.py") ) api_endpoints = importlib.util.module_from_spec(spec) spec.loader.exec_module(api_endpoints) api_router = api_endpoints.router # Load environment variables load_dotenv() SECRET_KEY = os.getenv("SECRET_TOKEN", "dev-secret") ALGORITHM = os.getenv("ALGORITHM", "HS256") MYSQL_USER = os.getenv("MYSQL_USER") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD") MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") MYSQL_DB = os.getenv("MYSQL_DB") MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306")) # Import and include chat router try: from services.api.chat_endpoints import router as chat_router except ImportError: spec = importlib.util.spec_from_file_location( "chat_endpoints", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "chat_endpoints.py") ) chat_endpoints = importlib.util.module_from_spec(spec) spec.loader.exec_module(chat_endpoints) chat_router = chat_endpoints.router # Import and include upload router try: from services.api.upload_endpoints import router as upload_router except ImportError: spec = importlib.util.spec_from_file_location( "upload_endpoints", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "upload_endpoints.py") ) upload_endpoints = importlib.util.module_from_spec(spec) spec.loader.exec_module(upload_endpoints) upload_router = upload_endpoints.router # Debug print of available routes print("DEBUG: Available routes in api_router:") for route in api_router.routes: print(f"Route: {route.path}, Methods: {route.methods}") # Include API routers app.include_router(api_router, prefix="/api") app.include_router(chat_router, prefix="/api") app.include_router(upload_router, prefix="/api") class LoginPayload(BaseModel): username: str password: str role: str class PasswordChangePayload(BaseModel): current_password: str new_password: str class PasswordChangePayload(BaseModel): current_password: str new_password: str # Token functions moved to token_utils.py @app.post("/login") async def login(response: Response, payload: LoginPayload): try: print(f"Login attempt: {payload.username}, role: {payload.role}") # Directly verify the user credentials table_map = { "Learner": "Learners", "Instructor": "Instructors" } table = table_map.get(payload.role) if not table: print(f"Invalid role: {payload.role}") raise HTTPException(status_code=400, detail="Invalid role") conn = connect_db() user_id = None full_name = None try: with conn.cursor() as cur: # Get password, user ID, and full name for token creation if payload.role == "Learner": query = f"SELECT Password, LearnerID, LearnerName FROM {table} WHERE AccountName=%s LIMIT 1" else: # Instructor query = f"SELECT Password, InstructorID, InstructorName FROM {table} WHERE AccountName=%s LIMIT 1" print(f"Executing query: {query} with username: {payload.username}") cur.execute(query, (payload.username,)) row = cur.fetchone() if not row: print(f"No user found with username: {payload.username} in table: {table}") raise HTTPException(status_code=401, detail="Incorrect username or password") password_valid = check_password(payload.password, row[0]) user_id = row[1] # Get the LearnerID or InstructorID full_name = row[2] # Get the LearnerName or InstructorName print(f"Password check result: {password_valid}, User ID: {user_id}, Full Name: {full_name}") if not password_valid: print(f"Authentication failed: Invalid password") raise HTTPException(status_code=401, detail="Incorrect username or password") except Exception as db_err: print(f"Database error: {str(db_err)}") raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}") finally: conn.close() # User authenticated successfully - include user_id and full name in token user_data = { "username": payload.username, "role": payload.role, "user_id": user_id, "full_name": full_name } print(f"Authentication successful for: {payload.username} (ID: {user_id}, Name: {full_name})") token = create_token(user_data) # Set cookie with settings that work for both Chrome and Safari # For localhost development, we need different settings than production is_localhost = os.getenv("ENVIRONMENT", "development") == "development" response.set_cookie( key="auth_token", value=token, httponly=False, # Allow JavaScript access for localStorage fallback samesite="Lax" if is_localhost else "None", # Lax for localhost, None for cross-origin secure=False if is_localhost else True, # False for HTTP localhost, True for HTTPS production path="/", max_age=604800, # 7 days domain=None # Let browser set the domain automatically ) return { "message": f"Login successful for {user_data['username']}", "username": user_data["username"], "role": user_data["role"], "user_id": user_data["user_id"], "full_name": user_data["full_name"], "token": token } except Exception as e: print(f"Login exception: {str(e)}") raise HTTPException(status_code=500, detail=f"Login error: {str(e)}") @app.get("/logout") def logout(response: Response): response.delete_cookie("auth_token") return HTMLResponse("