Testys commited on
Commit
ec5cc84
·
1 Parent(s): 5735639

FIX: Fixing the models.py part

Browse files
src/auth.py CHANGED
@@ -1,85 +1,100 @@
1
  from fastapi import Depends, HTTPException, status
2
- from fastapi.security import OAuth2PasswordBearer, APIKeyHeader
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
- def verify_password(plain_password: str, hashed_password: str) -> bool:
27
- """Verifies a plain password against a hashed password."""
28
- return pwd_context.verify(plain_password, hashed_password)
29
-
30
- def hash_password(password: str) -> str:
31
- """Hashes a plain password."""
32
- return pwd_context.hash(password)
33
 
34
- # --- JWT Token Utilities ---
 
 
35
 
36
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
37
- """
38
- Generates a new JWT access token.
39
- """
40
  to_encode = data.copy()
41
  if expires_delta:
42
- expire = datetime.now(timezone.utc) + expires_delta
43
  else:
44
- # Default expiration time: 15 minutes
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
- # --- User Authentication and Authorization ---
 
 
52
 
53
- async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_session)) -> User:
54
- """
55
- Decodes the JWT token to get the current user.
56
- Raises an exception if the token is invalid or the user doesn't exist.
57
- """
58
- credentials_exception = HTTPException(
59
- status_code=status.HTTP_401_UNAUTHORIZED,
60
- detail="Could not validate credentials",
61
- headers={"WWW-Authenticate": "Bearer"},
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
- async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
 
 
 
 
79
  """
80
- Ensures the user retrieved from the token is active.
 
81
  """
82
- if not current_user.is_active:
83
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
84
- return current_user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, # 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
@@ -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
- '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
  ]
 
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 enum import Enum as PyEnum
 
4
 
5
- # --- Enums for Controlled Vocabularies ---
6
- # Using enums ensures data consistency for categorical fields.
7
 
8
- class UserRole(str, PyEnum):
9
- STUDENT = "student"
10
- STAFF = "staff"
11
  ADMIN = "admin"
 
 
12
 
13
- class Department(str, PyEnum):
14
- LIBRARY = "library"
15
- BURSARY = "bursary"
16
- ALUMNI = "alumni"
17
- DEPARTMENTAL = "departmental"
18
-
19
- class ClearanceProcess(str, PyEnum):
 
 
 
 
 
 
 
 
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: Optional[str] = None
32
  hashed_password: str
33
- role: UserRole = Field(default=UserRole.STAFF)
34
- is_active: bool = Field(default=True)
 
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
- email: str = Field(unique=True)
 
45
  department: Department
46
- hashed_password: str
 
 
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: Department
72
- status: ClearanceProcess = Field(default=ClearanceProcess.PENDING)
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(index=True, unique=True)
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
- # For Users
 
 
 
 
 
89
  class UserCreate(SQLModel):
90
  username: str
91
- email: str
92
  password: str
93
- full_name: Optional[str] = None
94
- role: UserRole = UserRole.STAFF
 
 
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
- # For Students
 
 
 
 
 
 
 
 
103
  class StudentCreate(SQLModel):
104
- matric_no: str
105
  full_name: str
 
106
  email: str
107
- password: str
 
108
 
109
  class StudentUpdate(SQLModel):
110
  full_name: Optional[str] = None
 
111
  email: Optional[str] = None
112
- password: Optional[str] = None
113
 
114
- # For Devices
115
- class DeviceCreate(SQLModel):
116
- device_name: str
 
117
  department: Department
118
 
119
- # For Tag Linking
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  class TagLink(SQLModel):
121
  tag_id: str
122
  matric_no: Optional[str] = None
123
  username: Optional[str] = None
124
 
125
- # For Clearance Updates
126
- class ClearanceUpdate(SQLModel):
127
- matric_no: str
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 get_current_active_student
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(get_current_active_student)
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