Ali2206 commited on
Commit
68b2000
·
verified ·
1 Parent(s): de5608a

Update api/routes.py

Browse files
Files changed (1) hide show
  1. api/routes.py +402 -150
api/routes.py CHANGED
@@ -1,193 +1,445 @@
1
- from fastapi import APIRouter, HTTPException, Depends, Body
2
  from fastapi.security import OAuth2PasswordRequestForm
3
  from models.schemas import SignupForm, TokenResponse, PatientCreate, DoctorCreate, AppointmentCreate
4
  from db.mongo import users_collection, patients_collection, appointments_collection
5
  from core.security import hash_password, verify_password, create_access_token, get_current_user
6
- from datetime import datetime
7
  from bson import ObjectId
8
  from typing import Optional, List
9
- from pydantic import BaseModel
10
- from pymongo import UpdateOne
11
  import httpx
 
 
 
 
 
12
 
13
  router = APIRouter()
14
 
15
- # --- SIGNUP ---
16
- @router.post("/signup")
17
- async def signup(data: SignupForm):
18
- if data.role != "patient":
19
- raise HTTPException(status_code=403, detail="Only patients can sign up via this route")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  email = data.email.lower().strip()
22
  existing = await users_collection.find_one({"email": email})
23
  if existing:
24
- raise HTTPException(status_code=409, detail="Email already exists")
25
-
 
 
 
26
  hashed_pw = hash_password(data.password)
27
  user_doc = {
28
  "email": email,
29
  "full_name": data.full_name.strip(),
30
  "password": hashed_pw,
31
  "role": "patient",
32
- "created_at": datetime.utcnow()
 
 
 
 
 
 
 
 
33
  }
34
- await users_collection.insert_one(user_doc)
35
- return {"success": True, "message": "Patient account created"}
36
 
37
- # --- ADMIN: Create doctor account ---
38
- @router.post("/admin/create-doctor")
39
- async def create_doctor(data: DoctorCreate):
40
- existing = await users_collection.find_one({"email": data.email})
 
 
 
 
 
 
 
 
41
  if existing:
42
- raise HTTPException(status_code=409, detail="Email already exists")
43
-
 
 
 
44
  hashed_pw = hash_password(data.password)
45
- user_doc = {
46
- "matricule": data.matricule,
47
  "email": data.email.lower().strip(),
48
  "full_name": data.full_name.strip(),
49
- "specialty": data.specialty,
50
  "password": hashed_pw,
51
  "role": "doctor",
52
- "created_at": datetime.utcnow()
 
 
 
 
 
 
 
 
 
 
53
  }
54
- await users_collection.insert_one(user_doc)
55
- return {"success": True, "message": "Doctor account created"}
56
 
57
- # --- LOGIN ---
58
  @router.post("/login", response_model=TokenResponse)
59
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
60
- email = form_data.username.lower().strip()
61
- user = await users_collection.find_one({"email": email})
62
  if not user or not verify_password(form_data.password, user["password"]):
63
- raise HTTPException(status_code=401, detail="Invalid credentials")
64
-
 
 
 
 
65
  access_token = create_access_token(data={"sub": user["email"]})
66
- return {"access_token": access_token, "token_type": "bearer"}
 
 
 
 
67
 
68
- # --- GET CURRENT USER ---
69
  @router.get("/me")
70
  async def get_me(current_user: dict = Depends(get_current_user)):
71
  user = await users_collection.find_one({"email": current_user["email"]})
72
  if not user:
73
- raise HTTPException(status_code=404, detail="User not found")
74
-
 
 
 
