first commit
This commit is contained in:
commit
7a72438fd1
74 changed files with 7548 additions and 0 deletions
3
backend/app/routers/__init__.py
Normal file
3
backend/app/routers/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from app.routers import auth, resume, requirements, schedule, questions, pronunciation
|
||||
|
||||
__all__ = ["auth", "resume", "requirements", "schedule", "questions", "pronunciation"]
|
||||
BIN
backend/app/routers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/auth.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/pronunciation.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/pronunciation.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/questions.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/questions.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/requirements.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/requirements.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/resume.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/resume.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/routers/__pycache__/schedule.cpython-313.pyc
Normal file
BIN
backend/app/routers/__pycache__/schedule.cpython-313.pyc
Normal file
Binary file not shown.
130
backend/app/routers/auth.py
Normal file
130
backend/app/routers/auth.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
Google OAuth 2.0 flow:
|
||||
1. GET /api/auth/google/login → redirect to Google consent screen
|
||||
2. GET /api/auth/google/callback → exchange code for tokens → get user info
|
||||
→ upsert user in MongoDB → issue JWT
|
||||
→ redirect to frontend with token
|
||||
3. GET /api/auth/me → return current user (protected)
|
||||
4. POST /api/auth/logout → client-side only (stateless JWT), just 200
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
import httpx
|
||||
from datetime import datetime, timezone
|
||||
from app.config import get_settings
|
||||
from app.database import get_db
|
||||
from app.dependencies import create_access_token, get_current_user
|
||||
from app.models.user import UserOut
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
settings = get_settings()
|
||||
|
||||
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
|
||||
SCOPES = "openid email profile"
|
||||
|
||||
|
||||
# ── Step 1: Redirect to Google ─────────────────────────────────
|
||||
@router.get("/google/login")
|
||||
async def google_login():
|
||||
redirect_uri = f"{settings.backend_url}/api/auth/google/callback"
|
||||
params = (
|
||||
f"?client_id={settings.google_client_id}"
|
||||
f"&redirect_uri={redirect_uri}"
|
||||
f"&response_type=code"
|
||||
f"&scope={SCOPES}"
|
||||
f"&access_type=offline"
|
||||
f"&prompt=select_account"
|
||||
)
|
||||
return RedirectResponse(url=GOOGLE_AUTH_URL + params)
|
||||
|
||||
|
||||
# ── Step 2: Callback from Google ───────────────────────────────
|
||||
@router.get("/google/callback")
|
||||
async def google_callback(code: str, db=Depends(get_db)):
|
||||
redirect_uri = f"{settings.backend_url}/api/auth/google/callback"
|
||||
|
||||
# Exchange authorization code for tokens
|
||||
async with httpx.AsyncClient() as client:
|
||||
token_response = await client.post(
|
||||
GOOGLE_TOKEN_URL,
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": settings.google_client_id,
|
||||
"client_secret": settings.google_client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
error_url = f"{settings.frontend_url}/login?error=google_token_failed"
|
||||
return RedirectResponse(url=error_url)
|
||||
|
||||
tokens = token_response.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
# Fetch user profile from Google
|
||||
async with httpx.AsyncClient() as client:
|
||||
userinfo_response = await client.get(
|
||||
GOOGLE_USERINFO_URL,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
|
||||
if userinfo_response.status_code != 200:
|
||||
error_url = f"{settings.frontend_url}/login?error=google_userinfo_failed"
|
||||
return RedirectResponse(url=error_url)
|
||||
|
||||
google_user = userinfo_response.json()
|
||||
google_id = google_user.get("sub")
|
||||
email = google_user.get("email", "")
|
||||
name = google_user.get("name", "")
|
||||
picture = google_user.get("picture", "")
|
||||
|
||||
# Upsert user in MongoDB
|
||||
now = datetime.now(timezone.utc)
|
||||
await db["users"].update_one(
|
||||
{"google_id": google_id},
|
||||
{
|
||||
"$set": {
|
||||
"email": email,
|
||||
"name": name,
|
||||
"picture": picture,
|
||||
"last_login": now,
|
||||
},
|
||||
"$setOnInsert": {
|
||||
"google_id": google_id,
|
||||
"created_at": now,
|
||||
},
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
# Issue JWT
|
||||
jwt_token = create_access_token(
|
||||
data={"sub": google_id, "email": email, "name": name, "picture": picture}
|
||||
)
|
||||
|
||||
# Redirect to frontend with token
|
||||
frontend_redirect = f"{settings.frontend_url}/auth/callback?token={jwt_token}"
|
||||
return RedirectResponse(url=frontend_redirect)
|
||||
|
||||
|
||||
# ── Get current user (protected) ──────────────────────────────
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def get_me(current_user: dict = Depends(get_current_user)):
|
||||
return UserOut(
|
||||
google_id=current_user["google_id"],
|
||||
email=current_user["email"],
|
||||
name=current_user["name"],
|
||||
picture=current_user.get("picture"),
|
||||
)
|
||||
|
||||
|
||||
# ── Logout (stateless — client drops the JWT) ─────────────────
|
||||
@router.post("/logout")
|
||||
async def logout():
|
||||
return JSONResponse({"message": "Logged out successfully"})
|
||||
79
backend/app/routers/pronunciation.py
Normal file
79
backend/app/routers/pronunciation.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
import random
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.pronunciation import PronunciationDB
|
||||
from app.services.gemini_service import get_word_pronunciation
|
||||
|
||||
router = APIRouter(prefix="/api/pronunciation", tags=["pronunciation"])
|
||||
|
||||
TECH_TERMS = [
|
||||
"Kubernetes", "PostgreSQL", "Nginx", "Polymorphism", "Encapsulation",
|
||||
"Asynchronous", "Concurrency", "Django", "Vue", "TypeScript",
|
||||
"GraphQL", "Docker", "Microservices", "Idempotent", "Heuristic",
|
||||
"Tuple", "Boolean", "JSON", "YAML", "Ubuntu",
|
||||
"Vite", "Pinia", "Axios", "FastAPI", "Uvicorn",
|
||||
"OAuth", "JWT", "PyMuPDF", "GridFS", "MongoDB",
|
||||
"Motor", "Asynchronous", "Middleware", "Interceptor", "Composition",
|
||||
"Component", "Reactive", "Computed", "State", "Mutation",
|
||||
"Action", "Getter", "Router", "Guard", "Plugin"
|
||||
]
|
||||
|
||||
@router.get("/tech-terms", response_model=List[str])
|
||||
async def get_tech_terms():
|
||||
"""Returns a list of 50 common tech terms to prepopulate the UI."""
|
||||
return TECH_TERMS
|
||||
|
||||
@router.get("/word-of-the-day")
|
||||
async def get_word_of_the_day(db=Depends(get_db)):
|
||||
"""Returns a random word from the DB, or a default one if DB is empty."""
|
||||
count = await db["pronunciations"].count_documents({})
|
||||
if count == 0:
|
||||
return {"word": "Polymorphism", "phonetic": "/ˌpɒlɪˈmɔːfɪzəm/", "definition": "The provision of a single interface to entities of different types."}
|
||||
|
||||
# Pick a random document
|
||||
import random
|
||||
random_skip = random.randint(0, count - 1)
|
||||
doc = await db["pronunciations"].find().skip(random_skip).limit(1).to_list(length=1)
|
||||
|
||||
if doc:
|
||||
return {
|
||||
"word": doc[0]["word"],
|
||||
"phonetic": doc[0]["phonetic"],
|
||||
"definition": doc[0]["definition"]
|
||||
}
|
||||
return {"word": "Polymorphism", "phonetic": "/ˌpɒlɪˈmɔːfɪzəm/", "definition": "The provision of a single interface to entities of different types."}
|
||||
|
||||
@router.get("/{word}", response_model=PronunciationDB)
|
||||
async def get_pronunciation(word: str, db=Depends(get_db)):
|
||||
"""Fetches pronunciation info from MongoDB or Gemini."""
|
||||
word_lower = word.lower().strip()
|
||||
|
||||
# 1. Check DB Cache
|
||||
existing = await db["pronunciations"].find_one({"word_lower": word_lower})
|
||||
if existing:
|
||||
existing["id"] = str(existing.pop("_id"))
|
||||
return existing
|
||||
|
||||
# 2. Call Gemini
|
||||
try:
|
||||
data = await get_word_pronunciation(word)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# 3. Store in DB
|
||||
now = datetime.now(timezone.utc)
|
||||
doc = {
|
||||
"word": data["word"],
|
||||
"word_lower": word_lower,
|
||||
"phonetic": data["phonetic"],
|
||||
"syllables": data["syllables"],
|
||||
"definition": data["definition"],
|
||||
"created_at": now
|
||||
}
|
||||
|
||||
result = await db["pronunciations"].insert_one(doc)
|
||||
doc["id"] = str(result.inserted_id)
|
||||
return doc
|
||||
84
backend/app/routers/questions.py
Normal file
84
backend/app/routers/questions.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from bson import ObjectId
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.question import QuestionSetIn, QuestionSetOut, QuestionItem
|
||||
from app.services.gemini_service import generate_interview_questions
|
||||
|
||||
router = APIRouter(prefix="/api/questions", tags=["questions"])
|
||||
|
||||
@router.post("/generate", response_model=QuestionSetOut, status_code=status.HTTP_201_CREATED)
|
||||
async def generate_questions(
|
||||
req: QuestionSetIn,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
resume_oid = ObjectId(req.resume_id)
|
||||
req_oid = ObjectId(req.requirement_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID format")
|
||||
|
||||
# Fetch resume
|
||||
resume = await db["resumes"].find_one({"_id": resume_oid, "uploaded_by": current_user["google_id"]})
|
||||
if not resume:
|
||||
raise HTTPException(status_code=404, detail="Resume not found")
|
||||
|
||||
# Fetch requirements
|
||||
requirement = await db["requirements"].find_one({"_id": req_oid, "user_id": current_user["google_id"]})
|
||||
if not requirement:
|
||||
raise HTTPException(status_code=404, detail="Requirement not found")
|
||||
|
||||
# Generate questions via Gemini
|
||||
try:
|
||||
# Pass resume text and requirements to Gemini
|
||||
generated_data = await generate_interview_questions(
|
||||
resume_text=resume.get("parsed", {}).get("raw_text", ""),
|
||||
role_name=requirement.get("role_name", ""),
|
||||
skills=requirement.get("mandatory_skills", []) + requirement.get("nice_to_have_skills", []),
|
||||
experience=requirement.get("experience_years", 0),
|
||||
projects=resume.get("parsed", {}).get("projects", [])
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
# Save to MongoDB
|
||||
now = datetime.now(timezone.utc)
|
||||
doc = {
|
||||
"user_id": current_user["google_id"],
|
||||
"resume_id": req.resume_id,
|
||||
"requirement_id": req.requirement_id,
|
||||
"questions": generated_data,
|
||||
"created_at": now
|
||||
}
|
||||
|
||||
# Upsert logic to overwrite if same pair exists
|
||||
result = await db["questions"].find_one_and_update(
|
||||
{
|
||||
"user_id": current_user["google_id"],
|
||||
"resume_id": req.resume_id,
|
||||
"requirement_id": req.requirement_id
|
||||
},
|
||||
{"$set": doc},
|
||||
upsert=True,
|
||||
return_document=True
|
||||
)
|
||||
|
||||
result["id"] = str(result.pop("_id"))
|
||||
return result
|
||||
|
||||
@router.get("/{resume_id}", response_model=List[QuestionSetOut])
|
||||
async def get_questions_for_resume(
|
||||
resume_id: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
cursor = db["questions"].find({"user_id": current_user["google_id"], "resume_id": resume_id}).sort("created_at", -1)
|
||||
results = []
|
||||
async for doc in cursor:
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
results.append(doc)
|
||||
return results
|
||||
98
backend/app/routers/requirements.py
Normal file
98
backend/app/routers/requirements.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from bson import ObjectId
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.requirements import RequirementsIn, RequirementsOut
|
||||
|
||||
router = APIRouter(prefix="/api/requirements", tags=["requirements"])
|
||||
|
||||
@router.post("/", response_model=RequirementsOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_requirements(
|
||||
req: RequirementsIn,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
now = datetime.now(timezone.utc)
|
||||
doc = req.model_dump()
|
||||
doc["user_id"] = current_user["google_id"]
|
||||
doc["created_at"] = now
|
||||
doc["updated_at"] = now
|
||||
|
||||
result = await db["requirements"].insert_one(doc)
|
||||
doc["id"] = str(result.inserted_id)
|
||||
return doc
|
||||
|
||||
@router.get("/", response_model=List[RequirementsOut])
|
||||
async def list_requirements(
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
cursor = db["requirements"].find({"user_id": current_user["google_id"]}).sort("updated_at", -1)
|
||||
reqs = []
|
||||
async for doc in cursor:
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
reqs.append(doc)
|
||||
return reqs
|
||||
|
||||
@router.get("/{req_id}", response_model=RequirementsOut)
|
||||
async def get_requirement(
|
||||
req_id: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(req_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID")
|
||||
|
||||
doc = await db["requirements"].find_one({"_id": oid, "user_id": current_user["google_id"]})
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Requirement not found")
|
||||
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
return doc
|
||||
|
||||
@router.put("/{req_id}", response_model=RequirementsOut)
|
||||
async def update_requirement(
|
||||
req_id: str,
|
||||
req: RequirementsIn,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(req_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID")
|
||||
|
||||
update_data = req.model_dump()
|
||||
update_data["updated_at"] = datetime.now(timezone.utc)
|
||||
|
||||
result = await db["requirements"].find_one_and_update(
|
||||
{"_id": oid, "user_id": current_user["google_id"]},
|
||||
{"$set": update_data},
|
||||
return_document=True
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Requirement not found")
|
||||
|
||||
result["id"] = str(result.pop("_id"))
|
||||
return result
|
||||
|
||||
@router.delete("/{req_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_requirement(
|
||||
req_id: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(req_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID")
|
||||
|
||||
result = await db["requirements"].delete_one({"_id": oid, "user_id": current_user["google_id"]})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=404, detail="Requirement not found")
|
||||
180
backend/app/routers/resume.py
Normal file
180
backend/app/routers/resume.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from bson import ObjectId
|
||||
from datetime import datetime, timezone
|
||||
import io
|
||||
|
||||
from app.database import get_db, get_gridfs
|
||||
from app.dependencies import get_current_user
|
||||
from app.services.resume_parser import parse_pdf, parse_docx
|
||||
|
||||
router = APIRouter(prefix="/api/resume", tags=["resume"])
|
||||
|
||||
ALLOWED_TYPES = {
|
||||
"application/pdf": "pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
||||
"application/msword": "doc",
|
||||
}
|
||||
MAX_SIZE_MB = 10
|
||||
|
||||
|
||||
# ── Upload & Parse ──────────────────────────────────────────────
|
||||
@router.post("/upload", status_code=status.HTTP_201_CREATED)
|
||||
async def upload_resume(
|
||||
file: UploadFile = File(...),
|
||||
db=Depends(get_db),
|
||||
gridfs=Depends(get_gridfs),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
# Validate content type
|
||||
content_type = file.content_type or ""
|
||||
if content_type not in ALLOWED_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Only PDF and DOCX files are accepted.",
|
||||
)
|
||||
|
||||
file_bytes = await file.read()
|
||||
|
||||
# Validate size
|
||||
if len(file_bytes) > MAX_SIZE_MB * 1024 * 1024:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"File too large. Max size is {MAX_SIZE_MB}MB.",
|
||||
)
|
||||
|
||||
# Parse resume
|
||||
file_type = ALLOWED_TYPES[content_type]
|
||||
if file_type == "pdf":
|
||||
parsed = parse_pdf(file_bytes)
|
||||
else:
|
||||
parsed = parse_docx(file_bytes)
|
||||
|
||||
# Store file in GridFS
|
||||
gridfs_id = await gridfs.upload_from_stream(
|
||||
file.filename,
|
||||
io.BytesIO(file_bytes),
|
||||
metadata={"content_type": content_type, "uploaded_by": current_user["google_id"]},
|
||||
)
|
||||
|
||||
# Store metadata in MongoDB
|
||||
doc = {
|
||||
"filename": file.filename,
|
||||
"file_type": file_type,
|
||||
"content_type": content_type,
|
||||
"uploaded_by": current_user["google_id"],
|
||||
"gridfs_id": str(gridfs_id),
|
||||
"parsed": parsed,
|
||||
"uploaded_at": datetime.now(timezone.utc),
|
||||
}
|
||||
result = await db["resumes"].insert_one(doc)
|
||||
|
||||
return {
|
||||
"id": str(result.inserted_id),
|
||||
"filename": file.filename,
|
||||
"file_type": file_type,
|
||||
"parsed": parsed,
|
||||
"uploaded_at": doc["uploaded_at"].isoformat(),
|
||||
"message": "Resume uploaded and parsed successfully",
|
||||
}
|
||||
|
||||
|
||||
# ── List all resumes for current user ──────────────────────────
|
||||
@router.get("/")
|
||||
async def list_resumes(
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
cursor = db["resumes"].find(
|
||||
{"uploaded_by": current_user["google_id"]},
|
||||
{"parsed.raw_text": 0}, # exclude raw text from list view
|
||||
).sort("uploaded_at", -1)
|
||||
|
||||
resumes = []
|
||||
async for doc in cursor:
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
doc["uploaded_at"] = doc["uploaded_at"].isoformat() if doc.get("uploaded_at") else ""
|
||||
resumes.append(doc)
|
||||
|
||||
return resumes
|
||||
|
||||
|
||||
# ── Get single resume ──────────────────────────────────────────
|
||||
@router.get("/{resume_id}")
|
||||
async def get_resume(
|
||||
resume_id: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(resume_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid resume ID")
|
||||
|
||||
doc = await db["resumes"].find_one(
|
||||
{"_id": oid, "uploaded_by": current_user["google_id"]}
|
||||
)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Resume not found")
|
||||
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
doc["uploaded_at"] = doc["uploaded_at"].isoformat() if doc.get("uploaded_at") else ""
|
||||
return doc
|
||||
|
||||
|
||||
# ── Download original file ─────────────────────────────────────
|
||||
@router.get("/{resume_id}/download")
|
||||
async def download_resume(
|
||||
resume_id: str,
|
||||
db=Depends(get_db),
|
||||
gridfs=Depends(get_gridfs),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(resume_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid resume ID")
|
||||
|
||||
doc = await db["resumes"].find_one(
|
||||
{"_id": oid, "uploaded_by": current_user["google_id"]}
|
||||
)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Resume not found")
|
||||
|
||||
gridfs_id = ObjectId(doc["gridfs_id"])
|
||||
stream = await gridfs.open_download_stream(gridfs_id)
|
||||
content = await stream.read()
|
||||
|
||||
return StreamingResponse(
|
||||
io.BytesIO(content),
|
||||
media_type=doc["content_type"],
|
||||
headers={"Content-Disposition": f'attachment; filename="{doc["filename"]}"'},
|
||||
)
|
||||
|
||||
|
||||
# ── Delete resume ──────────────────────────────────────────────
|
||||
@router.delete("/{resume_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_resume(
|
||||
resume_id: str,
|
||||
db=Depends(get_db),
|
||||
gridfs=Depends(get_gridfs),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(resume_id)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid resume ID")
|
||||
|
||||
doc = await db["resumes"].find_one(
|
||||
{"_id": oid, "uploaded_by": current_user["google_id"]}
|
||||
)
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Resume not found")
|
||||
|
||||
# Delete from GridFS
|
||||
try:
|
||||
await gridfs.delete(ObjectId(doc["gridfs_id"]))
|
||||
except Exception:
|
||||
pass # GridFS file may already be gone
|
||||
|
||||
await db["resumes"].delete_one({"_id": oid})
|
||||
110
backend/app/routers/schedule.py
Normal file
110
backend/app/routers/schedule.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from bson import ObjectId
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models.schedule import ScheduleIn, ScheduleOut
|
||||
from app.services.email import send_interview_email
|
||||
|
||||
router = APIRouter(prefix="/api/schedule", tags=["schedule"])
|
||||
|
||||
@router.post("/", response_model=ScheduleOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_schedule(
|
||||
sched: ScheduleIn,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
now = datetime.now(timezone.utc)
|
||||
doc = sched.model_dump()
|
||||
doc["user_id"] = current_user["google_id"]
|
||||
doc["status"] = "Scheduled"
|
||||
doc["created_at"] = now
|
||||
doc["updated_at"] = now
|
||||
|
||||
result = await db["schedules"].insert_one(doc)
|
||||
doc["id"] = str(result.inserted_id)
|
||||
|
||||
# Try sending email
|
||||
email_sent = await send_interview_email(
|
||||
candidate_email=sched.candidate_email,
|
||||
candidate_name=sched.candidate_name,
|
||||
date=sched.interview_date,
|
||||
time=sched.interview_time,
|
||||
meeting_link=sched.meeting_link or "",
|
||||
notes=sched.notes or ""
|
||||
)
|
||||
|
||||
# Could log email_sent status if needed
|
||||
|
||||
return doc
|
||||
|
||||
@router.get("/", response_model=List[ScheduleOut])
|
||||
async def list_schedules(
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
cursor = db["schedules"].find({"user_id": current_user["google_id"]}).sort("interview_date", 1)
|
||||
scheds = []
|
||||
async for doc in cursor:
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
scheds.append(doc)
|
||||
return scheds
|
||||
|
||||
@router.get("/{sched_id}", response_model=ScheduleOut)
|
||||
async def get_schedule(
|
||||
sched_id: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(sched_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID")
|
||||
|
||||
doc = await db["schedules"].find_one({"_id": oid, "user_id": current_user["google_id"]})
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
|
||||
doc["id"] = str(doc.pop("_id"))
|
||||
return doc
|
||||
|
||||
@router.put("/{sched_id}/status", response_model=ScheduleOut)
|
||||
async def update_schedule_status(
|
||||
sched_id: str,
|
||||
status: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(sched_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID")
|
||||
|
||||
result = await db["schedules"].find_one_and_update(
|
||||
{"_id": oid, "user_id": current_user["google_id"]},
|
||||
{"$set": {"status": status, "updated_at": datetime.now(timezone.utc)}},
|
||||
return_document=True
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
|
||||
result["id"] = str(result.pop("_id"))
|
||||
return result
|
||||
|
||||
@router.delete("/{sched_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_schedule(
|
||||
sched_id: str,
|
||||
db=Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
oid = ObjectId(sched_id)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid ID")
|
||||
|
||||
result = await db["schedules"].delete_one({"_id": oid, "user_id": current_user["google_id"]})
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
Loading…
Add table
Add a link
Reference in a new issue