Spaces:
Running
Running
MisbahKhan0009 commited on
Commit ·
6931883
0
Parent(s):
all files added
Browse files- .env.example +24 -0
- MAILCHIMP_SETUP.md +342 -0
- README.md +208 -0
- database/apply_schema.js +65 -0
- database/schema.sql +123 -0
- database/seed_doctors.js +86 -0
- package-lock.json +1470 -0
- package.json +35 -0
- postman/care_people.postman_collection.json +291 -0
- src/app.js +45 -0
- src/config/db.js +42 -0
- src/config/env.js +52 -0
- src/middleware/auth.js +41 -0
- src/middleware/errorHandler.js +27 -0
- src/routes/ai.routes.js +36 -0
- src/routes/appointments.routes.js +80 -0
- src/routes/auth.routes.js +183 -0
- src/routes/doctors.routes.js +33 -0
- src/routes/health.routes.js +19 -0
- src/routes/index.js +19 -0
- src/routes/patients.routes.js +36 -0
- src/routes/prescriptions.routes.js +54 -0
- src/server.js +19 -0
- src/services/ai.service.js +342 -0
- src/services/appointments.service.js +277 -0
- src/services/auth.service.js +164 -0
- src/services/doctors.service.js +95 -0
- src/services/mailchimp.service.js +115 -0
- src/services/patients.service.js +152 -0
- src/services/prescriptions.service.js +305 -0
- src/utils/apiResponse.js +12 -0
- src/utils/date.js +30 -0
- src/utils/email.js +42 -0
- src/utils/httpError.js +9 -0
- src/utils/otpStore.js +51 -0
- src/utils/phone.js +26 -0
- src/utils/serializers.js +115 -0
- src/utils/tokens.js +53 -0
.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 |
+
};
|