File size: 4,634 Bytes
d2075b0
4c4a722
d2075b0
 
 
 
 
 
 
4c4a722
 
 
 
d2075b0
 
4c4a722
 
 
d2075b0
4c4a722
 
 
 
 
108b5b1
 
 
4c4a722
 
 
 
 
 
 
 
108b5b1
4c4a722
 
 
 
d2075b0
 
 
 
 
 
 
 
 
 
 
 
 
4c4a722
d2075b0
 
 
 
 
4c4a722
 
 
d2075b0
 
4c4a722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d2075b0
 
4c4a722
d2075b0
 
4c4a722
 
108b5b1
d2075b0
108b5b1
 
d2075b0
108b5b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c4a722
d2075b0
 
 
4c4a722
 
 
 
 
d2075b0
 
4c4a722
d2075b0
 
4c4a722
 
 
d2075b0
4c4a722
d2075b0
4c4a722
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""Dashboard API.

Two access levels share these routes:
- Admins (is_admin) see ALL teachers' data and can manage users.
- Regular teachers see ONLY their own data (their quizzes/students/results) and
  cannot reach user-management or other teachers' records.

Scoping is enforced server-side: list endpoints filter by the caller's teacher
id, and detail endpoints verify ownership before returning anything.
"""

from __future__ import annotations

from typing import Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

from .auth import get_current_admin, get_current_user
from .database import (
    admin_list_sessions,
    admin_list_students,
    delete_session,
    delete_user,
    get_parsed_data,
    get_quiz_results,
    get_raw_files,
    get_schema_version,
    get_session,
    get_stats,
    get_student,
    get_student_timeline,
    list_all_users,
    set_user_admin,
)
from .storage import create_signed_url

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


def _scope(user: dict) -> Optional[int]:
    """None for admins (see everything), else the teacher id to scope to."""
    return None if user.get("is_admin") else user["id"]


def _owns_quiz(user: dict, quiz: dict) -> bool:
    return user.get("is_admin") or quiz.get("teacher_id") == user["id"]


def _owns_student(user: dict, student: dict) -> bool:
    return user.get("is_admin") or student.get("teacher_id") == user["id"]


@router.get("/stats")
async def dashboard_stats(user=Depends(get_current_user)):
    stats = await get_stats(_scope(user))
    stats["is_admin"] = bool(user.get("is_admin"))
    if user.get("is_admin"):
        stats["schema_version"] = await get_schema_version()
    return stats


# ---- User management (admin only) ----

@router.get("/users")
async def admin_users(_admin=Depends(get_current_admin)):
    return {"users": await list_all_users()}


class SetAdminRequest(BaseModel):
    is_admin: bool


@router.put("/users/{user_id}/admin")
async def admin_set_admin(user_id: int, body: SetAdminRequest, admin=Depends(get_current_admin)):
    if user_id == admin["id"] and not body.is_admin:
        raise HTTPException(status_code=400, detail="You cannot remove your own admin access")
    await set_user_admin(user_id, body.is_admin)
    return {"status": "ok", "user_id": user_id, "is_admin": body.is_admin}


@router.delete("/users/{user_id}")
async def admin_delete_user(user_id: int, admin=Depends(get_current_admin)):
    if user_id == admin["id"]:
        raise HTTPException(status_code=400, detail="You cannot delete your own account here")
    await delete_user(user_id)
    return {"status": "deleted"}


# ---- Sessions / quizzes (scoped) ----

@router.get("/sessions")
async def dashboard_sessions(user=Depends(get_current_user)):
    return {"sessions": await admin_list_sessions(_scope(user))}


@router.get("/sessions/{session_id}")
async def dashboard_session_detail(session_id: int, user=Depends(get_current_user)):
    """Full quiz detail: raw files (signed URLs), structured parsed data, results."""
    quiz = await get_session(session_id)
    if not quiz or not _owns_quiz(user, quiz):
        raise HTTPException(status_code=404, detail="Session not found")

    parsed = {}
    for item in await get_parsed_data(session_id):
        parsed[item["data_type"]] = item["structured_data"]

    raw_files = []
    for f in await get_raw_files(session_id):
        f["download_url"] = await create_signed_url(f.get("storage_path", ""))
        raw_files.append(f)

    return {
        "quiz": quiz,
        "raw_files": raw_files,
        "parsed_data": parsed,
        "results": await get_quiz_results(session_id),
    }


@router.delete("/sessions/{session_id}")
async def dashboard_delete_session(session_id: int, user=Depends(get_current_user)):
    quiz = await get_session(session_id)
    if not quiz or not _owns_quiz(user, quiz):
        raise HTTPException(status_code=404, detail="Session not found")
    await delete_session(session_id)
    return {"status": "deleted"}


# ---- Students (scoped) ----

@router.get("/students")
async def dashboard_students(user=Depends(get_current_user)):
    return {"students": await admin_list_students(_scope(user))}


@router.get("/students/{student_id}")
async def dashboard_student_detail(student_id: int, user=Depends(get_current_user)):
    student = await get_student(student_id)
    if not student or not _owns_student(user, student):
        raise HTTPException(status_code=404, detail="Student not found")
    return {"student": student, "timeline": await get_student_timeline(student_id)}