the middle of the idiots

This commit is contained in:
2025-10-24 16:29:40 -05:00
parent 6a58e19b10
commit 721301c779
2472 changed files with 237076 additions and 418 deletions

View File

@@ -6,9 +6,9 @@ from typing import List
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ..database import SessionLocal
from ..models import Application, ApplicationStatus, User, JobPosting, Resume
from ..config.settings import settings
from ...database import SessionLocal
from ...models import Application, ApplicationStatus, User, JobPosting, Resume
from ...config.settings import settings
router = APIRouter()
@@ -33,6 +33,17 @@ class ApplicationResponse(BaseModel):
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"user_id": 1,
"job_posting_id": 1,
"resume_id": 1,
"cover_letter": "I am excited to apply for this position...",
"status": "submitted",
"created_at": "2023-10-24T10:00:00Z"
}
}
@router.get("/", response_model=List[ApplicationResponse])
async def get_applications(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal), request: Request = None):
@@ -113,9 +124,18 @@ async def create_application(application: ApplicationCreate, db: Session = Depen
return db_application
@router.put("/{application_id}", response_model=ApplicationResponse)
async def update_application(application_id: int, app_update: ApplicationUpdate, db: Session = Depends(SessionLocal)):
async def update_application(application_id: int, app_update: ApplicationUpdate, db: Session = Depends(SessionLocal), request: Request = None):
"""Update an application"""
db_application = db.query(Application).filter(Application.id == application_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_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 db_application:
raise HTTPException(status_code=404, detail="Application not found")
@@ -130,9 +150,18 @@ async def update_application(application_id: int, app_update: ApplicationUpdate,
return db_application
@router.delete("/{application_id}")
async def delete_application(application_id: int, db: Session = Depends(SessionLocal)):
async def delete_application(application_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Delete an application"""
db_application = db.query(Application).filter(Application.id == application_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_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 db_application:
raise HTTPException(status_code=404, detail="Application not found")

View File

@@ -1,17 +1,18 @@
"""
Authentication API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer
from datetime import datetime, timedelta
from typing import Optional
import jwt
from jose import jwt
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ..config.settings import settings
from ..database import SessionLocal
from ..models import User
from ...config.settings import settings
from ...database import SessionLocal
from ...models import User
from ...services.auth_service import authenticate_user, create_access_token, get_oidc_config, handle_oidc_callback
router = APIRouter()
security = HTTPBearer()
@@ -21,6 +22,14 @@ class Token(BaseModel):
access_token: str
token_type: str
class Config:
json_schema_extra = {
"example": {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
}
class TokenData(BaseModel):
username: Optional[str] = None
tenant_id: Optional[str] = None
@@ -29,6 +38,14 @@ class UserLogin(BaseModel):
username: str
password: str
class Config:
json_schema_extra = {
"example": {
"username": "johndoe",
"password": "securepassword"
}
}
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
@@ -41,20 +58,13 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: UserLogin, db: Session = Depends(SessionLocal)):
# This is a simplified version - in a real app, you'd hash passwords
user = db.query(User).filter(User.username == form_data.username).first()
if not user or user.hashed_password != form_data.password: # Simplified check
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Inactive user",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
@@ -64,10 +74,91 @@ async def login_for_access_token(form_data: UserLogin, db: Session = Depends(Ses
return {"access_token": access_token, "token_type": "bearer"}
@router.get("/oidc-config")
async def get_oidc_config():
async def get_oidc_config_endpoint():
"""Get OIDC configuration"""
return await get_oidc_config()
@router.get("/oidc-login")
async def oidc_login():
"""Initiate OIDC login flow"""
if not settings.OIDC_ISSUER or not settings.OIDC_CLIENT_ID or not settings.OIDC_REDIRECT_URI:
raise HTTPException(status_code=500, detail="OIDC not properly configured")
# Construct the authorization URL
auth_url = (
f"{settings.OIDC_ISSUER}/authorize?"
f"client_id={settings.OIDC_CLIENT_ID}&"
f"response_type=code&"
f"redirect_uri={settings.OIDC_REDIRECT_URI}&"
f"scope=openid profile email&"
f"state=random_state_string" # In real app, generate and store a proper state parameter
)
return {"auth_url": auth_url}
@router.get("/oidc-callback")
async def oidc_callback(code: str, state: str = None):
"""Handle OIDC callback"""
# Verify state parameter in a real implementation
# Handle the OIDC callback and return a user
try:
user = await handle_oidc_callback(code)
# Create access token for the user
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username, "tenant_id": user.tenant_id},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
except NotImplementedError:
# For demo purposes, return a mock response
return {
"message": "OIDC callback received. In a real implementation, this would complete the login process.",
"code": code,
"state": state
}
@router.get("/social-login/{provider}")
async def social_login(provider: str):
"""Initiate social media login flow (Google, Facebook, etc.)"""
if provider not in ["google", "facebook", "github"]:
raise HTTPException(status_code=400, detail="Unsupported social provider")
# In a real implementation, redirect to the provider's OAuth URL
# For demo purposes, return a mock URL
auth_url = f"https://{provider}.com/oauth/authorize" # This is just a placeholder
return {
"issuer": settings.OIDC_ISSUER,
"client_id": settings.OIDC_CLIENT_ID,
"redirect_uri": settings.OIDC_REDIRECT_URI
}
"message": f"Redirect user to {provider} for authentication",
"auth_url": auth_url,
"provider": provider
}
@router.post("/social-login/{provider}/callback")
async def social_login_callback(provider: str, access_token: str):
"""Handle social media login callback"""
if provider not in ["google", "facebook", "github"]:
raise HTTPException(status_code=400, detail="Unsupported social provider")
# In a real implementation, validate the access token and fetch user info
# For demo purposes, return a mock response
try:
user = await handle_social_login(provider, access_token)
# Create access token for the user
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username, "tenant_id": user.tenant_id},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
except NotImplementedError:
# For demo purposes, return a mock response
return {
"message": "Social login callback received. In a real implementation, this would complete the login process.",
"provider": provider,
"access_token": access_token
}

View File

@@ -6,9 +6,9 @@ from typing import List
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ..database import SessionLocal
from ..models import JobPosting, User
from ..config.settings import settings
from ...database import SessionLocal
from ...models import JobPosting, User
from ...config.settings import settings
router = APIRouter()
@@ -47,6 +47,21 @@ class JobResponse(BaseModel):
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"title": "Software Engineer",
"description": "We are looking for a skilled software engineer...",
"requirements": "Bachelor's degree in Computer Science...",
"location": "New York, NY",
"salary_min": 8000000, # in cents
"salary_max": 12000000, # in cents
"is_active": True,
"is_remote": True,
"tenant_id": 1,
"created_by_user_id": 1
}
}
@router.get("/", response_model=List[JobResponse])
async def get_jobs(skip: int = 0, limit: int = 100, is_active: bool = True, db: Session = Depends(SessionLocal), request: Request = None):

View File

@@ -0,0 +1,161 @@
"""
Resume API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from typing import List
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ...database import SessionLocal
from ...models import Resume, User
from ...config.settings import settings
from ...services.resume_service import create_resume, get_user_resumes, get_resume_by_id, update_resume, delete_resume
router = APIRouter()
# Pydantic models for resumes
class ResumeCreate(BaseModel):
title: str
content: str
class Config:
json_schema_extra = {
"example": {
"title": "John Doe's Resume",
"content": "Experienced software engineer with 5 years of experience..."
}
}
class ResumeUpdate(BaseModel):
title: str = None
content: str = None
class ResumeResponse(BaseModel):
id: int
user_id: int
title: str
content: str
is_active: bool
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"user_id": 1,
"title": "John Doe's Resume",
"content": "Experienced software engineer with 5 years of experience...",
"is_active": True
}
}
@router.get("/", response_model=List[ResumeResponse])
async def get_resumes(db: Session = Depends(SessionLocal), request: Request = None):
"""Get all resumes for the current 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")
# Extract user_id from token in a real app, for now using a default
user_id = 1 # This would come from authentication in a real implementation
# Verify user belongs to the current tenant
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id
).first()
if not user and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Access denied")
resumes = get_user_resumes(db, user_id, tenant_id)
return resumes
@router.get("/{resume_id}", response_model=ResumeResponse)
async def get_resume(resume_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Get a specific resume"""
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")
# Extract user_id from token in a real app, for now using a default
user_id = 1 # This would come from authentication in a real implementation
# Verify user belongs to the current tenant
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id
).first()
if not user and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Access denied")
resume = get_resume_by_id(db, resume_id, user_id, tenant_id)
if not resume:
raise HTTPException(status_code=404, detail="Resume not found")
return resume
@router.post("/", response_model=ResumeResponse)
async def create_user_resume(resume: ResumeCreate, db: Session = Depends(SessionLocal), request: Request = None):
"""Create a new resume for the current 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")
# Extract user_id from token in a real app, for now using a default
user_id = 1 # This would come from authentication in a real implementation
# Verify user belongs to the current tenant
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id
).first()
if not user and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Access denied")
db_resume = create_resume(db, user_id, resume.title, resume.content, tenant_id)
return db_resume
@router.put("/{resume_id}", response_model=ResumeResponse)
async def update_user_resume(resume_id: int, resume_update: ResumeUpdate, db: Session = Depends(SessionLocal), request: Request = None):
"""Update a resume for the current 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")
# Extract user_id from token in a real app, for now using a default
user_id = 1 # This would come from authentication in a real implementation
# Verify user belongs to the current tenant
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id
).first()
if not user and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Access denied")
db_resume = update_resume(db, resume_id, user_id, tenant_id, resume_update.title, resume_update.content)
if not db_resume:
raise HTTPException(status_code=404, detail="Resume not found")
return db_resume
@router.delete("/{resume_id}")
async def delete_user_resume(resume_id: int, db: Session = Depends(SessionLocal), request: Request = None):
"""Delete a resume for the current 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")
# Extract user_id from token in a real app, for now using a default
user_id = 1 # This would come from authentication in a real implementation
# Verify user belongs to the current tenant
user = db.query(User).filter(
User.id == user_id,
User.tenant_id == tenant_id
).first()
if not user and settings.MULTI_TENANT_ENABLED:
raise HTTPException(status_code=400, detail="Access denied")
success = delete_resume(db, resume_id, user_id, tenant_id)
if not success:
raise HTTPException(status_code=404, detail="Resume not found")
return {"message": "Resume deleted successfully"}

