Testys commited on
Commit
1771391
·
1 Parent(s): 37c6354

Pushing the code to main

Browse files
Files changed (5) hide show
  1. src/auth.py +2 -1
  2. src/crud.py +0 -368
  3. src/crud/__init__.py +39 -1
  4. src/crud/users.py +6 -0
  5. src/crud/utils.py +14 -0
src/auth.py CHANGED
@@ -3,7 +3,8 @@ 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
  from src.database import get_db
8
  from typing import Optional, Dict, Any # Added Any
9
  from datetime import datetime, timedelta
 
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
src/crud.py DELETED
@@ -1,368 +0,0 @@
1
- # ORM-based CRUD operations
2
- # All functions are now synchronous and expect a SQLAlchemy Session
3
-
4
- from sqlalchemy.orm import Session as SQLAlchemySessionType
5
- from sqlalchemy import func, and_ # Add other SQLAlchemy functions as needed
6
- from datetime import datetime, timedelta
7
- import secrets
8
- import bcrypt
9
- from fastapi import HTTPException, status
10
- from typing import List, Optional, Union # Added Union
11
-
12
- from src import models # ORM models and Pydantic models
13
- from src.database import initialize_student_clearance_statuses_orm # ORM based init
14
-
15
- # --- Password Hashing ---
16
- def hash_password(password: str) -> str:
17
- """Hashes a password using bcrypt."""
18
- return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
19
-
20
- def verify_password(plain_password: str, hashed_password_str: str) -> bool:
21
- """Verifies a plain password against a hashed password."""
22
- if not plain_password or not hashed_password_str:
23
- return False
24
- return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password_str.encode('utf-8'))
25
-
26
- # --- Tag Uniqueness Checks (ORM) ---
27
- def is_tag_id_unique_for_student(db: SQLAlchemySessionType, tag_id: str, exclude_student_pk: Optional[int] = None) -> bool:
28
- """Checks if tag_id is unique among students, optionally excluding one by PK."""
29
- query = db.query(models.Student).filter(models.Student.tag_id == tag_id)
30
- if exclude_student_pk:
31
- query = query.filter(models.Student.id != exclude_student_pk)
32
- return query.first() is None
33
-
34
- def is_tag_id_unique_for_user(db: SQLAlchemySessionType, tag_id: str, exclude_user_pk: Optional[int] = None) -> bool:
35
- """Checks if tag_id is unique among users, optionally excluding one by PK."""
36
- query = db.query(models.User).filter(models.User.tag_id == tag_id)
37
- if exclude_user_pk:
38
- query = query.filter(models.User.id != exclude_user_pk)
39
- return query.first() is None
40
-
41
- def check_tag_id_globally_unique_for_target(
42
- db: SQLAlchemySessionType,
43
- tag_id: str,
44
- target_type: models.TargetUserType, # Expecting the enum member
45
- target_pk: Optional[int] = None
46
- ) -> None:
47
- """
48
- Raises HTTPException if tag_id is not globally unique, excluding the target if provided.
49
- """
50
- if target_type == models.TargetUserType.STUDENT:
51
- if not is_tag_id_unique_for_student(db, tag_id, exclude_student_pk=target_pk):
52
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Tag ID '{tag_id}' is already assigned to another student.")
53
- if not is_tag_id_unique_for_user(db, tag_id):
54
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Tag ID '{tag_id}' is already assigned to a user.")
55
- elif target_type == models.TargetUserType.STAFF_ADMIN:
56
- if not is_tag_id_unique_for_user(db, tag_id, exclude_user_pk=target_pk):
57
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Tag ID '{tag_id}' is already assigned to another user.")
58
- if not is_tag_id_unique_for_student(db, tag_id):
59
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Tag ID '{tag_id}' is already assigned to a student.")
60
-
61
-
62
- # --- Student CRUD (ORM) ---
63
- def create_student(db: SQLAlchemySessionType, student_data: models.StudentCreate) -> models.Student:
64
- existing_student_by_id = db.query(models.Student).filter(models.Student.student_id == student_data.student_id).first()
65
- if existing_student_by_id:
66
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Student ID already registered")
67
-
68
- if student_data.email:
69
- existing_student_by_email = db.query(models.Student).filter(models.Student.email == student_data.email).first()
70
- if existing_student_by_email:
71
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
72
-
73
- if student_data.tag_id:
74
- check_tag_id_globally_unique_for_target(db, student_data.tag_id, models.TargetUserType.STUDENT)
75
-
76
- db_student = models.Student(
77
- student_id=student_data.student_id,
78
- name=student_data.name,
79
- email=student_data.email,
80
- department=student_data.department,
81
- tag_id=student_data.tag_id,
82
- created_at=datetime.utcnow(),
83
- updated_at=datetime.utcnow()
84
- )
85
- db.add(db_student)
86
- db.commit()
87
- db.refresh(db_student)
88
-
89
- initialize_student_clearance_statuses_orm(db, db_student.student_id)
90
- return db_student
91
-
92
- def get_all_students(db: SQLAlchemySessionType, skip: int = 0, limit: int = 100) -> List[models.Student]:
93
- return db.query(models.Student).offset(skip).limit(limit).all()
94
-
95
- def get_student_by_pk(db: SQLAlchemySessionType, student_pk: int) -> Optional[models.Student]:
96
- return db.query(models.Student).filter(models.Student.id == student_pk).first()
97
-
98
- def get_student_by_student_id(db: SQLAlchemySessionType, student_id: str) -> Optional[models.Student]:
99
- return db.query(models.Student).filter(models.Student.student_id == student_id).first()
100
-
101
- def get_student_by_tag_id(db: SQLAlchemySessionType, tag_id: str) -> Optional[models.Student]:
102
- return db.query(models.Student).filter(models.Student.tag_id == tag_id).first()
103
-
104
- def update_student_tag_id(db: SQLAlchemySessionType, student_id_str: str, new_tag_id: str) -> models.Student:
105
- student = get_student_by_student_id(db, student_id_str)
106
- if not student:
107
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Student not found")
108
-
109
- if student.tag_id == new_tag_id:
110
- return student
111
-
112
- check_tag_id_globally_unique_for_target(db, new_tag_id, models.TargetUserType.STUDENT, target_pk=student.id)
113
-
114
- student.tag_id = new_tag_id
115
- student.updated_at = datetime.utcnow()
116
- db.commit()
117
- db.refresh(student)
118
- return student
119
-
120
- # --- User (Staff/Admin) CRUD (ORM) ---
121
- def create_user(db: SQLAlchemySessionType, user_data: models.UserCreate) -> models.User:
122
- existing_user = db.query(models.User).filter(models.User.username == user_data.username).first()
123
- if existing_user:
124
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered")
125
-
126
- if user_data.tag_id:
127
- check_tag_id_globally_unique_for_target(db, user_data.tag_id, models.TargetUserType.STAFF_ADMIN)
128
-
129
- hashed_pass = hash_password(user_data.password)
130
- db_user = models.User(
131
- username=user_data.username,
132
- hashed_password=hashed_pass,
133
- role=user_data.role, # Pass the enum member directly
134
- department=user_data.department, # Pass the enum member directly (or None)
135
- tag_id=user_data.tag_id,
136
- is_active=user_data.is_active if user_data.is_active is not None else True,
137
- created_at=datetime.utcnow(),
138
- updated_at=datetime.utcnow()
139
- )
140
- db.add(db_user)
141
- db.commit()
142
- db.refresh(db_user)
143
- return db_user
144
-
145
- def get_users(db: SQLAlchemySessionType, skip: int = 0, limit: int = 100) -> List[models.User]:
146
- """Retrieves all staff/admin users with pagination."""
147
- return db.query(models.User).offset(skip).limit(limit).all()
148
-
149
- def get_user_by_pk(db: SQLAlchemySessionType, user_pk: int) -> Optional[models.User]:
150
- return db.query(models.User).filter(models.User.id == user_pk).first()
151
-
152
- def get_user_by_username(db: SQLAlchemySessionType, username: str) -> Optional[models.User]:
153
- return db.query(models.User).filter(models.User.username == username).first()
154
-
155
- def get_user_by_tag_id(db: SQLAlchemySessionType, tag_id: str) -> Optional[models.User]:
156
- return db.query(models.User).filter(models.User.tag_id == tag_id, models.User.is_active == True).first()
157
-
158
- def update_user_tag_id(db: SQLAlchemySessionType, username_str: str, new_tag_id: str) -> models.User:
159
- user = get_user_by_username(db, username_str)
160
- if not user:
161
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
162
- if not user.is_active:
163
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot update tag for an inactive user.")
164
-
165
- if user.tag_id == new_tag_id:
166
- return user
167
-
168
- check_tag_id_globally_unique_for_target(db, new_tag_id, models.TargetUserType.STAFF_ADMIN, target_pk=user.id)
169
-
170
- user.tag_id = new_tag_id
171
- user.updated_at = datetime.utcnow()
172
- db.commit()
173
- db.refresh(user)
174
- return user
175
-
176
- # --- Clearance Status CRUD (ORM) ---
177
- def create_or_update_clearance_status(
178
- db: SQLAlchemySessionType,
179
- status_data: models.ClearanceStatusCreate, # Pydantic model with enum members
180
- cleared_by_user_pk: Optional[int] = None
181
- ) -> models.ClearanceStatus:
182
-
183
- existing_status = db.query(models.ClearanceStatus).filter(
184
- models.ClearanceStatus.student_id == status_data.student_id,
185
- models.ClearanceStatus.department == status_data.department # Comparing enum member from Pydantic to ORM field
186
- ).first()
187
-
188
- current_time = datetime.utcnow()
189
-
190
- if existing_status:
191
- existing_status.status = status_data.status # Assigning enum member, SQLAlchemy handles value
192
- existing_status.remarks = status_data.remarks
193
- existing_status.cleared_by = cleared_by_user_pk
194
- existing_status.updated_at = current_time
195
- db_model = existing_status
196
- else:
197
- db_model = models.ClearanceStatus(
198
- student_id=status_data.student_id,
199
- department=status_data.department, # Assign Pydantic enum member
200
- status=status_data.status, # Assign Pydantic enum member
201
- remarks=status_data.remarks,
202
- cleared_by=cleared_by_user_pk,
203
- created_at=current_time,
204
- updated_at=current_time
205
- )
206
- db.add(db_model)
207
-
208
- db.commit()
209
- db.refresh(db_model)
210
- return db_model
211
-
212
- def get_clearance_statuses_by_student_id(db: SQLAlchemySessionType, student_id_str: str) -> List[models.ClearanceStatus]:
213
- return db.query(models.ClearanceStatus).filter(models.ClearanceStatus.student_id == student_id_str).all()
214
-
215
- # --- Device CRUD (ORM) ---
216
- def create_device(db: SQLAlchemySessionType, device_data: models.DeviceCreateAdmin) -> models.Device:
217
- if device_data.device_id:
218
- existing_device_hw_id = db.query(models.Device).filter(models.Device.device_id == device_data.device_id).first()
219
- if existing_device_hw_id:
220
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Device with hardware ID '{device_data.device_id}' already exists.")
221
-
222
- api_key = secrets.token_urlsafe(32)
223
- db_device = models.Device(
224
- name=device_data.name,
225
- description=device_data.description,
226
- api_key=api_key,
227
- device_id=device_data.device_id,
228
- location=device_data.location,
229
- is_active=True,
230
- created_at=datetime.utcnow(),
231
- updated_at=datetime.utcnow()
232
- )
233
- db.add(db_device)
234
- db.commit()
235
- db.refresh(db_device)
236
- return db_device
237
-
238
- def register_device_esp(db: SQLAlchemySessionType, device_data: models.DeviceRegister) -> models.Device:
239
- api_key = secrets.token_urlsafe(32)
240
- current_time = datetime.utcnow()
241
- existing_device = db.query(models.Device).filter(models.Device.device_id == device_data.device_id).first()
242
-
243
- if existing_device:
244
- existing_device.location = device_data.location
245
- existing_device.api_key = api_key
246
- existing_device.last_seen = current_time
247
- existing_device.updated_at = current_time
248
- existing_device.is_active = True
249
- db_model = existing_device
250
- else:
251
- db_model = models.Device(
252
- device_id=device_data.device_id,
253
- location=device_data.location,
254
- api_key=api_key,
255
- is_active=True,
256
- last_seen=current_time,
257
- created_at=current_time,
258
- updated_at=current_time
259
- )
260
- db.add(db_model)
261
-
262
- db.commit()
263
- db.refresh(db_model)
264
- return db_model
265
-
266
- def get_device_by_pk(db: SQLAlchemySessionType, device_pk: int) -> Optional[models.Device]:
267
- return db.query(models.Device).filter(models.Device.id == device_pk).first()
268
-
269
- def get_device_by_api_key(db: SQLAlchemySessionType, api_key: str) -> Optional[models.Device]:
270
- return db.query(models.Device).filter(models.Device.api_key == api_key).first()
271
-
272
- def get_device_by_hardware_id(db: SQLAlchemySessionType, hardware_id: str) -> Optional[models.Device]:
273
- return db.query(models.Device).filter(models.Device.device_id == hardware_id).first()
274
-
275
- def get_all_devices(db: SQLAlchemySessionType, skip: int = 0, limit: int = 100) -> List[models.Device]:
276
- return db.query(models.Device).offset(skip).limit(limit).all()
277
-
278
- def update_device_last_seen(db: SQLAlchemySessionType, device_pk: int):
279
- device = get_device_by_pk(db, device_pk)
280
- if device:
281
- device.last_seen = datetime.utcnow()
282
- device.updated_at = datetime.utcnow()
283
- db.commit()
284
-
285
- # --- Device Log CRUD (ORM) ---
286
- def create_device_log(
287
- db: SQLAlchemySessionType,
288
- device_pk: Optional[int],
289
- action: str,
290
- scanned_tag_id: Optional[str] = None,
291
- user_type: Optional[str] = "unknown", # This remains a string as UserRole doesn't cover 'unknown'
292
- actual_device_id_str: Optional[str] = None
293
- ):
294
- db_log = models.DeviceLog(
295
- device_fk_id=device_pk,
296
- actual_device_id_str=actual_device_id_str,
297
- tag_id_scanned=scanned_tag_id,
298
- user_type=user_type,
299
- action=action,
300
- timestamp=datetime.utcnow()
301
- )
302
- db.add(db_log)
303
- db.commit()
304
- return db_log
305
-
306
- # --- Pending Tag Link CRUD (ORM) ---
307
- def create_pending_tag_link(
308
- db: SQLAlchemySessionType,
309
- device_pk: int,
310
- target_user_type: models.TargetUserType, # Pydantic validated enum member
311
- target_identifier: str,
312
- initiated_by_user_pk: int,
313
- expires_in_minutes: int = 5
314
- ) -> models.PendingTagLink:
315
-
316
- device = get_device_by_pk(db, device_pk)
317
- if not device:
318
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Device with PK {device_pk} not found.")
319
- if not device.is_active:
320
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Device '{device.name or device.device_id}' is not active.")
321
-
322
- if target_user_type == models.TargetUserType.STUDENT:
323
- student_target = get_student_by_student_id(db, target_identifier)
324
- if not student_target:
325
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Target student with ID '{target_identifier}' not found.")
326
- if student_target.tag_id:
327
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Student '{target_identifier}' already has a tag linked.")
328
- elif target_user_type == models.TargetUserType.STAFF_ADMIN:
329
- user_target = get_user_by_username(db, target_identifier)
330
- if not user_target:
331
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Target user with username '{target_identifier}' not found.")
332
- if not user_target.is_active:
333
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Target user '{target_identifier}' is not active.")
334
- if user_target.tag_id:
335
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"User '{target_identifier}' already has a tag linked.")
336
-
337
- existing_link = db.query(models.PendingTagLink).filter(
338
- models.PendingTagLink.device_id_fk == device_pk,
339
- models.PendingTagLink.expires_at > datetime.utcnow()
340
- ).first()
341
- if existing_link:
342
- raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"Device '{device.name or device.device_id}' is already awaiting a tag scan.")
343
-
344
- expires_at = datetime.utcnow() + timedelta(minutes=expires_in_minutes)
345
- db_pending_link = models.PendingTagLink(
346
- device_id_fk=device_pk,
347
- target_user_type=target_user_type, # Assign Pydantic enum member
348
- target_identifier=target_identifier,
349
- initiated_by_user_id=initiated_by_user_pk,
350
- expires_at=expires_at,
351
- created_at=datetime.utcnow()
352
- )
353
- db.add(db_pending_link)
354
- db.commit()
355
- db.refresh(db_pending_link)
356
- return db_pending_link
357
-
358
- def get_active_pending_tag_link_by_device_pk(db: SQLAlchemySessionType, device_pk: int) -> Optional[models.PendingTagLink]:
359
- return db.query(models.PendingTagLink).filter(
360
- models.PendingTagLink.device_id_fk == device_pk,
361
- models.PendingTagLink.expires_at > datetime.utcnow()
362
- ).order_by(models.PendingTagLink.created_at.desc()).first()
363
-
364
- def delete_pending_tag_link(db: SQLAlchemySessionType, pending_link_pk: int):
365
- link = db.query(models.PendingTagLink).filter(models.PendingTagLink.id == pending_link_pk).first()
366
- if link:
367
- db.delete(link)
368
- db.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/crud/__init__.py CHANGED
@@ -7,6 +7,9 @@ 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,
@@ -33,10 +36,45 @@ from .devices import (
33
  from .clearance import (
34
  get_clearance_statuses_by_student_id,
35
  update_clearance_status,
36
- delete_clearance_status, # <-- Added the new delete function
37
  )
38
  from .tag_linking import (
39
  create_pending_tag_link,
40
  get_pending_link_by_device,
41
  delete_pending_link,
42
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  keeping the router imports clean.
8
  """
9
 
10
+ # Import utility functions first
11
+ from .utils import hash_password, verify_password
12
+
13
  from .students import (
14
  create_student,
15
  get_all_students,
 
36
  from .clearance import (
37
  get_clearance_statuses_by_student_id,
38
  update_clearance_status,
39
+ delete_clearance_status,
40
  )
41
  from .tag_linking import (
42
  create_pending_tag_link,
43
  get_pending_link_by_device,
44
  delete_pending_link,
45
  )
46
+
47
+ # Export all functions (optional, for explicit imports)
48
+ __all__ = [
49
+ # Utilities
50
+ 'hash_password',
51
+ 'verify_password',
52
+ # Students
53
+ 'create_student',
54
+ 'get_all_students',
55
+ 'get_student_by_student_id',
56
+ 'get_student_by_tag_id',
57
+ 'update_student_tag_id',
58
+ 'delete_student',
59
+ # Users
60
+ 'create_user',
61
+ 'get_user_by_username',
62
+ 'get_user_by_tag_id',
63
+ 'update_user_tag_id',
64
+ 'get_user_by_id',
65
+ 'delete_user',
66
+ # Devices
67
+ 'get_device_by_id_str',
68
+ 'get_device_by_api_key',
69
+ 'create_device_log',
70
+ 'update_device_last_seen',
71
+ 'delete_device',
72
+ # Clearance
73
+ 'get_clearance_statuses_by_student_id',
74
+ 'update_clearance_status',
75
+ 'delete_clearance_status',
76
+ # Tag Linking
77
+ 'create_pending_tag_link',
78
+ 'get_pending_link_by_device',
79
+ 'delete_pending_link',
80
+ ]
src/crud/users.py CHANGED
@@ -25,6 +25,12 @@ def get_all_users(db: Session, skip: int = 0, limit: int = 100) -> list[models.U
25
  """
26
  return db.query(models.User).offset(skip).limit(limit).all()
27
 
 
 
 
 
 
 
28
  def create_user(db: Session, user_data: models.UserCreate) -> models.User:
29
  """
30
  Creates a new user account.
 
25
  """
26
  return db.query(models.User).offset(skip).limit(limit).all()
27
 
28
+ def get_user_by_tag_id(db:Session, tag_id:int) -> models.User | None:
29
+ """
30
+ Retrieves a user by their TAG_ID.
31
+ """
32
+ return db.query(models.User).filter(models.User.tag_id == tag_id).first()
33
+
34
  def create_user(db: Session, user_data: models.UserCreate) -> models.User:
35
  """
36
  Creates a new user account.
src/crud/utils.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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'))