github-actions[bot] commited on
Commit
5ad314c
·
1 Parent(s): 65ba59e

🚀 Auto-deploy backend from GitHub (417cd05)

Browse files
Files changed (1) hide show
  1. main.py +67 -4
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
- role = _snapshot_to_dict(doc).get("role") if _snapshot_exists(doc) else None
 
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=getattr(user, "name", "Unknown"),
4767
- actor_email=getattr(user, "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,