75
  return {
76
  "id": str(user["_id"]),
77
  "email": user["email"],
78
  "full_name": user.get("full_name", ""),
79
- "role": user.get("role", "unknown"),
80
- "specialty": user.get("specialty", None) if user.get("role") == "doctor" else None,
81
- "created_at": user.get("created_at", "")
82
- }
83
-
84
- # --- FETCH AND STORE FHIR PATIENTS ---
85
- @router.post("/ehr/fetch-from-api")
86
- async def fetch_and_store_patients_from_fhir():
87
- fhir_patients_url = "https://hapi.fhir.org/baseR4/Patient?_count=50"
88
- fhir_encounters_url = "https://hapi.fhir.org/baseR4/Encounter?_count=100"
89
-
90
- try:
91
- async with httpx.AsyncClient() as client:
92
- patient_res = await client.get(fhir_patients_url)
93
- if patient_res.status_code != 200:
94
- raise HTTPException(status_code=502, detail="Failed to fetch patients")
95
- patient_entries = patient_res.json().get("entry", [])
96
-
97
- encounter_res = await client.get(fhir_encounters_url)
98
- if encounter_res.status_code != 200:
99
- raise HTTPException(status_code=502, detail="Failed to fetch encounters")
100
- encounter_entries = encounter_res.json().get("entry", [])
101
-
102
- patient_notes = {}
103
- for entry in encounter_entries:
104
- resource = entry.get("resource", {})
105
- ref = resource.get("subject", {}).get("reference")
106
- if not ref or "note" not in resource:
107
- continue
108
-
109
- patient_id = ref.split("/")[-1]
110
- notes_texts = [n.get("text") for n in resource.get("note", []) if n.get("text")]
111
- if notes_texts:
112
- patient_notes.setdefault(patient_id, []).extend(notes_texts)
113
-
114
- operations = []
115
- for entry in patient_entries:
116
- resource = entry.get("resource", {})
117
- fhir_id = resource.get("id")
118
- name_info = resource.get("name", [{}])[0]
119
- address_info = resource.get("address", [{}])[0]
120
-
121
- full_name = f"{name_info.get('given', [''])[0]} {name_info.get('family', '')}".strip()
122
- gender = resource.get("gender")
123
- birth_date = resource.get("birthDate")
124
- address = address_info.get("line", [""])[0]
125
- city = address_info.get("city", "")
126
- state = address_info.get("state", "")
127
- zip_code = address_info.get("postalCode", "")
128
-
129
- if not all([fhir_id, full_name, gender, birth_date, address, city, state, zip_code]):
130
- continue
131
-
132
- notes = patient_notes.get(fhir_id, [])
133
-
134
- operations.append(UpdateOne(
135
- {"fhir_id": fhir_id},
136
- {"$set": {
137
- "fhir_id": fhir_id,
138
- "full_name": full_name,
139
- "gender": gender,
140
- "date_of_birth": birth_date,
141
- "address": address,
142
- "city": city,
143
- "state": state,
144
- "zip": zip_code,
145
- "notes": notes,
146
- "created_at": datetime.utcnow()
147
- }},
148
- upsert=True
149
- ))
150
-
151
- if operations:
152
- result = await patients_collection.bulk_write(operations)
153
- return {
154
- "message": "Fetched and stored patients with linked notes successfully",
155
- "inserted": result.upserted_count,
156
- "modified": result.modified_count
157
- }
158
-
159
- return {"message": "No valid patients to store"}
160
-
161
- except Exception as e:
162
- raise HTTPException(status_code=500, detail=str(e))
163
-
164
- # --- GET FHIR PATIENTS ---
165
- @router.get("/ehr/fhir-patients")
166
- async def list_fhir_patients():
167
- cursor = patients_collection.find({"fhir_id": {"$exists": True}})
168
- patients = []
169
- async for p in cursor:
170
- patients.append({
171
- "id": str(p["_id"]),
172
- "full_name": p.get("full_name"),
173
- "gender": p.get("gender"),
174
- "date_of_birth": p.get("date_of_birth"),
175
- "notes": p.get("notes", [])
176
- })
177
- return patients
178
-
179
- # --- UPDATE A FHIR PATIENT ---
180
- @router.patch("/ehr/fhir-patients/{patient_id}")
181
- async def update_fhir_patient(patient_id: str, payload: dict = Body(...), current_user: dict = Depends(get_current_user)):
182
- if current_user.get("role") != "doctor":
183
- raise HTTPException(status_code=403, detail="Only doctors can update patient records")
184
-
185
- try:
186
- patient = await patients_collection.find_one({"_id": ObjectId(patient_id)})
187
- if not patient:
188
- raise HTTPException(status_code=404, detail="Patient not found")
189
-
190
- await patients_collection.update_one({"_id": ObjectId(patient_id)}, {"$set": payload})
191
- return {"message": "Patient updated successfully"}
192
- except Exception as e:
193
- raise HTTPException(status_code=500, detail=str(e))
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Body, Query, status
2
  from fastapi.security import OAuth2PasswordRequestForm
