Testys commited on
Commit
83767ea
·
1 Parent(s): d652933

Just to stash

Browse files
src/auth.py DELETED
@@ -1,205 +0,0 @@
1
- from fastapi import HTTPException, status, Header, Depends
2
- from fastapi.security import OAuth2PasswordBearer
3
- from fastapi.concurrency import run_in_threadpool # For calling sync crud in async auth
4
- from sqlalchemy.orm import Session as SQLAlchemySessionType
5
-
6
- from src import crud, models
7
-
8
- from src.database import get_db
9
- from typing import Optional, Dict, Any # Added Any
10
- from datetime import datetime, timedelta
11
- from typing import Union # For type hinting
12
- from jose import JWTError, jwt
13
- from passlib.context import CryptContext
14
-
15
- # JWT Configuration - Loaded from models.py (which loads from .env)
16
- SECRET_KEY = models.JWT_SECRET_KEY
17
- ALGORITHM = "HS256"
18
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
19
-
20
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token/login") # Path to token endpoint
21
-
22
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # Password hashing context
23
- # Password hashing context from models.py
24
-
25
- def verify_password(plain_password: str, hashed_password: str) -> bool:
26
- """
27
- Verifies a plain password against a hashed password.
28
- Uses the CryptContext to verify the password.
29
- """
30
- return pwd_context.verify(plain_password, hashed_password)
31
-
32
- def get_password_hash(password: str) -> str:
33
- """
34
- Hashes a password using the CryptContext.
35
- This is used when creating or updating user passwords.
36
- """
37
- return pwd_context.hash(password)
38
-
39
-
40
- # --- JWT Helper Functions ---
41
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
42
- to_encode = data.copy()
43
- if expires_delta:
44
- expire = datetime.utcnow() + expires_delta
45
- else:
46
- expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
47
- to_encode.update({"exp": expire, "iat": datetime.utcnow()}) # Add issued_at time
48
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
49
- return encoded_jwt
50
-
51
-
52
- async def get_current_user_from_token(
53
- token: str = Depends(oauth2_scheme),
54
- db: SQLAlchemySessionType = Depends(get_db)
55
- ) -> models.User: # Now aims to return the ORM User model
56
- credentials_exception = HTTPException(
57
- status_code=status.HTTP_401_UNAUTHORIZED,
58
- detail="Could not validate credentials",
59
- headers={"WWW-Authenticate": "Bearer"},
60
- )
61
- try:
62
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
63
- username: Optional[str] = payload.get("sub")
64
- if username is None:
65
- raise credentials_exception
66
- token_data = models.TokenData(username=username) # Use if TokenData has more fields
67
- except jwt.ExpiredSignatureError:
68
- raise HTTPException(
69
- status_code=status.HTTP_401_UNAUTHORIZED,
70
- detail="Token has expired",
71
- headers={"WWW-Authenticate": "Bearer"},)
72
- except jwt.PyJWTError:
73
- raise credentials_exception
74
-
75
- # User is fetched using sync ORM function, so run in threadpool if this dep is used by async endpoint
76
- user_orm = await run_in_threadpool(crud.get_user_by_username, db, username)
77
- if user_orm is None:
78
- raise credentials_exception
79
- return user_orm # Return the ORM model instance
80
-
81
- async def get_current_active_user(
82
- current_user: models.User = Depends(get_current_user_from_token)
83
- ) -> models.User: # Expects and returns ORM User
84
- if not current_user.is_active:
85
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
86
- return current_user
87
-
88
- async def get_current_active_staff_user_from_token(
89
- current_user: models.User = Depends(get_current_active_user)
90
- ) -> models.User: # Expects and returns ORM User
91
- if current_user.role not in [models.UserRole.STAFF, models.UserRole.ADMIN]:
92
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Staff or admin access required")
93
- return current_user
94
-
95
- async def get_current_active_admin_user_from_token(
96
- current_user: models.User = Depends(get_current_active_user)
97
- ) -> models.User: # Expects and returns ORM User
98
- if current_user.role != models.UserRole.ADMIN:
99
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
100
- return current_user
101
-
102
- # Dependency to get and verify API key from header (Device Authentication)
103
- async def get_verified_device(
104
- x_api_key: str = Header(..., description="The API Key for the ESP32 device."),
105
- db: SQLAlchemySessionType = Depends(get_db)
106
- ) -> models.Device: # Returns the ORM Device model
107
- """
108
- Verifies API key and returns the active ORM Device model.
109
- """
110
- if not x_api_key:
111
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing API key")
112
-
113
- # Use run_in_threadpool as crud.get_device_by_api_key is sync
114
- device_orm = await run_in_threadpool(crud.get_device_by_api_key, db, x_api_key)
115
-
116
- if not device_orm:
117
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API Key.")
118
- if not device_orm.is_active:
119
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Device is not active.")
120
- return device_orm
121
-
122
-
123
- def authenticate_user(
124
- username: str,
125
- password: str,
126
- db: SQLAlchemySessionType = Depends(get_db)
127
- ) -> models.User: # Returns ORM User model
128
- """
129
- Authenticates a user by username and password.
130
- Returns the ORM User model if successful, raises HTTPException otherwise.
131
- """
132
- user = crud.get_user_by_username(db, username)
133
-
134
- if not user:
135
- return None # User not found, return None
136
-
137
- is_password_valid = verify_password(password, user.hashed_password)
138
- if not is_password_valid:
139
- return None
140
-
141
- return user # Return the ORM User model if password is valid
142
-
143
- # Tag-based authentication (User/Student Authentication via RFID tag)
144
- async def authenticate_tag_user_or_student( # Renamed for clarity
145
- tag_id: str = Header(..., alias="X-User-Tag-ID", description="RFID Tag ID of the user or student"),
146
- db: SQLAlchemySessionType = Depends(get_db)
147
- ) -> Union[models.Student, models.User]: # Returns Student or User ORM model
148
- """
149
- Authenticates a tag and returns the corresponding Student or User ORM model.
150
- Used as a base for tag-based auth dependencies.
151
- """
152
- # Run sync ORM calls in threadpool
153
- student_orm = await run_in_threadpool(crud.get_student_by_tag_id, db, tag_id)
154
- if student_orm:
155
- return student_orm # Return Student ORM model
156
-
157
- user_orm = await run_in_threadpool(crud.get_user_by_tag_id, db, tag_id) # get_user_by_tag_id checks is_active
158
- if user_orm: # user_orm already checked for is_active in CRUD
159
- return user_orm # Return User ORM model
160
-
161
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tag not registered or associated user/student is inactive.")
162
-
163
- # Dependency for current student via Tag ID
164
- async def get_current_student_via_tag(
165
- authenticated_entity: Union[models.Student, models.User] = Depends(authenticate_tag_user_or_student)
166
- ) -> models.Student:
167
- if not isinstance(authenticated_entity, models.Student):
168
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access restricted to students only.")
169
- return authenticated_entity
170
-
171
- # Dependency for current staff or admin via Tag ID
172
- async def get_current_staff_or_admin_via_tag(
173
- authenticated_entity: Union[models.Student, models.User] = Depends(authenticate_tag_user_or_student)
174
- ) -> models.User:
175
- if not isinstance(authenticated_entity, models.User): # It's a student
176
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Staff or admin access required.")
177
- # authenticated_entity is User ORM model
178
- if authenticated_entity.role not in [models.UserRole.STAFF, models.UserRole.ADMIN]:
179
- # This case should ideally not be hit if authenticate_tag_user_or_student is correct
180
- # and users fetched by tag are always staff/admin.
181
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User role is not staff or admin.")
182
- if not authenticated_entity.is_active: # Double check, though crud.get_user_by_tag_id should handle this
183
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User is inactive.")
184
- return authenticated_entity
185
-
186
- # Dependency for current admin via Tag ID
187
- async def get_current_admin_via_tag(
188
- current_user: models.User = Depends(get_current_staff_or_admin_via_tag) # Leverages the staff_or_admin check
189
- ) -> models.User:
190
- if current_user.role != models.UserRole.ADMIN:
191
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required.")
192
- return current_user
193
-
194
-
195
- # Department Access Verification (this is a utility, not a dependency itself)
196
- def verify_department_access( # Made sync as it's pure logic
197
- user_role: models.UserRole,
198
- user_department: Optional[models.ClearanceDepartment],
199
- target_department: models.ClearanceDepartment
200
- ) -> bool:
201
- if user_role == models.UserRole.ADMIN:
202
- return True
203
- if user_role == models.UserRole.STAFF:
204
- return user_department == target_department
205
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config.py DELETED
@@ -1,15 +0,0 @@
1
- from pydantic_settings import BaseSettings
2
- import os
3
- from dotenv import load_dotenv
4
-
5
- load_dotenv()
6
-
7
- class Settings(BaseSettings):
8
- POSTGRES_URI: str
9
- JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "default_secret_key")
10
- ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
11
-
12
- class Config:
13
- env_file = ".env"
14
-
15
- settings = Settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/__init__.py DELETED
@@ -1,83 +0,0 @@
1
- """
2
- CRUD Package Initializer
3
-
4
- This file makes the 'crud' directory a Python package and imports all the
5
- public CRUD functions from the submodules. This allows you to import any
6
- CRUD function directly from `src.crud` instead of the specific submodule,
7
- keeping the router imports clean.
8
- """
9
-
10
- from .students import (
11
- create_student,
12
- get_all_students,
13
- get_student_by_student_id,
14
- get_student_by_tag_id,
15
- update_student_tag_id,
16
- delete_student,
17
- )
18
- from .users import (
19
- create_user,
20
- get_user_by_username,
21
- get_user_by_tag_id,
22
- update_user_tag_id,
23
- get_user_by_id,
24
- delete_user,
25
- hash_password, # Import hash_password from users module
26
- get_all_users
27
- )
28
- from .devices import (
29
- get_device_by_id_str,
30
- get_device_by_api_key,
31
- create_device_log,
32
- update_device_last_seen,
33
- delete_device,
34
- )
35
- from .clearance import (
36
- get_clearance_statuses_by_student_id,
37
- update_clearance_status,
38
- delete_clearance_status,
39
- get_all_clearance_status,
40
- get_student_clearance_status
41
- )
42
- from .tag_linking import (
43
- create_pending_tag_link,
44
- get_pending_link_by_id,
45
- delete_pending_link_by_device_id,
46
- get_pending_links,
47
- )
48
-
49
- # Export all functions
50
- __all__ = [
51
- # Users
52
- 'create_user',
53
- 'get_user_by_username',
54
- 'get_user_by_tag_id',
55
- 'update_user_tag_id',
56
- 'get_user_by_id',
57
- 'delete_user',
58
- 'hash_password',
59
- 'get_all_users',
60
- # Students
61
- 'create_student',
62
- 'get_all_students',
63
- 'get_student_by_student_id',
64
- 'get_student_by_tag_id',
65
- 'update_student_tag_id',
66
- 'delete_student',
67
- # Devices
68
- 'get_device_by_id_str',
69
- 'get_device_by_api_key',
70
- 'create_device_log',
71
- 'update_device_last_seen',
72
- 'delete_device',
73
- # Clearance
74
- 'get_clearance_statuses_by_student_id',
75
- 'update_clearance_status',
76
- 'delete_clearance_status',
77
- # Tag Linking
78
- 'create_pending_tag_link',
79
- 'get_pending_link_by_device_id',
80
- 'get_pending_link_by_token',
81
- 'delete_pending_link_by_id',
82
- 'get_all_pending_links',
83
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/clearance.py DELETED
@@ -1,102 +0,0 @@
1
- """
2
- CRUD operations for student Clearance Statuses.
3
- """
4
- from sqlalchemy.orm import Session
5
- from fastapi import HTTPException, status
6
-
7
- from src import models
8
-
9
- def get_clearance_statuses_by_student_id(db: Session, student_id: str) -> list[models.ClearanceStatus]:
10
- """
11
- Fetches all existing clearance status records for a given student.
12
- """
13
- return db.query(models.ClearanceStatus).filter(models.ClearanceStatus.student_id == student_id).all()
14
-
15
- def update_clearance_status(
16
- db: Session,
17
- student_id: str,
18
- department: models.ClearanceDepartment,
19
- new_status: models.ClearanceStatusEnum,
20
- remarks: str,
21
- cleared_by_user_id: int
22
- ) -> models.ClearanceStatus:
23
- """
24
- Updates or creates a clearance status for a student in a specific department.
25
- """
26
- student = db.query(models.Student).filter(models.Student.student_id == student_id).first()
27
- if not student:
28
- raise HTTPException(
29
- status_code=status.HTTP_404_NOT_FOUND,
30
- detail=f"Student with ID '{student_id}' not found."
31
- )
32
-
33
- existing_status = db.query(models.ClearanceStatus).filter(
34
- models.ClearanceStatus.student_id == student_id,
35
- models.ClearanceStatus.department == department
36
- ).first()
37
-
38
- if existing_status:
39
- existing_status.status = new_status
40
- existing_status.remarks = remarks
41
- existing_status.cleared_by = cleared_by_user_id
42
- db_status = existing_status
43
- else:
44
- db_status = models.ClearanceStatus(
45
- student_id=student_id,
46
- department=department,
47
- status=new_status,
48
- remarks=remarks,
49
- cleared_by=cleared_by_user_id
50
- )
51
- db.add(db_status)
52
-
53
- db.commit()
54
- db.refresh(db_status)
55
- return db_status
56
-
57
- def delete_clearance_status(
58
- db: Session,
59
- student_id: str,
60
- department: models.ClearanceDepartment
61
- ) -> models.ClearanceStatus | None:
62
- """
63
- Deletes a specific clearance status record for a student.
64
-
65
- This effectively resets the status for that department to the default state.
66
- Returns the deleted object if found, otherwise returns None.
67
- """
68
- status_to_delete = db.query(models.ClearanceStatus).filter(
69
- models.ClearanceStatus.student_id == student_id,
70
- models.ClearanceStatus.department == department
71
- ).first()
72
-
73
- if status_to_delete:
74
- db.delete(status_to_delete)
75
- db.commit()
76
-
77
- return status_to_delete
78
-
79
-
80
- def get_all_clearance_status(db: Session) -> list[models.ClearanceStatus]:
81
- """
82
- Retrieves all clearance statuses from the database.
83
-
84
- This function is useful for administrative purposes, allowing staff to view
85
- all clearance records across all students and departments.
86
- """
87
- return db.query(models.ClearanceStatus).all()
88
-
89
- def get_student_clearance_status(
90
- db: Session,
91
- student_id: str,
92
- department: models.ClearanceDepartment
93
- ) -> models.ClearanceStatus | None:
94
- """
95
- Retrieves the clearance status for a specific student in a specific department.
96
-
97
- Returns None if no status exists for that student and department.
98
- """
99
- return db.query(models.ClearanceStatus).filter(
100
- models.ClearanceStatus.student_id == student_id,
101
- models.ClearanceStatus.department == department
102
- ).first()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/devices.py DELETED
@@ -1,52 +0,0 @@
1
- """
2
- CRUD operations for Devices and Device Logs.
3
- """
4
- from sqlalchemy.orm import Session
5
- from datetime import datetime
6
- from fastapi import HTTPException, status
7
-
8
- from src import models
9
-
10
- def get_device_by_id_str(db: Session, device_id: str) -> models.Device | None:
11
- """Fetches a device by its public device_id string."""
12
- return db.query(models.Device).filter(models.Device.device_id == device_id).first()
13
-
14
- def get_device_by_api_key(db: Session, api_key: str) -> models.Device | None:
15
- """Fetches a device by its unique API key for authentication."""
16
- return db.query(models.Device).filter(models.Device.api_key == api_key).first()
17
-
18
- def update_device_last_seen(db: Session, device_id: int):
19
- """Updates the last_seen timestamp for a device."""
20
- db.query(models.Device).filter(models.Device.id == device_id).update({"last_seen": datetime.utcnow()})
21
- db.commit()
22
-
23
- def create_device_log(db: Session, log_data: dict):
24
- """Creates a new log entry for a device action."""
25
- new_log = models.DeviceLog(**log_data)
26
- db.add(new_log)
27
- db.commit()
28
- db.refresh(new_log)
29
- return new_log
30
-
31
- def delete_device(db: Session, device_id_str: str) -> models.Device:
32
- """
33
- Deletes a device and all of its associated records (logs, pending links).
34
- """
35
- device_to_delete = get_device_by_id_str(db, device_id_str)
36
- if not device_to_delete:
37
- raise HTTPException(
38
- status_code=status.HTTP_404_NOT_FOUND,
39
- detail=f"Device with ID '{device_id_str}' not found."
40
- )
41
-
42
- device_pk_id = device_to_delete.id
43
-
44
- # Delete all dependent records first to maintain foreign key integrity
45
- db.query(models.DeviceLog).filter(models.DeviceLog.device_fk_id == device_pk_id).delete(synchronize_session=False)
46
- db.query(models.PendingTagLink).filter(models.PendingTagLink.device_id_fk == device_pk_id).delete(synchronize_session=False)
47
-
48
- # Now delete the device itself
49
- db.delete(device_to_delete)
50
- db.commit()
51
-
52
- return device_to_delete
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/students.py DELETED
@@ -1,79 +0,0 @@
1
- """
2
- CRUD operations for Students.
3
- """
4
- from sqlalchemy.orm import Session
5
- from fastapi import HTTPException, status
6
-
7
- from src import models
8
-
9
- def get_student_by_student_id(db: Session, student_id: str) -> models.Student | None:
10
- """Fetches a student by their unique student ID."""
11
- return db.query(models.Student).filter(models.Student.student_id == student_id).first()
12
-
13
- def get_student_by_tag_id(db: Session, tag_id: str) -> models.Student | None:
14
- """Fetches a student by their RFID tag ID."""
15
- return db.query(models.Student).filter(models.Student.tag_id == tag_id).first()
16
-
17
- def get_all_students(db: Session, skip: int = 0, limit: int = 100) -> list[models.Student]:
18
- """Fetches all students with pagination."""
19
- return db.query(models.Student).offset(skip).limit(limit).all()
20
-
21
- def create_student(db: Session, student: models.StudentCreate) -> models.Student:
22
- """Creates a new student in the database."""
23
- db_student = get_student_by_student_id(db, student.student_id)
24
- if db_student:
25
- raise HTTPException(
26
- status_code=status.HTTP_409_CONFLICT,
27
- detail=f"Student with ID '{student.student_id}' already exists."
28
- )
29
-
30
- new_student = models.Student(**student.model_dump())
31
- db.add(new_student)
32
- db.commit()
33
- db.refresh(new_student)
34
- return new_student
35
-
36
- def update_student_tag_id(db: Session, student_id: str, tag_id: str) -> models.Student:
37
- """Updates the RFID tag ID for a specific student."""
38
- db_student = get_student_by_student_id(db, student_id)
39
- if not db_student:
40
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Student not found")
41
-
42
- existing_tag_user = get_student_by_tag_id(db, tag_id)
43
- if existing_tag_user and existing_tag_user.student_id != student_id:
44
- raise HTTPException(
45
- status_code=status.HTTP_409_CONFLICT,
46
- detail=f"Tag ID '{tag_id}' is already assigned to another student."
47
- )
48
-
49
- db_student.tag_id = tag_id
50
- db.commit()
51
- db.refresh(db_student)
52
- return db_student
53
-
54
- def delete_student(db: Session, student_id: str) -> models.Student:
55
- """
56
- Deletes a student and all of their associated clearance records.
57
-
58
- This function first finds the student, then deletes all related rows in the
59
- 'clearance_statuses' table before finally deleting the student record
60
- to maintain database integrity.
61
- """
62
- student_to_delete = get_student_by_student_id(db, student_id)
63
- if not student_to_delete:
64
- raise HTTPException(
65
- status_code=status.HTTP_404_NOT_FOUND,
66
- detail=f"Student with ID '{student_id}' not found."
67
- )
68
-
69
- # First, delete all associated clearance statuses for this student.
70
- # This is crucial to prevent foreign key constraint violations.
71
- db.query(models.ClearanceStatus).filter(
72
- models.ClearanceStatus.student_id == student_id
73
- ).delete(synchronize_session=False)
74
-
75
- # Now, delete the student record itself.
76
- db.delete(student_to_delete)
77
- db.commit()
78
-
79
- return student_to_delete
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/tag_linking.py DELETED
@@ -1,46 +0,0 @@
1
- """
2
- CRUD operations for the PendingTagLink model.
3
- """
4
- from sqlalchemy.orm import Session
5
- from datetime import datetime
6
-
7
- from src import models
8
-
9
- def create_pending_tag_link(db: Session, link_details: models.PrepareTagLinkRequest, initiated_by_id: int, expires_at: datetime) -> models.PendingTagLink:
10
- """Creates a new pending tag link request in the database."""
11
-
12
- device = db.query(models.Device).filter(models.Device.device_id == link_details.device_identifier).first()
13
- if not device:
14
- # This check should ideally be in the router, but adding here as a safeguard
15
- return None
16
-
17
- new_link = models.PendingTagLink(
18
- device_id_fk=device.id,
19
- target_user_type=link_details.target_user_type,
20
- target_identifier=link_details.target_identifier,
21
- initiated_by_user_id=initiated_by_id,
22
- expires_at=expires_at,
23
- )
24
- db.add(new_link)
25
- db.commit()
26
- db.refresh(new_link)
27
- return new_link
28
-
29
- def get_pending_links(db: Session, device_id: int) -> models.PendingTagLink | None:
30
- """
31
- Fetches the active (non-expired) pending tag link for a specific device.
32
- """
33
- return db.query(models.PendingTagLink).offset(0).limit(limit=1000).all()
34
-
35
- def get_pending_link_by_id(db: Session, link_id: int) -> models.PendingTagLink | None:
36
- """
37
- Fetches a pending tag link by its ID.
38
- """
39
- return db.query(models.PendingTagLink).filter(models.PendingTagLink.id == link_id).first()
40
-
41
- def delete_pending_link_by_device_id(db: Session, link_id: int):
42
- """Deletes a pending link, typically after it has been used."""
43
- link_to_delete = db.query(models.PendingTagLink).filter(models.PendingTagLink.id == link_id).first()
44
- if link_to_delete:
45
- db.delete(link_to_delete)
46
- db.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/users.py DELETED
@@ -1,189 +0,0 @@
1
- """
2
- CRUD operations for Users (Admins, Staff).
3
- """
4
- from sqlalchemy.orm import Session
5
- from fastapi import HTTPException, status
6
- import bcrypt
7
-
8
- from src import models
9
-
10
- def hash_password(password: str) -> str:
11
- """Hashes a password using bcrypt."""
12
- return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
13
-
14
- def get_user_by_username(db: Session, username: str) -> models.User | None:
15
- """
16
- Retrieves a user by their username.
17
- """
18
- return db.query(models.User).filter(models.User.username == username).first()
19
-
20
- def get_user_by_id(db: Session, user_id: int) -> models.User | None:
21
- """
22
- Retrieves a user by their ID.
23
- """
24
- return db.query(models.User).filter(models.User.id == user_id).first()
25
-
26
- def get_all_users(db: Session, skip: int = 0, limit: int = 100) -> list[models.User]:
27
- """
28
- Retrieves all users with pagination.
29
- """
30
- return db.query(models.User).offset(skip).limit(limit).all()
31
-
32
- def get_user_by_tag_id(db: Session, tag_id: str) -> models.User | None:
33
- """
34
- Retrieves a user by their TAG_ID.
35
- """
36
- return db.query(models.User).filter(models.User.tag_id == tag_id).first()
37
-
38
- def update_user_tag_id(db: Session, username: str, new_tag_id: str) -> models.User:
39
- """
40
- Updates a user's tag_id.
41
- """
42
- # Check if the new tag_id is already in use
43
- existing_user_with_tag = get_user_by_tag_id(db, new_tag_id)
44
- if existing_user_with_tag:
45
- raise HTTPException(
46
- status_code=status.HTTP_400_BAD_REQUEST,
47
- detail=f"Tag ID '{new_tag_id}' is already assigned to another user."
48
- )
49
-
50
- # Get the user to update
51
- user = get_user_by_username(db, username)
52
- if not user:
53
- raise HTTPException(
54
- status_code=status.HTTP_404_NOT_FOUND,
55
- detail=f"User '{username}' not found."
56
- )
57
-
58
- # Update the tag_id
59
- user.tag_id = new_tag_id
60
- db.commit()
61
- db.refresh(user)
62
-
63
- return user
64
-
65
- def create_user(db: Session, user_data: models.UserCreate) -> models.User:
66
- """
67
- Creates a new user account.
68
- """
69
- # Check if username already exists
70
- existing_user = get_user_by_username(db, user_data.username)
71
- if existing_user:
72
- raise HTTPException(
73
- status_code=status.HTTP_400_BAD_REQUEST,
74
- detail=f"Username '{user_data.username}' is already registered."
75
- )
76
-
77
- # Check if tag_id is already in use (if provided)
78
- if user_data.tag_id:
79
- existing_tag_user = get_user_by_tag_id(db, user_data.tag_id)
80
- if existing_tag_user:
81
- raise HTTPException(
82
- status_code=status.HTTP_400_BAD_REQUEST,
83
- detail=f"Tag ID '{user_data.tag_id}' is already assigned to another user."
84
- )
85
-
86
- # Hash the password
87
- hashed_password = hash_password(user_data.password)
88
-
89
- # Create new user
90
- db_user = models.User(
91
- username=user_data.username,
92
- name=user_data.name,
93
- hashed_password=hashed_password,
94
- role=user_data.role,
95
- department=user_data.department,
96
- tag_id=user_data.tag_id,
97
- is_active=user_data.is_active if user_data.is_active is not None else True
98
- )
99
-
100
- db.add(db_user)
101
- db.commit()
102
- db.refresh(db_user)
103
-
104
- return db_user
105
-
106
- def update_user(db: Session, user_id: int, user_update: models.UserCreate) -> models.User:
107
- """
108
- Updates an existing user.
109
- """
110
- db_user = get_user_by_id(db, user_id)
111
- if not db_user:
112
- raise HTTPException(
113
- status_code=status.HTTP_404_NOT_FOUND,
114
- detail=f"User with ID {user_id} not found."
115
- )
116
-
117
- # Check if new username is already taken (if changed)
118
- if user_update.username != db_user.username:
119
- existing_user = get_user_by_username(db, user_update.username)
120
- if existing_user:
121
- raise HTTPException(
122
- status_code=status.HTTP_400_BAD_REQUEST,
123
- detail=f"Username '{user_update.username}' is already taken."
124
- )
125
-
126
- # Check if new tag_id is already in use (if changed and provided)
127
- if user_update.tag_id and user_update.tag_id != db_user.tag_id:
128
- existing_tag_user = get_user_by_tag_id(db, user_update.tag_id)
129
- if existing_tag_user:
130
- raise HTTPException(
131
- status_code=status.HTTP_400_BAD_REQUEST,
132
- detail=f"Tag ID '{user_update.tag_id}' is already assigned to another user."
133
- )
134
-
135
- # Update fields
136
- db_user.username = user_update.username
137
- db_user.name = user_update.name
138
- if user_update.password: # Only update password if provided
139
- db_user.hashed_password = hash_password(user_update.password)
140
- db_user.role = user_update.role
141
- db_user.department = user_update.department
142
- db_user.tag_id = user_update.tag_id
143
- db_user.is_active = user_update.is_active if user_update.is_active is not None else True
144
-
145
- db.commit()
146
- db.refresh(db_user)
147
-
148
- return db_user
149
-
150
- def delete_user(db: Session, username_to_delete: str, current_admin: models.User) -> models.User:
151
- """
152
- Deletes a user, ensuring an admin cannot delete themselves or the last admin.
153
- """
154
- if username_to_delete == current_admin.username:
155
- raise HTTPException(
156
- status_code=status.HTTP_403_FORBIDDEN,
157
- detail="Admins cannot delete their own account."
158
- )
159
-
160
- user_to_delete = get_user_by_username(db, username_to_delete)
161
- if not user_to_delete:
162
- raise HTTPException(
163
- status_code=status.HTTP_404_NOT_FOUND,
164
- detail=f"User '{username_to_delete}' not found."
165
- )
166
-
167
- # Prevent deleting the last admin account
168
- if user_to_delete.role == models.UserRole.ADMIN:
169
- admin_count = db.query(models.User).filter(models.User.role == models.UserRole.ADMIN).count()
170
- if admin_count <= 1:
171
- raise HTTPException(
172
- status_code=status.HTTP_403_FORBIDDEN,
173
- detail="Cannot delete the last remaining admin account."
174
- )
175
-
176
- # Handle dependencies: set foreign keys to NULL where a user is referenced
177
- db.query(models.ClearanceStatus).filter(models.ClearanceStatus.cleared_by == user_to_delete.id).update({"cleared_by": None})
178
-
179
- # Check if PendingTagLink model exists before trying to delete
180
- try:
181
- db.query(models.PendingTagLink).filter(models.PendingTagLink.initiated_by_user_id == user_to_delete.id).delete()
182
- except AttributeError:
183
- # PendingTagLink model doesn't exist, skip this cleanup
184
- pass
185
-
186
- db.delete(user_to_delete)
187
- db.commit()
188
-
189
- return user_to_delete
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/utils.py DELETED
@@ -1,14 +0,0 @@
1
- """
2
- Utility functions for CRUD operations.
3
- """
4
- import bcrypt
5
-
6
- def hash_password(password: str) -> str:
7
- """Hashes a password using bcrypt."""
8
- return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
9
-
10
- def verify_password(plain_password: str, hashed_password_str: str) -> bool:
11
- """Verifies a plain password against a hashed password."""
12
- if not plain_password or not hashed_password_str:
13
- return False
14
- return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password_str.encode('utf-8'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/database.py DELETED
@@ -1,96 +0,0 @@
1
- """
2
- This module handles database connection, session management, table creation,
3
- and initial data seeding.
4
- """
5
- import sqlalchemy
6
- from sqlalchemy.orm import sessionmaker, Session as SQLAlchemySessionType
7
- from datetime import datetime
8
-
9
- # Import the centralized settings object
10
- from src.config import settings
11
-
12
- # Import Enums, Base, and specific ORM models needed for initialization
13
- from src.models import (
14
- Base,
15
- ClearanceDepartment,
16
- ClearanceStatusEnum,
17
- Student as StudentORM,
18
- ClearanceStatus as ClearanceStatusORM
19
- )
20
-
21
- # --- Database Engine Setup ---
22
- # Create the SQLAlchemy engine using the URI from our secure settings.
23
- engine = sqlalchemy.create_engine(
24
- settings.POSTGRES_URI,
25
- pool_pre_ping=True # Helps prevent errors from stale connections
26
- )
27
-
28
- # --- Session Management ---
29
- # Create a configured "Session" class. This is our session factory.
30
- SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
31
-
32
- def get_db():
33
- """
34
- FastAPI dependency that creates and yields a new database session
35
- for each request, and ensures it's closed afterward.
36
- """
37
- db = SessionLocal()
38
- try:
39
- yield db
40
- finally:
41
- db.close()
42
-
43
- # --- Database Initialization Functions ---
44
-
45
- def create_db_and_tables():
46
- """
47
- Creates all database tables defined in `src/models.py` if they do not exist.
48
- This function should be called once on application startup.
49
- """
50
- try:
51
- print("Attempting to create database tables (if they don't exist)...")
52
- Base.metadata.create_all(bind=engine)
53
- print("Database tables are ready.")
54
- except Exception as e:
55
- print(f"FATAL: Error during database table creation: {e}")
56
- print("Please ensure your database is accessible and the POSTGRES_URI is correct.")
57
- # In a real production app, you might want to exit here if the DB is critical
58
- raise
59
-
60
- def initialize_student_clearance_statuses(db: SQLAlchemySessionType, student_id_str: str):
61
- """
62
- Creates default 'NOT_COMPLETED' clearance status entries for all required
63
- departments for a newly created student. This ensures every student's
64
- clearance record is complete from the start.
65
- """
66
- student = db.query(StudentORM).filter(StudentORM.student_id == student_id_str).first()
67
- if not student:
68
- print(f"Warning: Student '{student_id_str}' not found when trying to initialize clearance statuses.")
69
- return
70
-
71
- for dept in ClearanceDepartment:
72
- # Check if a status for this department already exists
73
- exists = db.query(ClearanceStatusORM).filter(
74
- ClearanceStatusORM.student_id == student_id_str,
75
- ClearanceStatusORM.department == dept
76
- ).first()
77
-
78
- if not exists:
79
- # If it doesn't exist, create the default record
80
- new_status = ClearanceStatusORM(
81
- student_id=student_id_str,
82
- department=dept,
83
- status=ClearanceStatusEnum.NOT_COMPLETED
84
- )
85
- db.add(new_status)
86
-
87
- # Commit all the new statuses at once
88
- try:
89
- db.commit()
90
- except Exception as e:
91
- print(f"Error committing initial clearance statuses for student {student_id_str}: {e}")
92
- db.rollback()
93
- raise
94
-
95
- # Alias for backward compatibility
96
- initialize_student_clearance_statuses_orm = initialize_student_clearance_statuses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/models.py DELETED
@@ -1,304 +0,0 @@
1
- """
2
- Pydantic Models for data validation and serialization.
3
- SQLAlchemy Models for database table definitions.
4
- """
5
- from pydantic import BaseModel, Field
6
- from typing import List, Optional, Union
7
- from enum import Enum
8
- from datetime import datetime
9
-
10
- from sqlalchemy import (
11
- Boolean, Column, ForeignKey, Integer, String, DateTime,
12
- create_engine, Enum as SQLAlchemyEnum
13
- )
14
- from sqlalchemy.orm import relationship, sessionmaker, declarative_base
15
-
16
- # ==============================================================================
17
- # Shared Enums (used by both SQLAlchemy and Pydantic)
18
- # ==============================================================================
19
-
20
- class ClearanceStatusEnum(str, Enum):
21
- NOT_COMPLETED = "NOT_COMPLETED"
22
- PENDING = "PENDING"
23
- COMPLETED = "COMPLETED"
24
- REJECTED = "REJECTED"
25
-
26
- class ClearanceDepartment(str, Enum):
27
- DEPARTMENT = "DEPARTMENT"
28
- BURSARY = "BURSARY"
29
- LIBRARY = "LIBRARY"
30
- ALUMNI = "ALUMNI"
31
-
32
- class UserRole(str, Enum):
33
- ADMIN = "ADMIN"
34
- STAFF = "STAFF"
35
-
36
- class TargetUserType(str, Enum):
37
- STUDENT = "STUDENT"
38
- STAFF_ADMIN = "STAFF_ADMIN"
39
-
40
- class OverallClearanceStatusEnum(str, Enum):
41
- PENDING = "PENDING"
42
- COMPLETED = "COMPLETED"
43
-
44
- class UserTypeEnum(str, Enum):
45
- """Enum for user types."""
46
- STUDENT = "student"
47
- USER = "user"
48
-
49
-
50
- Base = declarative_base()
51
-
52
- class User(Base):
53
- """Database model for Users (Admins, Staff)."""
54
- __tablename__ = "users"
55
- id = Column(Integer, primary_key=True, index=True)
56
- username = Column(String, unique=True, index=True, nullable=False)
57
- hashed_password = Column(String, nullable=False)
58
- name = Column(String, nullable=False)
59
- role = Column(SQLAlchemyEnum(UserRole), default=UserRole.STAFF, nullable=False)
60
- department = Column(SQLAlchemyEnum(ClearanceDepartment), nullable=True)
61
- is_active = Column(Boolean, default=True)
62
- tag_id = Column(String, unique=True, index=True, nullable=True)
63
- created_at = Column(DateTime, default=datetime.utcnow)
64
- updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
65
-
66
-
67
- class Student(Base):
68
- """Database model for Students."""
69
- __tablename__ = "students"
70
- id = Column(Integer, primary_key=True, index=True)
71
- student_id = Column(String, unique=True, index=True, nullable=False)
72
- name = Column(String, nullable=False)
73
- department = Column(String, nullable=False)
74
- tag_id = Column(String, unique=True, index=True, nullable=True)
75
- created_at = Column(DateTime, default=datetime.utcnow)
76
- updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
77
-
78
- clearance_statuses = relationship("ClearanceStatus", back_populates="student")
79
-
80
-
81
- class ClearanceStatus(Base):
82
- """Database model for individual clearance items for a student."""
83
- __tablename__ = "clearance_statuses"
84
- id = Column(Integer, primary_key=True, index=True)
85
- student_id = Column(String, ForeignKey("students.student_id"), nullable=False)
86
- department = Column(SQLAlchemyEnum(ClearanceDepartment), nullable=False)
87
- status = Column(SQLAlchemyEnum(ClearanceStatusEnum), default=ClearanceStatusEnum.NOT_COMPLETED, nullable=False)
88
- remarks = Column(String, nullable=True)
89
- cleared_by = Column(Integer, ForeignKey("users.id"), nullable=True)
90
- created_at = Column(DateTime, default=datetime.utcnow)
91
- updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
92
-
93
- student = relationship("Student", back_populates="clearance_statuses")
94
- cleared_by_user = relationship("User", foreign_keys=[cleared_by])
95
-
96
-
97
- class Device(Base):
98
- """Database model for RFID reader devices."""
99
- __tablename__ = "devices"
100
- id = Column(Integer, primary_key=True, index=True)
101
- name = Column(String, nullable=False)
102
- description = Column(String, nullable=True)
103
- device_id = Column(String, unique=True, index=True, nullable=True)
104
- location = Column(String, nullable=True)
105
- api_key = Column(String, unique=True, index=True, nullable=False)
106
- is_active = Column(Boolean, default=True)
107
- created_at = Column(DateTime, default=datetime.utcnow)
108
- updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
109
-
110
-
111
- class PendingTagLink(Base):
112
- """Database model for pending tag link requests."""
113
- __tablename__ = "pending_tag_links"
114
- id = Column(Integer, primary_key=True, index=True)
115
- device_id_fk = Column(Integer, ForeignKey("devices.id"), nullable=False)
116
- target_user_type = Column(SQLAlchemyEnum(TargetUserType), nullable=False)
117
- target_identifier = Column(String, nullable=False)
118
- initiated_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
119
- expires_at = Column(DateTime, nullable=False)
120
- created_at = Column(DateTime, default=datetime.utcnow)
121
-
122
- # Relationships
123
- device = relationship("Device", foreign_keys=[device_id_fk])
124
- initiated_by = relationship("User", foreign_keys=[initiated_by_user_id])
125
-
126
-
127
-
128
- class DeviceLog(Base):
129
- """Database model for device activity logs."""
130
- __tablename__ = "device_logs"
131
- id = Column(Integer, primary_key=True, index=True)
132
- device_fk_id = Column(Integer, ForeignKey("devices.id"), nullable=True)
133
- actual_device_id_str = Column(String, nullable=True)
134
- tag_id_scanned = Column(String, nullable=True)
135
- user_type = Column(String, nullable=True)
136
- action = Column(String, nullable=False)
137
- timestamp = Column(DateTime, default=datetime.utcnow)
138
-
139
-
140
- # --- User and Auth Models ---
141
- class UserBase(BaseModel):
142
- username: str
143
- name: str
144
- role: UserRole = UserRole.STAFF
145
- department: Optional[ClearanceDepartment] = None
146
- tag_id: Optional[str] = None
147
- is_active: Optional[bool] = True
148
-
149
- class UserCreate(UserBase):
150
- password: str
151
-
152
- class UserResponse(BaseModel):
153
- id: int
154
- username: str
155
- name: str
156
- role: UserRole
157
- department: Optional[ClearanceDepartment] = None
158
- tag_id: Optional[str] = None
159
- is_active: bool
160
- created_at: datetime
161
- updated_at: datetime
162
-
163
- class Config:
164
- from_attributes = True
165
-
166
- class Token(BaseModel):
167
- access_token: str
168
- token_type: str
169
-
170
- class TokenData(BaseModel):
171
- username: Optional[str] = None
172
-
173
- # --- Student and Clearance Models ---
174
- class StudentBase(BaseModel):
175
- student_id: str = Field(..., example="CST/18/123")
176
- name: str = Field(..., example="John Doe")
177
- department: str = Field(..., example="Computer Science")
178
-
179
- class StudentCreate(StudentBase):
180
- tag_id: Optional[str] = None
181
-
182
- class StudentResponse(StudentBase):
183
- id: int
184
- tag_id: Optional[str] = None
185
- created_at: datetime
186
- updated_at: datetime
187
-
188
- class Config:
189
- from_attributes = True
190
-
191
- class ClearanceStatusCreate(BaseModel):
192
- student_id: str
193
- department: ClearanceDepartment
194
- status: ClearanceStatusEnum
195
- remarks: Optional[str] = None
196
-
197
- class ClearanceStatusResponse(BaseModel):
198
- id: int
199
- student_id: str
200
- department: ClearanceDepartment
201
- status: ClearanceStatusEnum
202
- remarks: Optional[str] = None
203
- cleared_by: Optional[int] = None
204
- created_at: datetime
205
- updated_at: datetime
206
-
207
- class Config:
208
- from_attributes = True
209
-
210
- class ClearanceStatusItem(BaseModel):
211
- department: ClearanceDepartment
212
- status: ClearanceStatusEnum
213
- remarks: Optional[str] = None
214
- updated_at: datetime
215
-
216
- class Config:
217
- from_attributes = True
218
-
219
- class ClearanceDetail(BaseModel):
220
- student_id: str
221
- name: str
222
- department: str
223
- overall_status: OverallClearanceStatusEnum
224
- clearance_items: List[ClearanceStatusItem]
225
-
226
- class ClearanceStatusUpdate(BaseModel):
227
- department: ClearanceDepartment
228
- status: ClearanceStatusEnum
229
- remarks: Optional[str] = None
230
-
231
- # --- Device Models ---
232
- class DeviceCreateAdmin(BaseModel):
233
- name: str
234
- description: Optional[str] = None
235
- device_id: Optional[str] = None
236
- location: Optional[str] = None
237
-
238
- class DeviceRegister(BaseModel):
239
- device_id: str
240
- location: str
241
-
242
- class DeviceResponse(BaseModel):
243
- id: int
244
- name: str
245
- description: Optional[str] = None
246
- device_id: Optional[str] = None
247
- location: Optional[str] = None
248
- api_key: str
249
- is_active: bool
250
- created_at: datetime
251
- updated_at: datetime
252
-
253
- class Config:
254
- from_attributes = True
255
-
256
- # --- Tag and Device Models ---
257
- class TagLinkRequest(BaseModel):
258
- tag_id: str = Field(..., example="A1B2C3D4")
259
-
260
- class PrepareDeviceRequest(BaseModel):
261
- device_id_str: str = Field(..., example="RFID-READER-01")
262
- user_id_str: str # Can be student_id or username
263
- user_type: UserTypeEnum
264
-
265
-
266
- class PrepareTagLinkRequest(BaseModel):
267
- """Request to prepare a device for tag linking."""
268
- device_identifier: str = Field(..., description="The device ID that will scan for tags")
269
- target_user_type: TargetUserType = Field(..., description="Type of user (STUDENT or STAFF_ADMIN)")
270
- target_identifier: str = Field(..., description="Student ID or username to link the tag to")
271
-
272
- class PendingTagLinkResponse(BaseModel):
273
- """Response model for pending tag link information."""
274
- id: int
275
- device_id_fk: int
276
- target_user_type: TargetUserType
277
- target_identifier: str
278
- initiated_by_user_id: int
279
- expires_at: datetime
280
- created_at: datetime
281
-
282
- class Config:
283
- from_attributes = True
284
-
285
- class ScannedTagSubmit(BaseModel):
286
- """Request when submitting a scanned tag for linking."""
287
- scanned_tag_id: str = Field(..., description="The scanned RFID tag ID")
288
-
289
-
290
- # --- New RFID Models ---
291
- class RfidScanRequest(BaseModel):
292
- """Request body for the unified RFID scan endpoint."""
293
- tag_id: str = Field(..., description="The ID scanned from the RFID tag.", example="A1B2C3D4")
294
- device_id: str = Field(..., description="The unique identifier of the RFID reader device.", example="RFID-READER-01")
295
-
296
- class RfidLinkSuccessResponse(BaseModel):
297
- """Success response when a tag is linked."""
298
- message: str = "Tag linked successfully."
299
- user_id: str
300
- user_type: UserTypeEnum
301
-
302
- # JWT Configuration
303
- import os
304
- JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/__init__.py DELETED
File without changes
src/routers/admin.py DELETED
@@ -1,83 +0,0 @@
1
- """
2
- Admin-only endpoints for managing system-wide operations like tag linking
3
- and deleting core resources like users and devices.
4
- """
5
- from fastapi import APIRouter, Depends, HTTPException, status
6
- from fastapi.concurrency import run_in_threadpool
7
- from sqlalchemy.orm import Session
8
- from datetime import datetime, timedelta
9
-
10
- from src import crud, models
11
- from src.auth import get_current_active_admin_user_from_token, get_current_active_user
12
- from src.database import get_db
13
-
14
- router = APIRouter(
15
- prefix="/api/admin",
16
- tags=["Admin"],
17
- dependencies=[Depends(get_current_active_admin_user_from_token)]
18
- )
19
-
20
- @router.post("/prepare-tag-link", status_code=status.HTTP_202_ACCEPTED, response_model=dict)
21
- async def prepare_device_for_tag_linking(
22
- request: models.PrepareTagLinkRequest,
23
- current_user: models.User = Depends(get_current_active_user),
24
- db: Session = Depends(get_db)
25
- ):
26
- """
27
- Admin: Initiates a request to link a tag. This creates a temporary,
28
- expiring 'PendingTagLink' record for a device.
29
- """
30
- device = await run_in_threadpool(crud.get_device_by_id_str, db, request.device_identifier)
31
- if not device or not device.is_active:
32
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Device not found or is inactive.")
33
-
34
- if request.target_user_type == models.TargetUserType.STUDENT:
35
- target = await run_in_threadpool(crud.get_student_by_student_id, db, request.target_identifier)
36
- else:
37
- target = await run_in_threadpool(crud.get_user_by_username, db, request.target_identifier)
38
-
39
- if not target:
40
- raise HTTPException(
41
- status_code=status.HTTP_404_NOT_FOUND,
42
- detail=f"{request.target_user_type.value} '{request.target_identifier}' not found."
43
- )
44
-
45
- expiry_time = datetime.utcnow() + timedelta(minutes=2)
46
- await run_in_threadpool(
47
- crud.create_pending_tag_link, db, request, current_user.id, expiry_time
48
- )
49
- return {
50
- "message": "Device is ready to link tag.",
51
- "device_id": device.device_id,
52
- "target": request.target_identifier,
53
- "expires_at": expiry_time.isoformat()
54
- }
55
-
56
- @router.delete("/users/{username}", status_code=status.HTTP_200_OK, response_model=dict)
57
- async def delete_user_endpoint(
58
- username: str,
59
- db: Session = Depends(get_db),
60
- current_admin: models.User = Depends(get_current_active_admin_user_from_token)
61
- ):
62
- """
63
- Admin: Permanently deletes a user (staff or other admin).
64
- """
65
- try:
66
- deleted_user = await run_in_threadpool(crud.delete_user, db, username, current_admin)
67
- return {"message": "User deleted successfully", "username": deleted_user.username}
68
- except HTTPException as e:
69
- raise e
70
-
71
- @router.delete("/devices/{device_id_str}", status_code=status.HTTP_200_OK, response_model=dict)
72
- async def delete_device_endpoint(
73
- device_id_str: str,
74
- db: Session = Depends(get_db)
75
- ):
76
- """
77
- Admin: Permanently deletes a registered RFID device and all its logs.
78
- """
79
- try:
80
- deleted_device = await run_in_threadpool(crud.delete_device, db, device_id_str)
81
- return {"message": "Device deleted successfully", "device_id": deleted_device.device_id}
82
- except HTTPException as e:
83
- raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/clearance.py DELETED
@@ -1,68 +0,0 @@
1
- """
2
- Router for staff and admin clearance operations.
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, status
5
- from sqlalchemy.orm import Session
6
- from fastapi.concurrency import run_in_threadpool
7
-
8
- from src import crud, models
9
- from src.database import get_db
10
- from src.auth import get_current_active_user, get_current_active_staff_user_from_token
11
- from src.utils import format_student_clearance_details
12
-
13
- router = APIRouter(
14
- prefix="/api/clearance",
15
- tags=["Clearance"],
16
- dependencies=[Depends(get_current_active_staff_user_from_token)]
17
- )
18
-
19
- class ClearanceUpdatePayload(models.BaseModel):
20
- status: models.ClearanceStatusEnum
21
- remarks: str | None = None
22
-
23
- @router.put("/{student_id_str}", response_model=models.ClearanceDetail)
24
- async def update_student_clearance(
25
- student_id_str: str,
26
- payload: ClearanceUpdatePayload,
27
- db: Session = Depends(get_db),
28
- current_user: models.User = Depends(get_current_active_staff_user_from_token)
29
- ):
30
- """
31
- Staff/Admin: Update a student's clearance status for their department.
32
- """
33
- if not current_user.department:
34
- raise HTTPException(status_code=403, detail="Your user account is not assigned to a clearable department.")
35
-
36
- await run_in_threadpool(
37
- crud.update_clearance_status, db, student_id_str, current_user.department, payload.status, payload.remarks, current_user.id
38
- )
39
-
40
- student_orm = await run_in_threadpool(crud.get_student_by_student_id, db, student_id_str)
41
- return await format_student_clearance_details(db, student_orm)
42
-
43
- @router.delete("/{student_id_str}/{department_str}", response_model=models.ClearanceDetail)
44
- async def reset_student_clearance(
45
- student_id_str: str,
46
- department_str: str,
47
- db: Session = Depends(get_db),
48
- current_user: models.User = Depends(get_current_active_staff_user_from_token)
49
- ):
50
- """
51
- Staff/Admin: Reset a student's clearance status for a department.
52
- Admins can reset for any department; staff only for their own.
53
- """
54
- try:
55
- target_department = models.ClearanceDepartment(department_str.upper())
56
- except ValueError:
57
- raise HTTPException(status_code=400, detail=f"'{department_str}' is not a valid department.")
58
-
59
- if current_user.role != models.UserRole.ADMIN and current_user.department != target_department:
60
- raise HTTPException(status_code=403, detail=f"You can only reset clearance for your own department.")
61
-
62
- await run_in_threadpool(crud.delete_clearance_status, db, student_id_str, target_department)
63
-
64
- student_orm = await run_in_threadpool(crud.get_student_by_student_id, db, student_id_str)
65
- if not student_orm:
66
- raise HTTPException(status_code=404, detail="Student not found.")
67
-
68
- return await format_student_clearance_details(db, student_orm)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/devices.py DELETED
@@ -1,89 +0,0 @@
1
- """
2
- Router for device interactions, specifically for submitting a scanned tag.
3
- This endpoint is intended to be called by the physical RFID reader device.
4
- """
5
- from fastapi import APIRouter, Depends, HTTPException, status, Header
6
- from fastapi.concurrency import run_in_threadpool
7
- from sqlalchemy.orm import Session
8
- from typing import Union
9
-
10
- from src import crud, models
11
- from src.database import get_db
12
- from src.utils import format_student_clearance_details
13
-
14
- async def get_authenticated_device(x_api_key: str = Header(...), db: Session = Depends(get_db)) -> models.Device:
15
- """Dependency to authenticate a device by its API key."""
16
- device = await run_in_threadpool(crud.get_device_by_api_key, db, x_api_key)
17
- if not device or not device.is_active:
18
- raise HTTPException(
19
- status_code=status.HTTP_401_UNAUTHORIZED,
20
- detail="Invalid API Key or Inactive Device",
21
- )
22
- return device
23
-
24
- router = APIRouter(
25
- prefix="/api/devices",
26
- tags=["Devices"],
27
- dependencies=[Depends(get_authenticated_device)]
28
- )
29
-
30
- @router.post("/devices/register", response_model=models.DeviceResponse)
31
- async def register_device_endpoint( # Async endpoint
32
- device_data: models.DeviceRegister, # Pydantic model from ESP32
33
- db: SQLAlchemySessionType = Depends(get_db)
34
- ):
35
- """
36
- ESP32 devices self-register or re-register. Uses ORM.
37
- """
38
- # crud.register_device_esp is sync, call with run_in_threadpool
39
- try:
40
- registered_device_orm = await run_in_threadpool(crud.register_device, db, device_data)
41
- except HTTPException as e: # Catch HTTPExceptions raised by CRUD (e.g., device already exists)
42
- raise e
43
- return registered_device_orm # Pydantic DeviceResponse converts from ORM model
44
-
45
-
46
- @router.post(
47
- "/submit-tag",
48
- response_model=Union[models.ClearanceDetail, models.UserResponse, dict],
49
- summary="Endpoint for RFID devices to submit a scanned tag ID."
50
- )
51
- async def device_submit_scanned_tag(
52
- scanned_tag: models.ScannedTagSubmit,
53
- device: models.Device = Depends(get_authenticated_device),
54
- db: Session = Depends(get_db)
55
- ):
56
- """
57
- Handles a tag submission from an authenticated RFID device.
58
- It can either link a tag if a pending link exists or fetch user details.
59
- """
60
- tag_id = scanned_tag.scanned_tag_id
61
- pending_link = await run_in_threadpool(crud.get_pending_link_by_device, db, device.id)
62
-
63
- if pending_link:
64
- # Registration Mode
65
- target_type, target_id = pending_link.target_user_type, pending_link.target_identifier
66
- try:
67
- if target_type == models.TargetUserType.STUDENT:
68
- await run_in_threadpool(crud.update_student_tag_id, db, target_id, tag_id)
69
- else:
70
- await run_in_threadpool(crud.update_user_tag_id, db, target_id, tag_id)
71
- finally:
72
- await run_in_threadpool(crud.delete_pending_link, db, pending_link.id)
73
-
74
- await run_in_threadpool(crud.create_device_log, db, {"device_fk_id": device.id, "tag_id_scanned": tag_id, "action": f"TAG_LINK_SUCCESS: {target_type.value} {target_id}"})
75
- return {"message": "Tag linked successfully", "user_id": target_id, "user_type": target_type}
76
- else:
77
- # Fetching Mode
78
- student = await run_in_threadpool(crud.get_student_by_tag_id, db, tag_id)
79
- if student:
80
- await run_in_threadpool(crud.create_device_log, db, {"device_fk_id": device.id, "tag_id_scanned": tag_id, "action": f"FETCH_SUCCESS: Student {student.student_id}"})
81
- return await format_student_clearance_details(db, student)
82
-
83
- user = await run_in_threadpool(crud.get_user_by_tag_id, db, tag_id)
84
- if user:
85
- await run_in_threadpool(crud.create_device_log, db, {"device_fk_id": device.id, "tag_id_scanned": tag_id, "action": f"FETCH_SUCCESS: User {user.username}"})
86
- return models.UserResponse.from_orm(user)
87
-
88
- await run_in_threadpool(crud.create_device_log, db, {"device_fk_id": device.id, "tag_id_scanned": tag_id, "action": "FETCH_FAIL: Tag not found"})
89
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Tag ID '{tag_id}' not found.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/rfid.py DELETED
@@ -1,83 +0,0 @@
1
- """
2
- Router for handling all RFID interactions.
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, status
5
- from fastapi.concurrency import run_in_threadpool
6
- from sqlalchemy.orm import Session
7
- from typing import Union
8
-
9
- from src import crud, models
10
- from src.database import get_db
11
- from src.utils import format_student_clearance_details # Using a utility for DRY code
12
-
13
- router = APIRouter(
14
- prefix="/api/rfid",
15
- tags=["RFID"],
16
- )
17
-
18
- @router.post(
19
- "/scan",
20
- response_model=Union[models.ClearanceDetail, models.RfidLinkSuccessResponse, models.UserResponse],
21
- summary="Unified endpoint for all RFID tag scans."
22
- )
23
- async def handle_rfid_scan(
24
- scan_data: models.RfidScanRequest,
25
- db: Session = Depends(get_db),
26
- ):
27
- """
28
- This endpoint intelligently handles an RFID tag scan.
29
-
30
- - **Registration Mode**: If an admin has prepared the device to link a tag
31
- to a user, this endpoint will perform the link and return a success message.
32
-
33
- - **Fetching Mode**: If the device is not in registration mode, this endpoint
34
- will look up the user/student by the tag ID and return their details
35
- (e.g., clearance status for a student).
36
- """
37
- # 1. Get the device that sent the scan
38
- device = await run_in_threadpool(crud.get_device_by_id, db, scan_data.device_id)
39
- if not device:
40
- raise HTTPException(
41
- status_code=status.HTTP_404_NOT_FOUND,
42
- detail=f"Device with ID '{scan_data.device_id}' not found."
43
- )
44
-
45
- # 2. Check if the device is in "Registration Mode"
46
- if device.link_for_user_id and device.link_for_user_type:
47
- user_id_to_link = device.link_for_user_id
48
- user_type_to_link = device.link_for_user_type
49
-
50
- # Link the tag based on the user type
51
- if user_type_to_link == models.UserTypeEnum.STUDENT:
52
- await run_in_threadpool(crud.update_student_tag_id, db, user_id_to_link, scan_data.tag_id)
53
- elif user_type_to_link == models.UserTypeEnum.USER:
54
- await run_in_threadpool(crud.update_user_tag_id, db, user_id_to_link, scan_data.tag_id)
55
-
56
- # Clear the registration mode from the device
57
- await run_in_threadpool(crud.clear_device_link_for_user, db, device.device_id_str)
58
-
59
- return models.RfidLinkSuccessResponse(
60
- user_id=user_id_to_link,
61
- user_type=user_type_to_link
62
- )
63
-
64
- # 3. If not in registration mode, it's "Fetching Mode"
65
- else:
66
- # First, check if the tag belongs to a student
67
- student = await run_in_threadpool(crud.get_student_by_tag_id, db, scan_data.tag_id)
68
- if student:
69
- # If a student is found, format and return their full clearance details
70
- return await format_student_clearance_details(db, student)
71
-
72
- # If not a student, check if it belongs to other users (staff/admin)
73
- user = await run_in_threadpool(crud.get_user_by_tag_id, db, scan_data.tag_id)
74
- if user:
75
- # For a staff/admin, just return their basic profile info
76
- return models.UserResponse.from_orm(user)
77
-
78
- # If the tag is not found anywhere, raise an error
79
- raise HTTPException(
80
- status_code=status.HTTP_404_NOT_FOUND,
81
- detail=f"Tag ID '{scan_data.tag_id}' is not associated with any user."
82
- )
83
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/students.py DELETED
@@ -1,64 +0,0 @@
1
- """
2
- Router for all CRUD operations related to students.
3
- """
4
- from fastapi import APIRouter, HTTPException, status, Depends
5
- from fastapi.concurrency import run_in_threadpool
6
- from sqlalchemy.orm import Session as SQLAlchemySessionType
7
- from typing import List
8
-
9
- from src import crud, models
10
- from src.auth import get_current_active_admin_user_from_token
11
- from src.database import get_db
12
- from src.utils import format_student_clearance_details
13
-
14
- router = APIRouter(
15
- prefix="/api/students",
16
- tags=["students"],
17
- )
18
-
19
- @router.post("/", response_model=models.StudentResponse, status_code=status.HTTP_201_CREATED)
20
- async def create_student_endpoint(
21
- student_data: models.StudentCreate,
22
- db: SQLAlchemySessionType = Depends(get_db),
23
- ):
24
- """Admin: Create a new student."""
25
- try:
26
- created_student_orm = await run_in_threadpool(crud.create_student, db, student_data)
27
- return created_student_orm
28
- except HTTPException as e:
29
- raise e
30
-
31
- @router.get("/", response_model=List[models.StudentResponse])
32
- async def get_students_endpoint(
33
- skip: int = 0,
34
- limit: int = 100,
35
- db: SQLAlchemySessionType = Depends(get_db),
36
- ):
37
- """Admin: Get a list of all students."""
38
- students_orm_list = await run_in_threadpool(crud.get_all_students, db, skip, limit)
39
- return students_orm_list
40
-
41
- @router.get("/{student_id_str}", response_model=models.ClearanceDetail)
42
- async def get_student_clearance_endpoint(
43
- student_id_str: str,
44
- db: SQLAlchemySessionType = Depends(get_db),
45
- ):
46
- """Admin: Get detailed clearance status for a specific student."""
47
- student_orm = await run_in_threadpool(crud.get_student_by_student_id, db, student_id_str)
48
- if not student_orm:
49
- raise HTTPException(status_code=404, detail="Student not found")
50
- return await format_student_clearance_details(db, student_orm)
51
-
52
- @router.delete("/{student_id_str}", status_code=status.HTTP_200_OK, response_model=dict)
53
- async def delete_student_endpoint(
54
- student_id_str: str,
55
- db: SQLAlchemySessionType = Depends(get_db),
56
- ):
57
- """
58
- Admin: Permanently deletes a student and all associated records.
59
- """
60
- try:
61
- deleted_student = await run_in_threadpool(crud.delete_student, db, student_id_str)
62
- return {"message": "Student deleted successfully", "student_id": deleted_student.student_id}
63
- except HTTPException as e:
64
- raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/token.py DELETED
@@ -1,45 +0,0 @@
1
- """
2
- Router for handling user authentication and issuing JWT tokens.
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, status
5
- from fastapi.security import OAuth2PasswordRequestForm
6
- from fastapi.concurrency import run_in_threadpool
7
- from sqlalchemy.orm import Session
8
- from datetime import timedelta
9
-
10
- from src import models
11
- from src.database import get_db
12
- from src.auth import create_access_token, authenticate_user
13
- from src.config import settings
14
-
15
- router = APIRouter(
16
- prefix="/api",
17
- tags=["Authentication Token"]
18
- )
19
-
20
- @router.post("/token", response_model=models.Token)
21
- async def login_for_access_token(
22
- form_data: OAuth2PasswordRequestForm = Depends(),
23
- db: Session = Depends(get_db)
24
- ):
25
- """
26
- Provides a JWT token for authenticated users.
27
-
28
- This is the primary login endpoint. It takes a username and password
29
- and returns an access token if the credentials are valid.
30
- """
31
- user = authenticate_user(db, form_data.username, form_data.password)
32
- if not user:
33
- raise HTTPException(
34
- status_code=status.HTTP_401_UNAUTHORIZED,
35
- detail="Incorrect username or password",
36
- headers={"WWW-Authenticate": "Bearer"},
37
- )
38
-
39
- access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
40
- access_token = create_access_token(
41
- data={"sub": user.username, "role": user.role.value},
42
- expires_delta=access_token_expires
43
- )
44
-
45
- return {"access_token": access_token, "token_type": "bearer"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/routers/users.py DELETED
@@ -1,50 +0,0 @@
1
- """
2
- Router for managing users (staff, admins).
3
- """
4
- from fastapi import APIRouter, Depends, HTTPException, status
5
- from fastapi.concurrency import run_in_threadpool
6
- from sqlalchemy.orm import Session
7
- from typing import List
8
-
9
- from src import crud, models
10
- from src.database import get_db
11
- from src.auth import get_current_active_user, get_current_active_admin_user_from_token
12
-
13
- router = APIRouter(
14
- prefix="/api/users",
15
- tags=["Users"],
16
- )
17
-
18
- @router.post("/", response_model=models.UserResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_current_active_admin_user_from_token)])
19
- async def create_new_user(user: models.UserCreate, db: Session = Depends(get_db)):
20
- """
21
- Admin: Create a new user (staff or admin).
22
- """
23
- db_user = await run_in_threadpool(crud.get_user_by_username, db, user.username)
24
- if db_user:
25
- raise HTTPException(status_code=400, detail="Username already registered")
26
- return await run_in_threadpool(crud.create_user, db, user)
27
-
28
- @router.get("/", response_model=List[models.UserResponse], dependencies=[Depends(get_current_active_admin_user_from_token)])
29
- async def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
30
- """
31
- Admin: Retrieve a list of all users.
32
- """
33
- users = await crud.get_all_users(db, skip=skip, limit=limit) # Assumes get_all_users exists in crud.users
34
- return users
35
-
36
- @router.get("/all", response_model=list[models.UserResponse], dependencies=[Depends(get_current_active_admin_user_from_token)])
37
- async def get_all_users(db: Session = Depends(get_db)):
38
- """
39
- Admin: Get a list of all users.
40
- """
41
- users = await run_in_threadpool(crud.get_all_users, db)
42
- return users
43
-
44
- @router.get("/me", response_model=models.UserResponse)
45
- async def read_users_me(current_user: models.User = Depends(get_current_active_user)):
46
- """
47
- Get profile information for the currently authenticated user.
48
- """
49
- return current_user
50
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/utils.py DELETED
@@ -1,51 +0,0 @@
1
- from typing import List, Dict, Any
2
- from sqlalchemy.orm import Session as SQLAlchemySessionType
3
- from fastapi.concurrency import run_in_threadpool
4
-
5
- from src import crud, models
6
-
7
- async def format_student_clearance_details(
8
- db: SQLAlchemySessionType,
9
- student_orm: models.Student
10
- ) -> models.ClearanceDetail:
11
- """
12
- Formats clearance details for a student.
13
-
14
- Args:
15
- db (SQLAlchemySessionType): Database session.
16
- student_orm (models.Student): ORM model of the student.
17
-
18
- Returns:
19
- models.ClearanceDetail: Formatted clearance details.
20
- """
21
- statuses_orm_list = await run_in_threadpool(crud.get_clearance_statuses_by_student_id, db, student_orm.student_id)
22
-
23
- clearance_items_models: List[models.ClearanceStatusItem] = []
24
- overall_status_val = models.OverallClearanceStatusEnum.COMPLETED
25
-
26
-
27
- if not statuses_orm_list:
28
- overall_status_val = models.OverallClearanceStatusEnum.PENDING
29
-
30
- for status_orm in statuses_orm_list:
31
- item = models.ClearanceStatusItem(
32
- department=status_orm.department,
33
- status=status_orm.status,
34
- remarks=status_orm.remarks,
35
- updated_at=status_orm.updated_at
36
- )
37
- clearance_items_models.append(item)
38
- if item.status != models.ClearanceStatusEnum.COMPLETED:
39
- overall_status_val = models.OverallClearanceStatusEnum.PENDING
40
-
41
- if not statuses_orm_list and overall_status_val == models.OverallClearanceStatusEnum.COMPLETED:
42
- overall_status_val = models.OverallClearanceStatusEnum.PENDING
43
-
44
- return models.ClearanceDetail(
45
- student_id=student_orm.student_id,
46
- name=student_orm.name,
47
- department=student_orm.department,
48
- clearance_items=clearance_items_models,
49
- overall_status=overall_status_val
50
- )
51
-