This commit is contained in:
2025-10-24 14:54:44 -05:00
parent cb06217ef7
commit 6a58e19b10
16 changed files with 1172 additions and 138 deletions

View File

@@ -1,7 +1,7 @@
"""
Applications API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List
from pydantic import BaseModel
from sqlalchemy.orm import Session
@@ -35,101 +35,107 @@ class ApplicationResponse(BaseModel):
from_attributes = True
@router.get("/", response_model=List[ApplicationResponse])
async def get_applications(skip: int = 0, limit: int = 100):
"""Get all applications"""
db = SessionLocal()
try:
applications = db.query(Application).offset(skip).limit(limit).all()
return applications
finally:
db.close()
async def get_applications(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
"""Get all applications for the current tenant"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
# Get applications for jobs in the current tenant or applications by users in the current tenant
applications = db.query(Application).join(JobPosting).filter(
(JobPosting.tenant_id == tenant_id) | (Application.user_id.in_(
db.query(User.id).filter(User.tenant_id == tenant_id)
))
).offset(skip).limit(limit).all()
return applications
@router.get("/{application_id}", response_model=ApplicationResponse)
async def get_application(application_id: int):
async def get_application(application_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific application"""
db = SessionLocal()
try:
application = db.query(Application).filter(Application.id == application_id).first()
if not application:
raise HTTPException(status_code=404, detail="Application not found")
return application
finally:
db.close()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
application = db.query(Application).join(JobPosting).filter(
Application.id == application_id,
(JobPosting.tenant_id == tenant_id) | (Application.user_id.in_(
db.query(User.id).filter(User.tenant_id == tenant_id)
))
).first()
if not application:
raise HTTPException(status_code=404, detail="Application not found")
return application
@router.post("/", response_model=ApplicationResponse)
async def create_application(application: ApplicationCreate, user_id: int = 1): # In real app, get from auth context
async def create_application(application: ApplicationCreate, db: Session = Depends(SessionLocal), request: Request = None, user_id: int = 1): # In real app, get from auth context
"""Create a new job application"""
db = SessionLocal()
try:
# Verify user exists and has permission to apply
user = db.query(User).filter(User.id == user_id).first()
if not user or user.role != "job_seeker":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only job seekers can apply for jobs"
)
# Verify job posting exists and is active
job_posting = db.query(JobPosting).filter(
JobPosting.id == application.job_posting_id,
JobPosting.is_active == True
).first()
if not job_posting:
raise HTTPException(status_code=404, detail="Job posting not found or inactive")
# Verify resume exists and belongs to user
resume = db.query(Resume).filter(
Resume.id == application.resume_id,
Resume.user_id == user_id
).first()
if not resume:
raise HTTPException(status_code=404, detail="Resume not found")
db_application = Application(
user_id=user_id,
job_posting_id=application.job_posting_id,
resume_id=application.resume_id,
cover_letter=application.cover_letter
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
# Verify user exists and has permission to apply
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Make sure user belongs to current tenant
).first()
if not user or user.role != "job_seeker":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only job seekers can apply for jobs"
)
db.add(db_application)
db.commit()
db.refresh(db_application)
return db_application
finally:
db.close()
# Verify job posting exists and is active and belongs to the same tenant
job_posting = db.query(JobPosting).filter(
JobPosting.id == application.job_posting_id,
JobPosting.is_active == True,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not job_posting:
raise HTTPException(status_code=404, detail="Job posting not found or inactive or not in your tenant")
# Verify resume exists and belongs to user
resume = db.query(Resume).filter(
Resume.id == application.resume_id,
Resume.user_id == user_id
).first()
if not resume:
raise HTTPException(status_code=404, detail="Resume not found")
db_application = Application(
user_id=user_id,
job_posting_id=application.job_posting_id,
resume_id=application.resume_id,
cover_letter=application.cover_letter
)
db.add(db_application)
db.commit()
db.refresh(db_application)
return db_application
@router.put("/{application_id}", response_model=ApplicationResponse)
async def update_application(application_id: int, app_update: ApplicationUpdate):
async def update_application(application_id: int, app_update: ApplicationUpdate, db: Session = Depends(SessionLocal)):
"""Update an application"""
db = SessionLocal()
try:
db_application = db.query(Application).filter(Application.id == application_id).first()
if not db_application:
raise HTTPException(status_code=404, detail="Application not found")
# Update fields if provided
if app_update.status is not None:
db_application.status = app_update.status.value
if app_update.cover_letter is not None:
db_application.cover_letter = app_update.cover_letter
db.commit()
db.refresh(db_application)
return db_application
finally:
db.close()
db_application = db.query(Application).filter(Application.id == application_id).first()
if not db_application:
raise HTTPException(status_code=404, detail="Application not found")
# Update fields if provided
if app_update.status is not None:
db_application.status = app_update.status.value
if app_update.cover_letter is not None:
db_application.cover_letter = app_update.cover_letter
db.commit()
db.refresh(db_application)
return db_application
@router.delete("/{application_id}")
async def delete_application(application_id: int):
async def delete_application(application_id: int, db: Session = Depends(SessionLocal)):
"""Delete an application"""
db = SessionLocal()
try:
db_application = db.query(Application).filter(Application.id == application_id).first()
if not db_application:
raise HTTPException(status_code=404, detail="Application not found")
db.delete(db_application)
db.commit()
return {"message": "Application deleted successfully"}
finally:
db.close()
db_application = db.query(Application).filter(Application.id == application_id).first()
if not db_application:
raise HTTPException(status_code=404, detail="Application not found")
db.delete(db_application)
db.commit()
return {"message": "Application deleted successfully"}

View File

@@ -1,7 +1,7 @@
"""
Jobs API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List
from pydantic import BaseModel
from sqlalchemy.orm import Session
@@ -49,18 +49,29 @@ class JobResponse(BaseModel):
from_attributes = True
@router.get("/", response_model=List[JobResponse])
async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal)):
"""Get all jobs"""
query = db.query(JobPosting)
async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal), request: Request = None):
"""Get all jobs for the current tenant"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(JobPosting).filter(JobPosting.tenant_id == tenant_id)
if is_active is not None:
query = query.filter(JobPosting.is_active == is_active)
jobs = query.offset(skip).limit(limit).all()
return jobs
@router.get("/{job_id}", response_model=JobResponse)
async def get_job(job_id: int, db: Session = Depends(SessionLocal)):
async def get_job(job_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific job"""
job = db.query(JobPosting).filter(JobPosting.id == job_id).first()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
job = db.query(JobPosting).filter(
JobPosting.id == job_id,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if not job.is_active:
@@ -68,10 +79,17 @@ async def get_job(job_id: int, db: Session = Depends(SessionLocal)):
return job
@router.post("/", response_model=JobResponse)
async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_id: int = 1): # In real app, get from auth context
async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), request: Request = None, user_id: int = 1): # In real app, get from auth context
"""Create a new job posting"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
# Verify user exists and has permission to create job postings
user = db.query(User).filter(User.id == user_id).first()
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Ensure user belongs to current tenant
).first()
if not user or user.role not in ["job_provider", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -86,7 +104,7 @@ async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_i
salary_min=job.salary_min,
salary_max=job.salary_max,
is_remote=job.is_remote,
tenant_id=user.tenant_id, # Use user's tenant
tenant_id=tenant_id, # Use current tenant
created_by_user_id=user_id
)
db.add(db_job)
@@ -95,9 +113,16 @@ async def create_job(job: JobCreate, db: Session = Depends(SessionLocal), user_i
return db_job
@router.put("/{job_id}", response_model=JobResponse)
async def update_job(job_id: int, job_update: JobUpdate, db: Session = Depends(SessionLocal)):
async def update_job(job_id: int, job_update: JobUpdate, db: Session = Depends(SessionLocal), request: Request = None):
"""Update a job posting"""
db_job = db.query(JobPosting).filter(JobPosting.id == job_id).first()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_job = db.query(JobPosting).filter(
JobPosting.id == job_id,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not db_job:
raise HTTPException(status_code=404, detail="Job not found")
@@ -110,9 +135,16 @@ async def update_job(job_id: int, job_update: JobUpdate, db: Session = Depends(S
return db_job
@router.delete("/{job_id}")
async def delete_job(job_id: int, db: Session = Depends(SessionLocal)):
async def delete_job(job_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Delete a job posting (soft delete by setting is_active to False)"""
db_job = db.query(JobPosting).filter(JobPosting.id == job_id).first()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_job = db.query(JobPosting).filter(
JobPosting.id == job_id,
JobPosting.tenant_id == tenant_id # Ensure job belongs to current tenant
).first()
if not db_job:
raise HTTPException(status_code=404, detail="Job not found")