3
  from models.schemas import SignupForm, TokenResponse, PatientCreate, DoctorCreate, AppointmentCreate
4
  from db.mongo import users_collection, patients_collection, appointments_collection
5
  from core.security import hash_password, verify_password, create_access_token, get_current_user
6
+ from datetime import datetime, timedelta
7
  from bson import ObjectId
8
  from typing import Optional, List
9
+ from pydantic import BaseModel, Field
10
+ from pymongo import UpdateOne, InsertOne, IndexModel
11
  import httpx
12
+ import os
13
+ import json
14
+ from pathlib import Path
15
+ import glob
16
+ from motor.motor_asyncio import AsyncIOMotorClient
17
 
18
  router = APIRouter()
19
 
20
+ # Configuration
21
+ SYNTHEA_DATA_DIR = "./output/fhir"
22
+ os.makedirs(SYNTHEA_DATA_DIR, exist_ok=True)
23
+
24
+ # Models
25
+ class Note(BaseModel):
26
+ date: str = Field(..., example="2023-01-01T12:00:00Z")
27
+ type: str = Field(..., example="Progress Note")
28
+ text: str = Field(..., example="Patient reported improvement in symptoms")
29
+ author: Optional[str] = Field(None, example="Dr. Smith")
30
+
31
+ class Condition(BaseModel):
32
+ code: str = Field(..., example="Hypertension")
33
+ status: str = Field(..., example="Active")
34
+ onset_date: str = Field(..., example="2020-05-15")
35
+
36
+ class Medication(BaseModel):
37
+ name: str = Field(..., example="Lisinopril 10mg")
38
+ status: str = Field(..., example="Active")
39
+ prescribed_date: str = Field(..., example="2021-02-10")
40
+
41
+ class PatientUpdate(BaseModel):
42
+ notes: Optional[List[Note]] = None
43
+ conditions: Optional[List[Condition]] = None
44
+ medications: Optional[List[Medication]] = None
45
+
46
+ # Indexes
47
+ async def create_indexes():
48
+ await patients_collection.create_indexes([
49
+ IndexModel([("fhir_id", 1)], unique=True),
50
+ IndexModel([("full_name", "text")]),
51
+ IndexModel([("date_of_birth", 1)]),
52
+ IndexModel([("notes.date", -1)])
53
+ ])
54
+
55
+ # Helper Functions
56
+ def calculate_age(birth_date: str) -> Optional[int]:
57
+ if not birth_date:
58
+ return None
59
+ try:
60
+ birth_date = datetime.strptime(birth_date.split('T')[0], "%Y-%m-%d")
61
+ today = datetime.now()
62
+ return today.year - birth_date.year - (
63
+ (today.month, today.day) < (birth_date.month, birth_date.day))
64
+ except ValueError:
65
+ return None
66
+
67
+ async def process_synthea_patient(bundle: dict) -> Optional[dict]:
68
+ patient_data = {}
69
+ notes = []
70
+ conditions = []
71
+ medications = []
72
+ encounters = []
73
+
74
+ for entry in bundle.get('entry', []):
75
+ resource = entry.get('resource', {})
76
+ resource_type = resource.get('resourceType')
77
+
78
+ if resource_type == 'Patient':
79
+ name = resource.get('name', [{}])[0]
80
+ address = resource.get('address', [{}])[0]
81
+
82
+ patient_data = {
83
+ 'fhir_id': resource.get('id'),
84
+ 'full_name': f"{' '.join(name.get('given', ['']))} {name.get('family', '')}".strip(),
85
+ 'gender': resource.get('gender', 'unknown'),
86
+ 'date_of_birth': resource.get('birthDate', ''),
87
+ 'address': ' '.join(address.get('line', [''])),
88
+ 'city': address.get('city', ''),
89
+ 'state': address.get('state', ''),
90
+ 'postal_code': address.get('postalCode', ''),
91
+ 'country': address.get('country', ''),
92
+ 'marital_status': resource.get('maritalStatus', {}).get('text', ''),
93
+ 'language': resource.get('communication', [{}])[0].get('language', {}).get('text', ''),
94
+ 'source': 'synthea',
95
+ 'last_updated': datetime.utcnow()
96
+ }
97
+
98
+ elif resource_type == 'Encounter':
99
+ encounter = {
100
+ 'id': resource.get('id'),
101
+ 'type': resource.get('type', [{}])[0].get('text', ''),
102
+ 'status': resource.get('status'),
103
+ 'period': resource.get('period', {}),
104
+ 'service_provider': resource.get('serviceProvider', {}).get('display', '')
105
+ }
106
+ encounters.append(encounter)
107
+
108
+ for note in resource.get('note', []):
109
+ if note.get('text'):
110
+ notes.append({
111
+ 'date': resource.get('period', {}).get('start', datetime.utcnow().isoformat()),
112
+ 'type': resource.get('type', [{}])[0].get('text', 'Encounter Note'),
113
+ 'text': note.get('text'),
114
+ 'context': f"Encounter: {encounter.get('type')}",
115
+ 'author': 'System Generated'
116
+ })
117
+
118
+ elif resource_type == 'Condition':
119
+ conditions.append({
120
+ 'id': resource.get('id'),
121
+ 'code': resource.get('code', {}).get('text', ''),
122
+ 'status': resource.get('clinicalStatus', {}).get('text', ''),
123
+ 'onset_date': resource.get('onsetDateTime'),
124
+ 'recorded_date': resource.get('recordedDate'),
125
+ 'verification_status': resource.get('verificationStatus', {}).get('text', '')
126
+ })
127
+
128
+ elif resource_type == 'MedicationRequest':
129
+ medications.append({
130
+ 'id': resource.get('id'),
131
+ 'name': resource.get('medicationCodeableConcept', {}).get('text', ''),
132
+ 'status': resource.get('status'),
133
+ 'prescribed_date': resource.get('authoredOn'),
134
+ 'requester': resource.get('requester', {}).get('display', ''),
135
+ 'dosage': resource.get('dosageInstruction', [{}])[0].get('text', '')
136
+ })
137
+
138
+ if patient_data:
139
+ patient_data.update({
140
+ 'notes': notes,
141
+ 'conditions': conditions,
142
+ 'medications': medications,
143
+ 'encounters': encounters,
144
+ 'import_date': datetime.utcnow()
145
+ })
146
+ return patient_data
147
+ return None
148
+
149
+ # Routes
150
+ @router.post("/ehr/import", status_code=status.HTTP_201_CREATED)
151
+ async def import_patients(
152
+ limit: int = Query(100, ge=1, le=1000),
153
+ current_user: dict = Depends(get_current_user)
154
+ ):
155
+ if current_user.get('role') not in ['admin', 'doctor']:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_403_FORBIDDEN,
158
+ detail="Only administrators and doctors can import data"
159
+ )
160
+
161
+ try:
162
+ await create_indexes()
163
+ files = glob.glob(f"{SYNTHEA_DATA_DIR}/*.json")
164
+ operations = []
165
+ imported = 0
166
+
167
+ for file_path in files[:limit]:
168
+ try:
169
+ with open(file_path, 'r') as f:
170
+ bundle = json.load(f)
171
+ patient = await process_synthea_patient(bundle)
172
+ if patient:
173
+ operations.append(UpdateOne(
174
+ {"fhir_id": patient['fhir_id']},
175
+ {"$setOnInsert": patient},
176
+ upsert=True
177
+ ))
178
+ imported += 1
179
+ except Exception as e:
180
+ continue
181
+
182
+ if operations:
183
+ result = await patients_collection.bulk_write(operations)
184
+ return {
185
+ "status": "success",
186
+ "imported": imported,
187
+ "upserted": result.upserted_count,
188
+ "existing": len(operations) - result.upserted_count
189
+ }
190
+ return {"status": "success", "message": "No new patients found to import"}
191
+
192
+ except Exception as e:
193
+ raise HTTPException(
194
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
195
+ detail=f"Import failed: {str(e)}"
196
+ )
197
 
