File size: 8,686 Bytes
96b013c
 
 
 
 
 
 
 
a09ee49
96b013c
 
92f5a3c
 
 
96b013c
92f5a3c
96b013c
99f4045
 
 
96b013c
 
 
 
 
 
 
92f5a3c
96b013c
 
 
92f5a3c
96b013c
92f5a3c
 
 
96b013c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92f5a3c
96b013c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a09ee49
 
 
 
96b013c
a09ee49
96b013c
a09ee49
96b013c
a09ee49
96b013c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a09ee49
 
 
96b013c
 
 
 
 
 
 
a09ee49
96b013c
 
a09ee49
96b013c
a09ee49
96b013c
 
 
 
 
a09ee49
96b013c
 
a09ee49
 
 
 
 
96b013c
 
 
a09ee49
 
 
 
 
96b013c
 
 
 
 
 
 
 
 
 
 
a09ee49
96b013c
 
 
 
 
 
a09ee49
 
 
 
96b013c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
from pydantic import BaseModel, Field, EmailStr
from typing import List, Optional, Dict
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum as SQLAlchemyEnum
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime, timedelta
import enum
import os

Base = declarative_base()

JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-default-secret-key-for-dev-only-CHANGE-ME")
if JWT_SECRET_KEY == "your-default-secret-key-for-dev-only-CHANGE-ME":
    print("WARNING: Using default JWT_SECRET_KEY. Please set a strong JWT_SECRET_KEY environment variable for production.")

# Enums - Values are now ALL CAPS to match the PostgreSQL ENUM type labels, based on testing.
class UserRole(str, enum.Enum):
    STUDENT = "student"
    STAFF = "staff"
    ADMIN = "admin"

class ClearanceStatusEnum(str, enum.Enum):
    COMPLETED = "COMPLETED"
    NOT_COMPLETED = "NOT_COMPLETED"
    PENDING = "PENDING"

class ClearanceDepartment(str, enum.Enum):
    DEPARTMENTAL = "DEPARTMENTAL"
    LIBRARY = "LIBRARY"
    BURSARY = "BURSARY"
    ALUMNI = "ALUMNI"

class TargetUserType(str, enum.Enum):
    # Corrected to ALL CAPS to match other enums and likely DB schema
    STUDENT = "STUDENT"
    STAFF_ADMIN = "STAFF_ADMIN"

class OverallClearanceStatusEnum(str, enum.Enum):
    COMPLETED = "COMPLETED"
    PENDING = "PENDING"

# Helper for SQLAlchemyEnum to ensure values are used
def enum_values_callable(obj):
    return [e.value for e in obj]

# --- ORM Model Definitions ---