View File

@@ -1,14 +1,14 @@
"""
Users API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List
from pydantic import BaseModel
import hashlib
from sqlalchemy.orm import Session
from ..database import SessionLocal
from ..models import User, UserRole
from ..models import User, UserRole, Tenant
from ..config.settings import settings
router = APIRouter()
@@ -42,22 +42,42 @@ def hash_password(password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
@router.get("/", response_model=List[UserResponse])
async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal)):
"""Get all users"""
users = db.query(User).offset(skip).limit(limit).all()
async def get_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
"""Get all users for the current tenant"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(User)
if settings.MULTI_TENANT_ENABLED:
query = query.filter(User.tenant_id == tenant_id)
users = query.offset(skip).limit(limit).all()
return users
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: Session = Depends(SessionLocal)):
async def get_user(user_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific user"""
user = db.query(User).filter(User.id == user_id).first()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(User).filter(User.id == user_id)
if settings.MULTI_TENANT_ENABLED:
query = query.filter(User.tenant_id == tenant_id)
user = query.first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.post("/", response_model=UserResponse)
async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
async def create_user(user: UserCreate, db: Session = Depends(SessionLocal), request: Request = None):
"""Create a new user"""
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
# Check if user already exists
existing_user = db.query(User).filter(
(User.email == user.email) | (User.username == user.username)
@@ -73,7 +93,7 @@ async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
username=user.username,
hashed_password=hashed_pwd,
role=user.role.value,
tenant_id=1 # Default tenant, in real app would come from context
tenant_id=tenant_id # Use the current tenant
)
db.add(db_user)
db.commit()
@@ -81,9 +101,16 @@ async def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
return db_user
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(SessionLocal)):
async def update_user(user_id: int, user_update: UserUpdate, db: Session = Depends(SessionLocal), request: Request = None):
"""Update a user"""
db_user = db.query(User).filter(User.id == user_id).first()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Ensure user belongs to current tenant
).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
@@ -100,9 +127,16 @@ async def update_user(user_id: int, user_update: UserUpdate, db: Session = Depen
return db_user
@router.delete("/{user_id}")
async def delete_user(user_id: int, db: Session = Depends(SessionLocal)):
async def delete_user(user_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Delete a user"""
db_user = db.query(User).filter(User.id == user_id).first()
tenant_id = getattr(request.state, 'tenant_id', None)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Tenant ID is required")
db_user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id # Ensure user belongs to current tenant
).first()
if not db_user:
raise HTTPException(status_code=404, detail="User not found")