198
+ @router.get("/ehr/patients", response_model=List[dict])
199
+ async def list_patients(
200
+ search: Optional[str] = Query(None),
201
+ min_notes: int = Query(0, ge=0),
202
+ min_conditions: int = Query(0, ge=0),
203
+ limit: int = Query(100, ge=1, le=500),
204
+ skip: int = Query(0, ge=0)
205
+ ):
206
+ query = {"source": "synthea"}
207
+
208
+ if search:
209
+ query["$or"] = [
210
+ {"full_name": {"$regex": search, "$options": "i"}},
211
+ {"fhir_id": search}
212
+ ]
213
+
214
+ if min_notes > 0:
215
+ query[f"notes.{min_notes-1}"] = {"$exists": True}
216
+
217
+ if min_conditions > 0:
218
+ query[f"conditions.{min_conditions-1}"] = {"$exists": True}
219
+
220
+ projection = {
221
+ "fhir_id": 1,
222
+ "full_name": 1,
223
+ "gender": 1,
224
+ "date_of_birth": 1,
225
+ "notes": {"$slice": 1},
226
+ "conditions": {"$slice": 1},
227
+ "medications": {"$slice": 1}
228
+ }
229
+
230
+ cursor = patients_collection.find(query, projection).skip(skip).limit(limit)
231
+ patients = []
232
+
233
+ async for patient in cursor:
234
+ patients.append({
235
+ "id": str(patient["_id"]),
236
+ "fhir_id": patient.get("fhir_id"),
237
+ "full_name": patient.get("full_name"),
238
+ "gender": patient.get("gender"),
239
+ "age": calculate_age(patient.get("date_of_birth")),
240
+ "stats": {
241
+ "notes": len(patient.get("notes", [])),
242
+ "conditions": len(patient.get("conditions", [])),
243
+ "medications": len(patient.get("medications", []))
244
+ }
245
+ })
246
+
247
+ return patients
248
+
249
+ @router.get("/ehr/patients/{patient_id}", response_model=dict)
250
+ async def get_patient(patient_id: str):
251
+ try:
252
+ patient = await patients_collection.find_one({
253
+ "$or": [
254
+ {"_id": ObjectId(patient_id)},
255
+ {"fhir_id": patient_id}
256
+ ]
257
+ })
258
+
259
+ if not patient:
260
+ raise HTTPException(
261
+ status_code=status.HTTP_404_NOT_FOUND,
262
+ detail="Patient not found"
263
+ )
264
+
265
+ return {
266
+ "demographics": {
267
+ "id": str(patient["_id"]),
268
+ "fhir_id": patient.get("fhir_id"),
269
+ "full_name": patient.get("full_name"),
270
+ "gender": patient.get("gender"),
271
+ "date_of_birth": patient.get("date_of_birth"),
272
+ "age": calculate_age(patient.get("date_of_birth")),
273
+ "address": {
274
+ "line": patient.get("address"),
275
+ "city": patient.get("city"),
276
+ "state": patient.get("state"),
277
+ "postal_code": patient.get("postal_code"),
278
+ "country": patient.get("country")
279
+ },
280
+ "marital_status": patient.get("marital_status"),
281
+ "language": patient.get("language")
282
+ },
283
+ "clinical_data": {
284
+ "notes": patient.get("notes", []),
285
+ "conditions": patient.get("conditions", []),
286
+ "medications": patient.get("medications", []),
287
+ "encounters": patient.get("encounters", [])
288
+ },
289
+ "metadata": {
290
+ "source": patient.get("source"),
291
+ "import_date": patient.get("import_date"),
292
+ "last_updated": patient.get("last_updated")
293
+ }
294
+ }
295
+
296
+ except Exception as e:
297
+ raise HTTPException(
298
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
299
+ detail=str(e)
300
+ )
301
+
302
+ @router.post("/ehr/patients/{patient_id}/notes", status_code=status.HTTP_201_CREATED)
303
+ async def add_note(
304
+ patient_id: str,
305
+ note: Note,
306
+ current_user: dict = Depends(get_current_user)
307
+ ):
308
+ if current_user.get('role') not in ['doctor', 'admin']:
309
+ raise HTTPException(
310
+ status_code=status.HTTP_403_FORBIDDEN,
311
+ detail="Only clinicians can add notes"
312
+ )
313
+
314
+ try:
315
+ note_data = note.dict()
316
+ note_data.update({
317
+ "author": current_user.get('full_name', 'System'),
318
+ "timestamp": datetime.utcnow()
319
+ })
320
+
321
+ result = await patients_collection.update_one(
322
+ {"$or": [
323
+ {"_id": ObjectId(patient_id)},
324
+ {"fhir_id": patient_id}
325
+ ]},
326
+ {
327
+ "$push": {"notes": note_data},
328
+ "$set": {"last_updated": datetime.utcnow()}
329
+ }
330
+ )
331
+
332
+ if result.modified_count == 0:
333
+ raise HTTPException(
334
+ status_code=status.HTTP_404_NOT_FOUND,
335
+ detail="Patient not found"
336
+ )
337
+
338
+ return {"status": "success", "message": "Note added"}
339
+
340
+ except Exception as e:
341
+ raise HTTPException(
342
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
343
+ detail=str(e)
344
+ )
345
+
346
+ # Original Auth Routes (fully implemented)
347
+ @router.post("/signup", status_code=status.HTTP_201_CREATED)
348
+ async def signup(data: SignupForm):
349
  email = data.email.lower().strip()
