Spaces:
Runtime error
Runtime error
FIX: Fixing the models.py part
Browse files- src/auth.py +80 -65
- src/config.py +7 -1
- src/crud/__init__.py +21 -28
- src/crud/devices.py +4 -0
- src/models.py +121 -74
- src/routers/students.py +2 -2
src/auth.py
CHANGED
@@ -1,85 +1,100 @@
|
|
1 |
from fastapi import Depends, HTTPException, status
|
2 |
-
from fastapi.security import
|
3 |
from jose import JWTError, jwt
|
4 |
-
from passlib.context import CryptContext
|
5 |
-
from datetime import datetime, timedelta, timezone
|
6 |
-
from typing import Optional
|
7 |
from sqlmodel import Session, select
|
|
|
|
|
|
|
8 |
|
9 |
-
from src.database import get_session
|
10 |
-
from src.models import User
|
11 |
-
from src.config import settings
|
12 |
-
|
13 |
-
# --- Security Configuration ---
|
14 |
-
|
15 |
-
# Password hashing context using bcrypt
|
16 |
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
17 |
-
|
18 |
-
# OAuth2 scheme for token-based authentication
|
19 |
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
|
20 |
-
|
21 |
-
# API key header for device authentication
|
22 |
-
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=True)
|
23 |
-
|
24 |
-
# --- Password Utilities ---
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
"""Hashes a plain password."""
|
32 |
-
return pwd_context.hash(password)
|
33 |
|
34 |
-
# ---
|
|
|
|
|
35 |
|
36 |
-
|
37 |
-
|
38 |
-
Generates a new JWT access token.
|
39 |
-
"""
|
40 |
to_encode = data.copy()
|
41 |
if expires_delta:
|
42 |
-
expire = datetime.
|
43 |
else:
|
44 |
-
|
45 |
-
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
|
46 |
-
|
47 |
to_encode.update({"exp": expire})
|
48 |
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
49 |
return encoded_jwt
|
50 |
|
51 |
-
# ---
|
|
|
|
|
52 |
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
)
|
63 |
-
|
64 |
-
try:
|
65 |
-
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
66 |
-
username: Optional[str] = payload.get("sub")
|
67 |
-
if username is None:
|
68 |
-
raise credentials_exception
|
69 |
-
except JWTError:
|
70 |
-
raise credentials_exception
|
71 |
-
|
72 |
-
user = db.exec(select(User).where(User.username == username)).first()
|
73 |
-
if user is None:
|
74 |
-
raise credentials_exception
|
75 |
-
|
76 |
return user
|
77 |
|
78 |
-
|
|
|
|
|
|
|
|
|
79 |
"""
|
80 |
-
|
|
|
81 |
"""
|
82 |
-
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
|
|
|
|
1 |
from fastapi import Depends, HTTPException, status
|
2 |
+
from fastapi.security import APIKeyHeader, OAuth2PasswordBearer
|
3 |
from jose import JWTError, jwt
|
|
|
|
|
|
|
4 |
from sqlmodel import Session, select
|
5 |
+
from typing import List, Optional
|
6 |
+
from typing import List, Optional
|
7 |
+
from datetime import datetime, timedelta
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
+
from src.config import settings
|
11 |
+
from src.database import get_session
|
12 |
+
from src.crud import users as user_crud
|
13 |
+
from src.crud import devices as device_crud
|
14 |
+
from src.models import User, Role, Device
|
|
|
|
|
15 |
|
16 |
+
# --- Configuration ---
|
17 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
18 |
+
api_key_header = APIKeyHeader(name="x-api-key", auto_error=True)
|
19 |
|
20 |
+
# --- JWT Token Functions ---
|
21 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
|
|
|
22 |
to_encode = data.copy()
|
23 |
if expires_delta:
|
24 |
+
expire = datetime.utcnow() + expires_delta
|
25 |
else:
|
26 |
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
|
|
|
|
27 |
to_encode.update({"exp": expire})
|
28 |
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
29 |
return encoded_jwt
|
30 |
|
31 |
+
# --- Password Hashing ---
|
32 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
33 |
+
return settings.PWD_CONTEXT.verify(plain_password, hashed_password)
|
34 |
|
35 |
+
def hash_password(password: str) -> str:
|
36 |
+
return settings.PWD_CONTEXT.hash(password)
|
37 |
+
|
38 |
+
# --- User Authentication ---
|
39 |
+
def authenticate_user(db: Session, username: str, password: str):
|
40 |
+
"""Authenticate user by username and password."""
|
41 |
+
user = user_crud.get_user_by_username(db, username=username)
|
42 |
+
if not user:
|
43 |
+
return False
|
44 |
+
if not verify_password(password, user.hashed_password):
|
45 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
return user
|
47 |
|
48 |
+
# --- Dependency for API Key Authentication ---
|
49 |
+
|
50 |
+
def get_api_key(
|
51 |
+
key: str = Depends(api_key_header), db: Session = Depends(get_session)
|
52 |
+
) -> Device:
|
53 |
"""
|
54 |
+
Dependency to validate the API key from the x-api-key header.
|
55 |
+
Ensures the device is registered in the database.
|
56 |
"""
|
57 |
+
db_device = device_crud.get_device_by_api_key(db, api_key=key)
|
58 |
+
if not db_device:
|
59 |
+
raise HTTPException(
|
60 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
61 |
+
detail="Invalid or missing API Key",
|
62 |
+
)
|
63 |
+
return db_device
|
64 |
+
|
65 |
+
|
66 |
+
# --- Dependency for User Authentication and Authorization ---
|
67 |
+
def get_current_active_user(required_roles: List[Role] = None):
|
68 |
+
def dependency(
|
69 |
+
token: str = Depends(oauth2_scheme), db: Session = Depends(get_session)
|
70 |
+
) -> User:
|
71 |
+
credentials_exception = HTTPException(
|
72 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
73 |
+
detail="Could not validate credentials",
|
74 |
+
headers={"WWW-Authenticate": "Bearer"},
|
75 |
+
)
|
76 |
+
try:
|
77 |
+
payload = jwt.decode(
|
78 |
+
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
|
79 |
+
)
|
80 |
+
username: str = payload.get("sub")
|
81 |
+
if username is None:
|
82 |
+
raise credentials_exception
|
83 |
+
except JWTError:
|
84 |
+
raise credentials_exception
|
85 |
+
|
86 |
+
user = user_crud.get_user_by_username(db, username=username)
|
87 |
+
if user is None:
|
88 |
+
raise credentials_exception
|
89 |
+
|
90 |
+
# Check for roles if required
|
91 |
+
if required_roles:
|
92 |
+
is_authorized = any(role == user.role for role in required_roles)
|
93 |
+
if not is_authorized:
|
94 |
+
raise HTTPException(
|
95 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
96 |
+
detail="The user does not have adequate privileges",
|
97 |
+
)
|
98 |
+
return user
|
99 |
|
100 |
+
return dependency
|
src/config.py
CHANGED
@@ -1,14 +1,20 @@
|
|
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 |
|
|
|
1 |
from pydantic_settings import BaseSettings
|
2 |
+
from passlib.context import CryptContext
|
3 |
import os
|
4 |
from dotenv import load_dotenv
|
5 |
|
6 |
load_dotenv()
|
7 |
|
8 |
class Settings(BaseSettings):
|
9 |
+
POSTGRES_URI: str = os.getenv("POSTGRES_URI", "postgresql://user:password@localhost/dbname")
|
10 |
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "default_secret_key")
|
11 |
+
SECRET_KEY: str = JWT_SECRET_KEY # ADD THIS - referenced in auth.py
|
12 |
+
ALGORITHM: str = "HS256" # ADD THIS - referenced in auth.py
|
13 |
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
14 |
|
15 |
+
# ADD THIS - referenced in auth.py
|
16 |
+
PWD_CONTEXT: CryptContext = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
17 |
+
|
18 |
class Config:
|
19 |
env_file = ".env"
|
20 |
|
src/crud/__init__.py
CHANGED
@@ -10,40 +10,36 @@ keeping the router imports clean.
|
|
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,
|
26 |
get_all_users
|
27 |
)
|
28 |
from .devices import (
|
29 |
-
|
30 |
get_device_by_api_key,
|
31 |
-
|
32 |
-
|
33 |
delete_device,
|
34 |
)
|
35 |
from .clearance import (
|
36 |
-
get_clearance_statuses_by_student_id,
|
37 |
update_clearance_status,
|
38 |
-
|
39 |
-
get_all_clearance_status,
|
40 |
-
get_student_clearance_status
|
41 |
)
|
42 |
from .tag_linking import (
|
43 |
-
|
44 |
-
|
45 |
-
delete_pending_link_by_device_id,
|
46 |
-
get_pending_links,
|
47 |
)
|
48 |
|
49 |
# Export all functions
|
@@ -52,32 +48,29 @@ __all__ = [
|
|
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 |
-
'
|
|
|
64 |
'get_student_by_tag_id',
|
65 |
-
'
|
66 |
'delete_student',
|
67 |
# Devices
|
68 |
-
'
|
69 |
'get_device_by_api_key',
|
70 |
-
'
|
71 |
-
'
|
72 |
'delete_device',
|
73 |
# Clearance
|
74 |
-
'get_clearance_statuses_by_student_id',
|
75 |
'update_clearance_status',
|
76 |
-
'
|
77 |
# Tag Linking
|
78 |
-
'
|
79 |
-
'
|
80 |
-
'get_pending_link_by_token',
|
81 |
-
'delete_pending_link_by_id',
|
82 |
-
'get_all_pending_links',
|
83 |
]
|
|
|
10 |
from .students import (
|
11 |
create_student,
|
12 |
get_all_students,
|
13 |
+
get_student_by_id, # FIX: was get_student_by_student_id
|
14 |
+
get_student_by_matric_no, # ADD: missing import
|
15 |
get_student_by_tag_id,
|
16 |
+
update_student, # FIX: was update_student_tag_id
|
17 |
delete_student,
|
18 |
)
|
19 |
from .users import (
|
20 |
create_user,
|
21 |
get_user_by_username,
|
22 |
get_user_by_tag_id,
|
|
|
23 |
get_user_by_id,
|
24 |
+
update_user, # FIX: was update_user_tag_id
|
25 |
delete_user,
|
26 |
+
hash_password,
|
27 |
get_all_users
|
28 |
)
|
29 |
from .devices import (
|
30 |
+
create_device, # ADD: missing
|
31 |
get_device_by_api_key,
|
32 |
+
get_device_by_location, # ADD: missing
|
33 |
+
get_all_devices, # ADD: missing
|
34 |
delete_device,
|
35 |
)
|
36 |
from .clearance import (
|
|
|
37 |
update_clearance_status,
|
38 |
+
is_student_fully_cleared, # ADD: missing
|
|
|
|
|
39 |
)
|
40 |
from .tag_linking import (
|
41 |
+
link_tag,
|
42 |
+
unlink_tag,
|
|
|
|
|
43 |
)
|
44 |
|
45 |
# Export all functions
|
|
|
48 |
'create_user',
|
49 |
'get_user_by_username',
|
50 |
'get_user_by_tag_id',
|
|
|
51 |
'get_user_by_id',
|
52 |
+
'update_user',
|
53 |
'delete_user',
|
54 |
'hash_password',
|
55 |
'get_all_users',
|
56 |
# Students
|
57 |
'create_student',
|
58 |
'get_all_students',
|
59 |
+
'get_student_by_id',
|
60 |
+
'get_student_by_matric_no',
|
61 |
'get_student_by_tag_id',
|
62 |
+
'update_student',
|
63 |
'delete_student',
|
64 |
# Devices
|
65 |
+
'create_device',
|
66 |
'get_device_by_api_key',
|
67 |
+
'get_device_by_location',
|
68 |
+
'get_all_devices',
|
69 |
'delete_device',
|
70 |
# Clearance
|
|
|
71 |
'update_clearance_status',
|
72 |
+
'is_student_fully_cleared',
|
73 |
# Tag Linking
|
74 |
+
'link_tag',
|
75 |
+
'unlink_tag',
|
|
|
|
|
|
|
76 |
]
|
src/crud/devices.py
CHANGED
@@ -72,3 +72,7 @@ def delete_device(db: Session, device_id: int) -> Optional[Device]:
|
|
72 |
db.delete(db_device)
|
73 |
db.commit()
|
74 |
return db_device
|
|
|
|
|
|
|
|
|
|
72 |
db.delete(db_device)
|
73 |
db.commit()
|
74 |
return db_device
|
75 |
+
|
76 |
+
def get_device_by_location(db: Session, location: str) -> Optional[Device]:
|
77 |
+
"""Retrieves a device by its location."""
|
78 |
+
return db.exec(select(Device).where(Device.location == location)).first()
|
src/models.py
CHANGED
@@ -1,130 +1,177 @@
|
|
1 |
-
from sqlmodel import Field, Relationship, SQLModel
|
2 |
from typing import List, Optional
|
3 |
-
from
|
|
|
4 |
|
5 |
-
# --- Enums for
|
6 |
-
# Using enums ensures data consistency for categorical fields.
|
7 |
|
8 |
-
class
|
9 |
-
STUDENT = "student"
|
10 |
-
STAFF = "staff"
|
11 |
ADMIN = "admin"
|
|
|
|
|
12 |
|
13 |
-
class Department(str,
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
PENDING = "pending"
|
21 |
APPROVED = "approved"
|
22 |
REJECTED = "rejected"
|
23 |
|
24 |
# --- Database Table Models ---
|
25 |
|
26 |
-
# Represents a User (Staff or Admin)
|
27 |
class User(SQLModel, table=True):
|
28 |
id: Optional[int] = Field(default=None, primary_key=True)
|
29 |
username: str = Field(index=True, unique=True)
|
30 |
-
email: str = Field(unique=True)
|
31 |
-
full_name:
|
32 |
hashed_password: str
|
33 |
-
role:
|
34 |
-
|
|
|
35 |
|
36 |
-
# One-to-one relationship with an RFID tag
|
37 |
-
rfid_tag: Optional["RFIDTag"] = Relationship(back_populates="user")
|
38 |
-
|
39 |
-
# Represents a Student
|
40 |
class Student(SQLModel, table=True):
|
41 |
id: Optional[int] = Field(default=None, primary_key=True)
|
42 |
-
matric_no: str = Field(index=True, unique=True)
|
43 |
full_name: str
|
44 |
-
|
|
|
45 |
department: Department
|
46 |
-
|
|
|
|
|
47 |
|
48 |
-
# One-to-many relationship with clearance statuses
|
49 |
-
clearance_statuses: List["ClearanceStatus"] = Relationship(
|
50 |
-
back_populates="student", sa_relationship_kwargs={"cascade": "all, delete-orphan"}
|
51 |
-
)
|
52 |
-
# One-to-one relationship with an RFID tag
|
53 |
-
rfid_tag: Optional["RFIDTag"] = Relationship(back_populates="student")
|
54 |
-
|
55 |
-
# Represents an RFID tag, linking it to either a User or a Student
|
56 |
-
class RFIDTag(SQLModel, table=True):
|
57 |
-
id: Optional[int] = Field(default=None, primary_key=True)
|
58 |
-
tag_id: str = Field(index=True, unique=True, description="The unique ID from the RFID chip")
|
59 |
-
|
60 |
-
# Foreign keys to link to User or Student (only one should be set)
|
61 |
-
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
|
62 |
-
student_id: Optional[int] = Field(default=None, foreign_key="student.id")
|
63 |
-
|
64 |
-
# Relationships back to the owner of the tag
|
65 |
-
user: Optional[User] = Relationship(back_populates="rfid_tag")
|
66 |
-
student: Optional[Student] = Relationship(back_populates="rfid_tag")
|
67 |
-
|
68 |
-
# Represents a single clearance status for a student in a specific department
|
69 |
class ClearanceStatus(SQLModel, table=True):
|
70 |
id: Optional[int] = Field(default=None, primary_key=True)
|
71 |
-
department:
|
72 |
-
status:
|
73 |
-
|
74 |
student_id: int = Field(foreign_key="student.id")
|
75 |
-
student: Student = Relationship(back_populates="clearance_statuses")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
-
# Represents a physical ESP32 device
|
78 |
class Device(SQLModel, table=True):
|
79 |
id: Optional[int] = Field(default=None, primary_key=True)
|
80 |
-
device_name: str = Field(
|
81 |
-
api_key: str = Field(unique=True)
|
|
|
82 |
is_active: bool = Field(default=True)
|
83 |
-
department: Department
|
84 |
|
85 |
# --- Pydantic Models for API Operations ---
|
86 |
-
# These models define the shape of data for creating and updating records via the API.
|
87 |
|
88 |
-
#
|
|
|
|
|
|
|
|
|
|
|
89 |
class UserCreate(SQLModel):
|
90 |
username: str
|
91 |
-
email: str
|
92 |
password: str
|
93 |
-
|
94 |
-
|
|
|
|
|
95 |
|
96 |
class UserUpdate(SQLModel):
|
|
|
97 |
email: Optional[str] = None
|
98 |
full_name: Optional[str] = None
|
|
|
|
|
99 |
password: Optional[str] = None
|
100 |
-
is_active: Optional[bool] = None
|
101 |
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
class StudentCreate(SQLModel):
|
104 |
-
matric_no: str
|
105 |
full_name: str
|
|
|
106 |
email: str
|
107 |
-
|
|
|
108 |
|
109 |
class StudentUpdate(SQLModel):
|
110 |
full_name: Optional[str] = None
|
|
|
111 |
email: Optional[str] = None
|
112 |
-
password: Optional[str] = None
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
|
|
117 |
department: Department
|
118 |
|
119 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
class TagLink(SQLModel):
|
121 |
tag_id: str
|
122 |
matric_no: Optional[str] = None
|
123 |
username: Optional[str] = None
|
124 |
|
125 |
-
#
|
126 |
-
class
|
127 |
-
|
128 |
-
department: Department
|
129 |
-
status: ClearanceProcess
|
130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from typing import List, Optional
|
2 |
+
from sqlmodel import Field, Relationship, SQLModel
|
3 |
+
from enum import Enum
|
4 |
|
5 |
+
# --- Enums for choices ---
|
|
|
6 |
|
7 |
+
class Role(str, Enum):
|
|
|
|
|
8 |
ADMIN = "admin"
|
9 |
+
STAFF = "staff"
|
10 |
+
STUDENT = "student"
|
11 |
|
12 |
+
class Department(str, Enum):
|
13 |
+
COMPUTER_SCIENCE = "Computer Science"
|
14 |
+
ENGINEERING = "Engineering"
|
15 |
+
BUSINESS_ADMIN = "Business Administration"
|
16 |
+
LAW = "Law"
|
17 |
+
MEDICINE = "Medicine"
|
18 |
+
|
19 |
+
class ClearanceDepartment(str, Enum):
|
20 |
+
LIBRARY = "Library"
|
21 |
+
STUDENT_AFFAIRS = "Student Affairs"
|
22 |
+
BURSARY = "Bursary"
|
23 |
+
ACADEMIC_AFFAIRS = "Academic Affairs"
|
24 |
+
HEALTH_CENTER = "Health Center"
|
25 |
+
|
26 |
+
class ClearanceStatusEnum(str, Enum):
|
27 |
PENDING = "pending"
|
28 |
APPROVED = "approved"
|
29 |
REJECTED = "rejected"
|
30 |
|
31 |
# --- Database Table Models ---
|
32 |
|
|
|
33 |
class User(SQLModel, table=True):
|
34 |
id: Optional[int] = Field(default=None, primary_key=True)
|
35 |
username: str = Field(index=True, unique=True)
|
36 |
+
email: str = Field(index=True, unique=True)
|
37 |
+
full_name: str
|
38 |
hashed_password: str
|
39 |
+
role: Role
|
40 |
+
department: Optional[Department] = None # For staff members
|
41 |
+
rfid_tag: Optional["RFIDTag"] = Relationship(back_populates="user", sa_relationship_kwargs={"cascade": "all, delete-orphan"})
|
42 |
|
|
|
|
|
|
|
|
|
43 |
class Student(SQLModel, table=True):
|
44 |
id: Optional[int] = Field(default=None, primary_key=True)
|
|
|
45 |
full_name: str
|
46 |
+
matric_no: str = Field(index=True, unique=True)
|
47 |
+
email: str = Field(index=True, unique=True)
|
48 |
department: Department
|
49 |
+
# A student's login is handled by their associated User record, not directly here.
|
50 |
+
rfid_tag: Optional["RFIDTag"] = Relationship(back_populates="student", sa_relationship_kwargs={"cascade": "all, delete-orphan"})
|
51 |
+
clearance_statuses: List["ClearanceStatus"] = Relationship(back_populates="student", sa_relationship_kwargs={"cascade": "all, delete-orphan"})
|
52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
class ClearanceStatus(SQLModel, table=True):
|
54 |
id: Optional[int] = Field(default=None, primary_key=True)
|
55 |
+
department: ClearanceDepartment
|
56 |
+
status: ClearanceStatusEnum = Field(default=ClearanceStatusEnum.PENDING)
|
57 |
+
remarks: Optional[str] = None
|
58 |
student_id: int = Field(foreign_key="student.id")
|
59 |
+
student: "Student" = Relationship(back_populates="clearance_statuses")
|
60 |
+
|
61 |
+
class RFIDTag(SQLModel, table=True):
|
62 |
+
tag_id: str = Field(primary_key=True, index=True)
|
63 |
+
student_id: Optional[int] = Field(default=None, foreign_key="student.id", unique=True)
|
64 |
+
user_id: Optional[int] = Field(default=None, foreign_key="user.id", unique=True)
|
65 |
+
student: Optional["Student"] = Relationship(back_populates="rfid_tag")
|
66 |
+
user: Optional["User"] = Relationship(back_populates="rfid_tag")
|
67 |
|
|
|
68 |
class Device(SQLModel, table=True):
|
69 |
id: Optional[int] = Field(default=None, primary_key=True)
|
70 |
+
device_name: str = Field(unique=True, index=True)
|
71 |
+
api_key: str = Field(unique=True, index=True)
|
72 |
+
location: str
|
73 |
is_active: bool = Field(default=True)
|
|
|
74 |
|
75 |
# --- Pydantic Models for API Operations ---
|
|
|
76 |
|
77 |
+
# Token Model
|
78 |
+
class Token(SQLModel):
|
79 |
+
access_token: str
|
80 |
+
token_type: str
|
81 |
+
|
82 |
+
# User Models
|
83 |
class UserCreate(SQLModel):
|
84 |
username: str
|
|
|
85 |
password: str
|
86 |
+
email: str
|
87 |
+
full_name: str
|
88 |
+
role: Role
|
89 |
+
department: Optional[Department] = None
|
90 |
|
91 |
class UserUpdate(SQLModel):
|
92 |
+
username: Optional[str] = None
|
93 |
email: Optional[str] = None
|
94 |
full_name: Optional[str] = None
|
95 |
+
role: Optional[Role] = None
|
96 |
+
department: Optional[Department] = None
|
97 |
password: Optional[str] = None
|
|
|
98 |
|
99 |
+
class UserRead(SQLModel):
|
100 |
+
id: int
|
101 |
+
username: str
|
102 |
+
email: str
|
103 |
+
full_name: str
|
104 |
+
role: Role
|
105 |
+
department: Optional[Department] = None
|
106 |
+
|
107 |
+
# Student Models
|
108 |
class StudentCreate(SQLModel):
|
|
|
109 |
full_name: str
|
110 |
+
matric_no: str
|
111 |
email: str
|
112 |
+
department: Department
|
113 |
+
password: str # This will be used to create the associated User account for the student
|
114 |
|
115 |
class StudentUpdate(SQLModel):
|
116 |
full_name: Optional[str] = None
|
117 |
+
department: Optional[Department] = None
|
118 |
email: Optional[str] = None
|
|
|
119 |
|
120 |
+
class StudentRead(SQLModel):
|
121 |
+
id: int
|
122 |
+
full_name: str
|
123 |
+
matric_no: str
|
124 |
department: Department
|
125 |
|
126 |
+
# Clearance Status Models
|
127 |
+
class ClearanceStatusRead(SQLModel):
|
128 |
+
department: ClearanceDepartment
|
129 |
+
status: ClearanceStatusEnum
|
130 |
+
remarks: Optional[str] = None
|
131 |
+
|
132 |
+
class ClearanceUpdate(SQLModel):
|
133 |
+
matric_no: str
|
134 |
+
department: ClearanceDepartment
|
135 |
+
status: ClearanceStatusEnum
|
136 |
+
remarks: Optional[str] = None
|
137 |
+
|
138 |
+
# Combined Read Model
|
139 |
+
class StudentReadWithClearance(StudentRead):
|
140 |
+
clearance_statuses: List[ClearanceStatusRead] = []
|
141 |
+
rfid_tag: Optional["RFIDTagRead"] = None
|
142 |
+
|
143 |
+
# RFID Tag Models
|
144 |
+
class RFIDTagRead(SQLModel):
|
145 |
+
tag_id: str
|
146 |
+
student_id: Optional[int] = None
|
147 |
+
user_id: Optional[int] = None
|
148 |
+
|
149 |
class TagLink(SQLModel):
|
150 |
tag_id: str
|
151 |
matric_no: Optional[str] = None
|
152 |
username: Optional[str] = None
|
153 |
|
154 |
+
# RFID Device-Specific Models
|
155 |
+
class RFIDScanRequest(SQLModel):
|
156 |
+
tag_id: str
|
|
|
|
|
157 |
|
158 |
+
class RFIDStatusResponse(SQLModel):
|
159 |
+
status: str
|
160 |
+
full_name: Optional[str] = None
|
161 |
+
message: Optional[str] = None
|
162 |
+
clearance: Optional[str] = None
|
163 |
+
|
164 |
+
class TagScan(SQLModel):
|
165 |
+
tag_id: str
|
166 |
+
|
167 |
+
# Device Models
|
168 |
+
class DeviceCreate(SQLModel):
|
169 |
+
device_name: str
|
170 |
+
location: str
|
171 |
+
|
172 |
+
class DeviceRead(SQLModel):
|
173 |
+
id: int
|
174 |
+
device_name: str
|
175 |
+
api_key: str
|
176 |
+
location: str
|
177 |
+
is_active: bool
|
src/routers/students.py
CHANGED
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|
2 |
from sqlmodel import Session
|
3 |
|
4 |
from src.database import get_session
|
5 |
-
from src.auth import
|
6 |
from src.models import Student, StudentReadWithClearance
|
7 |
|
8 |
router = APIRouter(
|
@@ -14,7 +14,7 @@ router = APIRouter(
|
|
14 |
def read_student_me(
|
15 |
# This dependency ensures the user is an authenticated student
|
16 |
# and injects their database object into the 'current_student' parameter.
|
17 |
-
current_student: Student = Depends(
|
18 |
):
|
19 |
"""
|
20 |
Endpoint for a logged-in student to retrieve their own profile
|
|
|
2 |
from sqlmodel import Session
|
3 |
|
4 |
from src.database import get_session
|
5 |
+
from src.auth import get_current_active_user
|
6 |
from src.models import Student, StudentReadWithClearance
|
7 |
|
8 |
router = APIRouter(
|
|
|
14 |
def read_student_me(
|
15 |
# This dependency ensures the user is an authenticated student
|
16 |
# and injects their database object into the 'current_student' parameter.
|
17 |
+
current_student: Student = Depends(get_current_active_user)
|
18 |
):
|
19 |
"""
|
20 |
Endpoint for a logged-in student to retrieve their own profile
|