Spaces:
Sleeping
Sleeping
github-actions[bot] commited on
Commit ·
5ad314c
1
Parent(s): 65ba59e
🚀 Auto-deploy backend from GitHub (417cd05)
Browse files
main.py
CHANGED
|
@@ -379,6 +379,7 @@ ROLE_POLICIES: Dict[str, Set[str]] = {
|
|
| 379 |
"/api/learning-path": ALL_APP_ROLES,
|
| 380 |
"/api/analytics/daily-insight": TEACHER_OR_ADMIN,
|
| 381 |
"/api/upload/class-records": TEACHER_OR_ADMIN,
|
|
|
|
| 382 |
"/api/upload/class-records/risk-refresh/recent": TEACHER_OR_ADMIN,
|
| 383 |
"/api/import/student-accounts/preview": TEACHER_OR_ADMIN,
|
| 384 |
"/api/import/student-accounts/commit": TEACHER_OR_ADMIN,
|
|
@@ -546,6 +547,7 @@ logger.info("🚀 FastAPI app created, startup sequence beginning...")
|
|
| 546 |
class AuthenticatedUser(BaseModel):
|
| 547 |
uid: str
|
| 548 |
email: Optional[str] = None
|
|
|
|
| 549 |
role: str
|
| 550 |
claims: Dict[str, Any] = Field(default_factory=dict)
|
| 551 |
|
|
@@ -677,9 +679,10 @@ def _get_role_from_firestore(uid: str) -> Optional[str]:
|
|
| 677 |
|
| 678 |
try:
|
| 679 |
doc = cast(Any, firebase_firestore.client().collection("users").document(uid).get())
|
| 680 |
-
|
|
|
|
| 681 |
if isinstance(role, str):
|
| 682 |
-
_role_cache[uid] = {"role": role, "ts": now}
|
| 683 |
return role
|
| 684 |
except Exception as e:
|
| 685 |
_warn_firestore_role_lookup_once(f"Failed to resolve role from Firestore for {uid}: {e}")
|
|
@@ -1059,6 +1062,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
| 1059 |
request.state.user = AuthenticatedUser(
|
| 1060 |
uid=uid,
|
| 1061 |
email=decoded.get("email"),
|
|
|
|
| 1062 |
role=role,
|
| 1063 |
claims=decoded,
|
| 1064 |
)
|
|
@@ -4763,8 +4767,8 @@ def _write_access_audit_log(
|
|
| 4763 |
_get_audit_logger()(
|
| 4764 |
action=action,
|
| 4765 |
actor_uid=user.uid,
|
| 4766 |
-
actor_name=
|
| 4767 |
-
actor_email=
|
| 4768 |
actor_role=user.role,
|
| 4769 |
description=f"Action: {action} (Status: {status})",
|
| 4770 |
route=request.url.path,
|
|
@@ -7278,6 +7282,65 @@ async def get_recent_risk_refresh_status(
|
|
| 7278 |
raise HTTPException(status_code=500, detail=f"Risk refresh monitor lookup error: {str(e)}")
|
| 7279 |
|
| 7280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7281 |
@app.post("/api/upload/class-records")
|
| 7282 |
async def upload_class_records(
|
| 7283 |
request: Request,
|
|
|
|
| 379 |
"/api/learning-path": ALL_APP_ROLES,
|
| 380 |
"/api/analytics/daily-insight": TEACHER_OR_ADMIN,
|
| 381 |
"/api/upload/class-records": TEACHER_OR_ADMIN,
|
| 382 |
+
"/api/class-section/{class_section_id}": TEACHER_OR_ADMIN,
|
| 383 |
"/api/upload/class-records/risk-refresh/recent": TEACHER_OR_ADMIN,
|
| 384 |
"/api/import/student-accounts/preview": TEACHER_OR_ADMIN,
|
| 385 |
"/api/import/student-accounts/commit": TEACHER_OR_ADMIN,
|
|
|
|
| 547 |
class AuthenticatedUser(BaseModel):
|
| 548 |
uid: str
|
| 549 |
email: Optional[str] = None
|
| 550 |
+
name: Optional[str] = None
|
| 551 |
role: str
|
| 552 |
claims: Dict[str, Any] = Field(default_factory=dict)
|
| 553 |
|
|
|
|
| 679 |
|
| 680 |
try:
|
| 681 |
doc = cast(Any, firebase_firestore.client().collection("users").document(uid).get())
|
| 682 |
+
data = _snapshot_to_dict(doc) if _snapshot_exists(doc) else {}
|
| 683 |
+
role = data.get("role")
|
| 684 |
if isinstance(role, str):
|
| 685 |
+
_role_cache[uid] = {"role": role, "ts": now, "name": data.get("name") or data.get("displayName") or ""}
|
| 686 |
return role
|
| 687 |
except Exception as e:
|
| 688 |
_warn_firestore_role_lookup_once(f"Failed to resolve role from Firestore for {uid}: {e}")
|
|
|
|
| 1062 |
request.state.user = AuthenticatedUser(
|
| 1063 |
uid=uid,
|
| 1064 |
email=decoded.get("email"),
|
| 1065 |
+
name=decoded.get("name") or decoded.get("displayName") or (_role_cache.get(uid) or {}).get("name") or None,
|
| 1066 |
role=role,
|
| 1067 |
claims=decoded,
|
| 1068 |
)
|
|
|
|
| 4767 |
_get_audit_logger()(
|
| 4768 |
action=action,
|
| 4769 |
actor_uid=user.uid,
|
| 4770 |
+
actor_name=user.name or user.email or user.uid,
|
| 4771 |
+
actor_email=user.email or "",
|
| 4772 |
actor_role=user.role,
|
| 4773 |
description=f"Action: {action} (Status: {status})",
|
| 4774 |
route=request.url.path,
|
|
|
|
| 7282 |
raise HTTPException(status_code=500, detail=f"Risk refresh monitor lookup error: {str(e)}")
|
| 7283 |
|
| 7284 |
|
| 7285 |
+
@app.delete("/api/class-section/{class_section_id}")
|
| 7286 |
+
async def delete_class_section(request: Request, class_section_id: str):
|
| 7287 |
+
"""Delete a class section and all associated imported data."""
|
| 7288 |
+
user = get_current_user(request)
|
| 7289 |
+
if user.role not in ("teacher", "admin"):
|
| 7290 |
+
raise HTTPException(status_code=403, detail="Forbidden")
|
| 7291 |
+
|
| 7292 |
+
if not (_firebase_ready and firebase_firestore):
|
| 7293 |
+
raise HTTPException(status_code=503, detail="Firestore unavailable")
|
| 7294 |
+
|
| 7295 |
+
db = firebase_firestore.client()
|
| 7296 |
+
deleted = 0
|
| 7297 |
+
|
| 7298 |
+
def _delete_by_field(coll: str, field: str, value: str) -> int:
|
| 7299 |
+
count = 0
|
| 7300 |
+
docs = db.collection(coll).where(field, "==", value).stream()
|
| 7301 |
+
for d in docs:
|
| 7302 |
+
d.reference.delete()
|
| 7303 |
+
count += 1
|
| 7304 |
+
return count
|
| 7305 |
+
|
| 7306 |
+
# Verify ownership
|
| 7307 |
+
ownership_docs = list(db.collection("classSectionOwnership").where("classSectionId", "==", class_section_id).where("ownerTeacherId", "==", user.uid).stream())
|
| 7308 |
+
if not ownership_docs and user.role != "admin":
|
| 7309 |
+
raise HTTPException(status_code=403, detail="You do not own this class section")
|
| 7310 |
+
|
| 7311 |
+
# Delete associated data
|
| 7312 |
+
deleted += _delete_by_field("managedStudents", "classSectionId", class_section_id)
|
| 7313 |
+
deleted += _delete_by_field("normalizedClassRecords", "classSectionId", class_section_id)
|
| 7314 |
+
deleted += _delete_by_field("classRecordImports", "classSectionId", class_section_id)
|
| 7315 |
+
deleted += _delete_by_field("riskRefreshEvents", "classSectionId", class_section_id)
|
| 7316 |
+
deleted += _delete_by_field("importGroundedFeedbackEvents", "classSectionId", class_section_id)
|
| 7317 |
+
|
| 7318 |
+
# Delete classrooms linked to this section
|
| 7319 |
+
classroom_docs = list(db.collection("classrooms").where("classSectionId", "==", class_section_id).stream())
|
| 7320 |
+
for cd in classroom_docs:
|
| 7321 |
+
_delete_by_field("managedStudents", "classroomId", cd.id)
|
| 7322 |
+
cd.reference.delete()
|
| 7323 |
+
deleted += 1
|
| 7324 |
+
|
| 7325 |
+
# Delete ownership record
|
| 7326 |
+
for od in ownership_docs:
|
| 7327 |
+
od.reference.delete()
|
| 7328 |
+
deleted += 1
|
| 7329 |
+
|
| 7330 |
+
# Also try deleting by the section ID as document ID in classSectionOwnership
|
| 7331 |
+
try:
|
| 7332 |
+
ownership_ref = db.collection("classSectionOwnership").document(class_section_id)
|
| 7333 |
+
if ownership_ref.get().exists:
|
| 7334 |
+
ownership_ref.delete()
|
| 7335 |
+
deleted += 1
|
| 7336 |
+
except Exception:
|
| 7337 |
+
pass
|
| 7338 |
+
|
| 7339 |
+
_write_access_audit_log(request, action="delete_class_section", status="success", class_section_id=class_section_id, metadata={"deletedDocs": deleted})
|
| 7340 |
+
|
| 7341 |
+
return {"success": True, "deletedDocs": deleted, "classSectionId": class_section_id}
|
| 7342 |
+
|
| 7343 |
+
|
| 7344 |
@app.post("/api/upload/class-records")
|
| 7345 |
async def upload_class_records(
|
| 7346 |
request: Request,
|