class Student(Base):
    __tablename__ = "students"
    id = Column(Integer, primary_key=True, index=True)
    student_id = Column(String, unique=True, index=True, nullable=False)
    name = Column(String, nullable=False)
    email = Column(String, unique=True, index=True, nullable=True)
    department = Column(String, nullable=False)
    tag_id = Column(String, unique=True, index=True, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    role = Column(SQLAlchemyEnum(UserRole, name="userrole", create_type=False, values_callable=enum_values_callable), default=UserRole.STAFF, nullable=False)
    department = Column(SQLAlchemyEnum(ClearanceDepartment, name="clearancedepartment", create_type=False, values_callable=enum_values_callable), nullable=True)
    tag_id = Column(String, unique=True, index=True, nullable=True)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

class ClearanceStatus(Base):
    __tablename__ = "clearance_statuses"
    id = Column(Integer, primary_key=True, autoincrement=True, index=True)
    student_id = Column(String, ForeignKey("students.student_id"), index=True, nullable=False)
    department = Column(SQLAlchemyEnum(ClearanceDepartment, name="clearancedepartment", create_type=False, values_callable=enum_values_callable), index=True, nullable=False)
    status = Column(SQLAlchemyEnum(ClearanceStatusEnum, name="clearancestatusenum", create_type=False, values_callable=enum_values_callable), default=ClearanceStatusEnum.NOT_COMPLETED, nullable=False)
    remarks = Column(String, nullable=True)
    cleared_by = Column(Integer, ForeignKey("users.id"), nullable=True)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    created_at = Column(DateTime, default=datetime.utcnow)

class Device(Base):
    __tablename__ = "devices"
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    name = Column(String, index=True, nullable=True)
    device_id = Column(String, unique=True, index=True, nullable=True)
    location = Column(String, nullable=True)
    api_key = Column(String, unique=True, index=True, nullable=False)
    is_active = Column(Boolean, default=True)
    description = Column(String, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    last_seen = Column(DateTime, nullable=True)

class PendingTagLink(Base):
    __tablename__ = "pending_tag_links"
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    device_id_fk = Column(Integer, ForeignKey("devices.id"), index=True, nullable=False, name="device_id")
    # ** FIX: Correctly define the column using SQLAlchemyEnum **
    target_user_type = Column(SQLAlchemyEnum(TargetUserType, name="targetusertype", create_type=False, values_callable=enum_values_callable), nullable=False)
    target_identifier = Column(String, nullable=False)
    initiated_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    expires_at = Column(DateTime, nullable=False)

class DeviceLog(Base):
    __tablename__ = "device_logs"
    id = Column(Integer, primary_key=True, index=True, autoincrement=True)
    device_fk_id = Column(Integer, ForeignKey("devices.id"), index=True, nullable=True, name="device_id")
    actual_device_id_str = Column(String, index=True, nullable=True)
    tag_id_scanned = Column(String, index=True, nullable=True)
    user_type = Column(String, nullable=True)
    action = Column(String, nullable=False)
    timestamp = Column(DateTime, default=datetime.utcnow, nullable=False)

# --- Pydantic Models ---

class StudentCreate(BaseModel):
    student_id: str
    name: str
    email: Optional[EmailStr] = None
    department: str
    tag_id: Optional[str] = None

class StudentResponse(BaseModel):
    id: int
    student_id: str
    name: str
    email: Optional[EmailStr] = None
    department: str
    tag_id: Optional[str] = None
    created_at: datetime
    updated_at: datetime
    class Config: from_attributes = True

class UserBase(BaseModel):
    username: str
    role: UserRole
    department: Optional[ClearanceDepartment] = None
    tag_id: Optional[str] = None
    is_active: Optional[bool] = True

class UserCreate(UserBase):
    password: str

class UserResponse(BaseModel):
    id: int
    username: str
    role: UserRole
    department: Optional[ClearanceDepartment] = None
    tag_id: Optional[str] = None
    is_active: bool
    created_at: datetime
    updated_at: datetime
    class Config: from_attributes = True

class ClearanceStatusCreate(BaseModel):
    student_id: str
    department: ClearanceDepartment
    status: ClearanceStatusEnum
    remarks: Optional[str] = None

class ClearanceStatusItem(BaseModel):
    department: ClearanceDepartment
    status: ClearanceStatusEnum
    remarks: Optional[str] = None
    updated_at: datetime
    class Config: from_attributes = True

class ClearanceStatusResponse(BaseModel):
    id: int
    student_id: str
    department: ClearanceDepartment
    status: ClearanceStatusEnum
    remarks: Optional[str] = None
    cleared_by: Optional[int] = None
    updated_at: datetime
    created_at: datetime
    class Config: from_attributes = True

class ClearanceDetail(BaseModel):
    student_id: str
    name: str
    department: str
    clearance_items: List[ClearanceStatusItem]
    overall_status: OverallClearanceStatusEnum
    class Config: from_attributes = True

class DeviceRegister(BaseModel):
    device_id: str
    location: str

class DeviceCreateAdmin(BaseModel):
    name: str
    description: Optional[str] = None
    device_id: Optional[str] = None
    location: Optional[str] = None

class DeviceResponse(BaseModel):
    id: int
    name: Optional[str] = None
    device_id: Optional[str] = None
    location: Optional[str] = None
    api_key: str
    is_active: bool
    description: Optional[str] = None
    created_at: datetime
    updated_at: datetime
    last_seen: Optional[datetime] = None
    class Config: from_attributes = True

class TagScan(BaseModel):
    device_id: str
    tag_id: str
    timestamp: Optional[datetime] = Field(default_factory=datetime.utcnow)

class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: Optional[str] = None
    role: Optional[UserRole] = None

class TagLinkRequest(BaseModel):
    tag_id: str

class PrepareTagLinkRequest(BaseModel):
    device_identifier: str
    target_user_type: TargetUserType
    target_identifier: str

class ScannedTagSubmit(BaseModel):
    scanned_tag_id: str