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
10 changes: 9 additions & 1 deletion app/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,21 @@ class HRUserCreate(BaseModel):
email: EmailStr
full_name: str = Field(..., min_length=6, example="Trần Nam")
password: str
role: str = Field(default="hr", example="hr hoặc applicant")

@field_validator('password')
def validate_password(cls, v):
pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"
pattern = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#+\-_=])[A-Za-z\d@$!%*?&#+\-_=]{8,}$"
if not re.match(pattern, v):
raise ValueError("Mật khẩu phải từ 8 ký tự, gồm ít nhất 1 chữ hoa, 1 chữ thường, 1 số và 1 ký tự đặc biệt.")
return v

@field_validator('role')
def validate_role(cls, v):
if v not in ("hr", "applicant"):
raise ValueError("Role phải là 'hr' hoặc 'applicant'.")
return v

class HRUserLogin(BaseModel):
email: EmailStr
password: str
Expand All @@ -96,6 +103,7 @@ class HRUserDB(BaseModel):
hashed_password: str
avatar: str
original_avatar: str
role: str = Field(default="hr")
is_verified: bool = Field(default=False)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

Expand Down
3 changes: 3 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from fastapi.middleware.cors import CORSMiddleware
from app.database.config import connect_to_mongo, close_mongo_connection
from app.routers import auth_router, cv_router, job_router
from app.routers import admin_router, applicant_router
from contextlib import asynccontextmanager

@asynccontextmanager
Expand All @@ -28,6 +29,8 @@ async def lifespan(app: FastAPI):
app.include_router(auth_router.router)
app.include_router(cv_router.router)
app.include_router(job_router.router)
app.include_router(admin_router.router)
app.include_router(applicant_router.router)

@app.get("/", tags=["Health Check"])
def root():
Expand Down
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
55 changes: 55 additions & 0 deletions app/routers/admin_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from fastapi import APIRouter, Depends, HTTPException
from bson import ObjectId
from app.auth import get_current_user, get_current_user_with_role
from app.database.config import get_db
import os

router = APIRouter(prefix="/api/v1/admin", tags=["Admin"])

VALID_ROLES = ("hr", "applicant", "admin")
BOOTSTRAP_SECRET = os.getenv("ADMIN_BOOTSTRAP_SECRET", "")

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 user_info["email"]

@router.post("/bootstrap")
async def bootstrap_admin(body: dict):
"""Dùng 1 lần để tạo admin đầu tiên. Cần ADMIN_BOOTSTRAP_SECRET trong .env"""
if not BOOTSTRAP_SECRET:
raise HTTPException(status_code=403, detail="Bootstrap đã bị tắt")
if body.get("secret") != BOOTSTRAP_SECRET:
raise HTTPException(status_code=403, detail="Secret không đúng")
email = body.get("email")
if not email:
raise HTTPException(status_code=400, detail="Thiếu email")
db = get_db()
result = await db["hr_users"].update_one({"email": email}, {"$set": {"role": "admin"}})
if result.matched_count == 0:
raise HTTPException(status_code=404, detail="Không tìm thấy tài khoản")
return {"status": "success", "message": f"{email} đã được set làm Admin"}

@router.get("/users")
async def list_users(_: str = Depends(require_admin)):
db = get_db()
cursor = db["hr_users"].find({}, {"hashed_password": 0, "raw_text": 0})
users = await cursor.to_list(length=500)
for u in users:
u["id"] = str(u["_id"])
del u["_id"]
return users

@router.patch("/users/{user_id}/role")
async def update_user_role(user_id: str, body: dict, _: str = Depends(require_admin)):
new_role = body.get("role")
if new_role not in VALID_ROLES:
raise HTTPException(status_code=400, detail=f"Role không hợp lệ. Chọn: {VALID_ROLES}")
db = get_db()
result = await db["hr_users"].update_one(
{"_id": ObjectId(user_id)},
{"$set": {"role": new_role}}
)
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": f"Đã cập nhật role thành '{new_role}'"}
Loading
Loading