import logging import os import sys from datetime import datetime from pythonjsonlogger import jsonlogger from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware from prometheus_fastapi_instrumentator import Instrumentator, metrics from starlette.requests import Request from app.services.prometheus import number_of_users, number_of_transactions from app.services import bank_scraper from app.workers.celery_tasks import load_transactions, load_all_transactions from app.models.user import User, OAuthAccount from app.services.user_service import current_active_verified_user from app.api.auth import router as auth_router from app.api.csas import router as csas_router from app.api.categories import router as categories_router from app.api.transactions import router as transactions_router from app.services.user_service import auth_backend, current_active_verified_user, fastapi_users, get_oauth_provider, \ UserManager, get_jwt_strategy from app.core.security import extract_bearer_token, is_token_revoked, decode_and_verify_jwt from app.services.user_service import SECRET from fastapi import FastAPI import sentry_sdk from fastapi_users.db import SQLAlchemyUserDatabase from app.core.db import async_session_maker sentry_sdk.init( dsn=os.getenv("SENTRY_DSN"), send_default_pii=True, ) fastApi = FastAPI() # CORS for frontend dev server fastApi.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:5173", "http://127.0.0.1:5173", os.getenv("FRONTEND_DOMAIN_SCHEME", "") ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) prometheus = Instrumentator().instrument(fastApi) # Register custom metrics prometheus.add(number_of_users()).add(number_of_transactions()) prometheus.expose( fastApi, endpoint="/metrics", include_in_schema=True, ) fastApi.include_router(auth_router) fastApi.include_router(categories_router) fastApi.include_router(transactions_router) for h in list(logging.root.handlers): logging.root.removeHandler(h) _log_handler = logging.StreamHandler(sys.stdout) _formatter = jsonlogger.JsonFormatter( fmt='%(asctime)s %(levelname)s %(name)s %(message)s %(pathname)s %(lineno)d %(process)d %(thread)d' ) _log_handler.setFormatter(_formatter) logging.root.setLevel(logging.INFO) logging.root.addHandler(_log_handler) for _name in ("uvicorn", "uvicorn.error", "uvicorn.access"): _logger = logging.getLogger(_name) _logger.handlers = [_log_handler] _logger.propagate = True @fastApi.middleware("http") async def auth_guard(request: Request, call_next): # Enforce revoked/expired JWTs are rejected globally token = extract_bearer_token(request) if token: from fastapi import Response, status as _status # Deny if token is revoked if is_token_revoked(token): return Response(status_code=_status.HTTP_401_UNAUTHORIZED) # Deny if token is expired or invalid try: decode_and_verify_jwt(token, SECRET) except Exception: return Response(status_code=_status.HTTP_401_UNAUTHORIZED) return await call_next(request) @fastApi.middleware("http") async def log_traffic(request: Request, call_next): start_time = datetime.now() response = await call_next(request) process_time = (datetime.now() - start_time).total_seconds() client_host = request.client.host log_params = { "request_method": request.method, "request_url": str(request.url), "request_size": request.headers.get("content-length"), "request_headers": dict(request.headers), "response_status": response.status_code, "response_size": response.headers.get("content-length"), "response_headers": dict(response.headers), "process_time": process_time, "client_host": client_host } logging.getLogger(__name__).info("http_request", extra=log_params) return response fastApi.include_router( fastapi_users.get_oauth_router( get_oauth_provider("MojeID"), auth_backend, "SECRET", associate_by_email=True, redirect_url=os.getenv("FRONTEND_DOMAIN_SCHEME", "http://localhost:3000") + "/auth/mojeid/callback", ), prefix="/auth/mojeid", tags=["auth"], ) fastApi.include_router( fastapi_users.get_oauth_router( get_oauth_provider("BankID"), auth_backend, "SECRET", associate_by_email=True, redirect_url=os.getenv("FRONTEND_DOMAIN_SCHEME", "http://localhost:3000") + "/auth/bankid/callback", ), prefix="/auth/bankid", tags=["auth"], ) fastApi.include_router(csas_router) # Liveness/root endpoint @fastApi.get("/", include_in_schema=False) async def root(): return {"status": "ok"} @fastApi.get("/authenticated-route") async def authenticated_route(user: User = Depends(current_active_verified_user)): return {"message": f"Hello {user.email}!"} @fastApi.get("/debug/scrape/csas/all", tags=["debug"]) async def debug_scrape_csas_all(): logging.info("[Debug] Queueing CSAS scrape for all users via HTTP endpoint (Celery)") task = load_all_transactions.delay() return {"status": "queued", "action": "csas_scrape_all", "task_id": getattr(task, 'id', None)} @fastApi.post("/debug/scrape/csas/{user_id}", tags=["debug"]) async def debug_scrape_csas_user(user_id: str, user: User = Depends(current_active_verified_user)): logging.info("[Debug] Queueing CSAS scrape for single user via HTTP endpoint (Celery) | user_id=%s", user_id) task = load_transactions.delay(user_id) return {"status": "queued", "action": "csas_scrape_single", "user_id": user_id, "task_id": getattr(task, 'id', None)}