350
  existing = await users_collection.find_one({"email": email})
351
  if existing:
352
+ raise HTTPException(
353
+ status_code=status.HTTP_409_CONFLICT,
354
+ detail="Email already exists"
355
+ )
356
+
357
  hashed_pw = hash_password(data.password)
358
  user_doc = {
359
  "email": email,
360
  "full_name": data.full_name.strip(),
361
  "password": hashed_pw,
362
  "role": "patient",
363
+ "created_at": datetime.utcnow(),
364
+ "updated_at": datetime.utcnow()
365
+ }
366
+
367
+ result = await users_collection.insert_one(user_doc)
368
+ return {
369
+ "status": "success",
370
+ "id": str(result.inserted_id),
371
+ "email": email
372
  }
 
 
373
 
374
+ @router.post("/admin/doctors", status_code=status.HTTP_201_CREATED)
375
+ async def create_doctor(
376
+ data: DoctorCreate,
377
+ current_user: dict = Depends(get_current_user)
378
+ ):
379
+ if current_user.get('role') != 'admin':
380
+ raise HTTPException(
381
+ status_code=status.HTTP_403_FORBIDDEN,
382
+ detail="Only admins can create doctor accounts"
383
+ )
384
+
385
+ existing = await users_collection.find_one({"email": data.email.lower()})
386
  if existing:
