Skip to content
Merged
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
27 changes: 26 additions & 1 deletion app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,29 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
except jwt.PyJWTError:
raise credentials_exception

return email
return email

async def get_current_user_with_role(token: str = Depends(oauth2_scheme)):
"""Trả về cả email và role của user"""
from app.database.config import get_db

credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Không thể xác thực thông tin (Token không hợp lệ hoặc đã hết hạn)",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
except jwt.PyJWTError:
raise credentials_exception

# Lấy thông tin user từ database để có role
db = get_db()
user = await db["hr_users"].find_one({"email": email})
if not user:
raise credentials_exception

return {"email": email, "role": user.get("role", "hr")}
1 change: 1 addition & 0 deletions app/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Database package initialization
1 change: 1 addition & 0 deletions app/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Router package initialization
10 changes: 4 additions & 6 deletions app/routers/admin_router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException
from bson import ObjectId
from app.auth import get_current_user
from app.auth import get_current_user, get_current_user_with_role
from app.database.config import get_db
import os

Expand All @@ -9,12 +9,10 @@
VALID_ROLES = ("hr", "applicant", "admin")
BOOTSTRAP_SECRET = os.getenv("ADMIN_BOOTSTRAP_SECRET", "")

async def require_admin(current_user: str = Depends(get_current_user)):
db = get_db()
user = await db["hr_users"].find_one({"email": current_user})
if not user or user.get("role") != "admin":
async def require_admin(user_info: dict = Depends(get_current_user_with_role)):
if user_info["role"] != "admin":
raise HTTPException(status_code=403, detail="Chỉ Admin mới có quyền thực hiện thao tác này")
return current_user
return user_info["email"]

@router.post("/bootstrap")
async def bootstrap_admin(body: dict):
Expand Down
124 changes: 121 additions & 3 deletions app/routers/applicant_router.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from bson import ObjectId
from datetime import datetime, timezone
from typing import List, Optional
from pydantic import BaseModel

from app.auth import get_current_user
from app.database.config import get_db
Expand All @@ -10,6 +12,14 @@

MAX_FILE_SIZE = 5 * 1024 * 1024

class NotificationCreate(BaseModel):
title: str
message: str
type: str = "info" # success, error, info, warning
job_title: Optional[str] = None
application_id: Optional[str] = None
application_status: Optional[str] = None

async def require_applicant(current_user: str = Depends(get_current_user)):
db = get_db()
user = await db["hr_users"].find_one({"email": current_user})
Expand Down Expand Up @@ -147,19 +157,127 @@ async def apply_to_job(
return {
"status": "success",
"message": f"Nộp hồ sơ thành công cho vị trí '{job.get('title')}'",
"submission_id": str(result.inserted_id),
"ai_score": scoring_result
"submission_id": str(result.inserted_id)
}

@router.get("/my-applications")
async def my_applications(current_applicant: str = Depends(require_applicant)):
db = get_db()
cursor = db["applicant_submissions"].find(
{"applicant_email": current_applicant},
{"raw_text": 0}
{"raw_text": 0, "ai_score": 0}
).sort("submitted_at", -1)
apps = await cursor.to_list(length=100)
for a in apps:
a["id"] = str(a["_id"])
del a["_id"]
return apps

# Notification endpoints
@router.get("/notifications")
async def get_notifications(current_applicant: str = Depends(require_applicant)):
"""Lấy danh sách thông báo của ứng viên"""
db = get_db()
cursor = db["applicant_notifications"].find(
{"applicant_email": current_applicant}
).sort("created_at", -1)
notifications = await cursor.to_list(length=100)

for notification in notifications:
notification["id"] = str(notification["_id"])
del notification["_id"]

return notifications

@router.patch("/notifications/{notification_id}/read")
async def mark_notification_read(
notification_id: str,
current_applicant: str = Depends(require_applicant)
):
"""Đánh dấu thông báo đã đọc"""
db = get_db()

try:
result = await db["applicant_notifications"].update_one(
{
"_id": ObjectId(notification_id),
"applicant_email": current_applicant
},
{"$set": {"status": "read", "read_at": datetime.now(timezone.utc)}}
)

if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Không tìm thấy thông báo")

return {"status": "success", "message": "Đã đánh dấu thông báo là đã đọc"}
except Exception as e:
raise HTTPException(status_code=400, detail="ID thông báo không hợp lệ")

@router.patch("/notifications/read-all")
async def mark_all_notifications_read(current_applicant: str = Depends(require_applicant)):
"""Đánh dấu tất cả thông báo đã đọc"""
db = get_db()

result = await db["applicant_notifications"].update_many(
{
"applicant_email": current_applicant,
"status": "unread"
},
{"$set": {"status": "read", "read_at": datetime.now(timezone.utc)}}
)

return {
"status": "success",
"message": f"Đã đánh dấu {result.modified_count} thông báo là đã đọc"
}

@router.delete("/notifications/{notification_id}")
async def delete_notification(
notification_id: str,
current_applicant: str = Depends(require_applicant)
):
"""Xóa thông báo"""
db = get_db()

try:
result = await db["applicant_notifications"].delete_one(
{
"_id": ObjectId(notification_id),
"applicant_email": current_applicant
}
)

if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Không tìm thấy thông báo")

return {"status": "success", "message": "Đã xóa thông báo"}
except Exception as e:
raise HTTPException(status_code=400, detail="ID thông báo không hợp lệ")

# Helper function to create notifications
async def create_notification(
applicant_email: str,
title: str,
message: str,
notification_type: str = "info",
job_title: Optional[str] = None,
application_id: Optional[str] = None,
application_status: Optional[str] = None
):
"""Tạo thông báo mới cho ứng viên"""
db = get_db()

notification = {
"applicant_email": applicant_email,
"title": title,
"message": message,
"type": notification_type,
"status": "unread",
"job_title": job_title,
"application_id": application_id,
"application_status": application_status,
"created_at": datetime.now(timezone.utc)
}

await db["applicant_notifications"].insert_one(notification)
return notification
110 changes: 108 additions & 2 deletions app/routers/auth_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@
import httpx
from fastapi.security import OAuth2PasswordRequestForm

class ProfileUpdate(BaseModel):
full_name: str
phone: str = None
address: str = None
github: str = None
linkedin: str = None
bio: str = None
avatar: str = None

class PasswordChange(BaseModel):
current_password: str
new_password: str

router = APIRouter(prefix="/api/v1/auth", tags=["Authentication"])
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")

Expand Down Expand Up @@ -165,6 +178,7 @@ async def google_login(request: GoogleAuthRequest = Body(...)):
"hashed_password": hashed_pw,
"avatar": final_avatar,
"original_avatar": final_avatar,
"role": "applicant", # Mặc định là applicant khi đăng nhập bằng Google
"is_verified": True,
"created_at": datetime.now(timezone.utc)
}
Expand Down Expand Up @@ -193,19 +207,111 @@ async def google_login(request: GoogleAuthRequest = Body(...)):
except ValueError:
raise HTTPException(status_code=401, detail="Token từ Google không hợp lệ hoặc đã hết hạn")