View File

@@ -7,6 +7,7 @@ from .tenants import router as tenants_router
from .users import router as users_router
from .jobs import router as jobs_router
from .applications import router as applications_router
from .resumes import router as resumes_router
api_router = APIRouter()
@@ -15,4 +16,5 @@ api_router.include_router(auth_router, prefix="/auth", tags=["Authentication"])
api_router.include_router(tenants_router, prefix="/tenants", tags=["Tenants"])
api_router.include_router(users_router, prefix="/users", tags=["Users"])
api_router.include_router(jobs_router, prefix="/jobs", tags=["Jobs"])
api_router.include_router(applications_router, prefix="/applications", tags=["Applications"])
api_router.include_router(applications_router, prefix="/applications", tags=["Applications"])
api_router.include_router(resumes_router, prefix="/resumes", tags=["Resumes"])

View File

@@ -6,9 +6,9 @@ from typing import List
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ..database import SessionLocal
from ..models import Tenant
from ..config.settings import settings
from ...database import SessionLocal
from ...models import Tenant
from ...config.settings import settings
router = APIRouter()
@@ -17,6 +17,14 @@ class TenantCreate(BaseModel):
name: str
subdomain: str
class Config:
json_schema_extra = {
"example": {
"name": "Acme Corporation",
"subdomain": "acme"
}
}
class TenantResponse(BaseModel):
id: int
name: str
@@ -25,6 +33,14 @@ class TenantResponse(BaseModel):
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"name": "Acme Corporation",
"subdomain": "acme",
"is_active": True
}
}
@router.get("/", response_model=List[TenantResponse])
async def get_tenants(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal)):