387
+ raise HTTPException(
388
+ status_code=status.HTTP_409_CONFLICT,
389
+ detail="Email already exists"
390
+ )
391
+
392
  hashed_pw = hash_password(data.password)
393
+ doctor_doc = {
 
394
  "email": data.email.lower().strip(),
395
  "full_name": data.full_name.strip(),
 
396
  "password": hashed_pw,
397
  "role": "doctor",
398
+ "specialty": data.specialty,
399
+ "license_number": data.license_number,
400
+ "created_at": datetime.utcnow(),
401
+ "updated_at": datetime.utcnow()
402
+ }
403
+
404
+ result = await users_collection.insert_one(doctor_doc)
405
+ return {
406
+ "status": "success",
407
+ "id": str(result.inserted_id),
408
+ "email": data.email
409
  }
 
 
410
 
 
411
  @router.post("/login", response_model=TokenResponse)
412
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
413
+ user = await users_collection.find_one({"email": form_data.username.lower()})
 
414
  if not user or not verify_password(form_data.password, user["password"]):
415
+ raise HTTPException(
416
+ status_code=status.HTTP_401_UNAUTHORIZED,
417
+ detail="Invalid credentials",
418
+ headers={"WWW-Authenticate": "Bearer"},
419
+ )
420
+
421
  access_token = create_access_token(data={"sub": user["email"]})
422
+ return {
423
+ "access_token": access_token,
424
+ "token_type": "bearer",
425
+ "role": user.get("role", "patient")
426
+ }
427
 
 
428
  @router.get("/me")
429
  async def get_me(current_user: dict = Depends(get_current_user)):
430
  user = await users_collection.find_one({"email": current_user["email"]})
431
  if not user:
432
+ raise HTTPException(
433
+ status_code=status.HTTP_404_NOT_FOUND,
434
+ detail="User not found"
435
+ )
436
+
437
  return {
438
  "id": str(user["_id"]),
439
  "email": user["email"],
440
  "full_name": user.get("full_name", ""),
441
+ "role": user.get("role", "patient"),
442
+ "specialty": user.get("specialty"),
443
+ "created_at": user.get("created_at"),
444
+ "updated_at": user.get("updated_at")
445
+ }