MisbahKhan0009 commited on
Commit
6931883
·
0 Parent(s):

all files added

Browse files
.env.example ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PORT=3000
2
+ APP_ENV=development
3
+ FRONTEND_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080
4
+
5
+ DB_HOST=127.0.0.1
6
+ DB_PORT=3306
7
+ DB_NAME=care_people
8
+ DB_USER=root
9
+ DB_PASSWORD=
10
+
11
+ JWT_SECRET=change-this-access-secret
12
+ JWT_REFRESH_SECRET=change-this-refresh-secret
13
+ ACCESS_TOKEN_MINUTES=60
14
+ REFRESH_TOKEN_DAYS=2
15
+
16
+ OTP_DEV_MODE=true
17
+ OTP_TTL_MINUTES=5
18
+ OTP_LENGTH=6
19
+
20
+ MAILCHIMP_TRANSACTIONAL_ENABLED=false
21
+ MAILCHIMP_TRANSACTIONAL_API_KEY=
22
+ MAILCHIMP_FROM_EMAIL=
23
+ MAILCHIMP_FROM_NAME=Care People
24
+ MAILCHIMP_REPLY_TO=
MAILCHIMP_SETUP.md ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Mailchimp Setup Guide
2
+
3
+ This guide shows how to set up `Mailchimp Transactional` for Care People email OTP.
4
+
5
+ Last reviewed: `April 1, 2026`
6
+
7
+ ## Important
8
+
9
+ For OTP email, use `Mailchimp Transactional`, not the regular Mailchimp marketing campaign API.
10
+
11
+ Mailchimp says Transactional is:
12
+
13
+ - a paid add-on available with `Standard` plan or higher
14
+ - purchased in blocks of `25,000` emails per month
15
+ - available in `demo` mode for first-time users
16
+
17
+ Official docs:
18
+
19
+ - https://mailchimp.com/help/add-or-remove-transactional-email/
20
+ - https://mailchimp.com/developer/transactional/guides/quick-start/
21
+ - https://mailchimp.com/developer/transactional/api/messages/send-new-message/
22
+
23
+ ## What you need first
24
+
25
+ 1. A Mailchimp account on `Standard` plan or higher.
26
+ 2. A domain email you control, for example `noreply@yourdomain.com`.
27
+ 3. Access to your DNS records.
28
+
29
+ Mailchimp recommends verifying and authenticating your domain. Their Help Center also notes that free mailbox providers like Gmail, Yahoo, or AOL cannot be verified/authenticated as your sending domain.
30
+
31
+ Official doc:
32
+
33
+ - https://mailchimp.com/help/verify-a-domain/
34
+
35
+ ## Step 1: Enable Mailchimp Transactional
36
+
37
+ In Mailchimp:
38
+
39
+ 1. Open `Automations`.
40
+ 2. Open `Transactional`.
41
+ 3. If you have never used it before, click `Try demo`.
42
+ 4. When ready, upgrade to a paid Transactional plan.
43
+ 5. Choose your monthly email block amount.
44
+
45
+ Mailchimp's help article says first-time users can start from demo mode, and paid plans are bought in monthly 25,000-email blocks.
46
+
47
+ Official doc:
48
+
49
+ - https://mailchimp.com/help/add-or-remove-transactional-email/
50
+
51
+ ## Step 2: Launch the Transactional app
52
+
53
+ After enabling Transactional:
54
+
55
+ 1. Go to the `Transactional` page in Mailchimp.
56
+ 2. Click `Launch App`.
57
+ 3. This opens the Transactional/Mandrill area where API keys, templates, senders, and logs live.
58
+
59
+ ## Step 3: Create your API key
60
+
61
+ Mailchimp's Quick Start says to create the API key inside Transactional settings and copy it immediately because you cannot view it again later.
62
+
63
+ Steps:
64
+
65
+ 1. In the Transactional app, open `Settings`.
66
+ 2. Find the `API Keys` section.
67
+ 3. Click `Create New Key`.
68
+ 4. Give it a descriptive name, for example `care-people-local`.
69
+ 5. Copy the key immediately and store it safely.
70
+
71
+ Official doc:
72
+
73
+ - https://mailchimp.com/developer/transactional/guides/quick-start/
74
+
75
+ ## Step 4: Verify your sending domain
76
+
77
+ Mailchimp says domain verification proves you control the domain and helps inbox placement.
78
+
79
+ Steps:
80
+
81
+ 1. In Mailchimp, open `Account & billing`.
82
+ 2. Open `Domains`.
83
+ 3. In `Email Domains`, click `Add & Verify Domain`.
84
+ 4. Enter an address on your domain, for example `noreply@yourdomain.com`.
85
+ 5. Click `Send Verification Email`.
86
+ 6. Open the verification email and complete the verification.
87
+
88
+ Mailchimp also recommends domain authentication and DMARC after verification.
89
+
90
+ Official doc:
91
+
92
+ - https://mailchimp.com/help/verify-a-domain/
93
+
94
+ ## Step 5: Authenticate the domain
95
+
96
+ After verification, set up Mailchimp's recommended DNS authentication records for the same domain.
97
+
98
+ Why this matters:
99
+
100
+ - better deliverability
101
+ - lower spam-folder risk
102
+ - better sender reputation
103
+
104
+ From Mailchimp's verification article:
105
+
106
+ - they recommend both `verify` and `authenticate`
107
+ - they strongly recommend configuring `DMARC`
108
+
109
+ Start here:
110
+
111
+ - https://mailchimp.com/help/verify-a-domain/
112
+
113
+ ## Step 6: Choose your sender addresses
114
+
115
+ Recommended sender setup:
116
+
117
+ - From email: `noreply@yourdomain.com`
118
+ - Reply-To: `support@yourdomain.com`
119
+ - From name: `Care People`
120
+
121
+ Avoid using:
122
+
123
+ - personal Gmail addresses
124
+ - Yahoo/AOL senders
125
+ - addresses on domains you do not control
126
+
127
+ ## Step 7: Put the values into the backend
128
+
129
+ Update [backend/.env](/c:/Users/mkhan/Documents/Projects/carePeople/backend/.env):
130
+
131
+ ```env
132
+ MAILCHIMP_TRANSACTIONAL_ENABLED=true
133
+ MAILCHIMP_TRANSACTIONAL_API_KEY=your_real_transactional_api_key
134
+ MAILCHIMP_FROM_EMAIL=noreply@yourdomain.com
135
+ MAILCHIMP_FROM_NAME=Care People
136
+ MAILCHIMP_REPLY_TO=support@yourdomain.com
137
+
138
+ OTP_DEV_MODE=false
139
+ ```
140
+
141
+ Notes:
142
+
143
+ - Keep `OTP_DEV_MODE=true` until your Mailchimp setup is verified.
144
+ - Once real sending works, switch it to `false`.
145
+ - If `MAILCHIMP_TRANSACTIONAL_ENABLED=false`, the backend stays in dev fallback mode and returns OTP in the API response.
146
+
147
+ ## Step 8: Restart and test the backend
148
+
149
+ From [backend](/c:/Users/mkhan/Documents/Projects/carePeople/backend):
150
+
151
+ ```powershell
152
+ npm run dev
153
+ ```
154
+
155
+ Then test:
156
+
157
+ 1. Import [care_people.postman_collection.json](/c:/Users/mkhan/Documents/Projects/carePeople/backend/postman/care_people.postman_collection.json)
158
+ 2. Set `patientEmail` to a mailbox you can receive
159
+ 3. Run `Send Patient OTP`
160
+ 4. Confirm the OTP email arrives
161
+ 5. Run `Verify Patient OTP`
162
+
163
+ ## Step 9: Test your API key directly
164
+
165
+ Mailchimp's Quick Start recommends testing with `/users/ping`.
166
+
167
+ PowerShell example:
168
+
169
+ ```powershell
170
+ $body = @{
171
+ key = "YOUR_API_KEY"
172
+ } | ConvertTo-Json
173
+
174
+ Invoke-RestMethod `
175
+ -Method Post `
176
+ -Uri "https://mandrillapp.com/api/1.0/users/ping" `
177
+ -ContentType "application/json" `
178
+ -Body $body
179
+ ```
180
+
181
+ Expected result:
182
+
183
+ ```text
184
+ PONG!
185
+ ```
186
+
187
+ Official doc:
188
+
189
+ - https://mailchimp.com/developer/transactional/guides/quick-start/
190
+
191
+ ## Email templates
192
+
193
+ Current status in this repo:
194
+
195
+ - the backend currently sends OTP using inline HTML/text
196
+ - Mailchimp templates are optional right now
197
+ - you can still prepare templates now, then we can wire the backend to them in a follow-up step
198
+
199
+ Mailchimp's Transactional API supports both:
200
+
201
+ - classic Transactional templates via `/templates/...`
202
+ - Mailchimp Transactional templates via `/mctemplates/...`
203
+
204
+ The API reference also lists:
205
+
206
+ - `/messages/send-template`
207
+ - `/messages/send-mc-template`
208
+
209
+ Official doc:
210
+
211
+ - https://mailchimp.com/developer/transactional/api/messages/send-using-mailchimp-template/
212
+ - https://mailchimp.com/developer/transactional/api/templates/list-templates/
213
+
214
+ ## Recommended OTP template variables
215
+
216
+ If you want us to switch to templates later, create the template around these variables:
217
+
218
+ - `OTP_CODE`
219
+ - `OTP_MINUTES`
220
+ - `APP_NAME`
221
+ - `SUPPORT_EMAIL`
222
+
223
+ Suggested subject:
224
+
225
+ ```text
226
+ Your Care People OTP is *|OTP_CODE|*
227
+ ```
228
+
229
+ Suggested body:
230
+
231
+ ```html
232
+ <div style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.6;">
233
+ <p>Hello,</p>
234
+ <p>Your Care People OTP is:</p>
235
+ <p style="font-size: 28px; font-weight: bold; letter-spacing: 4px;">*|OTP_CODE|*</p>
236
+ <p>This code is valid for *|OTP_MINUTES|* minutes.</p>
237
+ <p>Please do not share this code with anyone.</p>
238
+ <p>Care People Team</p>
239
+ </div>
240
+ ```
241
+
242
+ ## Template workflow options
243
+
244
+ ### Option A: Create templates in the Transactional app UI
245
+
246
+ Use this when:
247
+
248
+ - non-developers may edit email copy later
249
+ - you want Mailchimp-managed template revisions
250
+
251
+ After the template exists, we can update the backend to send through the Mailchimp template route instead of inline HTML.
252
+
253
+ ### Option B: Create templates through the Transactional API
254
+
255
+ Mailchimp's API reference includes endpoints for:
256
+
257
+ - `/templates/add`
258
+ - `/templates/update`
259
+ - `/templates/publish`
260
+ - `/templates/list`
261
+ - `/templates/render`
262
+
263
+ Important:
264
+
265
+ - publish the template after editing
266
+ - Mailchimp says published content is what new sends will use
267
+
268
+ Official doc:
269
+
270
+ - https://mailchimp.com/developer/transactional/api/templates/list-templates/
271
+
272
+ ## Suggested naming
273
+
274
+ Use a clear name like:
275
+
276
+ ```text
277
+ care-people-otp-v1
278
+ ```
279
+
280
+ That makes it easy to version later:
281
+
282
+ - `care-people-otp-v2`
283
+ - `care-people-welcome-v1`
284
+ - `care-people-appointment-confirmation-v1`
285
+
286
+ ## Troubleshooting
287
+
288
+ ### API ping fails
289
+
290
+ Likely causes:
291
+
292
+ - wrong API key
293
+ - key copied incorrectly
294
+ - Transactional add-on not fully enabled yet
295
+
296
+ Check:
297
+
298
+ - Transactional app `Settings > API Keys`
299
+ - the `/users/ping` test
300
+
301
+ ### OTP sends in dev mode but not through Mailchimp
302
+
303
+ Check:
304
+
305
+ - `MAILCHIMP_TRANSACTIONAL_ENABLED=true`
306
+ - `MAILCHIMP_TRANSACTIONAL_API_KEY` is set
307
+ - `MAILCHIMP_FROM_EMAIL` is set
308
+ - `OTP_DEV_MODE=false`
309
+
310
+ ### Messages go to spam
311
+
312
+ Check:
313
+
314
+ - domain verified
315
+ - domain authenticated
316
+ - DMARC configured
317
+ - from-address is on your own domain
318
+
319
+ ### Verification email never arrives
320
+
321
+ Mailchimp's domain verification article suggests:
322
+
323
+ - allow more time
324
+ - resend to another address on the same domain
325
+ - add `accountservices@mailchimp.com` to safe senders
326
+ - allowlist Mailchimp IPs if your IT/mail server is strict
327
+
328
+ Official doc:
329
+
330
+ - https://mailchimp.com/help/verify-a-domain/
331
+
332
+ ## Recommended rollout
333
+
334
+ 1. Enable Transactional in Mailchimp.
335
+ 2. Create API key.
336
+ 3. Verify and authenticate your domain.
337
+ 4. Put real credentials in [backend/.env](/c:/Users/mkhan/Documents/Projects/carePeople/backend/.env).
338
+ 5. Keep `OTP_DEV_MODE=true` for the first test.
339
+ 6. Confirm live email delivery.
340
+ 7. Turn `OTP_DEV_MODE=false`.
341
+ 8. Optionally create and publish an OTP template.
342
+ 9. If you want, I can then switch the backend from inline HTML to template-based sending.
README.md ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Care People Backend
2
+
3
+ MySQL-backed backend for the Flutter `care_people` app using:
4
+
5
+ - Node.js + Express
6
+ - XAMPP MariaDB/MySQL
7
+ - JWT auth with 2-day refresh sessions
8
+ - Dev OTP flow for patient login
9
+ - Mailchimp Transactional-ready email OTP delivery
10
+
11
+ ## Local setup
12
+
13
+ This backend is already configured for your current XAMPP setup:
14
+
15
+ - Host: `127.0.0.1`
16
+ - Port: `3306`
17
+ - Database: `care_people`
18
+ - User: `root`
19
+ - Password: empty
20
+
21
+ Start from [backend/.env](/c:/Users/mkhan/Documents/Projects/carePeople/backend/.env).
22
+
23
+ Mailchimp email settings:
24
+
25
+ - `MAILCHIMP_TRANSACTIONAL_ENABLED`
26
+ - `MAILCHIMP_TRANSACTIONAL_API_KEY`
27
+ - `MAILCHIMP_FROM_EMAIL`
28
+ - `MAILCHIMP_FROM_NAME`
29
+ - `MAILCHIMP_REPLY_TO`
30
+
31
+ Groq AI settings:
32
+
33
+ - `GROQ_API_KEY`
34
+ - `GROQ_MODEL`
35
+
36
+ For OTP, use `Mailchimp Transactional` (Mandrill API), not the regular Mailchimp marketing campaign API.
37
+
38
+ Full setup guide:
39
+
40
+ - [MAILCHIMP_SETUP.md](/c:/Users/mkhan/Documents/Projects/carePeople/backend/MAILCHIMP_SETUP.md)
41
+
42
+ ## Commands
43
+
44
+ Run these inside [backend](/c:/Users/mkhan/Documents/Projects/carePeople/backend):
45
+
46
+ ```powershell
47
+ npm install
48
+ npm run db:init
49
+ npm run db:seed
50
+ npm run dev
51
+ ```
52
+
53
+ Health check:
54
+
55
+ ```powershell
56
+ Invoke-RestMethod http://localhost:3000/health
57
+ ```
58
+
59
+ ## Backend plan
60
+
61
+ 1. Move storage from local Flutter JSON and `SharedPreferences` into MySQL.
62
+ 2. Keep the current product flow intact: patient OTP login, patient profile, doctor login, doctor discovery, appointments, prescriptions.
63
+ 3. Send patient OTP through email, with a dev fallback until Mailchimp credentials are added.
64
+ 4. Preserve appointment history in MySQL by marking completed rows instead of deleting them.
65
+ 5. Add a Postman collection so every route is testable before app integration.
66
+
67
+ ## API list
68
+
69
+ ### Auth
70
+
71
+ - `POST /api/v1/auth/patient/send-otp`
72
+ - `POST /api/v1/auth/patient/verify-otp`
73
+ - `POST /api/v1/auth/doctor/login`
74
+ - `POST /api/v1/auth/refresh`
75
+ - `POST /api/v1/auth/logout`
76
+
77
+ ### AI
78
+
79
+ - `POST /api/v1/ai/patient-suggestion`
80
+ - `POST /api/v1/ai/doctor-assistant`
81
+
82
+ ### Patients
83
+
84
+ - `GET /api/v1/patients/me`
85
+ - `PUT /api/v1/patients/me`
86
+
87
+ ### Doctors
88
+
89
+ - `GET /api/v1/doctors`
90
+ - `GET /api/v1/doctors/:doctorId`
91
+
92
+ ### Appointments
93
+
94
+ - `GET /api/v1/appointments/doctor/:doctorId/availability?date=YYYY-MM-DD`
95
+ - `GET /api/v1/appointments/patient/me`
96
+ - `GET /api/v1/appointments/doctor/me`
97
+ - `POST /api/v1/appointments`
98
+ - `PATCH /api/v1/appointments/:appointmentId/status`
99
+
100
+ ### Prescriptions
101
+
102
+ - `GET /api/v1/prescriptions/patient/me`
103
+ - `GET /api/v1/prescriptions/doctor/me`
104
+ - `POST /api/v1/prescriptions`
105
+
106
+ ## Example request JSON
107
+
108
+ ### Send patient OTP
109
+
110
+ ```json
111
+ {
112
+ "phoneNumber": "01712345678",
113
+ "email": "patient@example.com"
114
+ }
115
+ ```
116
+
117
+ ### Save patient profile
118
+
119
+ ```json
120
+ {
121
+ "email": "patient@example.com",
122
+ "name": "Md Rahim",
123
+ "dateOfBirth": "1998-02-14",
124
+ "address": "Dhaka, Bangladesh",
125
+ "gender": "Male"
126
+ }
127
+ ```
128
+
129
+ ### Create appointment
130
+
131
+ ```json
132
+ {
133
+ "doctorId": "DOC001",
134
+ "date": "2026-04-06",
135
+ "timeSlot": "09:00 AM",
136
+ "notes": "Chest pain for 2 days"
137
+ }
138
+ ```
139
+
140
+ ### Create prescription
141
+
142
+ ```json
143
+ {
144
+ "appointmentId": 1,
145
+ "diagnosis": [
146
+ "Hypertension",
147
+ "Chest discomfort"
148
+ ],
149
+ "medicines": [
150
+ {
151
+ "name": "Amlodipine",
152
+ "dosage": "5 mg",
153
+ "frequency": "Once daily",
154
+ "duration": "30 days",
155
+ "notes": "After breakfast"
156
+ }
157
+ ],
158
+ "additionalNotes": "Reduce salt intake",
159
+ "pdfPath": null
160
+ }
161
+ ```
162
+
163
+ ### Patient AI suggestion
164
+
165
+ ```json
166
+ {
167
+ "symptoms": "Fever, sore throat, and body pain since last night",
168
+ "severity": "Medium"
169
+ }
170
+ ```
171
+
172
+ ### Doctor AI assistant
173
+
174
+ ```json
175
+ {
176
+ "goal": "Consult Note",
177
+ "input": "52 year old with chest discomfort for 2 days, no syncope, mild dizziness, blood pressure not yet recorded."
178
+ }
179
+ ```
180
+
181
+ ## Postman
182
+
183
+ Collection JSON:
184
+
185
+ - [care_people.postman_collection.json](/c:/Users/mkhan/Documents/Projects/carePeople/backend/postman/care_people.postman_collection.json)
186
+
187
+ Import it into Postman, then run the requests in this order:
188
+
189
+ 1. `Health`
190
+ 2. `Send Patient OTP`
191
+ 3. `Verify Patient OTP`
192
+ 4. `Save Patient Profile`
193
+ 5. `List Doctors`
194
+ 6. `Doctor Availability`
195
+ 7. `Create Appointment`
196
+ 8. `Patient AI Suggestion`
197
+ 9. `Doctor Login`
198
+ 10. `Doctor AI Assistant`
199
+ 11. `Create Prescription`
200
+
201
+ ## Notes
202
+
203
+ - Doctors are seeded from [doctors.json](/c:/Users/mkhan/Documents/Projects/carePeople/assets/data/doctors.json).
204
+ - Doctor passwords stay the same as the current Flutter asset file, but are stored hashed in MySQL.
205
+ - The backend returns camelCase JSON to match your current Flutter models more closely.
206
+ - Patient login now expects `phoneNumber + email`, and OTP is sent by email.
207
+ - When `MAILCHIMP_TRANSACTIONAL_ENABLED=false`, the backend stays in dev mode and returns the OTP in the API response for testing.
208
+ - AI guidance is generated through Groq on the backend so the API key stays server-side.
database/apply_schema.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const mysql = require('mysql2/promise');
4
+ const dotenv = require('dotenv');
5
+
6
+ dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
7
+
8
+ async function applySchema() {
9
+ const connection = await mysql.createConnection({
10
+ host: process.env.DB_HOST || '127.0.0.1',
11
+ port: Number(process.env.DB_PORT || 3306),
12
+ user: process.env.DB_USER || 'root',
13
+ password: process.env.DB_PASSWORD || '',
14
+ multipleStatements: true,
15
+ });
16
+
17
+ try {
18
+ const schemaPath = path.resolve(__dirname, 'schema.sql');
19
+ const schemaSql = fs.readFileSync(schemaPath, 'utf8');
20
+ await connection.query(schemaSql);
21
+ await connection.query(
22
+ `USE \`${process.env.DB_NAME || 'care_people'}\``,
23
+ );
24
+
25
+ const [emailColumnRows] = await connection.query(`
26
+ SELECT COUNT(*) AS total
27
+ FROM information_schema.COLUMNS
28
+ WHERE
29
+ TABLE_SCHEMA = DATABASE()
30
+ AND TABLE_NAME = 'patients'
31
+ AND COLUMN_NAME = 'email'
32
+ `);
33
+
34
+ if (Number(emailColumnRows[0].total || 0) === 0) {
35
+ await connection.query(
36
+ 'ALTER TABLE patients ADD COLUMN email VARCHAR(255) NULL AFTER phone_number',
37
+ );
38
+ }
39
+
40
+ const [emailIndexRows] = await connection.query(`
41
+ SELECT COUNT(*) AS total
42
+ FROM information_schema.STATISTICS
43
+ WHERE
44
+ TABLE_SCHEMA = DATABASE()
45
+ AND TABLE_NAME = 'patients'
46
+ AND INDEX_NAME = 'uniq_patients_email'
47
+ `);
48
+
49
+ if (Number(emailIndexRows[0].total || 0) === 0) {
50
+ await connection.query(
51
+ 'ALTER TABLE patients ADD UNIQUE KEY uniq_patients_email (email)',
52
+ );
53
+ }
54
+
55
+ console.log('MySQL schema applied successfully.');
56
+ } finally {
57
+ await connection.end();
58
+ }
59
+ }
60
+
61
+ applySchema().catch((error) => {
62
+ console.error('Failed to apply MySQL schema.');
63
+ console.error(error);
64
+ process.exit(1);
65
+ });
database/schema.sql ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CREATE DATABASE IF NOT EXISTS care_people
2
+ CHARACTER SET utf8mb4
3
+ COLLATE utf8mb4_unicode_ci;
4
+
5
+ USE care_people;
6
+
7
+ CREATE TABLE IF NOT EXISTS doctors (
8
+ id VARCHAR(10) PRIMARY KEY,
9
+ name VARCHAR(100) NOT NULL,
10
+ department VARCHAR(50) NOT NULL,
11
+ designation VARCHAR(100) NOT NULL,
12
+ degrees VARCHAR(200) NOT NULL,
13
+ room_number VARCHAR(10) NOT NULL,
14
+ consultation_fee DECIMAL(10, 2) NOT NULL,
15
+ consultation_days JSON NOT NULL,
16
+ consultation_times VARCHAR(100) NOT NULL,
17
+ password_hash VARCHAR(255) NOT NULL,
18
+ is_active TINYINT(1) NOT NULL DEFAULT 1,
19
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
21
+ ) ENGINE=InnoDB;
22
+
23
+ CREATE TABLE IF NOT EXISTS patients (
24
+ phone_number VARCHAR(15) PRIMARY KEY,
25
+ email VARCHAR(255) NULL,
26
+ name VARCHAR(100) NOT NULL,
27
+ date_of_birth DATE NOT NULL,
28
+ address TEXT NOT NULL,
29
+ gender ENUM('male', 'female', 'other') NOT NULL DEFAULT 'male',
30
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
31
+ updated_at DATETIME NULL DEFAULT NULL,
32
+ UNIQUE KEY uniq_patients_email (email)
33
+ ) ENGINE=InnoDB;
34
+
35
+ CREATE TABLE IF NOT EXISTS sessions (
36
+ id CHAR(36) PRIMARY KEY,
37
+ user_type ENUM('patient', 'doctor') NOT NULL,
38
+ user_identifier VARCHAR(32) NOT NULL,
39
+ refresh_token_hash CHAR(64) NOT NULL,
40
+ login_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
+ expires_at DATETIME NOT NULL,
42
+ revoked_at DATETIME NULL DEFAULT NULL,
43
+ metadata_json JSON NULL,
44
+ INDEX idx_sessions_user (user_type, user_identifier),
45
+ INDEX idx_sessions_expires (expires_at)
46
+ ) ENGINE=InnoDB;
47
+
48
+ CREATE TABLE IF NOT EXISTS appointments (
49
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
50
+ doctor_id VARCHAR(10) NOT NULL,
51
+ patient_phone VARCHAR(15) NOT NULL,
52
+ appointment_date DATE NOT NULL,
53
+ time_slot VARCHAR(20) NOT NULL,
54
+ serial_number INT NOT NULL,
55
+ status ENUM('confirmed', 'cancelled', 'completed') NOT NULL DEFAULT 'confirmed',
56
+ notes TEXT NULL,
57
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
58
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
59
+ UNIQUE KEY uniq_appointment_slot (doctor_id, appointment_date, time_slot),
60
+ INDEX idx_appointments_doctor_date (doctor_id, appointment_date),
61
+ INDEX idx_appointments_patient_date (patient_phone, appointment_date),
62
+ CONSTRAINT fk_appointments_doctor
63
+ FOREIGN KEY (doctor_id) REFERENCES doctors(id)
64
+ ON DELETE RESTRICT
65
+ ON UPDATE CASCADE,
66
+ CONSTRAINT fk_appointments_patient
67
+ FOREIGN KEY (patient_phone) REFERENCES patients(phone_number)
68
+ ON DELETE RESTRICT
69
+ ON UPDATE CASCADE
70
+ ) ENGINE=InnoDB;
71
+
72
+ CREATE TABLE IF NOT EXISTS prescriptions (
73
+ id VARCHAR(40) PRIMARY KEY,
74
+ doctor_id VARCHAR(10) NOT NULL,
75
+ patient_phone VARCHAR(15) NOT NULL,
76
+ appointment_id BIGINT UNSIGNED NULL,
77
+ appointment_date DATE NOT NULL,
78
+ issued_at DATETIME NOT NULL,
79
+ additional_notes TEXT NULL,
80
+ pdf_path VARCHAR(500) NULL,
81
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
82
+ INDEX idx_prescriptions_doctor_issue (doctor_id, issued_at),
83
+ INDEX idx_prescriptions_patient_issue (patient_phone, issued_at),
84
+ CONSTRAINT fk_prescriptions_doctor
85
+ FOREIGN KEY (doctor_id) REFERENCES doctors(id)
86
+ ON DELETE RESTRICT
87
+ ON UPDATE CASCADE,
88
+ CONSTRAINT fk_prescriptions_patient
89
+ FOREIGN KEY (patient_phone) REFERENCES patients(phone_number)
90
+ ON DELETE RESTRICT
91
+ ON UPDATE CASCADE,
92
+ CONSTRAINT fk_prescriptions_appointment
93
+ FOREIGN KEY (appointment_id) REFERENCES appointments(id)
94
+ ON DELETE SET NULL
95
+ ON UPDATE CASCADE
96
+ ) ENGINE=InnoDB;
97
+
98
+ CREATE TABLE IF NOT EXISTS prescription_diagnoses (
99
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
100
+ prescription_id VARCHAR(40) NOT NULL,
101
+ diagnosis_text TEXT NOT NULL,
102
+ sort_order INT NOT NULL DEFAULT 0,
103
+ INDEX idx_prescription_diagnoses_rx (prescription_id, sort_order),
104
+ CONSTRAINT fk_prescription_diagnoses_prescription
105
+ FOREIGN KEY (prescription_id) REFERENCES prescriptions(id)
106
+ ON DELETE CASCADE
107
+ ON UPDATE CASCADE
108
+ ) ENGINE=InnoDB;
109
+
110
+ CREATE TABLE IF NOT EXISTS prescribed_medicines (
111
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
112
+ prescription_id VARCHAR(40) NOT NULL,
113
+ name VARCHAR(200) NOT NULL,
114
+ dosage VARCHAR(100) NOT NULL,
115
+ frequency VARCHAR(100) NOT NULL,
116
+ duration VARCHAR(100) NOT NULL,
117
+ notes TEXT NULL,
118
+ INDEX idx_prescribed_medicines_rx (prescription_id),
119
+ CONSTRAINT fk_prescribed_medicines_prescription
120
+ FOREIGN KEY (prescription_id) REFERENCES prescriptions(id)
121
+ ON DELETE CASCADE
122
+ ON UPDATE CASCADE
123
+ ) ENGINE=InnoDB;
database/seed_doctors.js ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const bcrypt = require('bcryptjs');
4
+ const mysql = require('mysql2/promise');
5
+ const dotenv = require('dotenv');
6
+
7
+ dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
8
+
9
+ async function seedDoctors() {
10
+ const sourcePath = path.resolve(
11
+ __dirname,
12
+ '..',
13
+ '..',
14
+ 'assets',
15
+ 'data',
16
+ 'doctors.json',
17
+ );
18
+
19
+ const rawFile = fs.readFileSync(sourcePath, 'utf8');
20
+ const doctors = JSON.parse(rawFile).doctors || [];
21
+
22
+ const connection = await mysql.createConnection({
23
+ host: process.env.DB_HOST || '127.0.0.1',
24
+ port: Number(process.env.DB_PORT || 3306),
25
+ user: process.env.DB_USER || 'root',
26
+ password: process.env.DB_PASSWORD || '',
27
+ database: process.env.DB_NAME || 'care_people',
28
+ });
29
+
30
+ try {
31
+ for (const doctor of doctors) {
32
+ const passwordHash = await bcrypt.hash(String(doctor.password), 10);
33
+
34
+ await connection.execute(
35
+ `
36
+ INSERT INTO doctors (
37
+ id,
38
+ name,
39
+ department,
40
+ designation,
41
+ degrees,
42
+ room_number,
43
+ consultation_fee,
44
+ consultation_days,
45
+ consultation_times,
46
+ password_hash,
47
+ is_active
48
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
49
+ ON DUPLICATE KEY UPDATE
50
+ name = VALUES(name),
51
+ department = VALUES(department),
52
+ designation = VALUES(designation),
53
+ degrees = VALUES(degrees),
54
+ room_number = VALUES(room_number),
55
+ consultation_fee = VALUES(consultation_fee),
56
+ consultation_days = VALUES(consultation_days),
57
+ consultation_times = VALUES(consultation_times),
58
+ password_hash = VALUES(password_hash),
59
+ is_active = VALUES(is_active)
60
+ `,
61
+ [
62
+ doctor.id,
63
+ doctor.name,
64
+ doctor.department,
65
+ doctor.designation,
66
+ doctor.degrees,
67
+ doctor.roomNumber,
68
+ doctor.consultationFee,
69
+ JSON.stringify(doctor.consultationDays),
70
+ doctor.consultationTimes,
71
+ passwordHash,
72
+ ],
73
+ );
74
+ }
75
+
76
+ console.log(`Seeded ${doctors.length} doctors successfully.`);
77
+ } finally {
78
+ await connection.end();
79
+ }
80
+ }
81
+
82
+ seedDoctors().catch((error) => {
83
+ console.error('Failed to seed doctors.');
84
+ console.error(error);
85
+ process.exit(1);
86
+ });
package-lock.json ADDED
@@ -0,0 +1,1470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "care_people_backend",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "care_people_backend",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "bcryptjs": "^3.0.3",
13
+ "cors": "^2.8.6",
14
+ "dotenv": "^17.3.1",
15
+ "express": "^5.2.1",
16
+ "jsonwebtoken": "^9.0.3",
17
+ "mysql2": "^3.20.0"
18
+ },
19
+ "devDependencies": {
20
+ "nodemon": "^3.1.14"
21
+ }
22
+ },
23
+ "node_modules/@types/node": {
24
+ "version": "25.5.0",
25
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
26
+ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
27
+ "license": "MIT",
28
+ "peer": true,
29
+ "dependencies": {
30
+ "undici-types": "~7.18.0"
31
+ }
32
+ },
33
+ "node_modules/accepts": {
34
+ "version": "2.0.0",
35
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
36
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "mime-types": "^3.0.0",
40
+ "negotiator": "^1.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">= 0.6"
44
+ }
45
+ },
46
+ "node_modules/anymatch": {
47
+ "version": "3.1.3",
48
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
49
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
50
+ "dev": true,
51
+ "license": "ISC",
52
+ "dependencies": {
53
+ "normalize-path": "^3.0.0",
54
+ "picomatch": "^2.0.4"
55
+ },
56
+ "engines": {
57
+ "node": ">= 8"
58
+ }
59
+ },
60
+ "node_modules/aws-ssl-profiles": {
61
+ "version": "1.1.2",
62
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
63
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
64
+ "license": "MIT",
65
+ "engines": {
66
+ "node": ">= 6.0.0"
67
+ }
68
+ },
69
+ "node_modules/balanced-match": {
70
+ "version": "4.0.4",
71
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
72
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
73
+ "dev": true,
74
+ "license": "MIT",
75
+ "engines": {
76
+ "node": "18 || 20 || >=22"
77
+ }
78
+ },
79
+ "node_modules/bcryptjs": {
80
+ "version": "3.0.3",
81
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
82
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
83
+ "license": "BSD-3-Clause",
84
+ "bin": {
85
+ "bcrypt": "bin/bcrypt"
86
+ }
87
+ },
88
+ "node_modules/binary-extensions": {
89
+ "version": "2.3.0",
90
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
91
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
92
+ "dev": true,
93
+ "license": "MIT",
94
+ "engines": {
95
+ "node": ">=8"
96
+ },
97
+ "funding": {
98
+ "url": "https://github.com/sponsors/sindresorhus"
99
+ }
100
+ },
101
+ "node_modules/body-parser": {
102
+ "version": "2.2.2",
103
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
104
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
105
+ "license": "MIT",
106
+ "dependencies": {
107
+ "bytes": "^3.1.2",
108
+ "content-type": "^1.0.5",
109
+ "debug": "^4.4.3",
110
+ "http-errors": "^2.0.0",
111
+ "iconv-lite": "^0.7.0",
112
+ "on-finished": "^2.4.1",
113
+ "qs": "^6.14.1",
114
+ "raw-body": "^3.0.1",
115
+ "type-is": "^2.0.1"
116
+ },
117
+ "engines": {
118
+ "node": ">=18"
119
+ },
120
+ "funding": {
121
+ "type": "opencollective",
122
+ "url": "https://opencollective.com/express"
123
+ }
124
+ },
125
+ "node_modules/brace-expansion": {
126
+ "version": "5.0.5",
127
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
128
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
129
+ "dev": true,
130
+ "license": "MIT",
131
+ "dependencies": {
132
+ "balanced-match": "^4.0.2"
133
+ },
134
+ "engines": {
135
+ "node": "18 || 20 || >=22"
136
+ }
137
+ },
138
+ "node_modules/braces": {
139
+ "version": "3.0.3",
140
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
141
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
142
+ "dev": true,
143
+ "license": "MIT",
144
+ "dependencies": {
145
+ "fill-range": "^7.1.1"
146
+ },
147
+ "engines": {
148
+ "node": ">=8"
149
+ }
150
+ },
151
+ "node_modules/buffer-equal-constant-time": {
152
+ "version": "1.0.1",
153
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
154
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
155
+ "license": "BSD-3-Clause"
156
+ },
157
+ "node_modules/bytes": {
158
+ "version": "3.1.2",
159
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
160
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
161
+ "license": "MIT",
162
+ "engines": {
163
+ "node": ">= 0.8"
164
+ }
165
+ },
166
+ "node_modules/call-bind-apply-helpers": {
167
+ "version": "1.0.2",
168
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
169
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
170
+ "license": "MIT",
171
+ "dependencies": {
172
+ "es-errors": "^1.3.0",
173
+ "function-bind": "^1.1.2"
174
+ },
175
+ "engines": {
176
+ "node": ">= 0.4"
177
+ }
178
+ },
179
+ "node_modules/call-bound": {
180
+ "version": "1.0.4",
181
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
182
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
183
+ "license": "MIT",
184
+ "dependencies": {
185
+ "call-bind-apply-helpers": "^1.0.2",
186
+ "get-intrinsic": "^1.3.0"
187
+ },
188
+ "engines": {
189
+ "node": ">= 0.4"
190
+ },
191
+ "funding": {
192
+ "url": "https://github.com/sponsors/ljharb"
193
+ }
194
+ },
195
+ "node_modules/chokidar": {
196
+ "version": "3.6.0",
197
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
198
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
199
+ "dev": true,
200
+ "license": "MIT",
201
+ "dependencies": {
202
+ "anymatch": "~3.1.2",
203
+ "braces": "~3.0.2",
204
+ "glob-parent": "~5.1.2",
205
+ "is-binary-path": "~2.1.0",
206
+ "is-glob": "~4.0.1",
207
+ "normalize-path": "~3.0.0",
208
+ "readdirp": "~3.6.0"
209
+ },
210
+ "engines": {
211
+ "node": ">= 8.10.0"
212
+ },
213
+ "funding": {
214
+ "url": "https://paulmillr.com/funding/"
215
+ },
216
+ "optionalDependencies": {
217
+ "fsevents": "~2.3.2"
218
+ }
219
+ },
220
+ "node_modules/content-disposition": {
221
+ "version": "1.0.1",
222
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
223
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
224
+ "license": "MIT",
225
+ "engines": {
226
+ "node": ">=18"
227
+ },
228
+ "funding": {
229
+ "type": "opencollective",
230
+ "url": "https://opencollective.com/express"
231
+ }
232
+ },
233
+ "node_modules/content-type": {
234
+ "version": "1.0.5",
235
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
236
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
237
+ "license": "MIT",
238
+ "engines": {
239
+ "node": ">= 0.6"
240
+ }
241
+ },
242
+ "node_modules/cookie": {
243
+ "version": "0.7.2",
244
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
245
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
246
+ "license": "MIT",
247
+ "engines": {
248
+ "node": ">= 0.6"
249
+ }
250
+ },
251
+ "node_modules/cookie-signature": {
252
+ "version": "1.2.2",
253
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
254
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
255
+ "license": "MIT",
256
+ "engines": {
257
+ "node": ">=6.6.0"
258
+ }
259
+ },
260
+ "node_modules/cors": {
261
+ "version": "2.8.6",
262
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
263
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
264
+ "license": "MIT",
265
+ "dependencies": {
266
+ "object-assign": "^4",
267
+ "vary": "^1"
268
+ },
269
+ "engines": {
270
+ "node": ">= 0.10"
271
+ },
272
+ "funding": {
273
+ "type": "opencollective",
274
+ "url": "https://opencollective.com/express"
275
+ }
276
+ },
277
+ "node_modules/debug": {
278
+ "version": "4.4.3",
279
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
280
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
281
+ "license": "MIT",
282
+ "dependencies": {
283
+ "ms": "^2.1.3"
284
+ },
285
+ "engines": {
286
+ "node": ">=6.0"
287
+ },
288
+ "peerDependenciesMeta": {
289
+ "supports-color": {
290
+ "optional": true
291
+ }
292
+ }
293
+ },
294
+ "node_modules/denque": {
295
+ "version": "2.1.0",
296
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
297
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
298
+ "license": "Apache-2.0",
299
+ "engines": {
300
+ "node": ">=0.10"
301
+ }
302
+ },
303
+ "node_modules/depd": {
304
+ "version": "2.0.0",
305
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
306
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
307
+ "license": "MIT",
308
+ "engines": {
309
+ "node": ">= 0.8"
310
+ }
311
+ },
312
+ "node_modules/dotenv": {
313
+ "version": "17.3.1",
314
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
315
+ "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
316
+ "license": "BSD-2-Clause",
317
+ "engines": {
318
+ "node": ">=12"
319
+ },
320
+ "funding": {
321
+ "url": "https://dotenvx.com"
322
+ }
323
+ },
324
+ "node_modules/dunder-proto": {
325
+ "version": "1.0.1",
326
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
327
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
328
+ "license": "MIT",
329
+ "dependencies": {
330
+ "call-bind-apply-helpers": "^1.0.1",
331
+ "es-errors": "^1.3.0",
332
+ "gopd": "^1.2.0"
333
+ },
334
+ "engines": {
335
+ "node": ">= 0.4"
336
+ }
337
+ },
338
+ "node_modules/ecdsa-sig-formatter": {
339
+ "version": "1.0.11",
340
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
341
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
342
+ "license": "Apache-2.0",
343
+ "dependencies": {
344
+ "safe-buffer": "^5.0.1"
345
+ }
346
+ },
347
+ "node_modules/ee-first": {
348
+ "version": "1.1.1",
349
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
350
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
351
+ "license": "MIT"
352
+ },
353
+ "node_modules/encodeurl": {
354
+ "version": "2.0.0",
355
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
356
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
357
+ "license": "MIT",
358
+ "engines": {
359
+ "node": ">= 0.8"
360
+ }
361
+ },
362
+ "node_modules/es-define-property": {
363
+ "version": "1.0.1",
364
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
365
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
366
+ "license": "MIT",
367
+ "engines": {
368
+ "node": ">= 0.4"
369
+ }
370
+ },
371
+ "node_modules/es-errors": {
372
+ "version": "1.3.0",
373
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
374
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
375
+ "license": "MIT",
376
+ "engines": {
377
+ "node": ">= 0.4"
378
+ }
379
+ },
380
+ "node_modules/es-object-atoms": {
381
+ "version": "1.1.1",
382
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
383
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
384
+ "license": "MIT",
385
+ "dependencies": {
386
+ "es-errors": "^1.3.0"
387
+ },
388
+ "engines": {
389
+ "node": ">= 0.4"
390
+ }
391
+ },
392
+ "node_modules/escape-html": {
393
+ "version": "1.0.3",
394
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
395
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
396
+ "license": "MIT"
397
+ },
398
+ "node_modules/etag": {
399
+ "version": "1.8.1",
400
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
401
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
402
+ "license": "MIT",
403
+ "engines": {
404
+ "node": ">= 0.6"
405
+ }
406
+ },
407
+ "node_modules/express": {
408
+ "version": "5.2.1",
409
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
410
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
411
+ "license": "MIT",
412
+ "dependencies": {
413
+ "accepts": "^2.0.0",
414
+ "body-parser": "^2.2.1",
415
+ "content-disposition": "^1.0.0",
416
+ "content-type": "^1.0.5",
417
+ "cookie": "^0.7.1",
418
+ "cookie-signature": "^1.2.1",
419
+ "debug": "^4.4.0",
420
+ "depd": "^2.0.0",
421
+ "encodeurl": "^2.0.0",
422
+ "escape-html": "^1.0.3",
423
+ "etag": "^1.8.1",
424
+ "finalhandler": "^2.1.0",
425
+ "fresh": "^2.0.0",
426
+ "http-errors": "^2.0.0",
427
+ "merge-descriptors": "^2.0.0",
428
+ "mime-types": "^3.0.0",
429
+ "on-finished": "^2.4.1",
430
+ "once": "^1.4.0",
431
+ "parseurl": "^1.3.3",
432
+ "proxy-addr": "^2.0.7",
433
+ "qs": "^6.14.0",
434
+ "range-parser": "^1.2.1",
435
+ "router": "^2.2.0",
436
+ "send": "^1.1.0",
437
+ "serve-static": "^2.2.0",
438
+ "statuses": "^2.0.1",
439
+ "type-is": "^2.0.1",
440
+ "vary": "^1.1.2"
441
+ },
442
+ "engines": {
443
+ "node": ">= 18"
444
+ },
445
+ "funding": {
446
+ "type": "opencollective",
447
+ "url": "https://opencollective.com/express"
448
+ }
449
+ },
450
+ "node_modules/fill-range": {
451
+ "version": "7.1.1",
452
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
453
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
454
+ "dev": true,
455
+ "license": "MIT",
456
+ "dependencies": {
457
+ "to-regex-range": "^5.0.1"
458
+ },
459
+ "engines": {
460
+ "node": ">=8"
461
+ }
462
+ },
463
+ "node_modules/finalhandler": {
464
+ "version": "2.1.1",
465
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
466
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "debug": "^4.4.0",
470
+ "encodeurl": "^2.0.0",
471
+ "escape-html": "^1.0.3",
472
+ "on-finished": "^2.4.1",
473
+ "parseurl": "^1.3.3",
474
+ "statuses": "^2.0.1"
475
+ },
476
+ "engines": {
477
+ "node": ">= 18.0.0"
478
+ },
479
+ "funding": {
480
+ "type": "opencollective",
481
+ "url": "https://opencollective.com/express"
482
+ }
483
+ },
484
+ "node_modules/forwarded": {
485
+ "version": "0.2.0",
486
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
487
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
488
+ "license": "MIT",
489
+ "engines": {
490
+ "node": ">= 0.6"
491
+ }
492
+ },
493
+ "node_modules/fresh": {
494
+ "version": "2.0.0",
495
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
496
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
497
+ "license": "MIT",
498
+ "engines": {
499
+ "node": ">= 0.8"
500
+ }
501
+ },
502
+ "node_modules/fsevents": {
503
+ "version": "2.3.3",
504
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
505
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
506
+ "dev": true,
507
+ "hasInstallScript": true,
508
+ "license": "MIT",
509
+ "optional": true,
510
+ "os": [
511
+ "darwin"
512
+ ],
513
+ "engines": {
514
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
515
+ }
516
+ },
517
+ "node_modules/function-bind": {
518
+ "version": "1.1.2",
519
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
520
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
521
+ "license": "MIT",
522
+ "funding": {
523
+ "url": "https://github.com/sponsors/ljharb"
524
+ }
525
+ },
526
+ "node_modules/generate-function": {
527
+ "version": "2.3.1",
528
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
529
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
530
+ "license": "MIT",
531
+ "dependencies": {
532
+ "is-property": "^1.0.2"
533
+ }
534
+ },
535
+ "node_modules/get-intrinsic": {
536
+ "version": "1.3.0",
537
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
538
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
539
+ "license": "MIT",
540
+ "dependencies": {
541
+ "call-bind-apply-helpers": "^1.0.2",
542
+ "es-define-property": "^1.0.1",
543
+ "es-errors": "^1.3.0",
544
+ "es-object-atoms": "^1.1.1",
545
+ "function-bind": "^1.1.2",
546
+ "get-proto": "^1.0.1",
547
+ "gopd": "^1.2.0",
548
+ "has-symbols": "^1.1.0",
549
+ "hasown": "^2.0.2",
550
+ "math-intrinsics": "^1.1.0"
551
+ },
552
+ "engines": {
553
+ "node": ">= 0.4"
554
+ },
555
+ "funding": {
556
+ "url": "https://github.com/sponsors/ljharb"
557
+ }
558
+ },
559
+ "node_modules/get-proto": {
560
+ "version": "1.0.1",
561
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
562
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
563
+ "license": "MIT",
564
+ "dependencies": {
565
+ "dunder-proto": "^1.0.1",
566
+ "es-object-atoms": "^1.0.0"
567
+ },
568
+ "engines": {
569
+ "node": ">= 0.4"
570
+ }
571
+ },
572
+ "node_modules/glob-parent": {
573
+ "version": "5.1.2",
574
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
575
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
576
+ "dev": true,
577
+ "license": "ISC",
578
+ "dependencies": {
579
+ "is-glob": "^4.0.1"
580
+ },
581
+ "engines": {
582
+ "node": ">= 6"
583
+ }
584
+ },
585
+ "node_modules/gopd": {
586
+ "version": "1.2.0",
587
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
588
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
589
+ "license": "MIT",
590
+ "engines": {
591
+ "node": ">= 0.4"
592
+ },
593
+ "funding": {
594
+ "url": "https://github.com/sponsors/ljharb"
595
+ }
596
+ },
597
+ "node_modules/has-flag": {
598
+ "version": "3.0.0",
599
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
600
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
601
+ "dev": true,
602
+ "license": "MIT",
603
+ "engines": {
604
+ "node": ">=4"
605
+ }
606
+ },
607
+ "node_modules/has-symbols": {
608
+ "version": "1.1.0",
609
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
610
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
611
+ "license": "MIT",
612
+ "engines": {
613
+ "node": ">= 0.4"
614
+ },
615
+ "funding": {
616
+ "url": "https://github.com/sponsors/ljharb"
617
+ }
618
+ },
619
+ "node_modules/hasown": {
620
+ "version": "2.0.2",
621
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
622
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
623
+ "license": "MIT",
624
+ "dependencies": {
625
+ "function-bind": "^1.1.2"
626
+ },
627
+ "engines": {
628
+ "node": ">= 0.4"
629
+ }
630
+ },
631
+ "node_modules/http-errors": {
632
+ "version": "2.0.1",
633
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
634
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
635
+ "license": "MIT",
636
+ "dependencies": {
637
+ "depd": "~2.0.0",
638
+ "inherits": "~2.0.4",
639
+ "setprototypeof": "~1.2.0",
640
+ "statuses": "~2.0.2",
641
+ "toidentifier": "~1.0.1"
642
+ },
643
+ "engines": {
644
+ "node": ">= 0.8"
645
+ },
646
+ "funding": {
647
+ "type": "opencollective",
648
+ "url": "https://opencollective.com/express"
649
+ }
650
+ },
651
+ "node_modules/iconv-lite": {
652
+ "version": "0.7.2",
653
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
654
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
655
+ "license": "MIT",
656
+ "dependencies": {
657
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
658
+ },
659
+ "engines": {
660
+ "node": ">=0.10.0"
661
+ },
662
+ "funding": {
663
+ "type": "opencollective",
664
+ "url": "https://opencollective.com/express"
665
+ }
666
+ },
667
+ "node_modules/ignore-by-default": {
668
+ "version": "1.0.1",
669
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
670
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
671
+ "dev": true,
672
+ "license": "ISC"
673
+ },
674
+ "node_modules/inherits": {
675
+ "version": "2.0.4",
676
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
677
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
678
+ "license": "ISC"
679
+ },
680
+ "node_modules/ipaddr.js": {
681
+ "version": "1.9.1",
682
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
683
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
684
+ "license": "MIT",
685
+ "engines": {
686
+ "node": ">= 0.10"
687
+ }
688
+ },
689
+ "node_modules/is-binary-path": {
690
+ "version": "2.1.0",
691
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
692
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
693
+ "dev": true,
694
+ "license": "MIT",
695
+ "dependencies": {
696
+ "binary-extensions": "^2.0.0"
697
+ },
698
+ "engines": {
699
+ "node": ">=8"
700
+ }
701
+ },
702
+ "node_modules/is-extglob": {
703
+ "version": "2.1.1",
704
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
705
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
706
+ "dev": true,
707
+ "license": "MIT",
708
+ "engines": {
709
+ "node": ">=0.10.0"
710
+ }
711
+ },
712
+ "node_modules/is-glob": {
713
+ "version": "4.0.3",
714
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
715
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
716
+ "dev": true,
717
+ "license": "MIT",
718
+ "dependencies": {
719
+ "is-extglob": "^2.1.1"
720
+ },
721
+ "engines": {
722
+ "node": ">=0.10.0"
723
+ }
724
+ },
725
+ "node_modules/is-number": {
726
+ "version": "7.0.0",
727
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
728
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
729
+ "dev": true,
730
+ "license": "MIT",
731
+ "engines": {
732
+ "node": ">=0.12.0"
733
+ }
734
+ },
735
+ "node_modules/is-promise": {
736
+ "version": "4.0.0",
737
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
738
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
739
+ "license": "MIT"
740
+ },
741
+ "node_modules/is-property": {
742
+ "version": "1.0.2",
743
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
744
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
745
+ "license": "MIT"
746
+ },
747
+ "node_modules/jsonwebtoken": {
748
+ "version": "9.0.3",
749
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
750
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
751
+ "license": "MIT",
752
+ "dependencies": {
753
+ "jws": "^4.0.1",
754
+ "lodash.includes": "^4.3.0",
755
+ "lodash.isboolean": "^3.0.3",
756
+ "lodash.isinteger": "^4.0.4",
757
+ "lodash.isnumber": "^3.0.3",
758
+ "lodash.isplainobject": "^4.0.6",
759
+ "lodash.isstring": "^4.0.1",
760
+ "lodash.once": "^4.0.0",
761
+ "ms": "^2.1.1",
762
+ "semver": "^7.5.4"
763
+ },
764
+ "engines": {
765
+ "node": ">=12",
766
+ "npm": ">=6"
767
+ }
768
+ },
769
+ "node_modules/jwa": {
770
+ "version": "2.0.1",
771
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
772
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
773
+ "license": "MIT",
774
+ "dependencies": {
775
+ "buffer-equal-constant-time": "^1.0.1",
776
+ "ecdsa-sig-formatter": "1.0.11",
777
+ "safe-buffer": "^5.0.1"
778
+ }
779
+ },
780
+ "node_modules/jws": {
781
+ "version": "4.0.1",
782
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
783
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
784
+ "license": "MIT",
785
+ "dependencies": {
786
+ "jwa": "^2.0.1",
787
+ "safe-buffer": "^5.0.1"
788
+ }
789
+ },
790
+ "node_modules/lodash.includes": {
791
+ "version": "4.3.0",
792
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
793
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
794
+ "license": "MIT"
795
+ },
796
+ "node_modules/lodash.isboolean": {
797
+ "version": "3.0.3",
798
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
799
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
800
+ "license": "MIT"
801
+ },
802
+ "node_modules/lodash.isinteger": {
803
+ "version": "4.0.4",
804
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
805
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
806
+ "license": "MIT"
807
+ },
808
+ "node_modules/lodash.isnumber": {
809
+ "version": "3.0.3",
810
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
811
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
812
+ "license": "MIT"
813
+ },
814
+ "node_modules/lodash.isplainobject": {
815
+ "version": "4.0.6",
816
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
817
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
818
+ "license": "MIT"
819
+ },
820
+ "node_modules/lodash.isstring": {
821
+ "version": "4.0.1",
822
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
823
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
824
+ "license": "MIT"
825
+ },
826
+ "node_modules/lodash.once": {
827
+ "version": "4.1.1",
828
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
829
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
830
+ "license": "MIT"
831
+ },
832
+ "node_modules/long": {
833
+ "version": "5.3.2",
834
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
835
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
836
+ "license": "Apache-2.0"
837
+ },
838
+ "node_modules/lru.min": {
839
+ "version": "1.1.4",
840
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
841
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
842
+ "license": "MIT",
843
+ "engines": {
844
+ "bun": ">=1.0.0",
845
+ "deno": ">=1.30.0",
846
+ "node": ">=8.0.0"
847
+ },
848
+ "funding": {
849
+ "type": "github",
850
+ "url": "https://github.com/sponsors/wellwelwel"
851
+ }
852
+ },
853
+ "node_modules/math-intrinsics": {
854
+ "version": "1.1.0",
855
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
856
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
857
+ "license": "MIT",
858
+ "engines": {
859
+ "node": ">= 0.4"
860
+ }
861
+ },
862
+ "node_modules/media-typer": {
863
+ "version": "1.1.0",
864
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
865
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
866
+ "license": "MIT",
867
+ "engines": {
868
+ "node": ">= 0.8"
869
+ }
870
+ },
871
+ "node_modules/merge-descriptors": {
872
+ "version": "2.0.0",
873
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
874
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
875
+ "license": "MIT",
876
+ "engines": {
877
+ "node": ">=18"
878
+ },
879
+ "funding": {
880
+ "url": "https://github.com/sponsors/sindresorhus"
881
+ }
882
+ },
883
+ "node_modules/mime-db": {
884
+ "version": "1.54.0",
885
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
886
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
887
+ "license": "MIT",
888
+ "engines": {
889
+ "node": ">= 0.6"
890
+ }
891
+ },
892
+ "node_modules/mime-types": {
893
+ "version": "3.0.2",
894
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
895
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
896
+ "license": "MIT",
897
+ "dependencies": {
898
+ "mime-db": "^1.54.0"
899
+ },
900
+ "engines": {
901
+ "node": ">=18"
902
+ },
903
+ "funding": {
904
+ "type": "opencollective",
905
+ "url": "https://opencollective.com/express"
906
+ }
907
+ },
908
+ "node_modules/minimatch": {
909
+ "version": "10.2.5",
910
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
911
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
912
+ "dev": true,
913
+ "license": "BlueOak-1.0.0",
914
+ "dependencies": {
915
+ "brace-expansion": "^5.0.5"
916
+ },
917
+ "engines": {
918
+ "node": "18 || 20 || >=22"
919
+ },
920
+ "funding": {
921
+ "url": "https://github.com/sponsors/isaacs"
922
+ }
923
+ },
924
+ "node_modules/ms": {
925
+ "version": "2.1.3",
926
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
927
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
928
+ "license": "MIT"
929
+ },
930
+ "node_modules/mysql2": {
931
+ "version": "3.20.0",
932
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz",
933
+ "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==",
934
+ "license": "MIT",
935
+ "dependencies": {
936
+ "aws-ssl-profiles": "^1.1.2",
937
+ "denque": "^2.1.0",
938
+ "generate-function": "^2.3.1",
939
+ "iconv-lite": "^0.7.2",
940
+ "long": "^5.3.2",
941
+ "lru.min": "^1.1.4",
942
+ "named-placeholders": "^1.1.6",
943
+ "sql-escaper": "^1.3.3"
944
+ },
945
+ "engines": {
946
+ "node": ">= 8.0"
947
+ },
948
+ "peerDependencies": {
949
+ "@types/node": ">= 8"
950
+ }
951
+ },
952
+ "node_modules/named-placeholders": {
953
+ "version": "1.1.6",
954
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
955
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
956
+ "license": "MIT",
957
+ "dependencies": {
958
+ "lru.min": "^1.1.0"
959
+ },
960
+ "engines": {
961
+ "node": ">=8.0.0"
962
+ }
963
+ },
964
+ "node_modules/negotiator": {
965
+ "version": "1.0.0",
966
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
967
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
968
+ "license": "MIT",
969
+ "engines": {
970
+ "node": ">= 0.6"
971
+ }
972
+ },
973
+ "node_modules/nodemon": {
974
+ "version": "3.1.14",
975
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
976
+ "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==",
977
+ "dev": true,
978
+ "license": "MIT",
979
+ "dependencies": {
980
+ "chokidar": "^3.5.2",
981
+ "debug": "^4",
982
+ "ignore-by-default": "^1.0.1",
983
+ "minimatch": "^10.2.1",
984
+ "pstree.remy": "^1.1.8",
985
+ "semver": "^7.5.3",
986
+ "simple-update-notifier": "^2.0.0",
987
+ "supports-color": "^5.5.0",
988
+ "touch": "^3.1.0",
989
+ "undefsafe": "^2.0.5"
990
+ },
991
+ "bin": {
992
+ "nodemon": "bin/nodemon.js"
993
+ },
994
+ "engines": {
995
+ "node": ">=10"
996
+ },
997
+ "funding": {
998
+ "type": "opencollective",
999
+ "url": "https://opencollective.com/nodemon"
1000
+ }
1001
+ },
1002
+ "node_modules/normalize-path": {
1003
+ "version": "3.0.0",
1004
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1005
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1006
+ "dev": true,
1007
+ "license": "MIT",
1008
+ "engines": {
1009
+ "node": ">=0.10.0"
1010
+ }
1011
+ },
1012
+ "node_modules/object-assign": {
1013
+ "version": "4.1.1",
1014
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1015
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1016
+ "license": "MIT",
1017
+ "engines": {
1018
+ "node": ">=0.10.0"
1019
+ }
1020
+ },
1021
+ "node_modules/object-inspect": {
1022
+ "version": "1.13.4",
1023
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1024
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1025
+ "license": "MIT",
1026
+ "engines": {
1027
+ "node": ">= 0.4"
1028
+ },
1029
+ "funding": {
1030
+ "url": "https://github.com/sponsors/ljharb"
1031
+ }
1032
+ },
1033
+ "node_modules/on-finished": {
1034
+ "version": "2.4.1",
1035
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1036
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1037
+ "license": "MIT",
1038
+ "dependencies": {
1039
+ "ee-first": "1.1.1"
1040
+ },
1041
+ "engines": {
1042
+ "node": ">= 0.8"
1043
+ }
1044
+ },
1045
+ "node_modules/once": {
1046
+ "version": "1.4.0",
1047
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1048
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1049
+ "license": "ISC",
1050
+ "dependencies": {
1051
+ "wrappy": "1"
1052
+ }
1053
+ },
1054
+ "node_modules/parseurl": {
1055
+ "version": "1.3.3",
1056
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1057
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1058
+ "license": "MIT",
1059
+ "engines": {
1060
+ "node": ">= 0.8"
1061
+ }
1062
+ },
1063
+ "node_modules/path-to-regexp": {
1064
+ "version": "8.4.1",
1065
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz",
1066
+ "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==",
1067
+ "license": "MIT",
1068
+ "funding": {
1069
+ "type": "opencollective",
1070
+ "url": "https://opencollective.com/express"
1071
+ }
1072
+ },
1073
+ "node_modules/picomatch": {
1074
+ "version": "2.3.2",
1075
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1076
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1077
+ "dev": true,
1078
+ "license": "MIT",
1079
+ "engines": {
1080
+ "node": ">=8.6"
1081
+ },
1082
+ "funding": {
1083
+ "url": "https://github.com/sponsors/jonschlinkert"
1084
+ }
1085
+ },
1086
+ "node_modules/proxy-addr": {
1087
+ "version": "2.0.7",
1088
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1089
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1090
+ "license": "MIT",
1091
+ "dependencies": {
1092
+ "forwarded": "0.2.0",
1093
+ "ipaddr.js": "1.9.1"
1094
+ },
1095
+ "engines": {
1096
+ "node": ">= 0.10"
1097
+ }
1098
+ },
1099
+ "node_modules/pstree.remy": {
1100
+ "version": "1.1.8",
1101
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1102
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1103
+ "dev": true,
1104
+ "license": "MIT"
1105
+ },
1106
+ "node_modules/qs": {
1107
+ "version": "6.15.0",
1108
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
1109
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
1110
+ "license": "BSD-3-Clause",
1111
+ "dependencies": {
1112
+ "side-channel": "^1.1.0"
1113
+ },
1114
+ "engines": {
1115
+ "node": ">=0.6"
1116
+ },
1117
+ "funding": {
1118
+ "url": "https://github.com/sponsors/ljharb"
1119
+ }
1120
+ },
1121
+ "node_modules/range-parser": {
1122
+ "version": "1.2.1",
1123
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1124
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1125
+ "license": "MIT",
1126
+ "engines": {
1127
+ "node": ">= 0.6"
1128
+ }
1129
+ },
1130
+ "node_modules/raw-body": {
1131
+ "version": "3.0.2",
1132
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
1133
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
1134
+ "license": "MIT",
1135
+ "dependencies": {
1136
+ "bytes": "~3.1.2",
1137
+ "http-errors": "~2.0.1",
1138
+ "iconv-lite": "~0.7.0",
1139
+ "unpipe": "~1.0.0"
1140
+ },
1141
+ "engines": {
1142
+ "node": ">= 0.10"
1143
+ }
1144
+ },
1145
+ "node_modules/readdirp": {
1146
+ "version": "3.6.0",
1147
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1148
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1149
+ "dev": true,
1150
+ "license": "MIT",
1151
+ "dependencies": {
1152
+ "picomatch": "^2.2.1"
1153
+ },
1154
+ "engines": {
1155
+ "node": ">=8.10.0"
1156
+ }
1157
+ },
1158
+ "node_modules/router": {
1159
+ "version": "2.2.0",
1160
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
1161
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
1162
+ "license": "MIT",
1163
+ "dependencies": {
1164
+ "debug": "^4.4.0",
1165
+ "depd": "^2.0.0",
1166
+ "is-promise": "^4.0.0",
1167
+ "parseurl": "^1.3.3",
1168
+ "path-to-regexp": "^8.0.0"
1169
+ },
1170
+ "engines": {
1171
+ "node": ">= 18"
1172
+ }
1173
+ },
1174
+ "node_modules/safe-buffer": {
1175
+ "version": "5.2.1",
1176
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1177
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1178
+ "funding": [
1179
+ {
1180
+ "type": "github",
1181
+ "url": "https://github.com/sponsors/feross"
1182
+ },
1183
+ {
1184
+ "type": "patreon",
1185
+ "url": "https://www.patreon.com/feross"
1186
+ },
1187
+ {
1188
+ "type": "consulting",
1189
+ "url": "https://feross.org/support"
1190
+ }
1191
+ ],
1192
+ "license": "MIT"
1193
+ },
1194
+ "node_modules/safer-buffer": {
1195
+ "version": "2.1.2",
1196
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1197
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1198
+ "license": "MIT"
1199
+ },
1200
+ "node_modules/semver": {
1201
+ "version": "7.7.4",
1202
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
1203
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
1204
+ "license": "ISC",
1205
+ "bin": {
1206
+ "semver": "bin/semver.js"
1207
+ },
1208
+ "engines": {
1209
+ "node": ">=10"
1210
+ }
1211
+ },
1212
+ "node_modules/send": {
1213
+ "version": "1.2.1",
1214
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
1215
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
1216
+ "license": "MIT",
1217
+ "dependencies": {
1218
+ "debug": "^4.4.3",
1219
+ "encodeurl": "^2.0.0",
1220
+ "escape-html": "^1.0.3",
1221
+ "etag": "^1.8.1",
1222
+ "fresh": "^2.0.0",
1223
+ "http-errors": "^2.0.1",
1224
+ "mime-types": "^3.0.2",
1225
+ "ms": "^2.1.3",
1226
+ "on-finished": "^2.4.1",
1227
+ "range-parser": "^1.2.1",
1228
+ "statuses": "^2.0.2"
1229
+ },
1230
+ "engines": {
1231
+ "node": ">= 18"
1232
+ },
1233
+ "funding": {
1234
+ "type": "opencollective",
1235
+ "url": "https://opencollective.com/express"
1236
+ }
1237
+ },
1238
+ "node_modules/serve-static": {
1239
+ "version": "2.2.1",
1240
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
1241
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
1242
+ "license": "MIT",
1243
+ "dependencies": {
1244
+ "encodeurl": "^2.0.0",
1245
+ "escape-html": "^1.0.3",
1246
+ "parseurl": "^1.3.3",
1247
+ "send": "^1.2.0"
1248
+ },
1249
+ "engines": {
1250
+ "node": ">= 18"
1251
+ },
1252
+ "funding": {
1253
+ "type": "opencollective",
1254
+ "url": "https://opencollective.com/express"
1255
+ }
1256
+ },
1257
+ "node_modules/setprototypeof": {
1258
+ "version": "1.2.0",
1259
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1260
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1261
+ "license": "ISC"
1262
+ },
1263
+ "node_modules/side-channel": {
1264
+ "version": "1.1.0",
1265
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1266
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1267
+ "license": "MIT",
1268
+ "dependencies": {
1269
+ "es-errors": "^1.3.0",
1270
+ "object-inspect": "^1.13.3",
1271
+ "side-channel-list": "^1.0.0",
1272
+ "side-channel-map": "^1.0.1",
1273
+ "side-channel-weakmap": "^1.0.2"
1274
+ },
1275
+ "engines": {
1276
+ "node": ">= 0.4"
1277
+ },
1278
+ "funding": {
1279
+ "url": "https://github.com/sponsors/ljharb"
1280
+ }
1281
+ },
1282
+ "node_modules/side-channel-list": {
1283
+ "version": "1.0.0",
1284
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1285
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1286
+ "license": "MIT",
1287
+ "dependencies": {
1288
+ "es-errors": "^1.3.0",
1289
+ "object-inspect": "^1.13.3"
1290
+ },
1291
+ "engines": {
1292
+ "node": ">= 0.4"
1293
+ },
1294
+ "funding": {
1295
+ "url": "https://github.com/sponsors/ljharb"
1296
+ }
1297
+ },
1298
+ "node_modules/side-channel-map": {
1299
+ "version": "1.0.1",
1300
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1301
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1302
+ "license": "MIT",
1303
+ "dependencies": {
1304
+ "call-bound": "^1.0.2",
1305
+ "es-errors": "^1.3.0",
1306
+ "get-intrinsic": "^1.2.5",
1307
+ "object-inspect": "^1.13.3"
1308
+ },
1309
+ "engines": {
1310
+ "node": ">= 0.4"
1311
+ },
1312
+ "funding": {
1313
+ "url": "https://github.com/sponsors/ljharb"
1314
+ }
1315
+ },
1316
+ "node_modules/side-channel-weakmap": {
1317
+ "version": "1.0.2",
1318
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1319
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1320
+ "license": "MIT",
1321
+ "dependencies": {
1322
+ "call-bound": "^1.0.2",
1323
+ "es-errors": "^1.3.0",
1324
+ "get-intrinsic": "^1.2.5",
1325
+ "object-inspect": "^1.13.3",
1326
+ "side-channel-map": "^1.0.1"
1327
+ },
1328
+ "engines": {
1329
+ "node": ">= 0.4"
1330
+ },
1331
+ "funding": {
1332
+ "url": "https://github.com/sponsors/ljharb"
1333
+ }
1334
+ },
1335
+ "node_modules/simple-update-notifier": {
1336
+ "version": "2.0.0",
1337
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1338
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1339
+ "dev": true,
1340
+ "license": "MIT",
1341
+ "dependencies": {
1342
+ "semver": "^7.5.3"
1343
+ },
1344
+ "engines": {
1345
+ "node": ">=10"
1346
+ }
1347
+ },
1348
+ "node_modules/sql-escaper": {
1349
+ "version": "1.3.3",
1350
+ "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
1351
+ "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
1352
+ "license": "MIT",
1353
+ "engines": {
1354
+ "bun": ">=1.0.0",
1355
+ "deno": ">=2.0.0",
1356
+ "node": ">=12.0.0"
1357
+ },
1358
+ "funding": {
1359
+ "type": "github",
1360
+ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
1361
+ }
1362
+ },
1363
+ "node_modules/statuses": {
1364
+ "version": "2.0.2",
1365
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1366
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1367
+ "license": "MIT",
1368
+ "engines": {
1369
+ "node": ">= 0.8"
1370
+ }
1371
+ },
1372
+ "node_modules/supports-color": {
1373
+ "version": "5.5.0",
1374
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1375
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1376
+ "dev": true,
1377
+ "license": "MIT",
1378
+ "dependencies": {
1379
+ "has-flag": "^3.0.0"
1380
+ },
1381
+ "engines": {
1382
+ "node": ">=4"
1383
+ }
1384
+ },
1385
+ "node_modules/to-regex-range": {
1386
+ "version": "5.0.1",
1387
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1388
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1389
+ "dev": true,
1390
+ "license": "MIT",
1391
+ "dependencies": {
1392
+ "is-number": "^7.0.0"
1393
+ },
1394
+ "engines": {
1395
+ "node": ">=8.0"
1396
+ }
1397
+ },
1398
+ "node_modules/toidentifier": {
1399
+ "version": "1.0.1",
1400
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1401
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1402
+ "license": "MIT",
1403
+ "engines": {
1404
+ "node": ">=0.6"
1405
+ }
1406
+ },
1407
+ "node_modules/touch": {
1408
+ "version": "3.1.1",
1409
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1410
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1411
+ "dev": true,
1412
+ "license": "ISC",
1413
+ "bin": {
1414
+ "nodetouch": "bin/nodetouch.js"
1415
+ }
1416
+ },
1417
+ "node_modules/type-is": {
1418
+ "version": "2.0.1",
1419
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
1420
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
1421
+ "license": "MIT",
1422
+ "dependencies": {
1423
+ "content-type": "^1.0.5",
1424
+ "media-typer": "^1.1.0",
1425
+ "mime-types": "^3.0.0"
1426
+ },
1427
+ "engines": {
1428
+ "node": ">= 0.6"
1429
+ }
1430
+ },
1431
+ "node_modules/undefsafe": {
1432
+ "version": "2.0.5",
1433
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1434
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1435
+ "dev": true,
1436
+ "license": "MIT"
1437
+ },
1438
+ "node_modules/undici-types": {
1439
+ "version": "7.18.2",
1440
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
1441
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
1442
+ "license": "MIT",
1443
+ "peer": true
1444
+ },
1445
+ "node_modules/unpipe": {
1446
+ "version": "1.0.0",
1447
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1448
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1449
+ "license": "MIT",
1450
+ "engines": {
1451
+ "node": ">= 0.8"
1452
+ }
1453
+ },
1454
+ "node_modules/vary": {
1455
+ "version": "1.1.2",
1456
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1457
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1458
+ "license": "MIT",
1459
+ "engines": {
1460
+ "node": ">= 0.8"
1461
+ }
1462
+ },
1463
+ "node_modules/wrappy": {
1464
+ "version": "1.0.2",
1465
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1466
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1467
+ "license": "ISC"
1468
+ }
1469
+ }
1470
+ }
package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "care_people_backend",
3
+ "version": "1.0.0",
4
+ "description": "Care People backend powered by Express and MySQL.",
5
+ "main": "src/server.js",
6
+ "scripts": {
7
+ "start": "node src/server.js",
8
+ "dev": "nodemon src/server.js",
9
+ "db:init": "node database/apply_schema.js",
10
+ "db:seed": "node database/seed_doctors.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/shishu25/carePeople.git"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "type": "commonjs",
20
+ "bugs": {
21
+ "url": "https://github.com/shishu25/carePeople/issues"
22
+ },
23
+ "homepage": "https://github.com/shishu25/carePeople#readme",
24
+ "dependencies": {
25
+ "bcryptjs": "^3.0.3",
26
+ "cors": "^2.8.6",
27
+ "dotenv": "^17.3.1",
28
+ "express": "^5.2.1",
29
+ "jsonwebtoken": "^9.0.3",
30
+ "mysql2": "^3.20.0"
31
+ },
32
+ "devDependencies": {
33
+ "nodemon": "^3.1.14"
34
+ }
35
+ }
postman/care_people.postman_collection.json ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "info": {
3
+ "name": "Care People Backend",
4
+ "_postman_id": "5eb7f81c-8c29-4df3-b696-7a3f3c0b1f2a",
5
+ "description": "Postman collection for the Care People Express + MySQL backend.",
6
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
7
+ },
8
+ "variable": [
9
+ { "key": "baseUrl", "value": "http://localhost:3000" },
10
+ { "key": "patientPhone", "value": "01712345678" },
11
+ { "key": "patientEmail", "value": "patient@example.com" },
12
+ { "key": "patientOtp", "value": "" },
13
+ { "key": "patientAccessToken", "value": "" },
14
+ { "key": "patientRefreshToken", "value": "" },
15
+ { "key": "doctorId", "value": "DOC001" },
16
+ { "key": "doctorPassword", "value": "Kamal@1234" },
17
+ { "key": "doctorAccessToken", "value": "" },
18
+ { "key": "doctorRefreshToken", "value": "" },
19
+ { "key": "appointmentDate", "value": "2026-04-06" },
20
+ { "key": "timeSlot", "value": "09:00 AM" },
21
+ { "key": "aiSymptoms", "value": "Fever, sore throat, and body pain since last night" },
22
+ { "key": "aiSeverity", "value": "Medium" },
23
+ { "key": "doctorAiGoal", "value": "Consult Note" },
24
+ { "key": "doctorAiInput", "value": "52 year old with chest discomfort for 2 days, no syncope, mild dizziness, blood pressure not yet recorded." },
25
+ { "key": "appointmentId", "value": "" },
26
+ { "key": "prescriptionId", "value": "" }
27
+ ],
28
+ "item": [
29
+ {
30
+ "name": "Health",
31
+ "request": {
32
+ "method": "GET",
33
+ "url": "{{baseUrl}}/health"
34
+ }
35
+ },
36
+ {
37
+ "name": "Send Patient OTP",
38
+ "event": [
39
+ {
40
+ "listen": "test",
41
+ "script": {
42
+ "exec": [
43
+ "const data = pm.response.json().data || {};",
44
+ "if (data.otpCode) { pm.collectionVariables.set('patientOtp', data.otpCode); }"
45
+ ],
46
+ "type": "text/javascript"
47
+ }
48
+ }
49
+ ],
50
+ "request": {
51
+ "method": "POST",
52
+ "header": [
53
+ { "key": "Content-Type", "value": "application/json" }
54
+ ],
55
+ "body": {
56
+ "mode": "raw",
57
+ "raw": "{\n \"phoneNumber\": \"{{patientPhone}}\",\n \"email\": \"{{patientEmail}}\"\n}"
58
+ },
59
+ "url": "{{baseUrl}}/api/v1/auth/patient/send-otp"
60
+ }
61
+ },
62
+ {
63
+ "name": "Verify Patient OTP",
64
+ "event": [
65
+ {
66
+ "listen": "test",
67
+ "script": {
68
+ "exec": [
69
+ "const data = pm.response.json().data || {};",
70
+ "pm.collectionVariables.set('patientAccessToken', data.accessToken || '');",
71
+ "pm.collectionVariables.set('patientRefreshToken', data.refreshToken || '');"
72
+ ],
73
+ "type": "text/javascript"
74
+ }
75
+ }
76
+ ],
77
+ "request": {
78
+ "method": "POST",
79
+ "header": [
80
+ { "key": "Content-Type", "value": "application/json" }
81
+ ],
82
+ "body": {
83
+ "mode": "raw",
84
+ "raw": "{\n \"phoneNumber\": \"{{patientPhone}}\",\n \"email\": \"{{patientEmail}}\",\n \"otpCode\": \"{{patientOtp}}\"\n}"
85
+ },
86
+ "url": "{{baseUrl}}/api/v1/auth/patient/verify-otp"
87
+ }
88
+ },
89
+ {
90
+ "name": "Save Patient Profile",
91
+ "request": {
92
+ "method": "PUT",
93
+ "header": [
94
+ { "key": "Content-Type", "value": "application/json" },
95
+ { "key": "Authorization", "value": "Bearer {{patientAccessToken}}" }
96
+ ],
97
+ "body": {
98
+ "mode": "raw",
99
+ "raw": "{\n \"email\": \"{{patientEmail}}\",\n \"name\": \"Md Rahim\",\n \"dateOfBirth\": \"1998-02-14\",\n \"address\": \"Dhaka, Bangladesh\",\n \"gender\": \"Male\"\n}"
100
+ },
101
+ "url": "{{baseUrl}}/api/v1/patients/me"
102
+ }
103
+ },
104
+ {
105
+ "name": "List Doctors",
106
+ "request": {
107
+ "method": "GET",
108
+ "url": "{{baseUrl}}/api/v1/doctors"
109
+ }
110
+ },
111
+ {
112
+ "name": "Get Doctor Detail",
113
+ "request": {
114
+ "method": "GET",
115
+ "url": "{{baseUrl}}/api/v1/doctors/{{doctorId}}"
116
+ }
117
+ },
118
+ {
119
+ "name": "Doctor Availability",
120
+ "request": {
121
+ "method": "GET",
122
+ "url": "{{baseUrl}}/api/v1/appointments/doctor/{{doctorId}}/availability?date={{appointmentDate}}"
123
+ }
124
+ },
125
+ {
126
+ "name": "Create Appointment",
127
+ "event": [
128
+ {
129
+ "listen": "test",
130
+ "script": {
131
+ "exec": [
132
+ "const data = pm.response.json().data || {};",
133
+ "if (data.id) { pm.collectionVariables.set('appointmentId', String(data.id)); }"
134
+ ],
135
+ "type": "text/javascript"
136
+ }
137
+ }
138
+ ],
139
+ "request": {
140
+ "method": "POST",
141
+ "header": [
142
+ { "key": "Content-Type", "value": "application/json" },
143
+ { "key": "Authorization", "value": "Bearer {{patientAccessToken}}" }
144
+ ],
145
+ "body": {
146
+ "mode": "raw",
147
+ "raw": "{\n \"doctorId\": \"{{doctorId}}\",\n \"date\": \"{{appointmentDate}}\",\n \"timeSlot\": \"{{timeSlot}}\",\n \"notes\": \"Chest pain for 2 days\"\n}"
148
+ },
149
+ "url": "{{baseUrl}}/api/v1/appointments"
150
+ }
151
+ },
152
+ {
153
+ "name": "Patient AI Suggestion",
154
+ "request": {
155
+ "method": "POST",
156
+ "header": [
157
+ { "key": "Content-Type", "value": "application/json" },
158
+ { "key": "Authorization", "value": "Bearer {{patientAccessToken}}" }
159
+ ],
160
+ "body": {
161
+ "mode": "raw",
162
+ "raw": "{\n \"symptoms\": \"{{aiSymptoms}}\",\n \"severity\": \"{{aiSeverity}}\"\n}"
163
+ },
164
+ "url": "{{baseUrl}}/api/v1/ai/patient-suggestion"
165
+ }
166
+ },
167
+ {
168
+ "name": "List Patient Appointments",
169
+ "request": {
170
+ "method": "GET",
171
+ "header": [
172
+ { "key": "Authorization", "value": "Bearer {{patientAccessToken}}" }
173
+ ],
174
+ "url": "{{baseUrl}}/api/v1/appointments/patient/me"
175
+ }
176
+ },
177
+ {
178
+ "name": "Doctor Login",
179
+ "event": [
180
+ {
181
+ "listen": "test",
182
+ "script": {
183
+ "exec": [
184
+ "const data = pm.response.json().data || {};",
185
+ "pm.collectionVariables.set('doctorAccessToken', data.accessToken || '');",
186
+ "pm.collectionVariables.set('doctorRefreshToken', data.refreshToken || '');"
187
+ ],
188
+ "type": "text/javascript"
189
+ }
190
+ }
191
+ ],
192
+ "request": {
193
+ "method": "POST",
194
+ "header": [
195
+ { "key": "Content-Type", "value": "application/json" }
196
+ ],
197
+ "body": {
198
+ "mode": "raw",
199
+ "raw": "{\n \"doctorId\": \"{{doctorId}}\",\n \"password\": \"{{doctorPassword}}\"\n}"
200
+ },
201
+ "url": "{{baseUrl}}/api/v1/auth/doctor/login"
202
+ }
203
+ },
204
+ {
205
+ "name": "List Doctor Appointments",
206
+ "request": {
207
+ "method": "GET",
208
+ "header": [
209
+ { "key": "Authorization", "value": "Bearer {{doctorAccessToken}}" }
210
+ ],
211
+ "url": "{{baseUrl}}/api/v1/appointments/doctor/me"
212
+ }
213
+ },
214
+ {
215
+ "name": "Doctor AI Assistant",
216
+ "request": {
217
+ "method": "POST",
218
+ "header": [
219
+ { "key": "Content-Type", "value": "application/json" },
220
+ { "key": "Authorization", "value": "Bearer {{doctorAccessToken}}" }
221
+ ],
222
+ "body": {
223
+ "mode": "raw",
224
+ "raw": "{\n \"goal\": \"{{doctorAiGoal}}\",\n \"input\": \"{{doctorAiInput}}\"\n}"
225
+ },
226
+ "url": "{{baseUrl}}/api/v1/ai/doctor-assistant"
227
+ }
228
+ },
229
+ {
230
+ "name": "Create Prescription",
231
+ "event": [
232
+ {
233
+ "listen": "test",
234
+ "script": {
235
+ "exec": [
236
+ "const data = pm.response.json().data || {};",
237
+ "if (data.id) { pm.collectionVariables.set('prescriptionId', data.id); }"
238
+ ],
239
+ "type": "text/javascript"
240
+ }
241
+ }
242
+ ],
243
+ "request": {
244
+ "method": "POST",
245
+ "header": [
246
+ { "key": "Content-Type", "value": "application/json" },
247
+ { "key": "Authorization", "value": "Bearer {{doctorAccessToken}}" }
248
+ ],
249
+ "body": {
250
+ "mode": "raw",
251
+ "raw": "{\n \"appointmentId\": {{appointmentId}},\n \"diagnosis\": [\n \"Hypertension\",\n \"Chest discomfort\"\n ],\n \"medicines\": [\n {\n \"name\": \"Amlodipine\",\n \"dosage\": \"5 mg\",\n \"frequency\": \"Once daily\",\n \"duration\": \"30 days\",\n \"notes\": \"After breakfast\"\n }\n ],\n \"additionalNotes\": \"Reduce salt intake\",\n \"pdfPath\": null\n}"
252
+ },
253
+ "url": "{{baseUrl}}/api/v1/prescriptions"
254
+ }
255
+ },
256
+ {
257
+ "name": "List Patient Prescriptions",
258
+ "request": {
259
+ "method": "GET",
260
+ "header": [
261
+ { "key": "Authorization", "value": "Bearer {{patientAccessToken}}" }
262
+ ],
263
+ "url": "{{baseUrl}}/api/v1/prescriptions/patient/me"
264
+ }
265
+ },
266
+ {
267
+ "name": "List Doctor Prescriptions",
268
+ "request": {
269
+ "method": "GET",
270
+ "header": [
271
+ { "key": "Authorization", "value": "Bearer {{doctorAccessToken}}" }
272
+ ],
273
+ "url": "{{baseUrl}}/api/v1/prescriptions/doctor/me"
274
+ }
275
+ },
276
+ {
277
+ "name": "Refresh Patient Token",
278
+ "request": {
279
+ "method": "POST",
280
+ "header": [
281
+ { "key": "Content-Type", "value": "application/json" }
282
+ ],
283
+ "body": {
284
+ "mode": "raw",
285
+ "raw": "{\n \"refreshToken\": \"{{patientRefreshToken}}\"\n}"
286
+ },
287
+ "url": "{{baseUrl}}/api/v1/auth/refresh"
288
+ }
289
+ }
290
+ ]
291
+ }
src/app.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+
4
+ const env = require('./config/env');
5
+ const apiRoutes = require('./routes');
6
+ const healthRoutes = require('./routes/health.routes');
7
+ const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');
8
+
9
+ const app = express();
10
+
11
+ const corsOptions = {
12
+ origin(origin, callback) {
13
+ if (!origin || env.frontendOrigins.length === 0) {
14
+ return callback(null, true);
15
+ }
16
+
17
+ if (env.frontendOrigins.includes(origin)) {
18
+ return callback(null, true);
19
+ }
20
+
21
+ return callback(new Error(`Origin not allowed: ${origin}`));
22
+ },
23
+ };
24
+
25
+ app.use(cors(corsOptions));
26
+ app.use(express.json());
27
+
28
+ app.get('/', (_req, res) => {
29
+ res.json({
30
+ success: true,
31
+ message: 'Care People backend is running.',
32
+ data: {
33
+ apiPrefix: '/api/v1',
34
+ health: '/health',
35
+ },
36
+ errorCode: null,
37
+ });
38
+ });
39
+
40
+ app.use('/health', healthRoutes);
41
+ app.use('/api/v1', apiRoutes);
42
+ app.use(notFoundHandler);
43
+ app.use(errorHandler);
44
+
45
+ module.exports = app;
src/config/db.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mysql = require('mysql2/promise');
2
+ const env = require('./env');
3
+
4
+ const pool = mysql.createPool({
5
+ host: env.dbHost,
6
+ port: env.dbPort,
7
+ user: env.dbUser,
8
+ password: env.dbPassword,
9
+ database: env.dbName,
10
+ waitForConnections: true,
11
+ connectionLimit: 10,
12
+ queueLimit: 0,
13
+ namedPlaceholders: true,
14
+ dateStrings: true,
15
+ });
16
+
17
+ async function query(sql, params = {}) {
18
+ const [rows] = await pool.execute(sql, params);
19
+ return rows;
20
+ }
21
+
22
+ async function withTransaction(callback) {
23
+ const connection = await pool.getConnection();
24
+
25
+ try {
26
+ await connection.beginTransaction();
27
+ const result = await callback(connection);
28
+ await connection.commit();
29
+ return result;
30
+ } catch (error) {
31
+ await connection.rollback();
32
+ throw error;
33
+ } finally {
34
+ connection.release();
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ pool,
40
+ query,
41
+ withTransaction,
42
+ };
src/config/env.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const path = require('path');
2
+ const dotenv = require('dotenv');
3
+
4
+ dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') });
5
+
6
+ function toNumber(value, fallback) {
7
+ const parsed = Number(value);
8
+ return Number.isFinite(parsed) ? parsed : fallback;
9
+ }
10
+
11
+ function toBoolean(value, fallback) {
12
+ if (value === undefined) {
13
+ return fallback;
14
+ }
15
+
16
+ return String(value).toLowerCase() === 'true';
17
+ }
18
+
19
+ const env = {
20
+ port: toNumber(process.env.PORT, 3000),
21
+ appEnv: process.env.APP_ENV || 'development',
22
+ frontendOrigins: (process.env.FRONTEND_ORIGINS || '')
23
+ .split(',')
24
+ .map((origin) => origin.trim())
25
+ .filter(Boolean),
26
+ dbHost: process.env.DB_HOST || '127.0.0.1',
27
+ dbPort: toNumber(process.env.DB_PORT, 3306),
28
+ dbName: process.env.DB_NAME || 'care_people',
29
+ dbUser: process.env.DB_USER || 'root',
30
+ dbPassword: process.env.DB_PASSWORD || '',
31
+ jwtSecret: process.env.JWT_SECRET || 'change-this-access-secret',
32
+ jwtRefreshSecret:
33
+ process.env.JWT_REFRESH_SECRET || 'change-this-refresh-secret',
34
+ accessTokenMinutes: toNumber(process.env.ACCESS_TOKEN_MINUTES, 60),
35
+ refreshTokenDays: toNumber(process.env.REFRESH_TOKEN_DAYS, 2),
36
+ otpDevMode: toBoolean(process.env.OTP_DEV_MODE, true),
37
+ otpTtlMinutes: toNumber(process.env.OTP_TTL_MINUTES, 5),
38
+ otpLength: toNumber(process.env.OTP_LENGTH, 6),
39
+ mailchimpTransactionalEnabled: toBoolean(
40
+ process.env.MAILCHIMP_TRANSACTIONAL_ENABLED,
41
+ false,
42
+ ),
43
+ mailchimpTransactionalApiKey:
44
+ process.env.MAILCHIMP_TRANSACTIONAL_API_KEY || '',
45
+ mailchimpFromEmail: process.env.MAILCHIMP_FROM_EMAIL || '',
46
+ mailchimpFromName: process.env.MAILCHIMP_FROM_NAME || 'Care People',
47
+ mailchimpReplyTo: process.env.MAILCHIMP_REPLY_TO || '',
48
+ groqApiKey: process.env.GROQ_API_KEY || '',
49
+ groqModel: process.env.GROQ_MODEL || 'llama-3.1-8b-instant',
50
+ };
51
+
52
+ module.exports = env;
src/middleware/auth.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const HttpError = require('../utils/httpError');
2
+ const { verifyAccessToken } = require('../utils/tokens');
3
+
4
+ function requireAuth(expectedRole) {
5
+ return (req, _res, next) => {
6
+ const authHeader = req.headers.authorization || '';
7
+
8
+ if (!authHeader.startsWith('Bearer ')) {
9
+ return next(
10
+ new HttpError(401, 'Missing Bearer token.', 'AUTH_TOKEN_MISSING'),
11
+ );
12
+ }
13
+
14
+ try {
15
+ const token = authHeader.slice(7);
16
+ const payload = verifyAccessToken(token);
17
+
18
+ if (expectedRole && payload.role !== expectedRole) {
19
+ return next(
20
+ new HttpError(403, 'You are not allowed to use this route.', 'FORBIDDEN'),
21
+ );
22
+ }
23
+
24
+ req.auth = {
25
+ userType: payload.role,
26
+ userIdentifier: payload.sub,
27
+ sessionId: payload.sessionId,
28
+ };
29
+
30
+ return next();
31
+ } catch (_error) {
32
+ return next(
33
+ new HttpError(401, 'Invalid or expired access token.', 'AUTH_INVALID'),
34
+ );
35
+ }
36
+ };
37
+ }
38
+
39
+ module.exports = {
40
+ requireAuth,
41
+ };
src/middleware/errorHandler.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function notFoundHandler(req, _res, next) {
2
+ const error = new Error(`Route not found: ${req.method} ${req.originalUrl}`);
3
+ error.statusCode = 404;
4
+ error.errorCode = 'ROUTE_NOT_FOUND';
5
+ next(error);
6
+ }
7
+
8
+ function errorHandler(error, _req, res, _next) {
9
+ const statusCode = error.statusCode || 500;
10
+ const errorCode = error.errorCode || 'INTERNAL_ERROR';
11
+
12
+ if (statusCode >= 500) {
13
+ console.error(error);
14
+ }
15
+
16
+ res.status(statusCode).json({
17
+ success: false,
18
+ message: error.message || 'Unexpected server error.',
19
+ data: null,
20
+ errorCode,
21
+ });
22
+ }
23
+
24
+ module.exports = {
25
+ notFoundHandler,
26
+ errorHandler,
27
+ };
src/routes/ai.routes.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const {
5
+ generateDoctorAssistantDraft,
6
+ generatePatientSuggestion,
7
+ } = require('../services/ai.service');
8
+ const { sendSuccess } = require('../utils/apiResponse');
9
+
10
+ const router = express.Router();
11
+
12
+ router.post('/patient-suggestion', requireAuth('patient'), async (req, res) => {
13
+ const suggestion = await generatePatientSuggestion({
14
+ symptoms: req.body.symptoms,
15
+ severity: req.body.severity,
16
+ });
17
+
18
+ return sendSuccess(res, {
19
+ message: 'AI patient suggestion generated successfully.',
20
+ data: suggestion,
21
+ });
22
+ });
23
+
24
+ router.post('/doctor-assistant', requireAuth('doctor'), async (req, res) => {
25
+ const draft = await generateDoctorAssistantDraft({
26
+ goal: req.body.goal,
27
+ input: req.body.input,
28
+ });
29
+
30
+ return sendSuccess(res, {
31
+ message: 'AI doctor assistant draft generated successfully.',
32
+ data: draft,
33
+ });
34
+ });
35
+
36
+ module.exports = router;
src/routes/appointments.routes.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const {
5
+ createAppointment,
6
+ getDoctorAvailability,
7
+ listDoctorAppointments,
8
+ listPatientAppointments,
9
+ updateAppointmentStatus,
10
+ } = require('../services/appointments.service');
11
+ const { sendSuccess } = require('../utils/apiResponse');
12
+
13
+ const router = express.Router();
14
+
15
+ router.get('/doctor/:doctorId/availability', async (req, res) => {
16
+ const availability = await getDoctorAvailability(
17
+ req.params.doctorId,
18
+ req.query.date,
19
+ );
20
+
21
+ return sendSuccess(res, {
22
+ message: 'Doctor availability fetched successfully.',
23
+ data: availability,
24
+ });
25
+ });
26
+
27
+ router.get('/patient/me', requireAuth('patient'), async (req, res) => {
28
+ const appointments = await listPatientAppointments(req.auth.userIdentifier);
29
+
30
+ return sendSuccess(res, {
31
+ message: 'Patient appointments fetched successfully.',
32
+ data: {
33
+ appointments,
34
+ total: appointments.length,
35
+ },
36
+ });
37
+ });
38
+
39
+ router.get('/doctor/me', requireAuth('doctor'), async (req, res) => {
40
+ const appointments = await listDoctorAppointments(req.auth.userIdentifier);
41
+
42
+ return sendSuccess(res, {
43
+ message: 'Doctor appointments fetched successfully.',
44
+ data: {
45
+ appointments,
46
+ total: appointments.length,
47
+ },
48
+ });
49
+ });
50
+
51
+ router.post('/', requireAuth('patient'), async (req, res) => {
52
+ const appointment = await createAppointment({
53
+ doctorId: String(req.body.doctorId || '').trim().toUpperCase(),
54
+ patientPhone: req.auth.userIdentifier,
55
+ appointmentDate: req.body.date,
56
+ timeSlot: req.body.timeSlot,
57
+ notes: req.body.notes,
58
+ });
59
+
60
+ return sendSuccess(res, {
61
+ status: 201,
62
+ message: 'Appointment created successfully.',
63
+ data: appointment,
64
+ });
65
+ });
66
+
67
+ router.patch('/:appointmentId/status', requireAuth('doctor'), async (req, res) => {
68
+ const appointment = await updateAppointmentStatus({
69
+ appointmentId: Number(req.params.appointmentId),
70
+ doctorId: req.auth.userIdentifier,
71
+ status: req.body.status,
72
+ });
73
+
74
+ return sendSuccess(res, {
75
+ message: 'Appointment status updated successfully.',
76
+ data: appointment,
77
+ });
78
+ });
79
+
80
+ module.exports = router;
src/routes/auth.routes.js ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const bcrypt = require('bcryptjs');
2
+ const express = require('express');
3
+
4
+ const env = require('../config/env');
5
+ const { issueSessionTokens, refreshSession, revokeSession } = require('../services/auth.service');
6
+ const { getDoctorForLogin } = require('../services/doctors.service');
7
+ const { getPatientByPhone, patientEmailMatches } = require('../services/patients.service');
8
+ const { sendOtpEmail } = require('../services/mailchimp.service');
9
+ const { createOtp, verifyOtp } = require('../utils/otpStore');
10
+ const { sendSuccess } = require('../utils/apiResponse');
11
+ const { maskEmail, validateEmail } = require('../utils/email');
12
+ const HttpError = require('../utils/httpError');
13
+ const { mapDoctor } = require('../utils/serializers');
14
+ const { validatePhone } = require('../utils/phone');
15
+
16
+ const router = express.Router();
17
+
18
+ function buildPatientOtpKey(phoneNumber, email) {
19
+ return `${phoneNumber}:${email}`;
20
+ }
21
+
22
+ router.post('/patient/send-otp', async (req, res) => {
23
+ const phoneNumber = validatePhone(req.body.phoneNumber);
24
+ const email = validateEmail(req.body.email);
25
+ const patient = await getPatientByPhone(phoneNumber);
26
+
27
+ if (!patientEmailMatches(patient, email)) {
28
+ throw new HttpError(
29
+ 409,
30
+ 'This email address does not match the saved patient profile.',
31
+ 'EMAIL_MISMATCH',
32
+ );
33
+ }
34
+
35
+ const otpPayload = createOtp(buildPatientOtpKey(phoneNumber, email));
36
+ const emailDelivery = await sendOtpEmail({
37
+ email,
38
+ otpCode: otpPayload.otpCode,
39
+ expiresInMinutes: env.otpTtlMinutes,
40
+ });
41
+
42
+ return sendSuccess(res, {
43
+ message: 'OTP sent to email successfully.',
44
+ data: {
45
+ phoneNumber,
46
+ email,
47
+ maskedEmail: maskEmail(email),
48
+ deliveryChannel: 'email',
49
+ provider: emailDelivery.provider,
50
+ expiresInSeconds: env.otpTtlMinutes * 60,
51
+ otpCode: env.otpDevMode ? otpPayload.otpCode : null,
52
+ },
53
+ });
54
+ });
55
+
56
+ router.post('/patient/verify-otp', async (req, res) => {
57
+ const phoneNumber = validatePhone(req.body.phoneNumber);
58
+ const email = validateEmail(req.body.email);
59
+ const otpCode = String(req.body.otpCode || '').trim();
60
+
61
+ if (!otpCode) {
62
+ throw new HttpError(400, 'otpCode is required.', 'OTP_REQUIRED');
63
+ }
64
+
65
+ if (!verifyOtp(buildPatientOtpKey(phoneNumber, email), otpCode)) {
66
+ throw new HttpError(401, 'Invalid or expired OTP.', 'OTP_INVALID');
67
+ }
68
+
69
+ const patient = await getPatientByPhone(phoneNumber);
70
+
71
+ if (!patientEmailMatches(patient, email)) {
72
+ throw new HttpError(
73
+ 409,
74
+ 'This email address does not match the saved patient profile.',
75
+ 'EMAIL_MISMATCH',
76
+ );
77
+ }
78
+
79
+ const tokens = await issueSessionTokens({
80
+ userType: 'patient',
81
+ userIdentifier: phoneNumber,
82
+ metadata: {
83
+ phoneNumber,
84
+ email,
85
+ },
86
+ });
87
+
88
+ return sendSuccess(res, {
89
+ message: 'Patient logged in successfully.',
90
+ data: {
91
+ ...tokens,
92
+ tokenType: 'Bearer',
93
+ userExists: Boolean(patient),
94
+ patient,
95
+ },
96
+ });
97
+ });
98
+
99
+ router.post('/doctor/login', async (req, res) => {
100
+ const doctorId = String(req.body.doctorId || '').trim().toUpperCase();
101
+ const password = String(req.body.password || '');
102
+
103
+ if (!doctorId || !password) {
104
+ throw new HttpError(
105
+ 400,
106
+ 'doctorId and password are required.',
107
+ 'LOGIN_FIELDS_REQUIRED',
108
+ );
109
+ }
110
+
111
+ const doctorRow = await getDoctorForLogin(doctorId);
112
+
113
+ if (!doctorRow || !doctorRow.is_active) {
114
+ throw new HttpError(
115
+ 401,
116
+ 'Invalid Doctor ID or password.',
117
+ 'DOCTOR_LOGIN_FAILED',
118
+ );
119
+ }
120
+
121
+ const passwordMatches = await bcrypt.compare(password, doctorRow.password_hash);
122
+
123
+ if (!passwordMatches) {
124
+ throw new HttpError(
125
+ 401,
126
+ 'Invalid Doctor ID or password.',
127
+ 'DOCTOR_LOGIN_FAILED',
128
+ );
129
+ }
130
+
131
+ const tokens = await issueSessionTokens({
132
+ userType: 'doctor',
133
+ userIdentifier: doctorRow.id,
134
+ metadata: {
135
+ doctorId: doctorRow.id,
136
+ },
137
+ });
138
+
139
+ return sendSuccess(res, {
140
+ message: 'Doctor logged in successfully.',
141
+ data: {
142
+ ...tokens,
143
+ tokenType: 'Bearer',
144
+ doctor: mapDoctor(doctorRow),
145
+ },
146
+ });
147
+ });
148
+
149
+ router.post('/refresh', async (req, res) => {
150
+ const refreshToken = String(req.body.refreshToken || '').trim();
151
+
152
+ if (!refreshToken) {
153
+ throw new HttpError(
154
+ 400,
155
+ 'refreshToken is required.',
156
+ 'REFRESH_TOKEN_REQUIRED',
157
+ );
158
+ }
159
+
160
+ const tokens = await refreshSession(refreshToken);
161
+
162
+ return sendSuccess(res, {
163
+ message: 'Token refreshed successfully.',
164
+ data: {
165
+ ...tokens,
166
+ tokenType: 'Bearer',
167
+ },
168
+ });
169
+ });
170
+
171
+ router.post('/logout', async (req, res) => {
172
+ const refreshToken = String(req.body.refreshToken || '').trim();
173
+
174
+ if (refreshToken) {
175
+ await revokeSession(refreshToken);
176
+ }
177
+
178
+ return sendSuccess(res, {
179
+ message: 'Session logged out successfully.',
180
+ });
181
+ });
182
+
183
+ module.exports = router;
src/routes/doctors.routes.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+
3
+ const { getDoctorById, listDoctors } = require('../services/doctors.service');
4
+ const { sendSuccess } = require('../utils/apiResponse');
5
+
6
+ const router = express.Router();
7
+
8
+ router.get('/', async (req, res) => {
9
+ const doctors = await listDoctors({
10
+ department: req.query.department,
11
+ search: req.query.search,
12
+ day: req.query.day,
13
+ });
14
+
15
+ return sendSuccess(res, {
16
+ message: 'Doctors fetched successfully.',
17
+ data: {
18
+ doctors,
19
+ total: doctors.length,
20
+ },
21
+ });
22
+ });
23
+
24
+ router.get('/:doctorId', async (req, res) => {
25
+ const doctor = await getDoctorById(req.params.doctorId);
26
+
27
+ return sendSuccess(res, {
28
+ message: 'Doctor fetched successfully.',
29
+ data: doctor,
30
+ });
31
+ });
32
+
33
+ module.exports = router;
src/routes/health.routes.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const { query } = require('../config/db');
3
+ const { sendSuccess } = require('../utils/apiResponse');
4
+
5
+ const router = express.Router();
6
+
7
+ router.get('/', async (_req, res) => {
8
+ await query('SELECT 1 AS db_ok');
9
+
10
+ return sendSuccess(res, {
11
+ message: 'Care People backend is healthy.',
12
+ data: {
13
+ status: 'ok',
14
+ database: 'connected',
15
+ },
16
+ });
17
+ });
18
+
19
+ module.exports = router;
src/routes/index.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+
3
+ const aiRoutes = require('./ai.routes');
4
+ const authRoutes = require('./auth.routes');
5
+ const doctorRoutes = require('./doctors.routes');
6
+ const patientRoutes = require('./patients.routes');
7
+ const appointmentRoutes = require('./appointments.routes');
8
+ const prescriptionRoutes = require('./prescriptions.routes');
9
+
10
+ const router = express.Router();
11
+
12
+ router.use('/ai', aiRoutes);
13
+ router.use('/auth', authRoutes);
14
+ router.use('/doctors', doctorRoutes);
15
+ router.use('/patients', patientRoutes);
16
+ router.use('/appointments', appointmentRoutes);
17
+ router.use('/prescriptions', prescriptionRoutes);
18
+
19
+ module.exports = router;
src/routes/patients.routes.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const { getPatientOrThrow, upsertPatient } = require('../services/patients.service');
5
+ const { sendSuccess } = require('../utils/apiResponse');
6
+
7
+ const router = express.Router();
8
+
9
+ router.use(requireAuth('patient'));
10
+
11
+ router.get('/me', async (req, res) => {
12
+ const patient = await getPatientOrThrow(req.auth.userIdentifier);
13
+
14
+ return sendSuccess(res, {
15
+ message: 'Patient profile fetched successfully.',
16
+ data: patient,
17
+ });
18
+ });
19
+
20
+ router.put('/me', async (req, res) => {
21
+ const patient = await upsertPatient({
22
+ phoneNumber: req.auth.userIdentifier,
23
+ email: req.body.email,
24
+ name: req.body.name,
25
+ dateOfBirth: req.body.dateOfBirth,
26
+ address: req.body.address,
27
+ gender: req.body.gender,
28
+ });
29
+
30
+ return sendSuccess(res, {
31
+ message: 'Patient profile saved successfully.',
32
+ data: patient,
33
+ });
34
+ });
35
+
36
+ module.exports = router;
src/routes/prescriptions.routes.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const {
5
+ createPrescription,
6
+ listDoctorPrescriptions,
7
+ listPatientPrescriptions,
8
+ } = require('../services/prescriptions.service');
9
+ const { sendSuccess } = require('../utils/apiResponse');
10
+
11
+ const router = express.Router();
12
+
13
+ router.get('/patient/me', requireAuth('patient'), async (req, res) => {
14
+ const prescriptions = await listPatientPrescriptions(req.auth.userIdentifier);
15
+
16
+ return sendSuccess(res, {
17
+ message: 'Patient prescriptions fetched successfully.',
18
+ data: {
19
+ prescriptions,
20
+ total: prescriptions.length,
21
+ },
22
+ });
23
+ });
24
+
25
+ router.get('/doctor/me', requireAuth('doctor'), async (req, res) => {
26
+ const prescriptions = await listDoctorPrescriptions(req.auth.userIdentifier);
27
+
28
+ return sendSuccess(res, {
29
+ message: 'Doctor prescriptions fetched successfully.',
30
+ data: {
31
+ prescriptions,
32
+ total: prescriptions.length,
33
+ },
34
+ });
35
+ });
36
+
37
+ router.post('/', requireAuth('doctor'), async (req, res) => {
38
+ const prescription = await createPrescription({
39
+ doctorId: req.auth.userIdentifier,
40
+ appointmentId: Number(req.body.appointmentId),
41
+ diagnosis: req.body.diagnosis,
42
+ medicines: req.body.medicines,
43
+ additionalNotes: req.body.additionalNotes,
44
+ pdfPath: req.body.pdfPath,
45
+ });
46
+
47
+ return sendSuccess(res, {
48
+ status: 201,
49
+ message: 'Prescription created successfully.',
50
+ data: prescription,
51
+ });
52
+ });
53
+
54
+ module.exports = router;
src/server.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const app = require('./app');
2
+ const env = require('./config/env');
3
+ const { query } = require('./config/db');
4
+
5
+ async function startServer() {
6
+ await query('SELECT 1 AS db_ok');
7
+
8
+ app.listen(env.port, () => {
9
+ console.log(
10
+ `Care People backend listening on http://localhost:${env.port}`,
11
+ );
12
+ });
13
+ }
14
+
15
+ startServer().catch((error) => {
16
+ console.error('Failed to start the backend server.');
17
+ console.error(error);
18
+ process.exit(1);
19
+ });
src/services/ai.service.js ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const env = require('../config/env');
2
+ const HttpError = require('../utils/httpError');
3
+
4
+ const GROQ_CHAT_COMPLETIONS_URL =
5
+ 'https://api.groq.com/openai/v1/chat/completions';
6
+ const PATIENT_URGENCY_LEVELS = new Set(['routine', 'priority', 'emergency']);
7
+ const DOCTOR_GOALS = new Set([
8
+ 'Consult Note',
9
+ 'Follow-up Advice',
10
+ 'Prescription Check',
11
+ ]);
12
+
13
+ function requireGroqConfig() {
14
+ if (!env.groqApiKey) {
15
+ throw new HttpError(
16
+ 500,
17
+ 'Groq API is not configured on the server.',
18
+ 'AI_PROVIDER_NOT_CONFIGURED',
19
+ );
20
+ }
21
+ }
22
+
23
+ function normalizeSeverity(value) {
24
+ const normalized = String(value || '').trim().toLowerCase();
25
+
26
+ if (normalized === 'mild') {
27
+ return 'Mild';
28
+ }
29
+
30
+ if (normalized === 'medium') {
31
+ return 'Medium';
32
+ }
33
+
34
+ if (normalized === 'high') {
35
+ return 'High';
36
+ }
37
+
38
+ throw new HttpError(
39
+ 400,
40
+ 'Severity must be Mild, Medium, or High.',
41
+ 'INVALID_SEVERITY',
42
+ );
43
+ }
44
+
45
+ function normalizeDoctorGoal(value) {
46
+ const goal = String(value || '').trim();
47
+
48
+ if (!DOCTOR_GOALS.has(goal)) {
49
+ throw new HttpError(
50
+ 400,
51
+ 'Goal must be Consult Note, Follow-up Advice, or Prescription Check.',
52
+ 'INVALID_AI_GOAL',
53
+ );
54
+ }
55
+
56
+ return goal;
57
+ }
58
+
59
+ function parseJsonResponse(rawContent) {
60
+ let content = String(rawContent || '').trim();
61
+
62
+ if (!content) {
63
+ return null;
64
+ }
65
+
66
+ const fencedMatch = content.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
67
+ if (fencedMatch) {
68
+ content = fencedMatch[1].trim();
69
+ }
70
+
71
+ try {
72
+ return JSON.parse(content);
73
+ } catch (_error) {
74
+ const objectStart = content.indexOf('{');
75
+ const objectEnd = content.lastIndexOf('}');
76
+
77
+ if (objectStart === -1 || objectEnd <= objectStart) {
78
+ return null;
79
+ }
80
+
81
+ try {
82
+ return JSON.parse(content.slice(objectStart, objectEnd + 1));
83
+ } catch (_innerError) {
84
+ return null;
85
+ }
86
+ }
87
+ }
88
+
89
+ function normalizeStringList(value, fallback) {
90
+ if (!Array.isArray(value)) {
91
+ return fallback;
92
+ }
93
+
94
+ const items = value
95
+ .map((item) => String(item || '').trim())
96
+ .filter(Boolean)
97
+ .slice(0, 5);
98
+
99
+ return items.length > 0 ? items : fallback;
100
+ }
101
+
102
+ function inferPatientUrgency(symptoms, severity) {
103
+ const normalizedSymptoms = symptoms.toLowerCase();
104
+ const emergencyKeywords = [
105
+ 'chest pain',
106
+ 'shortness of breath',
107
+ 'trouble breathing',
108
+ 'stroke',
109
+ 'seizure',
110
+ 'heavy bleeding',
111
+ 'passed out',
112
+ 'unconscious',
113
+ ];
114
+
115
+ if (
116
+ severity === 'High' ||
117
+ emergencyKeywords.some((keyword) => normalizedSymptoms.includes(keyword))
118
+ ) {
119
+ return 'emergency';
120
+ }
121
+
122
+ const priorityKeywords = ['fever', 'vomit', 'pain', 'dizzy', 'infection'];
123
+ if (
124
+ severity === 'Medium' ||
125
+ priorityKeywords.some((keyword) => normalizedSymptoms.includes(keyword))
126
+ ) {
127
+ return 'priority';
128
+ }
129
+
130
+ return 'routine';
131
+ }
132
+
133
+ function normalizeUrgency(value, fallback) {
134
+ const urgency = String(value || '').trim().toLowerCase();
135
+ return PATIENT_URGENCY_LEVELS.has(urgency) ? urgency : fallback;
136
+ }
137
+
138
+ function buildPatientFallback(urgencyLevel) {
139
+ if (urgencyLevel === 'emergency') {
140
+ return {
141
+ headline: 'Get urgent medical help now',
142
+ recommendation:
143
+ 'Your symptom description may need urgent attention. Use Emergency Service in the app or seek immediate medical care.',
144
+ nextSteps: [
145
+ 'Do not delay getting urgent help.',
146
+ 'Use the Emergency Service contact or go to the nearest emergency facility.',
147
+ 'Avoid self-medicating while symptoms are severe or worsening.',
148
+ ],
149
+ };
150
+ }
151
+
152
+ if (urgencyLevel === 'priority') {
153
+ return {
154
+ headline: 'Book a doctor visit soon',
155
+ recommendation:
156
+ 'A prompt doctor review is recommended. Monitor symptoms closely and book an appointment through the app.',
157
+ nextSteps: [
158
+ 'Book an appointment as soon as possible.',
159
+ 'Rest, hydrate, and note any new or worsening symptoms.',
160
+ 'Use Emergency Service if you feel unsafe or symptoms become severe.',
161
+ ],
162
+ };
163
+ }
164
+
165
+ return {
166
+ headline: 'Monitor and arrange routine care',
167
+ recommendation:
168
+ 'This sounds more suitable for routine follow-up. Keep monitoring symptoms and arrange a standard appointment if they persist.',
169
+ nextSteps: [
170
+ 'Rest and keep fluids up.',
171
+ 'Track symptoms for changes over the next 24 to 48 hours.',
172
+ 'Book a routine appointment if symptoms continue or worsen.',
173
+ ],
174
+ };
175
+ }
176
+
177
+ async function requestGroqCompletion(messages) {
178
+ requireGroqConfig();
179
+
180
+ const response = await fetch(GROQ_CHAT_COMPLETIONS_URL, {
181
+ method: 'POST',
182
+ headers: {
183
+ 'Content-Type': 'application/json',
184
+ Authorization: `Bearer ${env.groqApiKey}`,
185
+ },
186
+ body: JSON.stringify({
187
+ model: env.groqModel,
188
+ temperature: 0.2,
189
+ max_completion_tokens: 700,
190
+ messages,
191
+ }),
192
+ });
193
+
194
+ const payload = await response.json().catch(() => null);
195
+
196
+ if (!response.ok) {
197
+ throw new HttpError(
198
+ 502,
199
+ payload?.error?.message ||
200
+ payload?.message ||
201
+ 'Groq AI request failed.',
202
+ 'AI_PROVIDER_ERROR',
203
+ );
204
+ }
205
+
206
+ const content = payload?.choices?.[0]?.message?.content;
207
+
208
+ if (!content || !String(content).trim()) {
209
+ throw new HttpError(
210
+ 502,
211
+ 'Groq AI returned an empty response.',
212
+ 'AI_EMPTY_RESPONSE',
213
+ );
214
+ }
215
+
216
+ return String(content).trim();
217
+ }
218
+
219
+ async function generatePatientSuggestion({ symptoms, severity }) {
220
+ const symptomText = String(symptoms || '').trim();
221
+ const normalizedSeverity = normalizeSeverity(severity);
222
+
223
+ if (symptomText.length < 6) {
224
+ throw new HttpError(
225
+ 400,
226
+ 'Please describe the symptoms in a little more detail.',
227
+ 'SYMPTOMS_TOO_SHORT',
228
+ );
229
+ }
230
+
231
+ const content = await requestGroqCompletion([
232
+ {
233
+ role: 'system',
234
+ content: [
235
+ 'You are the Care People patient triage assistant.',
236
+ 'Provide brief, careful health guidance based only on the symptom description and severity.',
237
+ 'Do not claim to diagnose, do not prescribe medication, and do not replace a clinician.',
238
+ 'If symptoms sound severe, worsening, or potentially life-threatening, direct the patient to urgent or emergency care immediately.',
239
+ 'Return only valid JSON with these keys:',
240
+ 'headline, urgencyLevel, summary, recommendation, nextSteps, disclaimer.',
241
+ 'urgencyLevel must be one of: routine, priority, emergency.',
242
+ 'summary and recommendation should each stay under 80 words.',
243
+ 'nextSteps must be an array of 2 to 4 concise strings.',
244
+ ].join(' '),
245
+ },
246
+ {
247
+ role: 'user',
248
+ content: JSON.stringify({
249
+ severity: normalizedSeverity,
250
+ symptoms: symptomText,
251
+ }),
252
+ },
253
+ ]);
254
+
255
+ const parsed = parseJsonResponse(content);
256
+ const fallbackUrgency = inferPatientUrgency(symptomText, normalizedSeverity);
257
+ const fallback = buildPatientFallback(fallbackUrgency);
258
+
259
+ return {
260
+ provider: 'groq',
261
+ model: env.groqModel,
262
+ severity: normalizedSeverity,
263
+ headline: String(parsed?.headline || '').trim() || fallback.headline,
264
+ urgencyLevel: normalizeUrgency(parsed?.urgencyLevel, fallbackUrgency),
265
+ summary:
266
+ String(parsed?.summary || '').trim() ||
267
+ `Symptoms reported: ${symptomText}. Severity selected: ${normalizedSeverity}.`,
268
+ recommendation:
269
+ String(parsed?.recommendation || '').trim() || fallback.recommendation,
270
+ nextSteps: normalizeStringList(parsed?.nextSteps, fallback.nextSteps),
271
+ disclaimer:
272
+ String(parsed?.disclaimer || '').trim() ||
273
+ 'This is general guidance, not a diagnosis. Seek urgent medical care now if symptoms are severe, rapidly worsening, or you feel unsafe.',
274
+ };
275
+ }
276
+
277
+ async function generateDoctorAssistantDraft({ goal, input }) {
278
+ const normalizedGoal = normalizeDoctorGoal(goal);
279
+ const workingInput = String(input || '').trim();
280
+
281
+ if (workingInput.length < 10) {
282
+ throw new HttpError(
283
+ 400,
284
+ 'Please enter more clinical context before generating a draft.',
285
+ 'AI_INPUT_TOO_SHORT',
286
+ );
287
+ }
288
+
289
+ const content = await requestGroqCompletion([
290
+ {
291
+ role: 'system',
292
+ content: [
293
+ 'You are the Care People doctor drafting assistant.',
294
+ 'Draft concise clinical text for doctors from the provided notes.',
295
+ 'Do not invent vitals, test results, diagnoses, or medication details that are not present in the input.',
296
+ 'Keep the draft factual and clearly action-oriented.',
297
+ 'Return only valid JSON with these keys: title, draft, bullets, disclaimer.',
298
+ 'bullets must be an array of 2 to 5 concise strings.',
299
+ ].join(' '),
300
+ },
301
+ {
302
+ role: 'user',
303
+ content: JSON.stringify({
304
+ goal: normalizedGoal,
305
+ input: workingInput,
306
+ }),
307
+ },
308
+ ]);
309
+
310
+ const parsed = parseJsonResponse(content);
311
+
312
+ const fallbackDraftByGoal = {
313
+ 'Consult Note':
314
+ `Clinical summary based on the provided note:\n${workingInput}\n\nPlan:\n- Review symptom timeline and red flags.\n- Confirm examination findings and diagnosis.\n- Document medications and follow-up clearly.`,
315
+ 'Follow-up Advice':
316
+ `Follow-up guidance:\n${workingInput}\n\nSuggested follow-up:\n- Reassess symptom progression in the advised timeframe.\n- Return earlier if red-flag symptoms develop.\n- Confirm medication adherence and review any new concerns.`,
317
+ 'Prescription Check':
318
+ `Prescription safety review:\n${workingInput}\n\nChecklist:\n- Confirm dosage, frequency, and duration.\n- Add meal timing or precautions where relevant.\n- Include return precautions for worsening symptoms.`,
319
+ };
320
+
321
+ return {
322
+ provider: 'groq',
323
+ model: env.groqModel,
324
+ goal: normalizedGoal,
325
+ title: String(parsed?.title || '').trim() || `${normalizedGoal} Draft`,
326
+ draft:
327
+ String(parsed?.draft || '').trim() || fallbackDraftByGoal[normalizedGoal],
328
+ bullets: normalizeStringList(parsed?.bullets, [
329
+ 'Review the draft against the actual visit details.',
330
+ 'Add any missing examination findings or medications manually.',
331
+ 'Make sure urgent return precautions are documented when relevant.',
332
+ ]),
333
+ disclaimer:
334
+ String(parsed?.disclaimer || '').trim() ||
335
+ 'AI output is a drafting aid only. The treating doctor must review and finalize all clinical documentation.',
336
+ };
337
+ }
338
+
339
+ module.exports = {
340
+ generateDoctorAssistantDraft,
341
+ generatePatientSuggestion,
342
+ };
src/services/appointments.service.js ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { query, withTransaction } = require('../config/db');
2
+ const { normalizeDate } = require('../utils/date');
3
+ const HttpError = require('../utils/httpError');
4
+ const { mapAppointment, parseJsonValue } = require('../utils/serializers');
5
+
6
+ const APPOINTMENT_SELECT = `
7
+ SELECT
8
+ a.id,
9
+ a.doctor_id,
10
+ d.name AS doctor_name,
11
+ d.department AS doctor_department,
12
+ d.designation AS doctor_designation,
13
+ d.degrees AS doctor_degrees,
14
+ a.patient_phone,
15
+ p.name AS patient_name,
16
+ a.appointment_date,
17
+ a.time_slot,
18
+ a.serial_number,
19
+ a.status,
20
+ a.notes,
21
+ a.created_at,
22
+ a.updated_at
23
+ FROM appointments a
24
+ INNER JOIN doctors d ON d.id = a.doctor_id
25
+ INNER JOIN patients p ON p.phone_number = a.patient_phone
26
+ `;
27
+
28
+ function getWeekdayLabel(dateString) {
29
+ const [year, month, day] = dateString.split('-').map(Number);
30
+ return new Date(Date.UTC(year, month - 1, day)).toLocaleDateString('en-US', {
31
+ weekday: 'long',
32
+ timeZone: 'UTC',
33
+ });
34
+ }
35
+
36
+ function normalizeStatus(status) {
37
+ const normalizedStatus = String(status || '').trim().toLowerCase();
38
+ const allowedStatuses = new Set(['confirmed', 'cancelled', 'completed']);
39
+
40
+ if (!allowedStatuses.has(normalizedStatus)) {
41
+ throw new HttpError(
42
+ 400,
43
+ 'Status must be confirmed, cancelled, or completed.',
44
+ 'INVALID_STATUS',
45
+ );
46
+ }
47
+
48
+ return normalizedStatus;
49
+ }
50
+
51
+ async function getAppointmentById(appointmentId) {
52
+ const rows = await query(
53
+ `${APPOINTMENT_SELECT} WHERE a.id = :appointmentId LIMIT 1`,
54
+ { appointmentId },
55
+ );
56
+
57
+ if (rows.length === 0) {
58
+ throw new HttpError(404, 'Appointment not found.', 'APPOINTMENT_NOT_FOUND');
59
+ }
60
+
61
+ return mapAppointment(rows[0]);
62
+ }
63
+
64
+ async function listPatientAppointments(patientPhone) {
65
+ const rows = await query(
66
+ `${APPOINTMENT_SELECT} WHERE a.patient_phone = :patientPhone ORDER BY a.appointment_date DESC, a.time_slot ASC`,
67
+ { patientPhone },
68
+ );
69
+
70
+ return rows.map(mapAppointment);
71
+ }
72
+
73
+ async function listDoctorAppointments(doctorId) {
74
+ const rows = await query(
75
+ `${APPOINTMENT_SELECT} WHERE a.doctor_id = :doctorId ORDER BY a.appointment_date DESC, a.time_slot ASC`,
76
+ { doctorId },
77
+ );
78
+
79
+ return rows.map(mapAppointment);
80
+ }
81
+
82
+ async function getDoctorAvailability(doctorId, appointmentDate) {
83
+ const normalizedDate = normalizeDate(appointmentDate, 'date');
84
+
85
+ const rows = await query(
86
+ `
87
+ SELECT time_slot
88
+ FROM appointments
89
+ WHERE
90
+ doctor_id = :doctorId
91
+ AND appointment_date = :appointmentDate
92
+ AND status <> 'cancelled'
93
+ ORDER BY time_slot
94
+ `,
95
+ {
96
+ doctorId,
97
+ appointmentDate: normalizedDate,
98
+ },
99
+ );
100
+
101
+ const serialRows = await query(
102
+ `
103
+ SELECT COALESCE(MAX(serial_number), 0) + 1 AS next_serial
104
+ FROM appointments
105
+ WHERE
106
+ doctor_id = :doctorId
107
+ AND appointment_date = :appointmentDate
108
+ `,
109
+ {
110
+ doctorId,
111
+ appointmentDate: normalizedDate,
112
+ },
113
+ );
114
+
115
+ return {
116
+ doctorId,
117
+ date: normalizedDate,
118
+ bookedTimeSlots: rows.map((row) => row.time_slot),
119
+ nextSerialNumber: Number(serialRows[0]?.next_serial || 1),
120
+ };
121
+ }
122
+
123
+ async function createAppointment({
124
+ doctorId,
125
+ patientPhone,
126
+ appointmentDate,
127
+ timeSlot,
128
+ notes = null,
129
+ }) {
130
+ const normalizedDate = normalizeDate(appointmentDate, 'date');
131
+
132
+ if (!timeSlot || !String(timeSlot).trim()) {
133
+ throw new HttpError(400, 'timeSlot is required.', 'INVALID_TIME_SLOT');
134
+ }
135
+
136
+ const appointmentId = await withTransaction(async (connection) => {
137
+ const [doctorRows] = await connection.execute(
138
+ `
139
+ SELECT
140
+ id,
141
+ consultation_days
142
+ FROM doctors
143
+ WHERE id = ? AND is_active = 1
144
+ LIMIT 1
145
+ `,
146
+ [doctorId],
147
+ );
148
+
149
+ if (doctorRows.length === 0) {
150
+ throw new HttpError(404, 'Doctor not found.', 'DOCTOR_NOT_FOUND');
151
+ }
152
+
153
+ const availableDays = parseJsonValue(doctorRows[0].consultation_days);
154
+ const weekdayLabel = getWeekdayLabel(normalizedDate);
155
+
156
+ if (!availableDays.includes(weekdayLabel)) {
157
+ throw new HttpError(
158
+ 400,
159
+ `Doctor is not available on ${weekdayLabel}.`,
160
+ 'DOCTOR_UNAVAILABLE',
161
+ );
162
+ }
163
+
164
+ const [patientRows] = await connection.execute(
165
+ `
166
+ SELECT phone_number
167
+ FROM patients
168
+ WHERE phone_number = ?
169
+ LIMIT 1
170
+ `,
171
+ [patientPhone],
172
+ );
173
+
174
+ if (patientRows.length === 0) {
175
+ throw new HttpError(404, 'Patient profile not found.', 'PATIENT_NOT_FOUND');
176
+ }
177
+
178
+ const [takenSlotRows] = await connection.execute(
179
+ `
180
+ SELECT id
181
+ FROM appointments
182
+ WHERE
183
+ doctor_id = ?
184
+ AND appointment_date = ?
185
+ AND time_slot = ?
186
+ AND status <> 'cancelled'
187
+ LIMIT 1
188
+ FOR UPDATE
189
+ `,
190
+ [doctorId, normalizedDate, String(timeSlot).trim()],
191
+ );
192
+
193
+ if (takenSlotRows.length > 0) {
194
+ throw new HttpError(
195
+ 409,
196
+ 'This time slot is already booked.',
197
+ 'TIME_SLOT_TAKEN',
198
+ );
199
+ }
200
+
201
+ const [serialRows] = await connection.execute(
202
+ `
203
+ SELECT COALESCE(MAX(serial_number), 0) + 1 AS next_serial
204
+ FROM appointments
205
+ WHERE doctor_id = ? AND appointment_date = ?
206
+ FOR UPDATE
207
+ `,
208
+ [doctorId, normalizedDate],
209
+ );
210
+
211
+ const nextSerial = Number(serialRows[0].next_serial || 1);
212
+
213
+ const [result] = await connection.execute(
214
+ `
215
+ INSERT INTO appointments (
216
+ doctor_id,
217
+ patient_phone,
218
+ appointment_date,
219
+ time_slot,
220
+ serial_number,
221
+ status,
222
+ notes
223
+ ) VALUES (?, ?, ?, ?, ?, 'confirmed', ?)
224
+ `,
225
+ [
226
+ doctorId,
227
+ patientPhone,
228
+ normalizedDate,
229
+ String(timeSlot).trim(),
230
+ nextSerial,
231
+ notes ? String(notes).trim() : null,
232
+ ],
233
+ );
234
+
235
+ return Number(result.insertId);
236
+ });
237
+
238
+ return getAppointmentById(appointmentId);
239
+ }
240
+
241
+ async function updateAppointmentStatus({ appointmentId, doctorId, status }) {
242
+ const normalizedStatus = normalizeStatus(status);
243
+
244
+ const result = await query(
245
+ `
246
+ UPDATE appointments
247
+ SET
248
+ status = :status,
249
+ updated_at = CURRENT_TIMESTAMP
250
+ WHERE id = :appointmentId AND doctor_id = :doctorId
251
+ `,
252
+ {
253
+ appointmentId,
254
+ doctorId,
255
+ status: normalizedStatus,
256
+ },
257
+ );
258
+
259
+ if (result.affectedRows === 0) {
260
+ throw new HttpError(
261
+ 404,
262
+ 'Appointment not found for this doctor.',
263
+ 'APPOINTMENT_NOT_FOUND',
264
+ );
265
+ }
266
+
267
+ return getAppointmentById(appointmentId);
268
+ }
269
+
270
+ module.exports = {
271
+ createAppointment,
272
+ getAppointmentById,
273
+ getDoctorAvailability,
274
+ listDoctorAppointments,
275
+ listPatientAppointments,
276
+ updateAppointmentStatus,
277
+ };
src/services/auth.service.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const crypto = require('crypto');
2
+
3
+ const env = require('../config/env');
4
+ const { query } = require('../config/db');
5
+ const { mysqlDateTimeFromNow } = require('../utils/date');
6
+ const HttpError = require('../utils/httpError');
7
+ const {
8
+ hashToken,
9
+ signAccessToken,
10
+ signRefreshToken,
11
+ verifyRefreshToken,
12
+ } = require('../utils/tokens');
13
+
14
+ function buildTokenPayload({ userType, userIdentifier, sessionId }) {
15
+ const accessToken = signAccessToken({
16
+ userType,
17
+ userIdentifier,
18
+ sessionId,
19
+ });
20
+
21
+ const refreshToken = signRefreshToken({
22
+ userType,
23
+ userIdentifier,
24
+ sessionId,
25
+ });
26
+
27
+ return {
28
+ accessToken,
29
+ refreshToken,
30
+ expiresIn: env.accessTokenMinutes * 60,
31
+ };
32
+ }
33
+
34
+ async function issueSessionTokens({ userType, userIdentifier, metadata = null }) {
35
+ const sessionId = crypto.randomUUID();
36
+ const tokens = buildTokenPayload({
37
+ userType,
38
+ userIdentifier,
39
+ sessionId,
40
+ });
41
+
42
+ await query(
43
+ `
44
+ INSERT INTO sessions (
45
+ id,
46
+ user_type,
47
+ user_identifier,
48
+ refresh_token_hash,
49
+ expires_at,
50
+ metadata_json
51
+ ) VALUES (
52
+ :id,
53
+ :userType,
54
+ :userIdentifier,
55
+ :refreshTokenHash,
56
+ :expiresAt,
57
+ :metadataJson
58
+ )
59
+ `,
60
+ {
61
+ id: sessionId,
62
+ userType,
63
+ userIdentifier,
64
+ refreshTokenHash: hashToken(tokens.refreshToken),
65
+ expiresAt: mysqlDateTimeFromNow(env.refreshTokenDays),
66
+ metadataJson: metadata ? JSON.stringify(metadata) : null,
67
+ },
68
+ );
69
+
70
+ return tokens;
71
+ }
72
+
73
+ async function refreshSession(refreshToken) {
74
+ let payload;
75
+
76
+ try {
77
+ payload = verifyRefreshToken(refreshToken);
78
+ } catch (_error) {
79
+ throw new HttpError(
80
+ 401,
81
+ 'Invalid or expired refresh token.',
82
+ 'REFRESH_INVALID',
83
+ );
84
+ }
85
+
86
+ const rows = await query(
87
+ `
88
+ SELECT
89
+ id,
90
+ user_type,
91
+ user_identifier,
92
+ refresh_token_hash,
93
+ expires_at,
94
+ revoked_at
95
+ FROM sessions
96
+ WHERE id = :sessionId
97
+ LIMIT 1
98
+ `,
99
+ { sessionId: payload.sessionId },
100
+ );
101
+
102
+ if (rows.length === 0) {
103
+ throw new HttpError(401, 'Session not found.', 'SESSION_NOT_FOUND');
104
+ }
105
+
106
+ const session = rows[0];
107
+
108
+ if (session.revoked_at) {
109
+ throw new HttpError(401, 'Session has been revoked.', 'SESSION_REVOKED');
110
+ }
111
+
112
+ if (hashToken(refreshToken) !== session.refresh_token_hash) {
113
+ throw new HttpError(401, 'Refresh token does not match.', 'REFRESH_INVALID');
114
+ }
115
+
116
+ if (new Date(session.expires_at).getTime() < Date.now()) {
117
+ throw new HttpError(401, 'Session has expired.', 'SESSION_EXPIRED');
118
+ }
119
+
120
+ const nextTokens = buildTokenPayload({
121
+ userType: session.user_type,
122
+ userIdentifier: session.user_identifier,
123
+ sessionId: session.id,
124
+ });
125
+
126
+ await query(
127
+ `
128
+ UPDATE sessions
129
+ SET
130
+ refresh_token_hash = :refreshTokenHash,
131
+ expires_at = :expiresAt
132
+ WHERE id = :sessionId
133
+ `,
134
+ {
135
+ refreshTokenHash: hashToken(nextTokens.refreshToken),
136
+ expiresAt: mysqlDateTimeFromNow(env.refreshTokenDays),
137
+ sessionId: session.id,
138
+ },
139
+ );
140
+
141
+ return nextTokens;
142
+ }
143
+
144
+ async function revokeSession(refreshToken) {
145
+ try {
146
+ const payload = verifyRefreshToken(refreshToken);
147
+ await query(
148
+ `
149
+ UPDATE sessions
150
+ SET revoked_at = CURRENT_TIMESTAMP
151
+ WHERE id = :sessionId
152
+ `,
153
+ { sessionId: payload.sessionId },
154
+ );
155
+ } catch (_error) {
156
+ return;
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ issueSessionTokens,
162
+ refreshSession,
163
+ revokeSession,
164
+ };
src/services/doctors.service.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { query } = require('../config/db');
2
+ const HttpError = require('../utils/httpError');
3
+ const { mapDoctor } = require('../utils/serializers');
4
+
5
+ const DOCTOR_SELECT = `
6
+ SELECT
7
+ id,
8
+ name,
9
+ department,
10
+ designation,
11
+ degrees,
12
+ room_number,
13
+ consultation_fee,
14
+ consultation_days,
15
+ consultation_times,
16
+ is_active,
17
+ created_at,
18
+ updated_at
19
+ FROM doctors
20
+ `;
21
+
22
+ async function listDoctors({ department, search, day }) {
23
+ const whereClauses = ['is_active = 1'];
24
+ const params = {};
25
+
26
+ if (department) {
27
+ whereClauses.push('department = :department');
28
+ params.department = department;
29
+ }
30
+
31
+ if (search) {
32
+ whereClauses.push(
33
+ '(name LIKE :search OR department LIKE :search OR designation LIKE :search)',
34
+ );
35
+ params.search = `%${search}%`;
36
+ }
37
+
38
+ if (day) {
39
+ whereClauses.push('JSON_CONTAINS(consultation_days, JSON_QUOTE(:day))');
40
+ params.day = day;
41
+ }
42
+
43
+ const rows = await query(
44
+ `${DOCTOR_SELECT} WHERE ${whereClauses.join(' AND ')} ORDER BY department, name`,
45
+ params,
46
+ );
47
+
48
+ return rows.map(mapDoctor);
49
+ }
50
+
51
+ async function getDoctorById(doctorId) {
52
+ const rows = await query(
53
+ `${DOCTOR_SELECT} WHERE id = :doctorId LIMIT 1`,
54
+ { doctorId: String(doctorId || '').trim().toUpperCase() },
55
+ );
56
+
57
+ if (rows.length === 0) {
58
+ throw new HttpError(404, 'Doctor not found.', 'DOCTOR_NOT_FOUND');
59
+ }
60
+
61
+ return mapDoctor(rows[0]);
62
+ }
63
+
64
+ async function getDoctorForLogin(doctorId) {
65
+ const rows = await query(
66
+ `
67
+ SELECT
68
+ id,
69
+ name,
70
+ department,
71
+ designation,
72
+ degrees,
73
+ room_number,
74
+ consultation_fee,
75
+ consultation_days,
76
+ consultation_times,
77
+ password_hash,
78
+ is_active,
79
+ created_at,
80
+ updated_at
81
+ FROM doctors
82
+ WHERE id = :doctorId
83
+ LIMIT 1
84
+ `,
85
+ { doctorId: String(doctorId || '').trim().toUpperCase() },
86
+ );
87
+
88
+ return rows[0] || null;
89
+ }
90
+
91
+ module.exports = {
92
+ getDoctorById,
93
+ getDoctorForLogin,
94
+ listDoctors,
95
+ };
src/services/mailchimp.service.js ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const env = require('../config/env');
2
+ const HttpError = require('../utils/httpError');
3
+
4
+ async function sendOtpEmail({ email, otpCode, expiresInMinutes }) {
5
+ const subject = `Your Care People OTP is ${otpCode}`;
6
+ const text = [
7
+ 'Hello,',
8
+ '',
9
+ `Your Care People OTP is: ${otpCode}`,
10
+ '',
11
+ `This code is valid for ${expiresInMinutes} minutes.`,
12
+ 'Please do not share this code with anyone.',
13
+ '',
14
+ '- Care People Team',
15
+ ].join('\n');
16
+
17
+ const html = `
18
+ <div style="font-family: Arial, sans-serif; line-height: 1.6; color: #1f2937;">
19
+ <p>Hello,</p>
20
+ <p>Your Care People OTP is:</p>
21
+ <p style="font-size: 28px; font-weight: bold; letter-spacing: 4px;">${otpCode}</p>
22
+ <p>This code is valid for ${expiresInMinutes} minutes.</p>
23
+ <p>Please do not share this code with anyone.</p>
24
+ <p>Care People Team</p>
25
+ </div>
26
+ `;
27
+
28
+ if (!env.mailchimpTransactionalEnabled) {
29
+ if (!env.otpDevMode) {
30
+ throw new HttpError(
31
+ 500,
32
+ 'Mailchimp Transactional is disabled and OTP dev mode is off.',
33
+ 'EMAIL_PROVIDER_NOT_CONFIGURED',
34
+ );
35
+ }
36
+
37
+ console.log(`[DEV EMAIL OTP] ${email} -> ${otpCode}`);
38
+ return {
39
+ provider: 'dev',
40
+ status: 'sent',
41
+ messageId: null,
42
+ };
43
+ }
44
+
45
+ if (!env.mailchimpTransactionalApiKey || !env.mailchimpFromEmail) {
46
+ throw new HttpError(
47
+ 500,
48
+ 'Mailchimp Transactional is enabled but not fully configured.',
49
+ 'EMAIL_PROVIDER_NOT_CONFIGURED',
50
+ );
51
+ }
52
+
53
+ const response = await fetch(
54
+ 'https://mandrillapp.com/api/1.0/messages/send.json',
55
+ {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ },
60
+ body: JSON.stringify({
61
+ key: env.mailchimpTransactionalApiKey,
62
+ message: {
63
+ from_email: env.mailchimpFromEmail,
64
+ from_name: env.mailchimpFromName,
65
+ headers: env.mailchimpReplyTo
66
+ ? { 'Reply-To': env.mailchimpReplyTo }
67
+ : undefined,
68
+ subject,
69
+ text,
70
+ html,
71
+ to: [
72
+ {
73
+ email,
74
+ type: 'to',
75
+ },
76
+ ],
77
+ tags: ['otp', 'care-people'],
78
+ },
79
+ }),
80
+ },
81
+ );
82
+
83
+ const payload = await response.json();
84
+
85
+ if (!response.ok) {
86
+ throw new HttpError(
87
+ 502,
88
+ payload.message || 'Mailchimp Transactional request failed.',
89
+ 'EMAIL_PROVIDER_ERROR',
90
+ );
91
+ }
92
+
93
+ const firstResult = Array.isArray(payload) ? payload[0] : null;
94
+
95
+ if (
96
+ !firstResult ||
97
+ ['rejected', 'invalid'].includes(String(firstResult.status || '').toLowerCase())
98
+ ) {
99
+ throw new HttpError(
100
+ 502,
101
+ firstResult?.reject_reason || 'Mailchimp Transactional rejected the email.',
102
+ 'EMAIL_DELIVERY_FAILED',
103
+ );
104
+ }
105
+
106
+ return {
107
+ provider: 'mailchimp_transactional',
108
+ status: firstResult.status || 'sent',
109
+ messageId: firstResult._id || null,
110
+ };
111
+ }
112
+
113
+ module.exports = {
114
+ sendOtpEmail,
115
+ };
src/services/patients.service.js ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { query } = require('../config/db');
2
+ const { normalizeDate } = require('../utils/date');
3
+ const { normalizeEmail, validateEmail } = require('../utils/email');
4
+ const HttpError = require('../utils/httpError');
5
+ const { mapPatient, titleCase } = require('../utils/serializers');
6
+
7
+ const ALLOWED_GENDERS = new Set(['male', 'female', 'other']);
8
+
9
+ async function getPatientByPhone(phoneNumber) {
10
+ const rows = await query(
11
+ `
12
+ SELECT
13
+ phone_number,
14
+ email,
15
+ name,
16
+ date_of_birth,
17
+ address,
18
+ gender,
19
+ created_at,
20
+ updated_at
21
+ FROM patients
22
+ WHERE phone_number = :phoneNumber
23
+ LIMIT 1
24
+ `,
25
+ { phoneNumber },
26
+ );
27
+
28
+ return rows[0] ? mapPatient(rows[0]) : null;
29
+ }
30
+
31
+ async function getPatientOrThrow(phoneNumber) {
32
+ const patient = await getPatientByPhone(phoneNumber);
33
+
34
+ if (!patient) {
35
+ throw new HttpError(404, 'Patient profile not found.', 'PATIENT_NOT_FOUND');
36
+ }
37
+
38
+ return patient;
39
+ }
40
+
41
+ async function ensureEmailAvailable(email, phoneNumber) {
42
+ const rows = await query(
43
+ `
44
+ SELECT phone_number
45
+ FROM patients
46
+ WHERE email = :email AND phone_number <> :phoneNumber
47
+ LIMIT 1
48
+ `,
49
+ {
50
+ email,
51
+ phoneNumber,
52
+ },
53
+ );
54
+
55
+ if (rows.length > 0) {
56
+ throw new HttpError(
57
+ 409,
58
+ 'This email address is already used by another patient.',
59
+ 'EMAIL_ALREADY_IN_USE',
60
+ );
61
+ }
62
+ }
63
+
64
+ async function upsertPatient({
65
+ phoneNumber,
66
+ email,
67
+ name,
68
+ dateOfBirth,
69
+ address,
70
+ gender,
71
+ }) {
72
+ const normalizedGender = String(gender || '').trim().toLowerCase();
73
+ const normalizedEmail = validateEmail(email);
74
+
75
+ if (!name || name.trim().length < 3) {
76
+ throw new HttpError(
77
+ 400,
78
+ 'Name must be at least 3 characters long.',
79
+ 'INVALID_NAME',
80
+ );
81
+ }
82
+
83
+ if (!address || !String(address).trim()) {
84
+ throw new HttpError(400, 'Address is required.', 'INVALID_ADDRESS');
85
+ }
86
+
87
+ if (!ALLOWED_GENDERS.has(normalizedGender)) {
88
+ throw new HttpError(
89
+ 400,
90
+ 'Gender must be Male, Female, or Other.',
91
+ 'INVALID_GENDER',
92
+ );
93
+ }
94
+
95
+ await ensureEmailAvailable(normalizedEmail, phoneNumber);
96
+
97
+ await query(
98
+ `
99
+ INSERT INTO patients (
100
+ phone_number,
101
+ email,
102
+ name,
103
+ date_of_birth,
104
+ address,
105
+ gender
106
+ ) VALUES (
107
+ :phoneNumber,
108
+ :email,
109
+ :name,
110
+ :dateOfBirth,
111
+ :address,
112
+ :gender
113
+ )
114
+ ON DUPLICATE KEY UPDATE
115
+ email = VALUES(email),
116
+ name = VALUES(name),
117
+ date_of_birth = VALUES(date_of_birth),
118
+ address = VALUES(address),
119
+ gender = VALUES(gender),
120
+ updated_at = CURRENT_TIMESTAMP
121
+ `,
122
+ {
123
+ phoneNumber,
124
+ email: normalizedEmail,
125
+ name: String(name).trim(),
126
+ dateOfBirth: normalizeDate(dateOfBirth, 'dateOfBirth'),
127
+ address: String(address).trim(),
128
+ gender: normalizedGender,
129
+ },
130
+ );
131
+
132
+ const patient = await getPatientOrThrow(phoneNumber);
133
+ return {
134
+ ...patient,
135
+ gender: titleCase(normalizedGender),
136
+ };
137
+ }
138
+
139
+ function patientEmailMatches(patient, email) {
140
+ if (!patient || !patient.email) {
141
+ return true;
142
+ }
143
+
144
+ return normalizeEmail(patient.email) === normalizeEmail(email);
145
+ }
146
+
147
+ module.exports = {
148
+ getPatientByPhone,
149
+ getPatientOrThrow,
150
+ patientEmailMatches,
151
+ upsertPatient,
152
+ };
src/services/prescriptions.service.js ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const crypto = require('crypto');
2
+
3
+ const { query, withTransaction } = require('../config/db');
4
+ const HttpError = require('../utils/httpError');
5
+ const { mapPrescription } = require('../utils/serializers');
6
+
7
+ const PRESCRIPTION_SELECT = `
8
+ SELECT
9
+ pr.id,
10
+ pr.appointment_id,
11
+ pr.doctor_id,
12
+ d.name AS doctor_name,
13
+ d.department AS doctor_department,
14
+ d.designation AS doctor_designation,
15
+ d.degrees AS doctor_degrees,
16
+ pr.patient_phone,
17
+ p.name AS patient_name,
18
+ pr.appointment_date,
19
+ pr.issued_at,
20
+ pr.additional_notes,
21
+ pr.pdf_path,
22
+ pr.created_at
23
+ FROM prescriptions pr
24
+ INNER JOIN doctors d ON d.id = pr.doctor_id
25
+ INNER JOIN patients p ON p.phone_number = pr.patient_phone
26
+ `;
27
+
28
+ async function loadPrescriptionChildren(prescriptionIds) {
29
+ if (prescriptionIds.length === 0) {
30
+ return {
31
+ diagnosisMap: new Map(),
32
+ medicineMap: new Map(),
33
+ };
34
+ }
35
+
36
+ const placeholders = prescriptionIds.map(() => '?').join(', ');
37
+
38
+ const diagnosisRows = await query(
39
+ `
40
+ SELECT
41
+ prescription_id,
42
+ diagnosis_text,
43
+ sort_order
44
+ FROM prescription_diagnoses
45
+ WHERE prescription_id IN (${placeholders})
46
+ ORDER BY prescription_id, sort_order ASC, id ASC
47
+ `,
48
+ prescriptionIds,
49
+ );
50
+
51
+ const medicineRows = await query(
52
+ `
53
+ SELECT
54
+ prescription_id,
55
+ name,
56
+ dosage,
57
+ frequency,
58
+ duration,
59
+ notes
60
+ FROM prescribed_medicines
61
+ WHERE prescription_id IN (${placeholders})
62
+ ORDER BY prescription_id, id ASC
63
+ `,
64
+ prescriptionIds,
65
+ );
66
+
67
+ const diagnosisMap = new Map();
68
+ const medicineMap = new Map();
69
+
70
+ for (const row of diagnosisRows) {
71
+ const currentValues = diagnosisMap.get(row.prescription_id) || [];
72
+ currentValues.push(row.diagnosis_text);
73
+ diagnosisMap.set(row.prescription_id, currentValues);
74
+ }
75
+
76
+ for (const row of medicineRows) {
77
+ const currentValues = medicineMap.get(row.prescription_id) || [];
78
+ currentValues.push({
79
+ name: row.name,
80
+ dosage: row.dosage,
81
+ frequency: row.frequency,
82
+ duration: row.duration,
83
+ notes: row.notes,
84
+ });
85
+ medicineMap.set(row.prescription_id, currentValues);
86
+ }
87
+
88
+ return {
89
+ diagnosisMap,
90
+ medicineMap,
91
+ };
92
+ }
93
+
94
+ async function buildPrescriptionList(whereClause, params) {
95
+ const rows = await query(
96
+ `${PRESCRIPTION_SELECT} WHERE ${whereClause} ORDER BY pr.issued_at DESC`,
97
+ params,
98
+ );
99
+
100
+ const prescriptionIds = rows.map((row) => row.id);
101
+ const { diagnosisMap, medicineMap } = await loadPrescriptionChildren(
102
+ prescriptionIds,
103
+ );
104
+
105
+ return rows.map((row) =>
106
+ mapPrescription(
107
+ row,
108
+ diagnosisMap.get(row.id) || [],
109
+ medicineMap.get(row.id) || [],
110
+ ),
111
+ );
112
+ }
113
+
114
+ async function getPrescriptionById(prescriptionId) {
115
+ const prescriptions = await buildPrescriptionList('pr.id = :prescriptionId', {
116
+ prescriptionId,
117
+ });
118
+
119
+ if (prescriptions.length === 0) {
120
+ throw new HttpError(
121
+ 404,
122
+ 'Prescription not found.',
123
+ 'PRESCRIPTION_NOT_FOUND',
124
+ );
125
+ }
126
+
127
+ return prescriptions[0];
128
+ }
129
+
130
+ async function listPatientPrescriptions(patientPhone) {
131
+ return buildPrescriptionList('pr.patient_phone = :patientPhone', {
132
+ patientPhone,
133
+ });
134
+ }
135
+
136
+ async function listDoctorPrescriptions(doctorId) {
137
+ return buildPrescriptionList('pr.doctor_id = :doctorId', {
138
+ doctorId,
139
+ });
140
+ }
141
+
142
+ function validatePrescriptionInput({ diagnosis, medicines }) {
143
+ if (!Array.isArray(diagnosis) || diagnosis.length === 0) {
144
+ throw new HttpError(
145
+ 400,
146
+ 'At least one diagnosis is required.',
147
+ 'INVALID_DIAGNOSIS',
148
+ );
149
+ }
150
+
151
+ if (!Array.isArray(medicines) || medicines.length === 0) {
152
+ throw new HttpError(
153
+ 400,
154
+ 'At least one medicine is required.',
155
+ 'INVALID_MEDICINES',
156
+ );
157
+ }
158
+
159
+ medicines.forEach((medicine, index) => {
160
+ if (
161
+ !medicine ||
162
+ !medicine.name ||
163
+ !medicine.dosage ||
164
+ !medicine.frequency ||
165
+ !medicine.duration
166
+ ) {
167
+ throw new HttpError(
168
+ 400,
169
+ `Medicine at index ${index} is missing a required field.`,
170
+ 'INVALID_MEDICINES',
171
+ );
172
+ }
173
+ });
174
+ }
175
+
176
+ async function createPrescription({
177
+ doctorId,
178
+ appointmentId,
179
+ diagnosis,
180
+ medicines,
181
+ additionalNotes = null,
182
+ pdfPath = null,
183
+ }) {
184
+ validatePrescriptionInput({ diagnosis, medicines });
185
+
186
+ const prescriptionId = await withTransaction(async (connection) => {
187
+ const [appointmentRows] = await connection.execute(
188
+ `
189
+ SELECT
190
+ a.id,
191
+ a.doctor_id,
192
+ a.patient_phone,
193
+ a.appointment_date,
194
+ a.status
195
+ FROM appointments a
196
+ WHERE a.id = ? AND a.doctor_id = ?
197
+ LIMIT 1
198
+ FOR UPDATE
199
+ `,
200
+ [appointmentId, doctorId],
201
+ );
202
+
203
+ if (appointmentRows.length === 0) {
204
+ throw new HttpError(
205
+ 404,
206
+ 'Appointment not found for this doctor.',
207
+ 'APPOINTMENT_NOT_FOUND',
208
+ );
209
+ }
210
+
211
+ const appointment = appointmentRows[0];
212
+
213
+ if (appointment.status === 'completed') {
214
+ throw new HttpError(
215
+ 409,
216
+ 'Prescription already exists for this appointment.',
217
+ 'ALREADY_COMPLETED',
218
+ );
219
+ }
220
+
221
+ const nextPrescriptionId = `RX${Date.now()}${crypto.randomInt(100, 999)}`;
222
+
223
+ await connection.execute(
224
+ `
225
+ INSERT INTO prescriptions (
226
+ id,
227
+ doctor_id,
228
+ patient_phone,
229
+ appointment_id,
230
+ appointment_date,
231
+ issued_at,
232
+ additional_notes,
233
+ pdf_path
234
+ ) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)
235
+ `,
236
+ [
237
+ nextPrescriptionId,
238
+ doctorId,
239
+ appointment.patient_phone,
240
+ appointment.id,
241
+ appointment.appointment_date,
242
+ additionalNotes ? String(additionalNotes).trim() : null,
243
+ pdfPath ? String(pdfPath).trim() : null,
244
+ ],
245
+ );
246
+
247
+ for (const [index, diagnosisText] of diagnosis.entries()) {
248
+ await connection.execute(
249
+ `
250
+ INSERT INTO prescription_diagnoses (
251
+ prescription_id,
252
+ diagnosis_text,
253
+ sort_order
254
+ ) VALUES (?, ?, ?)
255
+ `,
256
+ [nextPrescriptionId, String(diagnosisText).trim(), index + 1],
257
+ );
258
+ }
259
+
260
+ for (const medicine of medicines) {
261
+ await connection.execute(
262
+ `
263
+ INSERT INTO prescribed_medicines (
264
+ prescription_id,
265
+ name,
266
+ dosage,
267
+ frequency,
268
+ duration,
269
+ notes
270
+ ) VALUES (?, ?, ?, ?, ?, ?)
271
+ `,
272
+ [
273
+ nextPrescriptionId,
274
+ String(medicine.name).trim(),
275
+ String(medicine.dosage).trim(),
276
+ String(medicine.frequency).trim(),
277
+ String(medicine.duration).trim(),
278
+ medicine.notes ? String(medicine.notes).trim() : null,
279
+ ],
280
+ );
281
+ }
282
+
283
+ await connection.execute(
284
+ `
285
+ UPDATE appointments
286
+ SET
287
+ status = 'completed',
288
+ updated_at = CURRENT_TIMESTAMP
289
+ WHERE id = ?
290
+ `,
291
+ [appointment.id],
292
+ );
293
+
294
+ return nextPrescriptionId;
295
+ });
296
+
297
+ return getPrescriptionById(prescriptionId);
298
+ }
299
+
300
+ module.exports = {
301
+ createPrescription,
302
+ getPrescriptionById,
303
+ listDoctorPrescriptions,
304
+ listPatientPrescriptions,
305
+ };
src/utils/apiResponse.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function sendSuccess(res, { status = 200, message = 'Success', data = null }) {
2
+ return res.status(status).json({
3
+ success: true,
4
+ message,
5
+ data,
6
+ errorCode: null,
7
+ });
8
+ }
9
+
10
+ module.exports = {
11
+ sendSuccess,
12
+ };
src/utils/date.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const HttpError = require('./httpError');
2
+
3
+ function normalizeDate(dateValue, fieldName = 'date') {
4
+ const rawValue = String(dateValue || '').trim();
5
+
6
+ if (/^\d{4}-\d{2}-\d{2}$/.test(rawValue)) {
7
+ return rawValue;
8
+ }
9
+
10
+ if (/^\d{2}\/\d{2}\/\d{4}$/.test(rawValue)) {
11
+ const [day, month, year] = rawValue.split('/');
12
+ return `${year}-${month}-${day}`;
13
+ }
14
+
15
+ throw new HttpError(
16
+ 400,
17
+ `${fieldName} must use YYYY-MM-DD or DD/MM/YYYY format.`,
18
+ 'INVALID_DATE',
19
+ );
20
+ }
21
+
22
+ function mysqlDateTimeFromNow(daysAhead) {
23
+ const expiryDate = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000);
24
+ return expiryDate.toISOString().slice(0, 19).replace('T', ' ');
25
+ }
26
+
27
+ module.exports = {
28
+ normalizeDate,
29
+ mysqlDateTimeFromNow,
30
+ };
src/utils/email.js ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const HttpError = require('./httpError');
2
+
3
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4
+
5
+ function normalizeEmail(email) {
6
+ return String(email || '').trim().toLowerCase();
7
+ }
8
+
9
+ function validateEmail(email) {
10
+ const normalizedEmail = normalizeEmail(email);
11
+
12
+ if (!EMAIL_REGEX.test(normalizedEmail)) {
13
+ throw new HttpError(
14
+ 400,
15
+ 'A valid email address is required.',
16
+ 'INVALID_EMAIL',
17
+ );
18
+ }
19
+
20
+ return normalizedEmail;
21
+ }
22
+
23
+ function maskEmail(email) {
24
+ const normalizedEmail = normalizeEmail(email);
25
+ const [localPart, domainPart] = normalizedEmail.split('@');
26
+
27
+ if (!localPart || !domainPart) {
28
+ return normalizedEmail;
29
+ }
30
+
31
+ if (localPart.length <= 2) {
32
+ return `${localPart[0] || '*'}*@${domainPart}`;
33
+ }
34
+
35
+ return `${localPart[0]}${'*'.repeat(Math.max(localPart.length - 2, 1))}${localPart.slice(-1)}@${domainPart}`;
36
+ }
37
+
38
+ module.exports = {
39
+ maskEmail,
40
+ normalizeEmail,
41
+ validateEmail,
42
+ };
src/utils/httpError.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ class HttpError extends Error {
2
+ constructor(statusCode, message, errorCode = 'INTERNAL_ERROR') {
3
+ super(message);
4
+ this.statusCode = statusCode;
5
+ this.errorCode = errorCode;
6
+ }
7
+ }
8
+
9
+ module.exports = HttpError;
src/utils/otpStore.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const crypto = require('crypto');
2
+ const env = require('../config/env');
3
+
4
+ const otpStore = new Map();
5
+
6
+ function generateOtp() {
7
+ const minimum = 10 ** (env.otpLength - 1);
8
+ const maximum = 10 ** env.otpLength;
9
+ return String(minimum + crypto.randomInt(0, maximum - minimum));
10
+ }
11
+
12
+ function createOtp(targetKey) {
13
+ const otpCode = generateOtp();
14
+ const expiresAt = Date.now() + env.otpTtlMinutes * 60 * 1000;
15
+
16
+ otpStore.set(targetKey, {
17
+ otpCode,
18
+ expiresAt,
19
+ });
20
+
21
+ return {
22
+ otpCode,
23
+ expiresAt,
24
+ };
25
+ }
26
+
27
+ function verifyOtp(targetKey, otpCode) {
28
+ const record = otpStore.get(targetKey);
29
+
30
+ if (!record) {
31
+ return false;
32
+ }
33
+
34
+ if (record.expiresAt < Date.now()) {
35
+ otpStore.delete(targetKey);
36
+ return false;
37
+ }
38
+
39
+ const isValid = record.otpCode === String(otpCode || '').trim();
40
+
41
+ if (isValid) {
42
+ otpStore.delete(targetKey);
43
+ }
44
+
45
+ return isValid;
46
+ }
47
+
48
+ module.exports = {
49
+ createOtp,
50
+ verifyOtp,
51
+ };
src/utils/phone.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const HttpError = require('./httpError');
2
+
3
+ const BANGLADESH_PHONE_REGEX = /^01\d{9}$/;
4
+
5
+ function normalizePhone(phoneNumber) {
6
+ return String(phoneNumber || '').replace(/\D/g, '');
7
+ }
8
+
9
+ function validatePhone(phoneNumber) {
10
+ const normalizedPhone = normalizePhone(phoneNumber);
11
+
12
+ if (!BANGLADESH_PHONE_REGEX.test(normalizedPhone)) {
13
+ throw new HttpError(
14
+ 400,
15
+ 'Phone number must be an 11 digit Bangladesh mobile number.',
16
+ 'INVALID_PHONE_NUMBER',
17
+ );
18
+ }
19
+
20
+ return normalizedPhone;
21
+ }
22
+
23
+ module.exports = {
24
+ normalizePhone,
25
+ validatePhone,
26
+ };
src/utils/serializers.js ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function parseJsonValue(value, fallback = []) {
2
+ if (value === null || value === undefined) {
3
+ return fallback;
4
+ }
5
+
6
+ if (Array.isArray(value) || typeof value === 'object') {
7
+ return value;
8
+ }
9
+
10
+ try {
11
+ return JSON.parse(value);
12
+ } catch (_error) {
13
+ return fallback;
14
+ }
15
+ }
16
+
17
+ function titleCase(value) {
18
+ const rawValue = String(value || '').trim();
19
+ if (!rawValue) {
20
+ return rawValue;
21
+ }
22
+
23
+ return rawValue.charAt(0).toUpperCase() + rawValue.slice(1).toLowerCase();
24
+ }
25
+
26
+ function formatDateForFlutter(dateValue) {
27
+ const rawValue = String(dateValue || '').trim();
28
+
29
+ if (/^\d{4}-\d{2}-\d{2}$/.test(rawValue)) {
30
+ const [year, month, day] = rawValue.split('-');
31
+ return `${day}/${month}/${year}`;
32
+ }
33
+
34
+ return rawValue;
35
+ }
36
+
37
+ function mapDoctor(row) {
38
+ return {
39
+ id: row.id,
40
+ name: row.name,
41
+ department: row.department,
42
+ designation: row.designation,
43
+ degrees: row.degrees,
44
+ roomNumber: row.room_number,
45
+ consultationFee: Number(row.consultation_fee),
46
+ consultationDays: parseJsonValue(row.consultation_days),
47
+ consultationTimes: row.consultation_times,
48
+ isActive: Boolean(row.is_active),
49
+ createdAt: row.created_at,
50
+ updatedAt: row.updated_at,
51
+ };
52
+ }
53
+
54
+ function mapPatient(row) {
55
+ return {
56
+ phoneNumber: row.phone_number,
57
+ email: row.email,
58
+ name: row.name,
59
+ dateOfBirth: formatDateForFlutter(row.date_of_birth),
60
+ address: row.address,
61
+ gender: titleCase(row.gender),
62
+ createdAt: row.created_at,
63
+ updatedAt: row.updated_at,
64
+ };
65
+ }
66
+
67
+ function mapAppointment(row) {
68
+ return {
69
+ id: Number(row.id),
70
+ doctorId: row.doctor_id,
71
+ doctorName: row.doctor_name,
72
+ doctorDepartment: row.doctor_department,
73
+ doctorDesignation: row.doctor_designation,
74
+ doctorDegrees: row.doctor_degrees,
75
+ patientPhone: row.patient_phone,
76
+ patientName: row.patient_name,
77
+ date: row.appointment_date,
78
+ timeSlot: row.time_slot,
79
+ serialNumber: Number(row.serial_number),
80
+ status: titleCase(row.status),
81
+ notes: row.notes,
82
+ createdAt: row.created_at,
83
+ updatedAt: row.updated_at,
84
+ };
85
+ }
86
+
87
+ function mapPrescription(row, diagnosis = [], medicines = []) {
88
+ return {
89
+ id: row.id,
90
+ appointmentId: row.appointment_id ? Number(row.appointment_id) : null,
91
+ doctorId: row.doctor_id,
92
+ doctorName: row.doctor_name,
93
+ doctorDepartment: row.doctor_department,
94
+ doctorDesignation: row.doctor_designation,
95
+ doctorDegrees: row.doctor_degrees,
96
+ patientPhone: row.patient_phone,
97
+ patientName: row.patient_name,
98
+ appointmentDate: row.appointment_date,
99
+ issuedAt: row.issued_at,
100
+ diagnosis,
101
+ medicines,
102
+ additionalNotes: row.additional_notes,
103
+ pdfPath: row.pdf_path,
104
+ createdAt: row.created_at,
105
+ };
106
+ }
107
+
108
+ module.exports = {
109
+ mapDoctor,
110
+ mapPatient,
111
+ mapAppointment,
112
+ mapPrescription,
113
+ parseJsonValue,
114
+ titleCase,
115
+ };
src/utils/tokens.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const crypto = require('crypto');
2
+ const jwt = require('jsonwebtoken');
3
+ const env = require('../config/env');
4
+
5
+ function signAccessToken({ userType, userIdentifier, sessionId }) {
6
+ return jwt.sign(
7
+ {
8
+ type: 'access',
9
+ role: userType,
10
+ sessionId,
11
+ },
12
+ env.jwtSecret,
13
+ {
14
+ subject: userIdentifier,
15
+ expiresIn: `${env.accessTokenMinutes}m`,
16
+ },
17
+ );
18
+ }
19
+
20
+ function signRefreshToken({ userType, userIdentifier, sessionId }) {
21
+ return jwt.sign(
22
+ {
23
+ type: 'refresh',
24
+ role: userType,
25
+ sessionId,
26
+ },
27
+ env.jwtRefreshSecret,
28
+ {
29
+ subject: userIdentifier,
30
+ expiresIn: `${env.refreshTokenDays}d`,
31
+ },
32
+ );
33
+ }
34
+
35
+ function verifyAccessToken(token) {
36
+ return jwt.verify(token, env.jwtSecret);
37
+ }
38
+
39
+ function verifyRefreshToken(token) {
40
+ return jwt.verify(token, env.jwtRefreshSecret);
41
+ }
42
+
43
+ function hashToken(token) {
44
+ return crypto.createHash('sha256').update(token).digest('hex');
45
+ }
46
+
47
+ module.exports = {
48
+ hashToken,
49
+ signAccessToken,
50
+ signRefreshToken,
51
+ verifyAccessToken,
52
+ verifyRefreshToken,
53
+ };