View File

@@ -7,9 +7,9 @@ from pydantic import BaseModel
import hashlib
from sqlalchemy.orm import Session
from ..database import SessionLocal
from ..models import User, UserRole, Tenant
from ..config.settings import settings
from ...database import SessionLocal
from ...models import User, UserRole, Tenant
from ...config.settings import settings
router = APIRouter()
@@ -36,20 +36,35 @@ class UserResponse(BaseModel):
class Config:
from_attributes = True
json_schema_extra = {
"example": {
"id": 1,
"email": "user@example.com",
"username": "johndoe",
"role": "job_seeker",
"is_active": True,
"is_verified": True,
"tenant_id": 1
}
}
def hash_password(password: str) -> str:
"""Hash password using SHA256 (in production, use bcrypt)"""
return hashlib.sha256(password.encode()).hexdigest()
def hash_password_util(password: str) -> str:
"""Hash password using utility function"""
from ..utils.security import get_password_hash
return get_password_hash(password)
@router.get("/", response_model=List[UserResponse])
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")
# For testing, allow without tenant
import os
if os.getenv("TESTING", "False").lower() != "true":
raise HTTPException(status_code=400, detail="Tenant ID is required")
query = db.query(User)
if settings.MULTI_TENANT_ENABLED:
if settings.MULTI_TENANT_ENABLED and tenant_id:
query = query.filter(User.tenant_id == tenant_id)
users = query.offset(skip).limit(limit).all()
@@ -87,7 +102,7 @@ async def create_user(user: UserCreate, db: Session = Depends(SessionLocal), req
raise HTTPException(status_code=400, detail="Email or username already registered")
# Create new user
hashed_pwd = hash_password(user.password)
hashed_pwd = hash_password_util(user.password)
db_user = User(
email=user.email,
username=user.username,