@router.get("/profile")
async def get_profile(email: str = Depends(get_current_user)):
"""Lấy thông tin profile đầy đủ của user"""
db = get_db()
user = await db["hr_users"].find_one({"email": email})

if not user:
raise HTTPException(status_code=404, detail="Không tìm thấy thông tin người dùng")

return {
"email": user["email"],
"full_name": user.get("full_name", ""),
"phone": user.get("phone", ""),
"address": user.get("address", ""),
"github": user.get("github", ""),
"linkedin": user.get("linkedin", ""),
"bio": user.get("bio", ""),
"avatar": user.get("avatar", ""),
"role": user.get("role", "hr")
}

@router.patch("/profile")
async def update_profile(profile_data: ProfileUpdate, email: str = Depends(get_current_user)):
"""Cập nhật thông tin profile"""
db = get_db()

# Prepare update data
update_data = {
"full_name": profile_data.full_name,
"phone": profile_data.phone,
"address": profile_data.address,
"github": profile_data.github,
"linkedin": profile_data.linkedin,
"bio": profile_data.bio,
"avatar": profile_data.avatar,
"updated_at": datetime.now(timezone.utc)
}

# Remove None values
update_data = {k: v for k, v in update_data.items() if v is not None}

result = await db["hr_users"].update_one(
{"email": email},
{"$set": update_data}
)

if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Không tìm thấy người dùng")

return {"status": "success", "message": "Cập nhật thông tin thành công"}

@router.patch("/change-password")
async def change_password(password_data: PasswordChange, email: str = Depends(get_current_user)):
"""Đổi mật khẩu"""
db = get_db()

# Get current user
user = await db["hr_users"].find_one({"email": email})
if not user:
raise HTTPException(status_code=404, detail="Không tìm thấy người dùng")

# Verify current password
if not verify_password(password_data.current_password, user["hashed_password"]):
raise HTTPException(status_code=400, detail="Mật khẩu hiện tại không đúng")

# Hash new password
new_hashed_password = get_password_hash(password_data.new_password)

# Update password
result = await db["hr_users"].update_one(
{"email": email},
{"$set": {
"hashed_password": new_hashed_password,
"updated_at": datetime.now(timezone.utc)
}}
)

if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Không tìm thấy người dùng")

return {"status": "success", "message": "Đổi mật khẩu thành công"}

@router.get("/me")
async def get_current_user_profile(email: str = Depends(get_current_user)):
db = get_db()
user = await db["hr_users"].find_one({"email": email})

if not user:
raise HTTPException(status_code=404, detail="Không tìm thấy thông tin người dùng")

# Lấy role, nếu không có thì mặc định là "applicant"
role = user.get("role", "applicant")

# Nếu user không có role trong DB, cập nhật luôn
if "role" not in user:
await db["hr_users"].update_one(
{"email": email},
{"$set": {"role": "applicant"}}
)

return {
"email": user["email"],
"full_name": user.get("full_name", "HR Manager"),
"full_name": user.get("full_name", "User"),
"avatar": user.get("avatar", ""),
"role": user.get("role", "hr")
"role": role
}

@router.post("/docs-login", response_model=Token, include_in_schema=False)
Expand Down
Loading
Loading