Spaces:
Runtime error
Runtime error
Just to stash
Browse files- src/auth.py +0 -205
- src/config.py +0 -15
- src/crud/__init__.py +0 -83
- src/crud/clearance.py +0 -102
- src/crud/devices.py +0 -52
- src/crud/students.py +0 -79
- src/crud/tag_linking.py +0 -46
- src/crud/users.py +0 -189
- src/crud/utils.py +0 -14
- src/database.py +0 -96
- src/models.py +0 -304
- src/routers/__init__.py +0 -0
- src/routers/admin.py +0 -83
- src/routers/clearance.py +0 -68
- src/routers/devices.py +0 -89
- src/routers/rfid.py +0 -83
- src/routers/students.py +0 -64
- src/routers/token.py +0 -45
- src/routers/users.py +0 -50
- src/utils.py +0 -51
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|