| import os |
| from pathlib import Path |
| from fastapi import FastAPI |
| from fastapi.middleware.cors import CORSMiddleware |
| from starlette.middleware.base import BaseHTTPMiddleware |
| from starlette.requests import Request |
| from dotenv import load_dotenv |
|
|
| |
| env_path = Path(__file__).parent.parent.parent / ".env" |
| load_dotenv(dotenv_path=env_path) |
|
|
| from app.core.database import Base, engine |
| from app.core.capabilities import assert_required_features, get_capabilities, log_capabilities_summary |
| import importlib |
| import logging |
|
|
|
|
| class HTTPSRedirectMiddleware(BaseHTTPMiddleware): |
| """ |
| Middleware to ensure redirects use HTTPS in production. |
| When deployed behind a reverse proxy the request arrives as HTTP |
| but should redirect to HTTPS. Starlette's redirect_slashes uses the request scheme, |
| so we wrap the scope to force HTTPS redirects in production. |
| Activated by setting DEPLOY_ENV=production or NODE_ENV=production. |
| """ |
| async def dispatch(self, request: Request, call_next): |
| |
| |
| if (os.getenv("NODE_ENV") == "production" or |
| os.getenv("DEPLOY_ENV") == "production"): |
| forwarded_proto = request.headers.get("x-forwarded-proto", "").lower() |
| if forwarded_proto == "https": |
| request.scope["scheme"] = "https" |
| |
| return await call_next(request) |
|
|
|
|
| |
| |
| app = FastAPI( |
| title="AI Talent Finder", |
| version="1.0.0", |
| |
| |
| |
| redirect_slashes=True, |
| ) |
|
|
| |
| if os.getenv("ENABLE_HTTPS_REDIRECT", "false").lower() == "true": |
| app.add_middleware(HTTPSRedirectMiddleware) |
|
|
| |
| allowed_origins = [ |
| "https://ai-talent-finder-flame.vercel.app", |
| "http://localhost:3000", |
| ] |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=allowed_origins, |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
|
|
| def include_optional_router(module_path: str, attr_name: str = "router"): |
| try: |
| module = importlib.import_module(module_path) |
| router = getattr(module, attr_name) |
| app.include_router(router) |
| logging.info(f"Included router {module_path}.{attr_name}") |
| except Exception as e: |
| logging.warning(f"Skipping router {module_path}.{attr_name}: {e}") |
|
|
|
|
| @app.on_event("startup") |
| def on_startup(): |
| |
| |
| if not os.getenv("HF_TOKEN") and os.getenv("HF_TOKEN_CHATBOT"): |
| os.environ["HF_TOKEN"] = os.environ["HF_TOKEN_CHATBOT"] |
| logging.info("HF_TOKEN aliased from HF_TOKEN_CHATBOT") |
|
|
| |
| |
| |
| try: |
| Base.metadata.create_all(bind=engine) |
| except Exception as e: |
| logging.exception("Failed to create database tables: %s", e) |
|
|
| capabilities = log_capabilities_summary() |
| assert_required_features(capabilities) |
|
|
| |
| try: |
| from app.core.database import SessionLocal |
| from app.core.settings_store import load_pipeline_config |
| _db = SessionLocal() |
| try: |
| load_pipeline_config(_db) |
| finally: |
| _db.close() |
| except Exception as e: |
| logging.warning("Could not preload pipeline config: %s", e) |
|
|
| |
| |
| |
| admin_email = os.getenv("ADMIN_EMAIL", "").strip() |
| admin_password = os.getenv("ADMIN_PASSWORD", "").strip() |
| admin_name = os.getenv("ADMIN_FULL_NAME", "Admin").strip() |
| if admin_email and admin_password: |
| try: |
| from app.core.database import SessionLocal |
| from app.core.security import get_password_hash |
| from app.models.models import User as UserModel, UserRole as DBUserRole |
| _db = SessionLocal() |
| try: |
| existing = _db.query(UserModel).filter(UserModel.email == admin_email).first() |
| if existing: |
| if existing.role != DBUserRole.admin: |
| existing.role = DBUserRole.admin |
| _db.commit() |
| logging.info("Admin role enforced for: %s", admin_email) |
| else: |
| admin_user = UserModel( |
| email=admin_email, |
| hashed_password=get_password_hash(admin_password), |
| full_name=admin_name, |
| role=DBUserRole.admin, |
| ) |
| _db.add(admin_user) |
| _db.commit() |
| logging.info("Admin account created: %s", admin_email) |
| finally: |
| _db.close() |
| except Exception as e: |
| logging.warning("Could not seed admin account: %s", e) |
| else: |
| logging.warning("ADMIN_EMAIL or ADMIN_PASSWORD not set — no admin account seeded.") |
|
|
| |
| |
| include_optional_router("app.api.auth") |
| include_optional_router("app.api.admin") |
| include_optional_router("app.api.candidates") |
| include_optional_router("app.api.skills") |
| include_optional_router("app.api.jobs") |
| include_optional_router("app.api.scoring") |
| include_optional_router("app.api.criteria", "criteria_router") |
| include_optional_router("app.api.criteria", "matching_router") |
| include_optional_router("app.api.favorites") |
| include_optional_router("app.api.experiences") |
| include_optional_router("app.api.educations") |
| include_optional_router("app.api.match_results") |
| include_optional_router("app.api.chat", "router") |
| include_optional_router("app.api.export", "router") |
| |
| include_optional_router("app.api.matching", "router") |
| |
| include_optional_router("app.api.feedback", "router") |
| |
| include_optional_router("app.api.admin", "router") |
|
|
|
|
| @app.get("/") |
| def root(): |
| return {"status": "ok", "service": "AI Talent Finder"} |
|
|
|
|
| |
| @app.get("/health") |
| def health(): |
| return {"status": "ok", "version": "1.0.0"} |
|
|
|
|
| @app.get("/health/deps") |
| def health_deps(): |
| return get_capabilities() |
|
|