View File

@@ -1,31 +1,44 @@
"""
Tenant middleware for handling multi-tenant requests
"""
import uuid
from typing import Optional
from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from sqlalchemy.orm import Session
from .config.settings import settings
from .models import Tenant
from .database import SessionLocal
class TenantMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Get tenant ID from header or subdomain
# Get tenant ID from header
tenant_id = request.headers.get(settings.TENANT_ID_HEADER)
# If not in header, try to extract from subdomain
if not tenant_id:
# Try to extract tenant from subdomain
host = request.headers.get("host", "")
tenant_id = self.extract_tenant_from_host(host)
if not tenant_id and settings.MULTI_TENANT_ENABLED:
# Look up tenant in database
tenant = None
if tenant_id:
db: Session = SessionLocal()
try:
tenant = db.query(Tenant).filter(Tenant.subdomain == tenant_id).first()
finally:
db.close()
if settings.MULTI_TENANT_ENABLED and not tenant:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID is required"
detail="Valid tenant ID is required"
)
# Attach tenant info to request
request.state.tenant_id = tenant_id
request.state.tenant = tenant
request.state.tenant_id = tenant.id if tenant else None
response = await call_next(request)
return response
@@ -34,7 +47,9 @@ class TenantMiddleware(BaseHTTPMiddleware):
"""
Extract tenant from host (subdomain.tenant.com)
"""
# For now, return a default tenant or None
# In a real implementation, you would parse the subdomain
# and look up the corresponding tenant in the database
return "default"
import re
# Match subdomain from host (e.g., "tenant1.example.com" -> "tenant1")
match = re.match(r'^([^.]+)\.', host)
if match:
return match.group(1)
return None