Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions apps/backend/app/api/router/v1/resume.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,27 +156,27 @@ async def score_and_improve(
headers=headers,
)
except ResumeNotFoundError as e:
logger.error(str(e))
logger.warning(f"Resume not found: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
except JobNotFoundError as e:
logger.error(str(e))
logger.warning(f"Job not found: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e),
)
except ResumeParsingError as e:
logger.error(str(e))
logger.error(f"Resume parsing error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)
except JobParsingError as e:
logger.error(str(e))
logger.error(f"Job parsing error: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)
except ResumeKeywordExtractionError as e:
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from sqlalchemy.exc import SQLAlchemyError

from .api import health_check, v1_router, RequestIDMiddleware
from .core import (
Expand All @@ -15,6 +16,7 @@
custom_http_exception_handler,
validation_exception_handler,
unhandled_exception_handler,
sqlalchemy_exception_handler,
)
from .models import Base

Expand Down Expand Up @@ -54,6 +56,7 @@ def create_app() -> FastAPI:

app.add_exception_handler(HTTPException, custom_http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(SQLAlchemyError, sqlalchemy_exception_handler)
app.add_exception_handler(Exception, unhandled_exception_handler)

if os.path.exists(settings.FRONTEND_PATH):
Expand Down
30 changes: 27 additions & 3 deletions apps/backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
import sys
import logging
import secrets
from pydantic import Field, validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List, Optional, Literal

Expand All @@ -9,17 +11,39 @@ class Settings(BaseSettings):
PROJECT_NAME: str = "Resume Matcher"
FRONTEND_PATH: str = os.path.join(os.path.dirname(__file__), "frontend", "assets")
ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://127.0.0.1:3000"]
SYNC_DATABASE_URL: Optional[str]
ASYNC_DATABASE_URL: Optional[str]
SESSION_SECRET_KEY: Optional[str]
SYNC_DATABASE_URL: Optional[str] = Field(default="sqlite:///./resume_matcher.db")
ASYNC_DATABASE_URL: Optional[str] = Field(default="sqlite+aiosqlite:///./resume_matcher.db")
SESSION_SECRET_KEY: Optional[str] = Field(default=None)
DB_ECHO: bool = False
PYTHONDONTWRITEBYTECODE: int = 1
ENV: str = Field(default="production")

model_config = SettingsConfigDict(
env_file=os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, ".env"),
env_file_encoding="utf-8",
)

@validator('SESSION_SECRET_KEY')
def validate_secret_key(cls, v):
if not v:
# Generate a secure random secret key if not provided
return secrets.token_urlsafe(32)
if len(v) < 32:
raise ValueError('SESSION_SECRET_KEY must be at least 32 characters long')
return v

@validator('SYNC_DATABASE_URL')
def validate_sync_db_url(cls, v):
if not v:
raise ValueError('SYNC_DATABASE_URL is required')
return v

@validator('ASYNC_DATABASE_URL')
def validate_async_db_url(cls, v):
if not v:
raise ValueError('ASYNC_DATABASE_URL is required')
return v


settings = Settings()

Expand Down
15 changes: 8 additions & 7 deletions apps/backend/app/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
class _DatabaseSettings:
"""Pulled from environment once at import-time."""

SYNC_DATABASE_URL: str = settings.SYNC_DATABASE_URL
ASYNC_DATABASE_URL: str = settings.ASYNC_DATABASE_URL
DB_ECHO: bool = settings.DB_ECHO

DB_CONNECT_ARGS = (
{"check_same_thread": False} if SYNC_DATABASE_URL.startswith("sqlite") else {}
)
def __init__(self):
self.SYNC_DATABASE_URL: str = settings.SYNC_DATABASE_URL
self.ASYNC_DATABASE_URL: str = settings.ASYNC_DATABASE_URL
self.DB_ECHO: bool = settings.DB_ECHO

self.DB_CONNECT_ARGS = (
{"check_same_thread": False} if self.SYNC_DATABASE_URL.startswith("sqlite") else {}
)


settings = _DatabaseSettings()
Expand Down
12 changes: 11 additions & 1 deletion apps/backend/app/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,19 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE

async def unhandled_exception_handler(request: Request, exc: Exception):
request_id = getattr(request.state, "request_id", "")

# Log the full exception details for debugging
logger.error(
f"Unhandled exception for request {request_id} to {request.url}: {exc.__class__.__name__}: {str(exc)}",
exc_info=True
)

return JSONResponse(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
content={"detail": "Internal Server Error", "request_id": request_id},
content={
"detail": "An internal error occurred. Please try again later.",
"request_id": request_id
},
)


Expand Down