thực hiện phân quyền admin giáo viên và học sinh xong

This commit is contained in:
2026-04-01 19:26:47 +07:00
parent 8546d41a26
commit 386871bd4a
14 changed files with 796 additions and 37 deletions
+83
View File
@@ -0,0 +1,83 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from ..database import get_db
from ..dependencies import require_admin
from ..models import User, UserStatus
from ..schemas import UserApprove, UserRoleUpdate, UserOut
router = APIRouter(prefix="/admin", tags=["admin"])
# ── GET /api/admin/users ──────────────────────────────────────────────────────
@router.get("/users", response_model=list[UserOut])
def list_users(
admin: User = Depends(require_admin),
db: Session = Depends(get_db),
):
"""Return all users (any role / status). Admin only."""
return db.query(User).order_by(User.created_at.desc()).all()
# ── PATCH /api/admin/users/{user_id}/status ───────────────────────────────────
@router.patch("/users/{user_id}/status", response_model=UserOut)
def update_user_status(
user_id: int,
body: UserApprove,
admin: User = Depends(require_admin),
db: Session = Depends(get_db),
):
"""Approve or reject a user account. Admin only."""
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found.")
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot change your own status.")
user.status = body.status
db.commit()
db.refresh(user)
return user
# ── PATCH /api/admin/users/{user_id}/role ─────────────────────────────────────
@router.patch("/users/{user_id}/role", response_model=UserOut)
def update_user_role(
user_id: int,
body: UserRoleUpdate,
admin: User = Depends(require_admin),
db: Session = Depends(get_db),
):
"""Change a user's role. Admin only."""
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found.")
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot change your own role.")
user.role = body.role
db.commit()
db.refresh(user)
return user
# ── DELETE /api/admin/users/{user_id} ─────────────────────────────────────────
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(
user_id: int,
admin: User = Depends(require_admin),
db: Session = Depends(get_db),
):
"""Permanently delete a user and all their data. Admin only."""
user = db.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found.")
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot delete your own account.")
db.delete(user)
db.commit()
+94 -1
View File
@@ -6,10 +6,50 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..dependencies import get_current_user
from ..models import Annotation, PDF, User
from ..models import Annotation, PDF, User, UserRole
from ..redis_client import ANN_TTL, ann_temp_key, ann_temp_page_pattern, get_redis
from ..schemas import AnnotationIn, AnnotationOut
def _do_delete_annotation(ann: Annotation, current_user: User, db: Session) -> None:
"""Shared RBAC + delete logic, used by both delete endpoints."""
from ..models import UserRole # avoid circular at module level
ann_owner = db.get(User, ann.user_id)
if current_user.role == UserRole.admin:
pass # full access
elif current_user.role == UserRole.teacher:
if ann.user_id == current_user.id:
pass # own annotation
elif ann_owner and ann_owner.role == UserRole.admin:
raise HTTPException(
status_code=http_status.HTTP_403_FORBIDDEN,
detail="Teachers cannot delete annotations belonging to an admin.",
)
else:
pass # can delete student annotations (or annotations of deleted users)
else: # student
if ann.user_id != current_user.id:
raise HTTPException(
status_code=http_status.HTTP_403_FORBIDDEN,
detail="You can only delete your own annotations.",
)
pdf_id = ann.pdf_id
page_number = ann.page_number
user_id = ann.user_id
db.delete(ann)
db.commit()
# Remove Redis temp entry so /all doesn't serve stale data
r = get_redis()
if r is not None:
try:
r.delete(ann_temp_key(pdf_id, page_number, user_id))
except Exception:
pass
router = APIRouter(prefix="/annotations", tags=["annotations"])
@@ -211,3 +251,56 @@ def upsert_annotation(
pass
return ann
# ── DELETE /api/annotations/{annotation_id} ───────────────────────────────────
@router.delete("/{annotation_id}", status_code=http_status.HTTP_204_NO_CONTENT)
def delete_annotation(
annotation_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Delete annotation by primary key (RBAC enforced)."""
ann = db.get(Annotation, annotation_id)
if not ann:
raise HTTPException(status_code=404, detail="Annotation not found.")
_do_delete_annotation(ann, current_user, db)
# ── DELETE /api/annotations/{pdf_id}/{page_number}/user/{target_user_id} ──────
@router.delete("/{pdf_id}/{page_number}/user/{target_user_id}", status_code=http_status.HTTP_204_NO_CONTENT)
def delete_user_page_annotation(
pdf_id: int,
page_number: int,
target_user_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""
Delete a specific user's annotation on a given page (RBAC enforced).
Useful for teachers clearing a student's page without knowing the annotation id.
Returns 204 even when no annotation exists (idempotent).
"""
_any_pdf_or_404(db, pdf_id)
ann = (
db.query(Annotation)
.filter(
Annotation.pdf_id == pdf_id,
Annotation.page_number == page_number,
Annotation.user_id == target_user_id,
)
.first()
)
if ann is None:
# Nothing in DB — still clean up any Redis temp entry
r = get_redis()
if r is not None:
try:
r.delete(ann_temp_key(pdf_id, page_number, target_user_id))
except Exception:
pass
return
_do_delete_annotation(ann, current_user, db)
+23 -3
View File
@@ -4,8 +4,8 @@ import os
from ..database import get_db
from ..dependencies import get_current_user
from ..models import User
from ..schemas import UserLogin, UserOut, UserRegister
from ..models import User, UserRole, UserStatus
from ..schemas import UserLogin, UserOut, UserRegister, UserApprove
from ..security import (
ACCESS_TOKEN_EXPIRE_MINUTES,
create_access_token,
@@ -42,16 +42,25 @@ def register(body: UserRegister, response: Response, db: Session = Depends(get_d
if db.query(User).filter(User.email == body.email).first():
raise HTTPException(status_code=400, detail="Email already registered.")
# Admin registers as approved immediately; others start as pending
initial_status = (
UserStatus.approved if body.role == UserRole.admin else UserStatus.pending
)
user = User(
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
role=body.role,
status=initial_status,
)
db.add(user)
db.commit()
db.refresh(user)
_set_auth_cookie(response, user.id)
# Only set auth cookie if immediately approved (admin self-registration)
if user.status == UserStatus.approved:
_set_auth_cookie(response, user.id)
return user
@@ -68,6 +77,17 @@ def login(body: UserLogin, response: Response, db: Session = Depends(get_db)):
if not user or not password_ok:
raise HTTPException(status_code=401, detail="Invalid username or password.")
if user.status == UserStatus.pending:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Your account is pending admin approval.",
)
if user.status == UserStatus.rejected:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Your account has been rejected.",
)
_set_auth_cookie(response, user.id)
return user