Testys's picture
Let's just finish this
3358b33
from fastapi import APIRouter, Depends, HTTPException, status, Security
from fastapi.security import APIKeyHeader
from sqlmodel import Session, SQLModel
from typing import Dict
from threading import Lock # Added for thread-safety on global state
from src.database import get_session
from src.auth import get_current_active_user, get_api_key
from src.models import (
RFIDStatusResponse, RFIDScanRequest, ClearanceStatusEnum,
TagScan, Device, Role, User
)
from src.crud import students as student_crud
from src.crud import users as user_crud
from src.crud import devices as device_crud
# Define the router and the API key security scheme
router = APIRouter(prefix="/rfid", tags=["RFID"])
api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
# --- Moved from admin.py: State for secure admin scanning ---
# Maps a device's API key to the admin user ID who activated it.
activated_scanners: Dict[str, int] = {}
# Stores the last tag scanned by a device, keyed by the admin ID who was waiting.
admin_scanned_tags: Dict[int, tuple[str, str]] = {}
# Locks for thread-safe access to global dicts (since FastAPI is async/multi-threaded)
activated_scanners_lock = Lock()
admin_scanned_tags_lock = Lock()
# --- Moved from admin.py: Secure Scanning Workflow ---
class ActivationRequest(SQLModel):
api_key: str
@router.post("/scanners/activate", status_code=status.HTTP_204_NO_CONTENT)
def activate_admin_scanner(
activation: ActivationRequest,
db: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user(required_roles=[Role.ADMIN, Role.STAFF]))
):
"""
STEP 1 (Browser): Admin clicks "Scan Card" in the UI.
The browser calls this endpoint to 'arm' their designated desk scanner.
"""
device = device_crud.get_device_by_api_key(db, api_key=activation.api_key)
if not device:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found.")
# Map the device's API key to the currently logged-in admin's ID (thread-safe).
with activated_scanners_lock:
activated_scanners[device.api_key] = current_user.id
return
@router.post("/scanners/scan", status_code=status.HTTP_204_NO_CONTENT)
def receive_scan_from_activated_device(
scan_data: TagScan,
db: Session = Depends(get_session) # Added back for device validation
):
"""
STEP 2 (Device): The ESP32 device sends the scanned tag to this endpoint.
No authentication required for free use, but validates device existence.
"""
# Validate that the device exists (minimal security check without full auth).
device = device_crud.get_device_by_api_key(db, api_key=scan_data.api_key)
if not device:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found.")
# Check if this device was activated by an admin (thread-safe).
with activated_scanners_lock:
admin_id = activated_scanners.pop(scan_data.api_key, None)
if admin_id is None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="This scanner has not been activated for a scan.")
# Store the scanned tag against the admin who was waiting for it (thread-safe).
with admin_scanned_tags_lock:
admin_scanned_tags[admin_id] = (scan_data.tag_id, scan_data.api_key)
return
@router.get("/scanners/retrieve", response_model=TagScan)
def retrieve_scanned_tag_for_ui(
current_user: User = Depends(get_current_active_user(required_roles=[Role.ADMIN, Role.STAFF]))
):
"""
STEP 3 (Browser): The browser polls this endpoint to get the tag ID
that the device reported in STEP 2.
"""
# Retrieve the tag data (thread-safe).
with admin_scanned_tags_lock:
tag_data = admin_scanned_tags.pop(current_user.id, None)
if not tag_data:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No tag has been scanned by the activated device yet.")
tag_id, api_key = tag_data
return TagScan(tag_id=tag_id, api_key=api_key)
# --- Existing /check-status endpoint remains ---
@router.post("/check-status", response_model=RFIDStatusResponse)
def check_rfid_status(
scan_data: RFIDScanRequest,
db: Session = Depends(get_session),
# This dependency ensures the request comes from a valid, registered device
api_key: str = Security(get_api_key),
):
"""
Public endpoint for hardware devices to check the status of a scanned RFID tag.
The device must provide a valid API key in the 'x-api-key' header.
"""
tag_id = scan_data.tag_id
# 1. Check if the tag belongs to a student
student = student_crud.get_student_by_tag_id(db, tag_id=tag_id)
if student:
# Check overall clearance status using proper enum comparison
is_cleared = all(
clearance.status == ClearanceStatusEnum.APPROVED
for clearance in student.clearance_statuses
)
clearance_status_str = "Fully Cleared" if is_cleared else "Pending Clearance"
return RFIDStatusResponse(
status="found",
full_name=student.full_name,
entity_type="Student",
clearance_status=clearance_status_str,
)
# 2. If not a student, check if it belongs to a user (staff/admin)
user = user_crud.get_user_by_tag_id(db, tag_id=tag_id)
if user:
return RFIDStatusResponse(
status="found",
full_name=user.full_name,
entity_type=user.role.value.title(), # "Admin" or "Staff"
clearance_status="N/A",
)
# 3. If the tag is not linked to anyone
return RFIDStatusResponse(
status="unregistered",
full_name=None,
entity_type=None,
clearance_status=None,
)