diff --git a/-home-enjy-work-GP-repo-Backend-src-dtos-.txt b/-home-enjy-work-GP-repo-Backend-src-dtos-.txt new file mode 100644 index 0000000..f44bfdd --- /dev/null +++ b/-home-enjy-work-GP-repo-Backend-src-dtos-.txt @@ -0,0 +1 @@ +/home/enjy/work/GP/repo/Backend/src/dtos/appointments.dto.ts \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0b2f116 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +# compiled output +.vscode +/node_modules + +# code formatter +.eslintrc +.eslintignore +.editorconfig +.huskyrc +.lintstagedrc.json +.prettierrc + +# test +jest.config.js + +# docker +Dockerfile +docker-compose.yml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3e22129 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/dist \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e073993 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,19 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["prettier", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "rules": { + "@typescript-eslint/explicit-member-accessibility": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/no-parameter-properties": 0, + "@typescript-eslint/interface-name-prefix": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-var-requires": "off", + "prettier/prettier": "off" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab3156e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Build and Deploy Blockchain-Based EMR System + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + setup-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npx prisma generate \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a05c1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,148 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ + +# Temporary folders +docker-compose-local.yml +docs +uploads +backblaze_cors.json + diff --git a/.huskyrc b/.huskyrc new file mode 100644 index 0000000..4d077c8 --- /dev/null +++ b/.huskyrc @@ -0,0 +1,5 @@ +{ + "hooks": { + "pre-commit": "lint-staged" + } +} diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..d2fe776 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,5 @@ +{ + "*.ts": [ + "npm run lint" + ] +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..93a5aaf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 150, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/.swcrc b/.swcrc new file mode 100644 index 0000000..c56acb7 --- /dev/null +++ b/.swcrc @@ -0,0 +1,40 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": false, + "dynamicImport": true, + "decorators": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "target": "es2017", + "externalHelpers": false, + "keepClassNames": true, + "loose": false, + "minify": { + "compress": false, + "mangle": false + }, + "baseUrl": "src", + "paths": { + "@/*": ["*"], + "@config": ["config"], + "@controllers/*": ["controllers/*"], + "@dtos/*": ["dtos/*"], + "@exceptions/*": ["exceptions/*"], + "@interfaces/*": ["interfaces/*"], + "@middlewares/*": ["middlewares/*"], + "@routes/*": ["routes/*"], + "@services/*": ["services/*"], + "@utils/*": ["utils/*"], + "@constants/*": ["constants/*"], + "@validators/*": ["validators/*"] + } + }, + "module": { + "type": "commonjs" + } +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..00ccfd7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node-terminal", + "request": "launch", + "name": "Dev typescript-express-starter", + "command": "npm run dev" + }, + { + "type": "node-terminal", + "request": "launch", + "name": "Start typescript-express-starter", + "command": "npm run start" + }, + { + "type": "node-terminal", + "request": "launch", + "name": "Test typescript-express-starter", + "command": "npm run test" + }, + { + "type": "node-terminal", + "request": "launch", + "name": "Lint typescript-express-starter", + "command": "npm run lint" + }, + { + "type": "node-terminal", + "request": "launch", + "name": "Lint:Fix typescript-express-starter", + "command": "npm run lint:fix" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4268241 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.formatOnSave": false, + "yaml.schemas": { + "https://www.schemastore.org/github-workflow.json": "file:///home/enjy/work/GP/repo/Backend/.github/workflows/ci.yml" + }, + "js/ts.tsdk.path": "node_modules\\typescript\\lib" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e53389 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# First stage: BUILD THE APP # + +# NodeJS Version 22 +FROM node:22-bullseye AS builder + +# Work to Dir +WORKDIR /app + +#copy package and prisma files +COPY package*.json ./ +COPY src/prisma ./src/prisma + +# Install Node Package +RUN npm ci --legacy-peer-deps + +# generate prisma client +RUN npx prisma generate + +# Copy rest of the app +COPY . . + + + +# second stage # + +FROM node:22-bullseye AS runner + +WORKDIR /app + +# copy only needed files +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/src/prisma ./src/prisma +COPY --from=builder /app/package*.json ./ + +# Set Env +ENV NODE_ENV=development + +EXPOSE 3000 + +# Cmd script +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/backup_neon_2026-02-03.sql b/backup_neon_2026-02-03.sql new file mode 100644 index 0000000..e69de29 diff --git a/data/backup_keys.json b/data/backup_keys.json new file mode 100644 index 0000000..b537e8d --- /dev/null +++ b/data/backup_keys.json @@ -0,0 +1,8 @@ +{ + "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa:614318c7-6890-44d1-971c-98899f46c3c9": "5d520490ead9510027da3b8100250cbc173c53641f755e61379f642ed45d5eed67b3358d2c10a865ca0e07f1ba9cd38c2b25e3b1bcabba9454228855", + "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa:f9538b49-2961-4a23-a6ef-e90ecaf46a58": "4bc632a9bf4dc85c7020f08ed93911efdfed0efa43f07119eda138c8299324a367751b485d9651258fbdca8fd429ece8d55b618d707d0e58fb59d957", + "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa:ef9983d2-4bef-49d6-b449-d29e009f3689": "88b271fffad3787c3affbb0c3be6d27ba2396229eda1119c3f0dc55443e610516a0e9d8945442820c14a942a8a722fffd03495f924a06be95cbdc7e0", + "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa:031577cd-2961-4504-b069-d206febd3496": "1af3e3c68c7a565ddb11c2fed2fd2b63b7777173ffedc10bb51e3b59066030d47c80119b55eedb1305e7c1e075897dcc649c95fbe2fc78bdbc44500b", + "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa:9a8cc315-c829-4b48-9cef-eec56b0dd340": "4e7051a9923735d4b4e51e3a21138bef0b375a9ae1a862c79c7a37a863adb895a9f1c22f2ee00db24ca3b4c4c4e3c211dd28f200ab934ee2dcf13316", + "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa:454f85de-5f53-4a39-8ecb-62f6add946c2": "bbf13ab3175b3169ffd292822249ce0dedc92cdab0d8fb7bf37d2e0ce872cee73c308bc9de1858a167bd48b5efb0a1e4bf49ae5dd65ca51cc81aa7e4" +} \ No newline at end of file diff --git a/data/backup_records.json b/data/backup_records.json new file mode 100644 index 0000000..df04a3c --- /dev/null +++ b/data/backup_records.json @@ -0,0 +1,56 @@ +[ + { + "patientId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "recordId": "614318c7-6890-44d1-971c-98899f46c3c9", + "doctorId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "type": "MEDICAL_HISTORY", + "ipfsCidKey": "bafkreiepvqgyjgffz6rqx63p5fgib5cu2lj2iq2kylhrzqsebkmjluhi5u", + "ownerMsp": "Org1MSP", + "authorizedMsps": [] + }, + { + "patientId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "recordId": "f9538b49-2961-4a23-a6ef-e90ecaf46a58", + "doctorId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "type": "MEDICAL_HISTORY", + "ipfsCidKey": "bafkreiexyo5c6hxzko3llm5ucgzn4ykudlk6rcrt3jqfy5knsqclwcynn4", + "ownerMsp": "Org1MSP", + "authorizedMsps": [] + }, + { + "patientId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "recordId": "ef9983d2-4bef-49d6-b449-d29e009f3689", + "doctorId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "type": "MEDICAL_HISTORY", + "ipfsCidKey": "bafybeigrcegro2w4fdoiovgzz7pjndemw2rhrc4buayhaca2wjxu5y574y", + "ownerMsp": "Org1MSP", + "authorizedMsps": [] + }, + { + "patientId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "recordId": "031577cd-2961-4504-b069-d206febd3496", + "doctorId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "type": "MEDICAL_HISTORY", + "ipfsCidKey": "bafybeicon54ajpytn557hvkup33adsn5un3iprsa3kvyg446573pfnzk5a", + "ownerMsp": "Org1MSP", + "authorizedMsps": [] + }, + { + "patientId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "recordId": "9a8cc315-c829-4b48-9cef-eec56b0dd340", + "doctorId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "type": "MEDICAL_HISTORY", + "ipfsCidKey": "bafkreifvjkay6n2yzi5hm4mf6pouo2ffxrbrpktde2j4vtjzl3suuiiddu", + "ownerMsp": "Org1MSP", + "authorizedMsps": [] + }, + { + "patientId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "recordId": "454f85de-5f53-4a39-8ecb-62f6add946c2", + "doctorId": "e8aa15ca-fc0a-43cf-8490-bd3b0be653fa", + "type": "MEDICAL_HISTORY", + "ipfsCidKey": "bafkreiehe3rnz5l6p2dfdhceatf6odbbyorzmz7qbeqim2l7auj3z7auey", + "ownerMsp": "Org1MSP", + "authorizedMsps": [] + } +] \ No newline at end of file diff --git a/data/fabric-identities.json b/data/fabric-identities.json new file mode 100644 index 0000000..f45f8d0 --- /dev/null +++ b/data/fabric-identities.json @@ -0,0 +1,143 @@ +[ + { + "clinicId": "default-clinic", + "mspId": "Org1MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nMIICKDCCAc+gAwIBAgIQB/y0JTTeIX9YJTG4B32NKzAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0yNjAzMTIyMTA5MDBaFw0zNjAzMDkyMTA5MDBa\nMGsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ4wDAYDVQQLEwVhZG1pbjEfMB0GA1UEAwwWQWRtaW5Ab3Jn\nMS5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAlxSOL5PKD3\ngf7sKVjrtdYc3lTmjGP7PF/H1esSeVoRFXZE2Xu9bQkmM1R9MdtBJeOaNBRdvLmw\nR7deaDvfdTajTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAIAz3e6oT1OeG2VfkhFHKxT8maPufyTbzAnPgsMK7GOruMAoGCCqGSM49\nBAMCA0cAMEQCIA7DcO+EFllKzGA64mMisRdWZ0T6Q326gvXqCJe4sQY9AiAidKZE\nGWDE7RL09PrGI4vWvJb3CXOw9kVOCPJoDbxXcQ==\n-----END CERTIFICATE-----", + "privateKey": "65a4e7218e736d7da865d2287a6a07ab:3b54797ec8eb91a9f041025f75582f19:6263a5005ac5c08cc55722b20dd0718714aa014c22f09cd21e9b8635bce3795d58ba8ccd1b30c9c8bcbf7b860551b2c99ca82de5e0a4f25429a33ad9c7c84ce018c668014c7db192cd152421ae7331e4aca3dfead5cd8b322177c06ba7ad5e9e36b71347ce9d65466d29cd7e536a7b80c33625abfef620c1b9d35272e5fa1e587ee227bba9a41a27c10c2acf5d711d05d09822acd46f8d46425c1c605e964940d699c6c569a1e984ec0bfa5be9c4fa9464392bd42f50abb2b36dfc6205b0a3a761c86df60849dcd9d903fff11dd87bb78dcf11a06cb636dfbc4bd7980fc57b7fd73100dc8ff5a3dcaf9997fd8f4101e3", + "peerEndpoint": "localhost:7051", + "peerHostAlias": "peer0.org1.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nMIICWDCCAf6gAwIBAgIRAIlhgIbqiuuU8Svs2N4e1/wwCgYIKoZIzj0EAwIwdjEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHzAdBgNVBAMTFnRs\nc2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMjYwMzEyMjEwOTAwWhcNMzYwMzA5MjEw\nOTAwWjB2MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE\nBxMNU2FuIEZyYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGFtcGxlLmNvbTEfMB0G\nA1UEAxMWdGxzY2Eub3JnMS5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49\nAwEHA0IABL2Zr9UiswvsOlBMQ6hV1/IIfZd/1rYPVLDBZUHsCsdILxQvNgvulrXf\nHVl6RAmqyX+ETaLIibVqPvNhv9hGTMijbTBrMA4GA1UdDwEB/wQEAwIBpjAdBgNV\nHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zApBgNV\nHQ4EIgQgO+IhPOwblZszvcBX5CIAi/MwT1rIpkoTbC1mN11GF7YwCgYIKoZIzj0E\nAwIDSAAwRQIhAJS/8CifyQA5P4zj9RN22wHZrfjCO/K0QDCVo5nPbLCwAiBJYCAm\n9x5eOopLkz+lc0plVVSjzJkyi/ZnzgZJPmAFng==\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": false, + "createdAt": "2026-03-08T17:44:00.694Z", + "updatedAt": "2026-03-12T21:26:18.651Z" + }, + { + "clinicId": "3928add5-ad4b-4819-ab4c-2bb319836c0c", + "mspId": "Org2MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nMIICKTCCAc+gAwIBAgIQKnCVhLWJTJRbYLhKcRwxeTAKBggqhkjOPQQDAjBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMi5leGFtcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMi5leGFtcGxlLmNvbTAeFw0yNjAzMTIyMTA5MDBaFw0zNjAzMDkyMTA5MDBa\nMGsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMQ4wDAYDVQQLEwVhZG1pbjEfMB0GA1UEAwwWQWRtaW5Ab3Jn\nMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHaLEL5li9DI\nq7eSNswj42cLGx2SB38AkrtLEwflWTKRXfsLk0hwcl1HZfJ7GhmoTAvsy76sEFt0\ncE7Di6tSEeqjTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1Ud\nIwQkMCKAICeIT9O7Dcy0uoIjis9k41LamWCsU7xZKJXVSs8zAc34MAoGCCqGSM49\nBAMCA0gAMEUCIQDA21YYc/aDEUHs6Ryn3vjzvRo2NkwD0dDj9UI+GAEdfQIgZQFo\nUsSeMvOD6vkHVXyvlxC/F0pBie0uqQSzo5PsT7E=\n-----END CERTIFICATE-----", + "privateKey": "8e90218a084c0dce3c2d1184e8d2ece0:0d15a4c6c131607674a595d43639e2f2:54753626cc14f81280ad3193fbd5fb8f477d94104a324e4e20faf038e4d9fb980f2dc27b91b732368050b3af1b730fcd926cafeca09083d6e76c6253cd4c88e5ec6827d150c465d00641d7876da7cbcbb4402c5650ca1205a715bbbc78c4a57ccfb2e9fb8b6593db832a8f4780c48a1453ca872710bf472a75ad0de19eaea2f3fa088b09f036724bda34c51cc18f7c1c0f12c4738218e9c780d166a02ce6e6b50389732662c54c85e3bb819dc96197d4a9244bf7fc4a6b2216b0d4f2aebdfd37ffab3bffec81bdee7148b56d8f9dea6066d530d4ecc55cb0879cea0f334e81cf13c1b1e0d69a46585f49683cef20066f", + "peerEndpoint": "localhost:9051", + "peerHostAlias": "peer0.org2.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nMIICWDCCAf6gAwIBAgIRAJ50tPpxZYvxeEKTBXxZN/AwCgYIKoZIzj0EAwIwdjEL\nMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG\ncmFuY2lzY28xGTAXBgNVBAoTEG9yZzIuZXhhbXBsZS5jb20xHzAdBgNVBAMTFnRs\nc2NhLm9yZzIuZXhhbXBsZS5jb20wHhcNMjYwMzEyMjEwOTAwWhcNMzYwMzA5MjEw\nOTAwWjB2MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE\nBxMNU2FuIEZyYW5jaXNjbzEZMBcGA1UEChMQb3JnMi5leGFtcGxlLmNvbTEfMB0G\nA1UEAxMWdGxzY2Eub3JnMi5leGFtcGxlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49\nAwEHA0IABI7C7U/w5OiekcEintFrDqoNPRJq4rKDAcRgUsl3U7Yxo10xfjSX95Oq\nGyPpqEDJCXzBJhrt5Zs/ho0UiCtppLGjbTBrMA4GA1UdDwEB/wQEAwIBpjAdBgNV\nHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zApBgNV\nHQ4EIgQgoo7nDFgwWZGFS3XOrh/p1C3FAyVtY1QapkjsLDYJYPswCgYIKoZIzj0E\nAwIDSAAwRQIgJPZJN14gdROzKM8jLOnJEd2lzmxTDMPSFL/n9ASLnzYCIQCM3hWh\nmvN3UdC2g9AvpmEqPtDihNcWVxBTwQb6MpGSag==\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": false, + "createdAt": "2026-03-08T17:44:00.694Z", + "updatedAt": "2026-03-12T21:26:12.849Z" + } + , + { + "clinicId": "3967d218-5197-4e9d-b833-9bc3e799f8ee", + "mspId": "Org3MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE1\n-----END CERTIFICATE-----", + "privateKey": "dummykey1:dummykey2:dummykey3", + "peerEndpoint": "localhost:11051", + "peerHostAlias": "peer0.org3.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT1\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:00:00.000Z", + "updatedAt": "2026-03-13T10:00:00.000Z" + }, + { + "clinicId": "39af5cd9-7704-4152-b1bd-bc2be7d17891", + "mspId": "Org4MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE2\n-----END CERTIFICATE-----", + "privateKey": "dummykey4:dummykey5:dummykey6", + "peerEndpoint": "localhost:12051", + "peerHostAlias": "peer0.org4.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT2\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:05:00.000Z", + "updatedAt": "2026-03-13T10:05:00.000Z" + }, + { + "clinicId": "499da8a2-f282-417b-b56d-a6395c45c6a3", + "mspId": "Org5MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE3\n-----END CERTIFICATE-----", + "privateKey": "dummykey7:dummykey8:dummykey9", + "peerEndpoint": "localhost:13051", + "peerHostAlias": "peer0.org5.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT3\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:10:00.000Z", + "updatedAt": "2026-03-13T10:10:00.000Z" + }, + { + "clinicId": "54ebf9e3-28e5-4199-91b8-4609a15ef341", + "mspId": "Org6MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE4\n-----END CERTIFICATE-----", + "privateKey": "dummykey10:dummykey11:dummykey12", + "peerEndpoint": "localhost:14051", + "peerHostAlias": "peer0.org6.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT4\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:15:00.000Z", + "updatedAt": "2026-03-13T10:15:00.000Z" + }, + { + "clinicId": "599eb1d4-4d44-40cb-a30a-5a73a13fe8dd", + "mspId": "Org7MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE5\n-----END CERTIFICATE-----", + "privateKey": "dummykey13:dummykey14:dummykey15", + "peerEndpoint": "localhost:15051", + "peerHostAlias": "peer0.org7.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT5\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:20:00.000Z", + "updatedAt": "2026-03-13T10:20:00.000Z" + }, + { + "clinicId": "71f687e2-3fe4-4032-be75-83b753a4a514", + "mspId": "Org8MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE6\n-----END CERTIFICATE-----", + "privateKey": "dummykey16:dummykey17:dummykey18", + "peerEndpoint": "localhost:16051", + "peerHostAlias": "peer0.org8.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT6\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:25:00.000Z", + "updatedAt": "2026-03-13T10:25:00.000Z" + }, + { + "clinicId": "7472ec01-186c-4363-8488-1e0fae6e6929", + "mspId": "Org9MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE7\n-----END CERTIFICATE-----", + "privateKey": "dummykey19:dummykey20:dummykey21", + "peerEndpoint": "localhost:17051", + "peerHostAlias": "peer0.org9.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT7\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:30:00.000Z", + "updatedAt": "2026-03-13T10:30:00.000Z" + }, + { + "clinicId": "b2436b3c-d080-4cd0-a29f-0168dbf41564", + "mspId": "Org10MSP", + "certificate": "-----BEGIN CERTIFICATE-----\nDUMMYCERTIFICATE8\n-----END CERTIFICATE-----", + "privateKey": "dummykey22:dummykey23:dummykey24", + "peerEndpoint": "localhost:18051", + "peerHostAlias": "peer0.org10.example.com", + "tlsCertificate": "-----BEGIN CERTIFICATE-----\nDUMMYTLSCERT8\n-----END CERTIFICATE-----", + "channelName": "mychannel", + "chaincodeName": "test", + "isDummy": true, + "createdAt": "2026-03-13T10:35:00.000Z", + "updatedAt": "2026-03-13T10:35:00.000Z" + } +] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..89a1fb7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,61 @@ + +services: + proxy: + container_name: proxy + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + restart: "unless-stopped" + depends_on: + - server + networks: + - backend + + server: + container_name: server + build: + context: ./ + dockerfile: Dockerfile + ports: + - "3001:3000" + - "5556:5555" + env_file: + - .env + environment: + NODE_ENV: development + volumes: + - ./:/app + - /app/node_modules + restart: "unless-stopped" + depends_on: + - postgres + networks: + - backend + command: npm run dev + + + postgres: + container_name: postgres_db + image: postgres:16 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=NG + - POSTGRES_PASSWORD=password + - POSTGRES_DB=EMR_DB + volumes: + - data:/var/lib/postgresql/data + restart: unless-stopped + networks: + - backend + + +networks: + backend: + driver: bridge + +volumes: + data: + driver: local diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..94cfa72 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,57 @@ +/** + * @description pm2 configuration file. + * @example + * production mode :: pm2 start ecosystem.config.js --only prod + * development mode :: pm2 start ecosystem.config.js --only dev + */ + module.exports = { + apps: [ + { + name: 'prod', // pm2 start App name + script: 'dist/server.js', + exec_mode: 'cluster', // 'cluster' or 'fork' + instance_var: 'INSTANCE_ID', // instance variable + instances: 2, // pm2 instance count + autorestart: true, // auto restart if process crash + watch: false, // files change automatic restart + ignore_watch: ['node_modules', 'logs'], // ignore files change + max_memory_restart: '1G', // restart if process use more than 1G memory + merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log + output: './logs/access.log', // pm2 log file + error: './logs/error.log', // pm2 error log file + env: { // environment variable + PORT: 3000, + NODE_ENV: 'production', + }, + }, + { + name: 'dev', // pm2 start App name + script: 'ts-node', // ts-node + args: '-r tsconfig-paths/register --transpile-only src/server.ts', // ts-node args + exec_mode: 'cluster', // 'cluster' or 'fork' + instance_var: 'INSTANCE_ID', // instance variable + instances: 2, // pm2 instance count + autorestart: true, // auto restart if process crash + watch: false, // files change automatic restart + ignore_watch: ['node_modules', 'logs'], // ignore files change + max_memory_restart: '1G', // restart if process use more than 1G memory + merge_logs: true, // if true, stdout and stderr will be merged and sent to pm2 log + output: './logs/access.log', // pm2 log file + error: './logs/error.log', // pm2 error log file + env: { // environment variable + PORT: 3000, + NODE_ENV: 'development', + }, + }, + ], + deploy: { + production: { + user: 'user', + host: '0.0.0.0', + ref: 'origin/master', + repo: 'git@github.com:repo.git', + path: 'dist/server.js', + 'post-deploy': 'npm install && npm run build && pm2 reload ecosystem.config.js --only prod', + }, + }, +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..8edf5ba --- /dev/null +++ b/jest.config.js @@ -0,0 +1,12 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig.json'); + +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/src' }), +}; diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..1450327 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,41 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + upstream api-server { + server server:3000; + keepalive 100; + } + + server { + listen 80; + server_name localhost; + + location / { + proxy_http_version 1.1; + proxy_pass http://api-server; + } + + } + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + include /etc/nginx/conf.d/*.conf; + client_max_body_size 5M; +} diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..9c4580d --- /dev/null +++ b/nodemon.json @@ -0,0 +1,12 @@ +{ + "watch": [ + "src", + ".env" + ], + "ext": "js,ts,json", + "ignore": [ + "src/logs/*", + "src/**/*.{spec,test}.ts" + ], + "exec": "ts-node -r tsconfig-paths/register --transpile-only src/server.ts" +} \ No newline at end of file diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..ec5fd8b --- /dev/null +++ b/notes.txt @@ -0,0 +1,35 @@ +const prisma = new PrismaClient(); + +export default prisma; +prisma.service.ts +prisma.TABLENAME + + +catchAsync--> to reduce try catch + +name of db fields to be snake Case + +Handle Image for user as general + +JIRA + +Admin: +- Create doctor +- Create Clinic +- Get all existing clinics to see if to link directly +- Link doctor to Clinic + +doctor: +- first time login to change password that is default +- according to clinic id , make the doctor schedule + +- Patient: +- Get all doctors with their clinic details respectively +- get the doctors schedule that is free and reserved +- each patient select one reservation per day +- Look for cancellation policy + +Clinic: +add clinic name +add location link as google maps + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8a31bb7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15964 @@ +{ + "name": "GP-Backend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "GP-Backend", + "version": "0.0.0", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.1002.0", + "@aws-sdk/s3-request-presigner": "^3.1002.0", + "@grpc/grpc-js": "^1.14.0", + "@hyperledger/fabric-gateway": "^1.9.0", + "@prisma/client": "6.18.0", + "agora-token": "^2.0.5", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cloudinary": "^2.8.0", + "compression": "^1.8.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "envalid": "^8.1.0", + "express": "^5.1.0", + "express-session": "^1.18.2", + "groq-sdk": "^0.37.0", + "helmet": "^8.1.0", + "hpp": "^0.2.3", + "ipfs-http-client": "^60.0.1", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", + "multer": "^2.0.2", + "node-cron": "^4.2.1", + "nodemailer": "^7.0.10", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "pinata": "^1.10.1", + "prisma": "6.18.0", + "reflect-metadata": "^0.2.2", + "socket.io": "^4.8.3", + "swagger-autogen": "^2.23.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "typedi": "^0.10.0", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "@swc/cli": "^0.7.8", + "@swc/core": "^1.14.0", + "@types/bcrypt": "^6.0.0", + "@types/compression": "^1.8.1", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/express-session": "^1.18.2", + "@types/hpp": "^0.2.7", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.0", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.3", + "@types/passport-google-oauth20": "^2.0.17", + "@types/socket.io": "^3.0.1", + "@types/supertest": "^6.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", + "cross-env": "^10.1.0", + "dotenv-cli": "^11.0.0", + "eslint": "^9.39.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "husky": "^9.1.7", + "jest": "^30.2.0", + "lint-staged": "^16.2.6", + "node-config": "^0.0.2", + "node-gyp": "^11.5.0", + "nodemon": "^3.1.10", + "pm2": "^6.0.13", + "prettier": "^3.6.2", + "supertest": "^7.1.4", + "ts-jest": "^29.4.5", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.16", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1002.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1002.0.tgz", + "integrity": "sha512-tc+vZgvjcm+1Ot+YhQjXZxVELKGGGO3D5cuR4p5xaeitXYX2+RRiz4/WdSak9slumIClnlXsdqhJ0OHognUT+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/credential-provider-node": "^3.972.16", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.6", + "@aws-sdk/middleware-expect-continue": "^3.972.6", + "@aws-sdk/middleware-flexible-checksums": "^3.973.3", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-location-constraint": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-sdk-s3": "^3.972.17", + "@aws-sdk/middleware-ssec": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.17", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/signature-v4-multi-region": "^3.996.5", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.2", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.7", + "@smithy/eventstream-serde-browser": "^4.2.10", + "@smithy/eventstream-serde-config-resolver": "^4.3.10", + "@smithy/eventstream-serde-node": "^4.2.10", + "@smithy/fetch-http-handler": "^5.3.12", + "@smithy/hash-blob-browser": "^4.2.11", + "@smithy/hash-node": "^4.2.10", + "@smithy/hash-stream-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/md5-js": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.21", + "@smithy/middleware-retry": "^4.4.38", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.13", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.37", + "@smithy/util-defaults-mode-node": "^4.2.40", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-stream": "^4.5.16", + "@smithy/util-utf8": "^4.2.1", + "@smithy/util-waiter": "^4.2.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.17.tgz", + "integrity": "sha512-VtgGP0TjbCeyp6DQpiBqJKbemTSIaN2bZc3UbeTDCani3lBCyxn75ouJYD6koSSp0bh7rKLEbUpiFsNCI7tr0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/xml-builder": "^3.972.9", + "@smithy/core": "^3.23.7", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.15.tgz", + "integrity": "sha512-RhHQG1lhkWHL4tK1C/KDjaOeis+9U0tAMnWDiwiSVQZMC7CsST9Xin+sK89XywJ5g/tyABtb7TvFePJ4Te5XSQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.17.tgz", + "integrity": "sha512-b/bDL76p51+yQ+0O9ZDH5nw/ioE0sRYkjwjOwFWAWZXo6it2kQZUOXhVpjohx3ldKyUxt/SwAivjUu1Nr/PWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/fetch-http-handler": "^5.3.12", + "@smithy/node-http-handler": "^4.4.13", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.16", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.15.tgz", + "integrity": "sha512-qWnM+wB8MmU2kKY7f4KowKjOjkwRosaFxrtseEEIefwoXn1SjN+CbHzXBVdTAQxxkbBiqhPgJ/WHiPtES4grRQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/credential-provider-env": "^3.972.15", + "@aws-sdk/credential-provider-http": "^3.972.17", + "@aws-sdk/credential-provider-login": "^3.972.15", + "@aws-sdk/credential-provider-process": "^3.972.15", + "@aws-sdk/credential-provider-sso": "^3.972.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.15", + "@aws-sdk/nested-clients": "^3.996.5", + "@aws-sdk/types": "^3.973.4", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.16.tgz", + "integrity": "sha512-7mlt14Ee4rPFAFUVgpWE7+0CBhetJJyzVFqfIsMp7sgyOSm9Y/+qHZOWAuK5I4JNc+Y5PltvJ9kssTzRo92iXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.15", + "@aws-sdk/credential-provider-http": "^3.972.17", + "@aws-sdk/credential-provider-ini": "^3.972.15", + "@aws-sdk/credential-provider-process": "^3.972.15", + "@aws-sdk/credential-provider-sso": "^3.972.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.15", + "@aws-sdk/types": "^3.973.4", + "@smithy/credential-provider-imds": "^4.2.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.15.tgz", + "integrity": "sha512-PrH3iTeD18y/8uJvQD2s/T87BTGhsdS/1KZU7ReWHXsplBwvCqi7AbnnNbML1pFlQwRWCE2RdSZFWDVId3CvkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.15.tgz", + "integrity": "sha512-M/+LBHTPKZxxXckM6m4dnJeR+jlm9NynH9b2YDswN4Zj2St05SK/crdL3Wy3WfJTZootnnhm3oTh87Usl7PS7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/nested-clients": "^3.996.5", + "@aws-sdk/token-providers": "3.1002.0", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.15.tgz", + "integrity": "sha512-QTH6k93v+UOfFam/ado8zc71tH+enTVyuvLy9uEWXX1x894dN5ovtf/MdBDgFwq3g6c9mbtgVJ4B+yBqDtXvdA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/nested-clients": "^3.996.5", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.6.tgz", + "integrity": "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.6.tgz", + "integrity": "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.6.tgz", + "integrity": "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.17.tgz", + "integrity": "sha512-uSyOGoVFMP44pTt29MIMfsOjegqE/7lT0K3HG0GWPiH2lD4rqZC/TRi/kH4zrGiOQdsaLc+dkfd7Sb2q8vh+gA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.23.7", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.16", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.17.tgz", + "integrity": "sha512-HHArkgWzomuwufXwheQqkddu763PWCpoNTq1dGjqXzJT/lojX3VlOqjNSR2Xvb6/T9ISfwYcMOcbFgUp4EWxXA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@smithy/core": "^3.23.7", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.5.tgz", + "integrity": "sha512-zn0WApcULn7Rtl6T+KP2CQTZo/7wOa2YV1yHQnbijTQoi4YXQHM8s21JcJzt33/mqPh8AdvWX1f+83KvKuxlZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.17", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.2", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.7", + "@smithy/fetch-http-handler": "^5.3.12", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.21", + "@smithy/middleware-retry": "^4.4.38", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.13", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.37", + "@smithy/util-defaults-mode-node": "^4.2.40", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.6.tgz", + "integrity": "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/config-resolver": "^4.4.9", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.5.tgz", + "integrity": "sha512-AVIhf74wRMzU1WBPVzcGPjlADF5VxZ8m8Ctm1v7eO4/reWMhZnEBn4tlR4vM4pOYFkdrYp3MTzYVZIikCO+53Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { + "version": "3.1002.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1002.0.tgz", + "integrity": "sha512-x972uKOydFn4Rb0PZJzLdNW59rH0KWC78Q2JbQzZpGlGt0DxjYdDRwBG6F42B1MyaEwHGqO/tkGc4r3/PRFfMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/nested-clients": "^3.996.5", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz", + "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-endpoints": "^3.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.6.tgz", + "integrity": "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.2.tgz", + "integrity": "sha512-lpaIuekdkpw7VRiik0IZmd6TyvEUcuLgKZ5fKRGpCA3I4PjrD/XH15sSwW+OptxQjNU4DEzSxag70spC9SluvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.9.tgz", + "integrity": "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.922.0.tgz", + "integrity": "sha512-cowHCdzir4KmT/MoRyp2RV3BAebjcpiyKU1pidu2D1lI87iGXlxNG7KXJ0W8mjQoGpKa2XcihDY/mtqd/6uVlA==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/signature-v4-multi-region": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.922.0.tgz", + "integrity": "sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.3.tgz", + "integrity": "sha512-UExeK+EFiq5LAcbHm96CQLSia+5pvpUVSAsVApscBzayb7/6dJBJKwV4/onsk4VbWSmqxDMcfuTD+pC4RxgZHg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.922.0.tgz", + "integrity": "sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.15.tgz", + "integrity": "sha512-x92FJy34/95wgu+qOGD8SHcgh1hZ9Qx2uFtQEGn4m9Ljou8ICIv3Ybq5yxdB7A60S8ZGCQB0mIopmjJwiLbh5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/nested-clients": "^3.996.5", + "@aws-sdk/types": "^3.973.4", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/shared-ini-file-loader": "^4.4.5", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.17.tgz", + "integrity": "sha512-VtgGP0TjbCeyp6DQpiBqJKbemTSIaN2bZc3UbeTDCani3lBCyxn75ouJYD6koSSp0bh7rKLEbUpiFsNCI7tr0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/xml-builder": "^3.972.9", + "@smithy/core": "^3.23.7", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.6.tgz", + "integrity": "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.6.tgz", + "integrity": "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.6.tgz", + "integrity": "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.17.tgz", + "integrity": "sha512-HHArkgWzomuwufXwheQqkddu763PWCpoNTq1dGjqXzJT/lojX3VlOqjNSR2Xvb6/T9ISfwYcMOcbFgUp4EWxXA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@smithy/core": "^3.23.7", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.5.tgz", + "integrity": "sha512-zn0WApcULn7Rtl6T+KP2CQTZo/7wOa2YV1yHQnbijTQoi4YXQHM8s21JcJzt33/mqPh8AdvWX1f+83KvKuxlZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/middleware-host-header": "^3.972.6", + "@aws-sdk/middleware-logger": "^3.972.6", + "@aws-sdk/middleware-recursion-detection": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.17", + "@aws-sdk/region-config-resolver": "^3.972.6", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-endpoints": "^3.996.3", + "@aws-sdk/util-user-agent-browser": "^3.972.6", + "@aws-sdk/util-user-agent-node": "^3.973.2", + "@smithy/config-resolver": "^4.4.9", + "@smithy/core": "^3.23.7", + "@smithy/fetch-http-handler": "^5.3.12", + "@smithy/hash-node": "^4.2.10", + "@smithy/invalid-dependency": "^4.2.10", + "@smithy/middleware-content-length": "^4.2.10", + "@smithy/middleware-endpoint": "^4.4.21", + "@smithy/middleware-retry": "^4.4.38", + "@smithy/middleware-serde": "^4.2.11", + "@smithy/middleware-stack": "^4.2.10", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/node-http-handler": "^4.4.13", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-body-length-browser": "^4.2.1", + "@smithy/util-body-length-node": "^4.2.2", + "@smithy/util-defaults-mode-browser": "^4.3.37", + "@smithy/util-defaults-mode-node": "^4.2.40", + "@smithy/util-endpoints": "^3.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-retry": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.6.tgz", + "integrity": "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/config-resolver": "^4.4.9", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz", + "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.10", + "@smithy/util-endpoints": "^3.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.6.tgz", + "integrity": "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.2.tgz", + "integrity": "sha512-lpaIuekdkpw7VRiik0IZmd6TyvEUcuLgKZ5fKRGpCA3I4PjrD/XH15sSwW+OptxQjNU4DEzSxag70spC9SluvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.9.tgz", + "integrity": "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", + "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", + "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", + "dev": true, + "dependencies": { + "@aws-sdk/client-sso": "3.922.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", + "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.6.tgz", + "integrity": "sha512-3H2bhvb7Cb/S6WFsBy/Dy9q2aegC9JmGH1inO8Lb2sWirSqpLJlZmvQHPE29h2tIxzv6el/14X/tLCQ8BQU6ZQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.6.tgz", + "integrity": "sha512-QMdffpU+GkSGC+bz6WdqlclqIeCsOfgX8JFZ5xvwDtX+UTj4mIXm3uXu7Ko6dBseRcJz1FA6T9OmlAAY6JgJUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.973.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.973.3.tgz", + "integrity": "sha512-C9Mu9pXMpQh7jBydx0MrfQxNIKwJvKbVbJJ0GZthM+cQ+KTChXA01MwttRsMq0ZRb4pBJZQtIKDUxXusDr5OKg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/crc64-nvme": "^3.972.3", + "@aws-sdk/types": "^3.973.4", + "@smithy/is-array-buffer": "^4.2.1", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.16", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.17.tgz", + "integrity": "sha512-VtgGP0TjbCeyp6DQpiBqJKbemTSIaN2bZc3UbeTDCani3lBCyxn75ouJYD6koSSp0bh7rKLEbUpiFsNCI7tr0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/xml-builder": "^3.972.9", + "@smithy/core": "^3.23.7", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.9.tgz", + "integrity": "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.6.tgz", + "integrity": "sha512-XdZ2TLwyj3Am6kvUc67vquQvs6+D8npXvXgyEUJAdkUDx5oMFJKOqpK+UpJhVDsEL068WAJl2NEGzbSik7dGJQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.922.0.tgz", + "integrity": "sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.6.tgz", + "integrity": "sha512-acvMUX9jF4I2Ew+Z/EA6gfaFaz9ehci5wxBmXCZeulLuv8m+iGf6pY9uKz8TPjg39bdAz3hxoE0eLP8Qz+IYlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", + "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", + "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1002.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1002.0.tgz", + "integrity": "sha512-vzbygdP2KMRoD7jheRNBlYVvrmGrwyeec+6KwHiM9AtFQ+tx4EvF8x0Wo+7FjVn1PL3t5Do7i54f4ozKCYJleQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "^3.996.5", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-format-url": "^3.972.6", + "@smithy/middleware-endpoint": "^4.4.21", + "@smithy/protocol-http": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/core": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.17.tgz", + "integrity": "sha512-VtgGP0TjbCeyp6DQpiBqJKbemTSIaN2bZc3UbeTDCani3lBCyxn75ouJYD6koSSp0bh7rKLEbUpiFsNCI7tr0w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/xml-builder": "^3.972.9", + "@smithy/core": "^3.23.7", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/property-provider": "^4.2.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.17.tgz", + "integrity": "sha512-uSyOGoVFMP44pTt29MIMfsOjegqE/7lT0K3HG0GWPiH2lD4rqZC/TRi/kH4zrGiOQdsaLc+dkfd7Sb2q8vh+gA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.17", + "@aws-sdk/types": "^3.973.4", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.23.7", + "@smithy/node-config-provider": "^4.3.10", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/smithy-client": "^4.12.1", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.1", + "@smithy/util-middleware": "^4.2.10", + "@smithy/util-stream": "^4.5.16", + "@smithy/util-utf8": "^4.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.5.tgz", + "integrity": "sha512-AVIhf74wRMzU1WBPVzcGPjlADF5VxZ8m8Ctm1v7eO4/reWMhZnEBn4tlR4vM4pOYFkdrYp3MTzYVZIikCO+53Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.17", + "@aws-sdk/types": "^3.973.4", + "@smithy/protocol-http": "^5.3.10", + "@smithy/signature-v4": "^5.3.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.9.tgz", + "integrity": "sha512-ItnlMgSqkPrUfJs7EsvU/01zw5UeIb2tNPhD09LBLHbg+g+HDiKibSLwpkuz/ZIlz4F2IMn+5XgE4AK/pfPuog==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner/node_modules/fast-xml-parser": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.922.0.tgz", + "integrity": "sha512-mmsgEEL5pE+A7gFYiJMDBCLVciaXq4EFI5iAP7bPpnHvOplnNOYxVy2IreKMllGvrfjVyLnwxzZYlo5zZ65FWg==", + "dev": true, + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.922.0.tgz", + "integrity": "sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==", + "dev": true, + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dev": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.6.tgz", + "integrity": "sha512-0YNVNgFyziCejXJx0rzxPiD2rkxTWco4c9wiMF6n37Tb9aQvIF8+t7GyEyIFCwQHZ0VMQaAl+nCZHOYz5I5EKw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.4", + "@smithy/querystring-builder": "^4.2.10", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url/node_modules/@aws-sdk/types": { + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz", + "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", + "dev": true, + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", + "dev": true, + "dependencies": { + "@smithy/types": "^4.8.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "dev": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@chainsafe/is-ip": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chainsafe/is-ip/-/is-ip-2.1.0.tgz", + "integrity": "sha512-KIjt+6IfysQ4GCv66xihEitBjvhU/bixbbbFxdJ1sqCp4uJ0wuZiYBPhksZoy4lfaF0k9cwNzY5upEW/VWdw3w==", + "license": "MIT" + }, + "node_modules/@chainsafe/netmask": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@chainsafe/netmask/-/netmask-2.0.0.tgz", + "integrity": "sha512-I3Z+6SWUoaljh3TBzCnCxjlUyN8tA+NAk5L6m9IxvCf1BENQTePzPMis97CoN/iMW1St3WN+AWCCRp+TTBRiDg==", + "license": "MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", + "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@hyperledger/fabric-gateway": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@hyperledger/fabric-gateway/-/fabric-gateway-1.9.0.tgz", + "integrity": "sha512-q5lFrzbKsKdMgMGhaEE4dVXtpQa4qyWMdD1RXJFki6BiiKOzZC7IEV3xj67ffSaD33iYztxomYxlHVQJqD21HQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.0", + "@hyperledger/fabric-protos": "^0.3.0", + "@noble/curves": "^1.9.4", + "google-protobuf": "^3.21.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "pkcs11js": "^2.1.0" + } + }, + "node_modules/@hyperledger/fabric-protos": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@hyperledger/fabric-protos/-/fabric-protos-0.3.7.tgz", + "integrity": "sha512-p69dVT+QKrL7OZOuWRrimopNUAQL+VpgVEovud5MGqHSMl20S5hZy0aWqmIW+qasRgJiHLNuU0T6xVfXJIeHKg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.11.0", + "google-protobuf": "^3.21.0" + }, + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/@ipld/dag-cbor": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-9.2.5.tgz", + "integrity": "sha512-84wSr4jv30biui7endhobYhXBQzQE4c/wdoWlFrKcfiwH+ofaPg8fwsM8okX9cOzkkrsAsNdDyH3ou+kiLquwQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "cborg": "^4.0.0", + "multiformats": "^13.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-cbor/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@ipld/dag-json": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-json/-/dag-json-10.2.5.tgz", + "integrity": "sha512-Q4Fr3IBDEN8gkpgNefynJ4U/ZO5Kwr7WSUMBDbZx0c37t0+IwQCTM9yJh8l5L4SRFjm31MuHwniZ/kM+P7GQ3Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "cborg": "^4.0.0", + "multiformats": "^13.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-json/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@ipld/dag-pb": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@ipld/dag-pb/-/dag-pb-4.1.5.tgz", + "integrity": "sha512-w4PZ2yPqvNmlAir7/2hsCRMqny1EY5jj26iZcSgxREJexmbAc2FI21jp26MqiNdfgAxvkCnf2N/TJI18GaDNwA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.1.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@ipld/dag-pb/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@libp2p/interface-connection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@libp2p/interface-connection/-/interface-connection-4.0.0.tgz", + "integrity": "sha512-6xx/NmEc84HX7QmsjSC3hHredQYjHv4Dkf4G27adAPf+qN+vnPxmQ7gaTnk243a0++DOFTbZ2gKX/15G2B6SRg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "@multiformats/multiaddr": "^12.0.0", + "it-stream-types": "^1.0.4", + "uint8arraylist": "^2.1.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/interface-connection/node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@libp2p/interface-connection/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@libp2p/interface-connection/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@libp2p/interface-keychain": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@libp2p/interface-keychain/-/interface-keychain-2.0.5.tgz", + "integrity": "sha512-mb7QNgn9fIvC7CaJCi06GJ+a6DN6RVT9TmEi0NmedZGATeCArPeWWG7r7IfxNVXb9cVOOE1RzV1swK0ZxEJF9Q==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "multiformats": "^11.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/interface-peer-id": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@libp2p/interface-peer-id/-/interface-peer-id-2.0.2.tgz", + "integrity": "sha512-9pZp9zhTDoVwzRmp0Wtxw0Yfa//Yc0GqBCJi3EznBDE6HGIAVvppR91wSh2knt/0eYg0AQj7Y35VSesUTzMCUg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^11.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/interface-peer-info": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@libp2p/interface-peer-info/-/interface-peer-info-1.0.10.tgz", + "integrity": "sha512-HQlo8NwQjMyamCHJrnILEZz+YwEOXCB2sIIw3slIrhVUYeYlTaia1R6d9umaAeLHa255Zmdm4qGH8rJLRqhCcg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@multiformats/multiaddr": "^12.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/interface-peer-info/node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@libp2p/interface-peer-info/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@libp2p/interface-peer-info/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@libp2p/interface-pubsub": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@libp2p/interface-pubsub/-/interface-pubsub-3.0.7.tgz", + "integrity": "sha512-+c74EVUBTfw2sx1GE/z/IjsYO6dhur+ukF0knAppeZsRQ1Kgg6K5R3eECtT28fC6dBWLjFpAvW/7QGfiDAL4RA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface-connection": "^4.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.0.0", + "it-pushable": "^3.0.0", + "uint8arraylist": "^2.1.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/interfaces": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@libp2p/interfaces/-/interfaces-3.3.2.tgz", + "integrity": "sha512-p/M7plbrxLzuQchvNwww1Was7ZeGE2NaOFulMaZBYIihU8z3fhaV+a033OqnC/0NTX/yhfdNOG7znhYq3XoR/g==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/logger": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@libp2p/logger/-/logger-2.1.1.tgz", + "integrity": "sha512-2UbzDPctg3cPupF6jrv6abQnAUTrbLybNOj0rmmrdGm1cN2HJ1o/hBu0sXuq4KF9P1h/eVRn1HIRbVIEKnEJrA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.2", + "@multiformats/multiaddr": "^12.1.3", + "debug": "^4.3.4", + "interface-datastore": "^8.2.0", + "multiformats": "^11.0.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@libp2p/logger/node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@libp2p/logger/node_modules/@multiformats/multiaddr/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@libp2p/logger/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@libp2p/logger/node_modules/uint8arrays/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@libp2p/peer-id": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@libp2p/peer-id/-/peer-id-2.0.4.tgz", + "integrity": "sha512-gcOsN8Fbhj6izIK+ejiWsqiqKeJ2yWPapi/m55VjOvDa52/ptQzZszxQP8jUk93u36de92ATFXDfZR/Bi6eeUQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.2.0", + "multiformats": "^11.0.0", + "uint8arrays": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@multiformats/dns": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@multiformats/dns/-/dns-1.0.10.tgz", + "integrity": "sha512-6X200ceQLns0b/CU0S/So16tGjB5eIXHJ1xvJMPoWaKFHWSgfpW2EhkWJrqap4U3+c37zcowVR0ToPXeYEL7Vw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "buffer": "^6.0.3", + "dns-packet": "^5.6.1", + "hashlru": "^2.3.0", + "p-queue": "^9.0.0", + "progress-events": "^1.0.0", + "uint8arrays": "^5.0.2" + } + }, + "node_modules/@multiformats/dns/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@multiformats/dns/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@multiformats/dns/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@multiformats/mafmt": { + "version": "12.1.6", + "resolved": "https://registry.npmjs.org/@multiformats/mafmt/-/mafmt-12.1.6.tgz", + "integrity": "sha512-tlJRfL21X+AKn9b5i5VnaTD6bNttpSpcqwKVmDmSHLwxoz97fAHaepqFOk/l1fIu94nImIXneNbhsJx/RQNIww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.0.0" + } + }, + "node_modules/@multiformats/mafmt/node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@multiformats/mafmt/node_modules/multiformats": { + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz", + "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@multiformats/mafmt/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@multiformats/multiaddr": { + "version": "11.6.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-11.6.1.tgz", + "integrity": "sha512-doST0+aB7/3dGK9+U5y3mtF3jq85KGbke1QiH0KE1F5mGQ9y56mFebTeu2D9FNOm+OT6UHb8Ss8vbSnpGjeLNw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "dns-over-http-resolver": "^2.1.0", + "err-code": "^3.0.1", + "multiformats": "^11.0.0", + "uint8arrays": "^4.0.2", + "varint": "^6.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@multiformats/multiaddr-to-uri": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-to-uri/-/multiaddr-to-uri-9.0.8.tgz", + "integrity": "sha512-4eiN5iEiQfy2A98BxekUfW410L/ivg0sgjYSgSqmklnrBhK+QyMz4yqgfkub8xDTXOc7O5jp4+LVyM3ZqMeWNw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/multiaddr": "^12.0.0" + } + }, + "node_modules/@multiformats/multiaddr-to-uri/node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/@multiformats/multiaddr-to-uri/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/@multiformats/multiaddr-to-uri/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/@multiformats/multiaddr/node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@pm2/agent": { + "version": "2.1.1", + "dev": true, + "license": "AGPL-3.0", + "dependencies": { + "async": "~3.2.0", + "chalk": "~3.0.0", + "dayjs": "~1.8.24", + "debug": "~4.3.1", + "eventemitter2": "~5.0.1", + "fast-json-patch": "^3.1.0", + "fclone": "~1.0.11", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.0", + "proxy-agent": "~6.4.0", + "semver": "~7.5.0", + "ws": "~7.5.10" + } + }, + "node_modules/@pm2/agent/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@pm2/agent/node_modules/dayjs": { + "version": "1.8.36", + "dev": true, + "license": "MIT" + }, + "node_modules/@pm2/agent/node_modules/debug": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/agent/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/agent/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@pm2/blessed": { + "version": "0.1.81", + "dev": true, + "license": "MIT", + "bin": { + "blessed": "bin/tput.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@pm2/io": { + "version": "6.1.0", + "dev": true, + "license": "Apache-2", + "dependencies": { + "async": "~2.6.1", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "require-in-the-middle": "^5.0.0", + "semver": "~7.5.4", + "shimmer": "^1.2.0", + "signal-exit": "^3.0.3", + "tslib": "1.9.3" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@pm2/io/node_modules/async": { + "version": "2.6.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/io/node_modules/debug": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/io/node_modules/eventemitter2": { + "version": "6.4.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@pm2/io/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pm2/io/node_modules/tslib": { + "version": "1.9.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@pm2/io/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@pm2/js-api": { + "version": "0.8.0", + "dev": true, + "license": "Apache-2", + "dependencies": { + "async": "^2.6.3", + "debug": "~4.3.1", + "eventemitter2": "^6.3.1", + "extrareqp2": "^1.0.0", + "ws": "^7.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@pm2/js-api/node_modules/async": { + "version": "2.6.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@pm2/js-api/node_modules/debug": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@pm2/js-api/node_modules/eventemitter2": { + "version": "6.4.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@pm2/pm2-version-check": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", + "integrity": "sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.1" + } + }, + "node_modules/@prisma/client": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", + "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz", + "integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", + "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==" + }, + "node_modules/@prisma/engines": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", + "integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==", + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.18.0", + "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "@prisma/fetch-engine": "6.18.0", + "@prisma/get-platform": "6.18.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz", + "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", + "integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==", + "dependencies": { + "@prisma/debug": "6.18.0", + "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", + "@prisma/get-platform": "6.18.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz", + "integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==", + "dependencies": { + "@prisma/debug": "6.18.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", + "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", + "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.8.tgz", + "integrity": "sha512-f7uPeBi7ehmLT4YF2u9j3qx6lSnurG1DLXOsTtJrIRNDF7VXio4BGHQ+SQteN/BrUVudbkuL4v7oOsRCzq4BqA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.12", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", + "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.11.tgz", + "integrity": "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.11.tgz", + "integrity": "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.11.tgz", + "integrity": "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.11.tgz", + "integrity": "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.11.tgz", + "integrity": "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", + "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.12.tgz", + "integrity": "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", + "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.11.tgz", + "integrity": "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", + "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.11.tgz", + "integrity": "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", + "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.22", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.22.tgz", + "integrity": "sha512-sc81w1o4Jy+/MAQlY3sQ8C7CmSpcvIi3TAzXblUv2hjG11BBSJi/Cw8vDx5BxMxapuH2I+Gc+45vWsgU07WZRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.8", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-middleware": "^4.2.11", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.39", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.39.tgz", + "integrity": "sha512-MCVCxaCzuZgiHtHGV2Ke44nh6t4+8/tO+rTYOzrr2+G4nMLU/qbzNCWKBX54lyEaVcGQrfOJiG2f8imtiw+nIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", + "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", + "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", + "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", + "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", + "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", + "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", + "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", + "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", + "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", + "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", + "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.2.tgz", + "integrity": "sha512-HezY3UuG0k4T+4xhFKctLXCA5N2oN+Rtv+mmL8Gt7YmsUY2yhmcLyW75qrSzldfj75IsCW/4UhY3s20KcFnZqA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.8", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", + "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.38", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.38.tgz", + "integrity": "sha512-c8P1mFLNxcsdAMabB8/VUQUbWzFmgujWi4bAXSggcqLYPc8V4U5abqFqOyn+dK4YT+q8UyCVkTO8807t4t2syA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.41", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.41.tgz", + "integrity": "sha512-/UG+9MT3UZAR0fLzOtMJMfWGcjjHvgggq924x/CRy8vRbL+yFf3Z6vETlvq8vDH92+31P/1gSOFoo7303wN8WQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.10", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", + "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", + "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", + "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.17", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", + "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.11.tgz", + "integrity": "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.11", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==" + }, + "node_modules/@swc/cli": { + "version": "0.7.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3", + "@xhmikosr/bin-wrapper": "^13.0.5", + "commander": "^8.3.0", + "minimatch": "^9.0.3", + "piscina": "^4.3.1", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3", + "tinyglobby": "^0.2.13" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 16.14.0" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^4.0.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/core": { + "version": "1.14.0", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.25" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.14.0", + "@swc/core-darwin-x64": "1.14.0", + "@swc/core-linux-arm-gnueabihf": "1.14.0", + "@swc/core-linux-arm64-gnu": "1.14.0", + "@swc/core-linux-arm64-musl": "1.14.0", + "@swc/core-linux-x64-gnu": "1.14.0", + "@swc/core-linux-x64-musl": "1.14.0", + "@swc/core-win32-arm64-msvc": "1.14.0", + "@swc/core-win32-ia32-msvc": "1.14.0", + "@swc/core-win32-x64-msvc": "1.14.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/hpp": { + "version": "0.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/nodemailer": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-fC8w49YQ868IuPWRXqPfLf+MuTRex5Z1qxMoG8rr70riqqbOp2F5xgOKE9fODEBPzpnvjkJXFgK6IL2xgMSTnA==", + "dev": true, + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz", + "integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/swagger-jsdoc": { + "version": "6.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.4", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.34", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@xhmikosr/archive-type": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^20.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/bin-check": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "isexe": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/bin-wrapper": { + "version": "13.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/bin-check": "^7.1.0", + "@xhmikosr/downloader": "^15.2.0", + "@xhmikosr/os-filter-obj": "^3.0.0", + "bin-version-check": "^5.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress": { + "version": "10.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.1.0", + "@xhmikosr/decompress-tarbz2": "^8.1.0", + "@xhmikosr/decompress-targz": "^8.1.0", + "@xhmikosr/decompress-unzip": "^7.1.0", + "graceful-fs": "^4.2.11", + "strip-dirs": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^20.5.0", + "is-stream": "^2.0.1", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^20.5.0", + "is-stream": "^2.0.1", + "seek-bzip": "^2.0.0", + "unbzip2-stream": "^1.4.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-targz": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^20.5.0", + "is-stream": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-unzip": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^20.5.0", + "get-stream": "^6.0.1", + "yauzl": "^3.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/downloader": { + "version": "15.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@xhmikosr/archive-type": "^7.1.0", + "@xhmikosr/decompress": "^10.2.0", + "content-disposition": "^0.5.4", + "defaults": "^2.0.2", + "ext-name": "^5.0.0", + "file-type": "^20.5.0", + "filenamify": "^6.0.0", + "get-stream": "^6.0.1", + "got": "^13.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/os-filter-obj": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^3.0.0" + }, + "engines": { + "node": "^14.14.0 || >=16.0.0" + } + }, + "node_modules/abbrev": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abort-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/abort-error/-/abort-error-1.0.1.tgz", + "integrity": "sha512-fxqCblJiIPdSXIUrxI0PL+eJG49QdP9SQ70qtB65MVAoMr2rASlOyAbJFOylfB467F/f+5BCLJJq58RYi7mGfg==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/agora-token": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/agora-token/-/agora-token-2.0.5.tgz", + "integrity": "sha512-0NcbzC3iuutlksv3b4bCMKHrW3pko6gdiGEMRo6APDice24kfXAuWyAlG9hRBrrPBVDShwm9/GUz2Scd3zuZQw==", + "license": "ISC", + "dependencies": { + "crc-32": "^1.2.0", + "cuint": "0.2.2", + "md5": "^2.3.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/amp": { + "version": "0.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/amp-message": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "amp": "0.3.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.0.0-node10", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/any-signal": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-3.0.1.tgz", + "integrity": "sha512-xgZgJtKEa9YmDqXodIgl7Fl1C8yNXr8w6gXjqK3LW4GcEiYT+6AQfJSE/8SPsEpLLmcvbv8YU+qet94UewHxqg==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/arch": { + "version": "3.0.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.7.3", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.8.1", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.22", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bin-version": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blob-to-it": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/blob-to-it/-/blob-to-it-2.0.10.tgz", + "integrity": "sha512-I39vO57y+LBEIcAV7fif0sn96fYOYVqrPiOD+53MxQGv4DBgt1/HHZh0BHheWx2hVe24q5LTSXxqeV1Y3Nzkgg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "browser-readablestream-to-it": "^2.0.0" + } + }, + "node_modules/bodec": { + "version": "0.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-readablestream-to-it": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/browser-readablestream-to-it/-/browser-readablestream-to-it-2.0.10.tgz", + "integrity": "sha512-I/9hEcRtjct8CzD9sVo9Mm4ntn0D+7tOVrjbPl69XAoOfgJ8NBdOQU+WX+5SHhcELJDb14mWt7zuvyqha+MEAQ==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/browserslist": { + "version": "4.27.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001752", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cborg": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/cborg/-/cborg-4.3.0.tgz", + "integrity": "sha512-vOXo1pB4mdeBw3LbpoynQlZNw/H3kZVHLtPYlp8kFMreL/2YfT54F70BM1s3iDoCtQ+3C9QmiRF4rfCSSTlhBw==", + "license": "Apache-2.0", + "bin": { + "cborg": "lib/bin.js" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/charm": { + "version": "0.1.2", + "dev": true, + "license": "MIT/X11" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-tableau": { + "version": "2.0.1", + "dev": true, + "dependencies": { + "chalk": "3.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/cli-tableau/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cloudinary": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.8.0.tgz", + "integrity": "sha512-s7frvR0HnQXeJsQSIsbLa/I09IMb1lOnVLEDH5b5E53WTiCYgrNNOBGV/i/nLHwrcEOUkqjfSwP1+enXWNYmdw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.2", + "license": "MIT", + "dependencies": { + "color-convert": "^3.0.1", + "color-string": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.2", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/croner": { + "version": "4.1.97", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", + "license": "MIT" + }, + "node_modules/culvert": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/dag-jose": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/dag-jose/-/dag-jose-4.0.0.tgz", + "integrity": "sha512-tw595L3UYoOUT9dSJPbBEG/qpRpw24kRZxa5SLRnlnr+g5L7O8oEs1d3W5TiVA1oJZbthVsf0Vi3zFN66qcEBA==", + "license": "(Apache-2.0 OR MIT)", + "dependencies": { + "@ipld/dag-cbor": "^9.0.0", + "multiformats": "^11.0.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.15", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, + "node_modules/degenerator": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-over-http-resolver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-2.1.3.tgz", + "integrity": "sha512-zjRYFhq+CsxPAouQWzOsxNMvEN+SHisjzhX8EMxd2Y0EG3thvn6wXQgMJLnTDImkhe4jhLbOQpXtL10nALBOSA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "debug": "^4.3.1", + "native-fetch": "^4.0.2", + "receptacle": "^1.3.2", + "undici": "^5.12.0" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-11.0.0.tgz", + "integrity": "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^17.1.0", + "dotenv-expand": "^12.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "dev": true, + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-fetch": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/electron-fetch/-/electron-fetch-1.9.1.tgz", + "integrity": "sha512-M9qw6oUILGVrcENMSRRefE1MbHPIz0h79EKIeJWK9v563aT9Qkh8aEHPO1H5vi970wPirNY+jO9OpFoLiMsMGA==", + "license": "MIT", + "dependencies": { + "encoding": "^0.1.13" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envalid": { + "version": "8.1.0", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "9.39.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter2": { + "version": "5.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit-x": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.1.0", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==" + }, + "node_modules/ext-list": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extrareqp2": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-check/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fclone": { + "version": "1.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/file-type": { + "version": "20.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-iterator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-iterator/-/get-iterator-1.0.2.tgz", + "integrity": "sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==", + "license": "MIT" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-node-fs": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/git-sha1": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "13.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/groq-sdk": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.37.0.tgz", + "integrity": "sha512-lT72pcT8b/X5XrzdKf+rWVzUGW1OQSKESmL8fFN5cTbsf02gq6oFam4SVeNtzELt9cYE2Pt3pdGgSImuTbHFDg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/groq-sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/groq-sdk/node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/groq-sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashlru": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.3.0.tgz", + "integrity": "sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hpp/node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC" + }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "kind-of": "^6.0.2" + } + }, + "node_modules/interface-datastore": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/interface-datastore/-/interface-datastore-8.3.2.tgz", + "integrity": "sha512-R3NLts7pRbJKc3qFdQf+u40hK8XWc0w4Qkx3OFEstC80VoaDUABY/dXA2EJPhtNC+bsrf41Ehvqb6+pnIclyRA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "interface-store": "^6.0.0", + "uint8arrays": "^5.1.0" + } + }, + "node_modules/interface-datastore/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/interface-datastore/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/interface-store": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/interface-store/-/interface-store-6.0.3.tgz", + "integrity": "sha512-+WvfEZnFUhRwFxgz+QCQi7UC6o9AM0EHM9bpIe2Nhqb100NHCsTvNAn4eJgvgV2/tmLo1MP9nGxQKEcZTAueLA==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipfs-core-types": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/ipfs-core-types/-/ipfs-core-types-0.14.1.tgz", + "integrity": "sha512-4ujF8NlM9bYi2I6AIqPP9wfGGX0x/gRCkMoFdOQfxxrFg6HcAdfS+0/irK8mp4e7znOHWReOHeWqCGw+dAPwsw==", + "deprecated": "js-IPFS has been deprecated in favour of Helia - please see https://github.com/ipfs/js-ipfs/issues/4336 for details", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-pb": "^4.0.0", + "@libp2p/interface-keychain": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-peer-info": "^1.0.2", + "@libp2p/interface-pubsub": "^3.0.0", + "@multiformats/multiaddr": "^11.1.5", + "@types/node": "^18.0.0", + "interface-datastore": "^7.0.0", + "ipfs-unixfs": "^9.0.0", + "multiformats": "^11.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-core-types/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ipfs-core-types/node_modules/interface-datastore": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/interface-datastore/-/interface-datastore-7.0.4.tgz", + "integrity": "sha512-Q8LZS/jfFFHz6XyZazLTAc078SSCoa27ZPBOfobWdpDiFO7FqPA2yskitUJIhaCgxNK8C+/lMBUTBNfVIDvLiw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "interface-store": "^3.0.0", + "nanoid": "^4.0.0", + "uint8arrays": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-core-types/node_modules/interface-store": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/interface-store/-/interface-store-3.0.4.tgz", + "integrity": "sha512-OjHUuGXbH4eXSBx1TF1tTySvjLldPLzRSYYXJwrEQI+XfH5JWYZofr0gVMV4F8XTwC+4V7jomDYkvGRmDSRKqQ==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-core-types/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/ipfs-core-utils": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/ipfs-core-utils/-/ipfs-core-utils-0.18.1.tgz", + "integrity": "sha512-P7jTpdfvlyBG3JR4o+Th3QJADlmXmwMxbkjszXry6VAjfSfLIIqXsdeYPoVRkV69GFEeQozuz2k/jR+U8cUH/Q==", + "deprecated": "js-IPFS has been deprecated in favour of Helia - please see https://github.com/ipfs/js-ipfs/issues/4336 for details", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@libp2p/logger": "^2.0.5", + "@multiformats/multiaddr": "^11.1.5", + "@multiformats/multiaddr-to-uri": "^9.0.1", + "any-signal": "^3.0.0", + "blob-to-it": "^2.0.0", + "browser-readablestream-to-it": "^2.0.0", + "err-code": "^3.0.1", + "ipfs-core-types": "^0.14.1", + "ipfs-unixfs": "^9.0.0", + "ipfs-utils": "^9.0.13", + "it-all": "^2.0.0", + "it-map": "^2.0.0", + "it-peekable": "^2.0.0", + "it-to-stream": "^1.0.0", + "merge-options": "^3.0.4", + "multiformats": "^11.0.0", + "nanoid": "^4.0.0", + "parse-duration": "^1.0.0", + "timeout-abort-controller": "^3.0.0", + "uint8arrays": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-core-utils/node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, + "node_modules/ipfs-http-client": { + "version": "60.0.1", + "resolved": "https://registry.npmjs.org/ipfs-http-client/-/ipfs-http-client-60.0.1.tgz", + "integrity": "sha512-amwM5TNuf077J+/q27jPHfatC05vJuIbX6ZnlYLjc2QsjOCKsORNBqV3brNw7l+fPrijV1yrwEDLG3JEnKsfMw==", + "deprecated": "js-IPFS has been deprecated in favour of Helia - please see https://github.com/ipfs/js-ipfs/issues/4336 for details", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@ipld/dag-cbor": "^9.0.0", + "@ipld/dag-json": "^10.0.0", + "@ipld/dag-pb": "^4.0.0", + "@libp2p/logger": "^2.0.5", + "@libp2p/peer-id": "^2.0.0", + "@multiformats/multiaddr": "^11.1.5", + "any-signal": "^3.0.0", + "dag-jose": "^4.0.0", + "err-code": "^3.0.1", + "ipfs-core-types": "^0.14.1", + "ipfs-core-utils": "^0.18.1", + "ipfs-utils": "^9.0.13", + "it-first": "^2.0.0", + "it-last": "^2.0.0", + "merge-options": "^3.0.4", + "multiformats": "^11.0.0", + "parse-duration": "^1.0.0", + "stream-to-it": "^0.2.2", + "uint8arrays": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-http-client/node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, + "node_modules/ipfs-unixfs": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-9.0.1.tgz", + "integrity": "sha512-jh2CbXyxID+v3jLml9CqMwjdSS9ZRnsGfQGGPOfem0/hT/L48xUeTPvh7qLFWkZcIMhZtG+fnS1teei8x5uGBg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "err-code": "^3.0.1", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-unixfs/node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, + "node_modules/ipfs-utils": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/ipfs-utils/-/ipfs-utils-9.0.14.tgz", + "integrity": "sha512-zIaiEGX18QATxgaS0/EOQNoo33W0islREABAcxXE8n7y2MGAlB+hdsxXn4J0hGZge8IqVQhW8sWIb+oJz2yEvg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "any-signal": "^3.0.0", + "browser-readablestream-to-it": "^1.0.0", + "buffer": "^6.0.1", + "electron-fetch": "^1.7.2", + "err-code": "^3.0.1", + "is-electron": "^2.2.0", + "iso-url": "^1.1.5", + "it-all": "^1.0.4", + "it-glob": "^1.0.1", + "it-to-stream": "^1.0.0", + "merge-options": "^3.0.4", + "nanoid": "^3.1.20", + "native-fetch": "^3.0.0", + "node-fetch": "^2.6.8", + "react-native-fetch-api": "^3.0.0", + "stream-to-it": "^0.2.2" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/ipfs-utils/node_modules/browser-readablestream-to-it": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/browser-readablestream-to-it/-/browser-readablestream-to-it-1.0.3.tgz", + "integrity": "sha512-+12sHB+Br8HIh6VAMVEG5r3UXCyESIgDW7kzk3BjIXa43DVqVwL7GC5TW3jeh+72dtcH99pPVpw0X8i0jt+/kw==", + "license": "ISC" + }, + "node_modules/ipfs-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/ipfs-utils/node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", + "license": "MIT" + }, + "node_modules/ipfs-utils/node_modules/it-all": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/it-all/-/it-all-1.0.6.tgz", + "integrity": "sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A==", + "license": "ISC" + }, + "node_modules/ipfs-utils/node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ipfs-utils/node_modules/native-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/native-fetch/-/native-fetch-3.0.0.tgz", + "integrity": "sha512-G3Z7vx0IFb/FQ4JxvtqGABsOTIqRWvgQz6e+erkB+JJD6LrszQtMozEHI4EkmgZQvnGHrpLVzUWk7t4sJCIkVw==", + "license": "MIT", + "peerDependencies": { + "node-fetch": "*" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ipfs": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/is-ipfs/-/is-ipfs-8.0.4.tgz", + "integrity": "sha512-upkO6a8WgBSZMMmuPzmF2NQLWXtiJtHxdEfEiMWrOzCKoZ+XEiM0XlK4fFMfo/PyiRmPMJ4PsNrXyvJeqMrJXA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@multiformats/mafmt": "^12.1.6", + "@multiformats/multiaddr": "^12.1.14", + "iso-url": "^1.1.3", + "multiformats": "^13.0.1", + "uint8arrays": "^5.0.1" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/is-ipfs/node_modules/@multiformats/multiaddr": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr/-/multiaddr-12.5.1.tgz", + "integrity": "sha512-+DDlr9LIRUS8KncI1TX/FfUn8F2dl6BIxJgshS/yFQCNB5IAF0OGzcwB39g5NLE22s4qqDePv0Qof6HdpJ/4aQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@chainsafe/netmask": "^2.0.0", + "@multiformats/dns": "^1.0.3", + "abort-error": "^1.0.1", + "multiformats": "^13.0.0", + "uint8-varint": "^2.0.1", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/is-ipfs/node_modules/multiformats": { + "version": "13.4.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.2.tgz", + "integrity": "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/is-ipfs/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/iso-url": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-1.2.1.tgz", + "integrity": "sha512-9JPDgCN4B7QPkLtYAAOrEuAWvP9rWvR5offAr0/SeF046wIkglqH3VXgYYP6NcsKslH80UIVgmPqNe3j7tG2ng==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/it-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/it-all/-/it-all-2.0.1.tgz", + "integrity": "sha512-9UuJcCRZsboz+HBQTNOau80Dw+ryGaHYFP/cPYzFBJBFcfDathMYnhHk4t52en9+fcyDGPTdLB+lFc1wzQIroA==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-first": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/it-first/-/it-first-2.0.1.tgz", + "integrity": "sha512-noC1oEQcWZZMUwq7VWxHNLML43dM+5bviZpfmkxkXlvBe60z7AFRqpZSga9uQBo792jKv9otnn1IjA4zwgNARw==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-glob": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/it-glob/-/it-glob-1.0.2.tgz", + "integrity": "sha512-Ch2Dzhw4URfB9L/0ZHyY+uqOnKvBNeS/SMcRiPmJfpHiM0TsUZn+GkpcZxAoF3dJVdPm/PuIk3A4wlV7SUo23Q==", + "license": "ISC", + "dependencies": { + "@types/minimatch": "^3.0.4", + "minimatch": "^3.0.4" + } + }, + "node_modules/it-glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/it-glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/it-last": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/it-last/-/it-last-2.0.1.tgz", + "integrity": "sha512-uVMedYW0wa2Cx0TAmcOCLbfuLLII7+vyURmhKa8Zovpd+aBTMsmINtsta2n364wJ5qsEDBH+akY1sUtAkaYBlg==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/it-map/-/it-map-2.0.1.tgz", + "integrity": "sha512-a2GcYDHiAh/eSU628xlvB56LA98luXZnniH2GlD0IdBzf15shEq9rBeb0Rg3o1SWtNILUAwqmQxEXcewGCdvmQ==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-peekable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/it-peekable/-/it-peekable-2.0.1.tgz", + "integrity": "sha512-fJ/YTU9rHRhGJOM2hhQKKEfRM6uKB9r4yGGFLBHqp72ACC8Yi6+7/FhuBAMG8cpN6mLoj9auVX7ZJ3ul6qFpTA==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-pushable": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/it-pushable/-/it-pushable-3.2.3.tgz", + "integrity": "sha512-gzYnXYK8Y5t5b/BnJUr7glfQLO4U5vyb05gPx/TyTw+4Bv1zM9gFk4YsOrnulWefMewlphCjKkakFvj1y99Tcg==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "p-defer": "^4.0.0" + } + }, + "node_modules/it-stream-types": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/it-stream-types/-/it-stream-types-1.0.5.tgz", + "integrity": "sha512-I88Ka1nHgfX62e5mi5LLL+oueqz7Ltg0bUdtsUKDe9SoUqbQPf2Mp5kxDTe9pNhHQGs4pvYPAINwuZ1HAt42TA==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/it-to-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/it-to-stream/-/it-to-stream-1.0.0.tgz", + "integrity": "sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "fast-fifo": "^1.0.0", + "get-iterator": "^1.0.2", + "p-defer": "^3.0.0", + "p-fifo": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/it-to-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/it-to-stream/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-git": { + "version": "0.7.8", + "dev": true, + "license": "MIT", + "dependencies": { + "bodec": "^0.1.0", + "culvert": "^0.1.2", + "git-sha1": "^0.1.2", + "pako": "^0.2.5" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.25", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-options/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multiformats": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-11.0.2.tgz", + "integrity": "sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "license": "ISC" + }, + "node_modules/mylas": { + "version": "2.1.13", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/native-fetch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/native-fetch/-/native-fetch-4.0.2.tgz", + "integrity": "sha512-4QcVlKFtv2EYVS5MBgsGX5+NWKtbDbIECdUXDBGDMAZXq3Jkv9zf+y8iS7Ub8fEdga3GpYeazp9gauNqXHJOCg==", + "license": "MIT", + "peerDependencies": { + "undici": "*" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-config": { + "version": "0.0.2", + "dev": true, + "engines": { + "node": ">=0.1.99" + } + }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==" + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-defer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-4.0.1.tgz", + "integrity": "sha512-Mr5KC5efvAK5VUptYEIopP1bakB85k2IWXaRC0rsh1uwn1L6M0LVml8OIQ4Gudg4oyZakf7FmeRLkMMtZW1i5A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-fifo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-fifo/-/p-fifo-1.0.0.tgz", + "integrity": "sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.0.0", + "p-defer": "^3.0.0" + } + }, + "node_modules/p-fifo/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.0.0.tgz", + "integrity": "sha512-KO1RyxstL9g1mK76530TExamZC/S2Glm080Nx8PE5sTd7nlduDQsAfEl4uXX+qZjLiwvDauvzXavufy3+rJ9zQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "0.2.9", + "dev": true, + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-duration": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.2.tgz", + "integrity": "sha512-p8EIONG8L0u7f8GFgfVlL4n8rnChTt8O5FSxgxMz2tjc9FMP199wxVKVB6IbKx11uTbKHACSvaLVIKNnoeNR/A==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/pend": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pidusage": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pinata": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/pinata/-/pinata-1.10.1.tgz", + "integrity": "sha512-/nB9C7zCEnC5YW+deO5FGX5RizSN23JXvbUdDLfzgwtKj4m5JC8ScuLkQ+iirfvpPQGQFQ/X5mR3aC/npTjFDA==", + "license": "MIT", + "dependencies": { + "is-ipfs": "^8.0.4", + "node-fetch": "^3.3.1" + } + }, + "node_modules/pinata/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/pinata/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "4.9.2", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkcs11js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/pkcs11js/-/pkcs11js-2.1.6.tgz", + "integrity": "sha512-+t5jxzB749q8GaEd1yNx3l98xYuaVK6WW/Vjg1Mk1Iy5bMu/A5W4O/9wZGrpOknWF6lFQSb12FXX+eSNxdriwA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/PeculiarVentures" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/pm2": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/pm2/-/pm2-6.0.14.tgz", + "integrity": "sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA==", + "dev": true, + "license": "AGPL-3.0", + "dependencies": { + "@pm2/agent": "~2.1.1", + "@pm2/blessed": "0.1.81", + "@pm2/io": "~6.1.0", + "@pm2/js-api": "~0.8.0", + "@pm2/pm2-version-check": "^1.0.4", + "ansis": "4.0.0-node10", + "async": "3.2.6", + "chokidar": "3.6.0", + "cli-tableau": "2.0.1", + "commander": "2.15.1", + "croner": "4.1.97", + "dayjs": "1.11.15", + "debug": "4.4.3", + "enquirer": "2.3.6", + "eventemitter2": "5.0.1", + "fclone": "1.0.11", + "js-yaml": "4.1.1", + "mkdirp": "1.0.4", + "needle": "2.4.0", + "pidusage": "3.0.2", + "pm2-axon": "~4.0.1", + "pm2-axon-rpc": "~0.7.1", + "pm2-deploy": "~1.0.2", + "pm2-multimeter": "^0.1.2", + "promptly": "2.2.0", + "semver": "7.7.2", + "source-map-support": "0.5.21", + "sprintf-js": "1.1.2", + "vizion": "~2.2.1" + }, + "bin": { + "pm2": "bin/pm2", + "pm2-dev": "bin/pm2-dev", + "pm2-docker": "bin/pm2-docker", + "pm2-runtime": "bin/pm2-runtime" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "pm2-sysmonit": "^1.2.8" + } + }, + "node_modules/pm2-axon": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "amp": "~0.3.1", + "amp-message": "~0.1.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-axon-rpc": { + "version": "0.7.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.1" + }, + "engines": { + "node": ">=5" + } + }, + "node_modules/pm2-deploy": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "run-series": "^1.1.8", + "tv4": "^1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pm2-multimeter": { + "version": "0.1.2", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "charm": "~0.1.1" + } + }, + "node_modules/pm2-sysmonit": { + "version": "1.2.8", + "dev": true, + "license": "Apache", + "optional": true, + "dependencies": { + "async": "^3.2.0", + "debug": "^4.3.1", + "pidusage": "^2.0.21", + "systeminformation": "^5.7", + "tx2": "~1.0.4" + } + }, + "node_modules/pm2-sysmonit/node_modules/pidusage": { + "version": "2.0.21", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pm2/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/pm2/node_modules/commander": { + "version": "2.15.1", + "dev": true, + "license": "MIT" + }, + "node_modules/pm2/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pm2/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pm2/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/pm2/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pm2/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pm2/node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", + "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", + "hasInstallScript": true, + "dependencies": { + "@prisma/config": "6.18.0", + "@prisma/engines": "6.18.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/progress-events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/progress-events/-/progress-events-1.0.1.tgz", + "integrity": "sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promptly": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "read": "^1.0.4" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/react-native-fetch-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-native-fetch-api/-/react-native-fetch-api-3.0.0.tgz", + "integrity": "sha512-g2rtqPjdroaboDKTsJCTlcmtw54E25OjyaunUP0anOZn4Fuo2IKs8BVfe02zVggA/UysbmfSnRJIqtNkAgggNA==", + "license": "MIT", + "dependencies": { + "p-defer": "^3.0.0" + } + }, + "node_modules/react-native-fetch-api/node_modules/p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/read": { + "version": "1.0.7", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/receptacle": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/receptacle/-/receptacle-1.3.2.tgz", + "integrity": "sha512-HrsFvqZZheusncQRiEE7GatOAETrARKV/lnfYicIm8lbvp/JQOdADOfhjBd2DajvoszEyxSM6RlAAIZgEoeu/A==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/retimer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-3.0.0.tgz", + "integrity": "sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==", + "license": "MIT" + }, + "node_modules/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-series": { + "version": "1.1.9", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "dev": true, + "license": "ISC" + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "12.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-to-it": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/stream-to-it/-/stream-to-it-0.2.4.tgz", + "integrity": "sha512-4vEbkSs83OahpmBybNJXlJd7d6/RxzkkSdT3I0mnGt79Xd2Kk+e1JqbvAvsQfCeKj3aKb0QIWkyK3/n0j506vQ==", + "license": "MIT", + "dependencies": { + "get-iterator": "^1.0.2" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-dirs": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "inspect-with-kind": "^1.0.5", + "is-plain-obj": "^1.1.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-autogen": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", + "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", + "license": "MIT", + "dependencies": { + "acorn": "^7.4.1", + "deepmerge": "^4.2.2", + "glob": "^7.1.7", + "json5": "^2.2.3" + } + }, + "node_modules/swagger-autogen/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/swagger-autogen/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-autogen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-autogen/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-jsdoc/node_modules/brace-expansion": { + "version": "1.1.12", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/swagger-jsdoc/node_modules/commander": { + "version": "6.2.0", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-jsdoc/node_modules/glob": { + "version": "7.1.6", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/swagger-jsdoc/node_modules/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-1", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.30.1", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/systeminformation": { + "version": "5.30.5", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.5.tgz", + "integrity": "sha512-DpWmpCckhwR3hG+6udb6/aQB7PpiqVnvSljrjbKxNSvTRsGsg7NVE3/vouoYf96xgwMxXFKcS4Ux+cnkFwYM7A==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tar": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.6.tgz", + "integrity": "sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/timeout-abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/timeout-abort-controller/-/timeout-abort-controller-3.0.0.tgz", + "integrity": "sha512-O3e+2B8BKrQxU2YRyEjC/2yFdb33slI22WRdUaDx6rvysfi9anloNZyR2q0l6LnePo5qH7gSM7uZtvvwZbc2yA==", + "license": "MIT", + "dependencies": { + "retimer": "^3.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsc-alias/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tsc-alias/node_modules/commander": { + "version": "9.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/tsc-alias/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tsc-alias/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tsc-alias/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tv4": { + "version": "1.3.0", + "dev": true, + "license": [ + { + "type": "Public Domain", + "url": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + { + "type": "MIT", + "url": "http://jsonary.com/LICENSE.txt" + } + ], + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tx2": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "json-stringify-safe": "^5.0.1" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typedi": { + "version": "0.10.0", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, + "node_modules/uint8-varint": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/uint8-varint/-/uint8-varint-2.0.4.tgz", + "integrity": "sha512-FwpTa7ZGA/f/EssWAb5/YV6pHgVF1fViKdW8cWaEarjB8t7NyofSWBdOTyFPaGuUG4gx3v1O3PQ8etsiOs3lcw==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arraylist": "^2.0.0", + "uint8arrays": "^5.0.0" + } + }, + "node_modules/uint8-varint/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/uint8-varint/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uint8arraylist": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/uint8arraylist/-/uint8arraylist-2.4.8.tgz", + "integrity": "sha512-vc1PlGOzglLF0eae1M8mLRTBivsvrGsdmJ5RbK3e+QRvRLOZfZhQROTwH/OfyF3+ZVUg9/8hE8bmKP2CvP9quQ==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "uint8arrays": "^5.0.1" + } + }, + "node_modules/uint8arraylist/node_modules/multiformats": { + "version": "13.4.1", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.4.1.tgz", + "integrity": "sha512-VqO6OSvLrFVAYYjgsr8tyv62/rCQhPgsZUXLTqoFLSgdkgiUYKYeArbt1uWLlEpkjxQe+P0+sHlbPEte1Bi06Q==", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/uint8arraylist/node_modules/uint8arrays": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-5.1.0.tgz", + "integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^13.0.0" + } + }, + "node_modules/uint8arrays": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.10.tgz", + "integrity": "sha512-AnJNUGGDJAgFw/eWu/Xb9zrVKEGlwJJCaeInlf3BkecE/zcTobk5YXYIPNQJO1q5Hh1QZrQQHf0JvcHqz2hqoA==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "multiformats": "^12.0.1" + } + }, + "node_modules/uint8arrays/node_modules/multiformats": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-12.1.3.tgz", + "integrity": "sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==", + "license": "Apache-2.0 OR MIT", + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vizion": { + "version": "2.2.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^2.6.3", + "git-node-fs": "^1.0.0", + "ini": "^1.3.5", + "js-git": "^0.7.8" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vizion/node_modules/async": { + "version": "2.6.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.18.3", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a0426ef --- /dev/null +++ b/package.json @@ -0,0 +1,110 @@ +{ + "name": "GP-Backend", + "version": "0.0.0", + "description": "TypeScript + Prisma + MySQL + Express API Server", + "author": "", + "license": "ISC", + "scripts": { + "start": "npm run build && cross-env NODE_ENV=production node dist/server.js", + "dev": "cross-env NODE_ENV=development nodemon", + "build": "swc src -d dist --source-maps --copy-files", + "build:tsc": "tsc && tsc-alias", + "test": "jest --forceExit --detectOpenHandles", + "lint": "eslint --ignore-path .gitignore --ext .ts src/", + "lint:fix": "npm run lint -- --fix", + "prisma:init": "prisma init", + "prisma:migrate": "dotenv -e .env.development.local -- npx prisma migrate dev --schema=src/prisma/schema.prisma", + "prisma:generate": "prisma generate", + "prisma:studio": "dotenv -e .env.development.local -- prisma studio", + "swagger:generate": "node ./src/swagger.mjs", + "deploy:prod": "npm run build && pm2 start ecosystem.config.js --only prod", + "deploy:dev": "pm2 start ecosystem.config.js --only dev" + }, + "prisma": { + "schema": "src/prisma/schema.prisma" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1002.0", + "@aws-sdk/s3-request-presigner": "^3.1002.0", + "@grpc/grpc-js": "^1.14.0", + "@hyperledger/fabric-gateway": "^1.9.0", + "@prisma/client": "6.18.0", + "agora-token": "^2.0.5", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "cloudinary": "^2.8.0", + "compression": "^1.8.1", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "envalid": "^8.1.0", + "express": "^5.1.0", + "express-session": "^1.18.2", + "groq-sdk": "^0.37.0", + "helmet": "^8.1.0", + "hpp": "^0.2.3", + "ipfs-http-client": "^60.0.1", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.1", + "multer": "^2.0.2", + "node-cron": "^4.2.1", + "nodemailer": "^7.0.10", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "pinata": "^1.10.1", + "prisma": "6.18.0", + "reflect-metadata": "^0.2.2", + "socket.io": "^4.8.3", + "swagger-autogen": "^2.23.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "typedi": "^0.10.0", + "winston": "^3.18.3", + "winston-daily-rotate-file": "^5.0.0" + }, + "devDependencies": { + "@swc/cli": "^0.7.8", + "@swc/core": "^1.14.0", + "@types/bcrypt": "^6.0.0", + "@types/compression": "^1.8.1", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.5", + "@types/express-session": "^1.18.2", + "@types/hpp": "^0.2.7", + "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.0", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^7.0.3", + "@types/passport-google-oauth20": "^2.0.17", + "@types/socket.io": "^3.0.1", + "@types/supertest": "^6.0.3", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", + "cross-env": "^10.1.0", + "dotenv-cli": "^11.0.0", + "eslint": "^9.39.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "husky": "^9.1.7", + "jest": "^30.2.0", + "lint-staged": "^16.2.6", + "node-config": "^0.0.2", + "node-gyp": "^11.5.0", + "nodemon": "^3.1.10", + "pm2": "^6.0.13", + "prettier": "^3.6.2", + "supertest": "^7.1.4", + "ts-jest": "^29.4.5", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.16", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.9.3" + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..9991f75 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,99 @@ +import 'reflect-metadata'; +import compression from 'compression'; +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express from 'express'; +import helmet from 'helmet'; +import hpp from 'hpp'; +import morgan from 'morgan'; +import swaggerUi from 'swagger-ui-express'; +import { NODE_ENV, PORT, LOG_FORMAT, ORIGIN, CREDENTIALS } from '@config'; +import { Routes } from '@interfaces/routes.interface'; +import { ErrorMiddleware } from '@middlewares/error.middleware'; +import { logger, stream } from '@utils/logger'; +// Google OAuth Imports +import passport from 'passport'; +import '@utils/passsportGoogle'; +import { createServer, Server as HttpServer } from 'http'; +import { SocketService } from '@/services/socket.service'; +import { VacationCronService } from '@/services/cron.service'; + +export class App { + public app: express.Application; + public env: string; + public port: string | number; + public httpServer: HttpServer; + private socketService: SocketService; + + constructor(routes: Routes[]) { + this.app = express(); + this.env = NODE_ENV || 'development'; + this.port = PORT || 3000; + this.httpServer = createServer(this.app); + + this.initializeMiddlewares(); + this.initializeRoutes(routes); + this.initializeErrorHandling(); + this.initializeSwagger(); + + this.socketService = new SocketService(); + this.socketService.initialize(this.httpServer); + + VacationCronService.startCronJobs(); + + } + + public listen() { + this.httpServer.listen(this.port); + + this.httpServer.on('listening', () => { + logger.info(`=================================`); + logger.info(`======= ENV: ${this.env} =======`); + logger.info(`🚀 App listeningg on the port ${this.port}`); + logger.info(`=================================`); + }); + + this.httpServer.on('error', (error: any) => { + logger.error('Server failed to start'); + logger.error(error); + process.exit(1); + }); + } + + public getServer() { + return this.app; + } + + + private initializeMiddlewares() { + this.app.use(morgan(LOG_FORMAT, { stream })); + this.app.use(cors({ origin: ORIGIN, credentials: CREDENTIALS })); + this.app.use(hpp()); + this.app.use(helmet()); + this.app.use(compression()); + this.app.use(express.json({ limit: '5mb' })); + this.app.use(express.urlencoded({ limit: '5mb', extended: true })); + this.app.use(cookieParser()); + this.app.use(passport.initialize()); + this.app.use(express.json({ limit: '5mb' })); + this.app.use(express.urlencoded({ limit: '5mb', extended: true })); + } + + + private initializeRoutes(routes: Routes[]) { + routes.forEach(route => { + this.app.use('/', route.router); + }); + } + + private initializeSwagger() { + const swaggerFile = require('./swagger-output.json'); // Path to the generated swagger file + const swaggerUi = require('swagger-ui-express'); + + this.app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)); + } + + private initializeErrorHandling() { + this.app.use(ErrorMiddleware); + } +} \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..9ec033c --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,17 @@ +import { config } from 'dotenv'; +config({ path: `.env.${process.env.NODE_ENV || 'development'}.local` }); + +export const CREDENTIALS = process.env.CREDENTIALS === 'true'; + +export const { NODE_ENV, PORT, SECRET_KEY, LOG_FORMAT, LOG_DIR, ORIGIN, REFRESH_TOKEN_SECRET, + GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, SESSION_SECRET, GOOGLE_CALLBACK_URL, + GMAIL_USER, GMAIL_APP_PASSWORD, + FRONTEND_URL, SENDER_EMAIL, + CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, + Agora_APP_ID, Agora_APP_CERTIFICATE, + B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY, B2_ENDPOINT, B2_BUCKET_NAME, B2_REGION_NAME, + GROQ_API_KEY + } = process.env; + +export const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || '7d'; // Default 7 days +export const ACCESS_TOKEN_EXPIRY = process.env.ACCESS_TOKEN_EXPIRY || '1h'; // Default 1 hour diff --git a/src/config/prisma.ts b/src/config/prisma.ts new file mode 100644 index 0000000..b904402 --- /dev/null +++ b/src/config/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export default prisma; \ No newline at end of file diff --git a/src/config/storage.ts b/src/config/storage.ts new file mode 100644 index 0000000..2e7689f --- /dev/null +++ b/src/config/storage.ts @@ -0,0 +1,15 @@ +import { B2_ENDPOINT, B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY, B2_REGION_NAME } from "."; + +import { S3Client } from "@aws-sdk/client-s3"; + +const s3Client = new S3Client({ + region: B2_REGION_NAME, + endpoint: B2_ENDPOINT, + credentials: { + accessKeyId: B2_APPLICATION_KEY_ID, + secretAccessKey: B2_APPLICATION_KEY, + }, + forcePathStyle: true, +}); + +export default s3Client; \ No newline at end of file diff --git a/src/constants/specializations.ts b/src/constants/specializations.ts new file mode 100644 index 0000000..27c7c2e --- /dev/null +++ b/src/constants/specializations.ts @@ -0,0 +1,162 @@ +export const SPECIALIZATIONS = { + CARDIOLOGY: { + en: 'Cardiology', + ar: 'أمراض القلب', + }, + DERMATOLOGY: { + en: 'Dermatology', + ar: 'الأمراض الجلدية', + }, + ENDOCRINOLOGY: { + en: 'Endocrinology', + ar: 'الغدد الصماء', + }, + GASTROENTEROLOGY: { + en: 'Gastroenterology', + ar: 'الجهاز الهضمي', + }, + GENERAL_PRACTICE: { + en: 'General Practice', + ar: 'الطب العام', + }, + GYNECOLOGY: { + en: 'Gynecology', + ar: 'أمراض النساء', + }, + HEMATOLOGY: { + en: 'Hematology', + ar: 'أمراض الدم', + }, + INTERNAL_MEDICINE: { + en: 'Internal Medicine', + ar: 'الباطنية', + }, + NEPHROLOGY: { + en: 'Nephrology', + ar: 'أمراض الكلى', + }, + NEUROLOGY: { + en: 'Neurology', + ar: 'الأمراض العصبية', + }, + NEUROSURGERY: { + en: 'Neurosurgery', + ar: 'جراحة المخ والأعصاب', + }, + OBSTETRICS: { + en: 'Obstetrics', + ar: 'التوليد', + }, + ONCOLOGY: { + en: 'Oncology', + ar: 'الأورام', + }, + OPHTHALMOLOGY: { + en: 'Ophthalmology', + ar: 'طب العيون', + }, + ORTHOPEDICS: { + en: 'Orthopedics', + ar: 'جراحة العظام', + }, + OTOLARYNGOLOGY: { + en: 'Otolaryngology (ENT)', + ar: 'الأنف والأذن والحنجرة', + }, + PEDIATRICS: { + en: 'Pediatrics', + ar: 'طب الأطفال', + }, + PSYCHIATRY: { + en: 'Psychiatry', + ar: 'الطب النفسي', + }, + PULMONOLOGY: { + en: 'Pulmonology', + ar: 'أمراض الصدر', + }, + RADIOLOGY: { + en: 'Radiology', + ar: 'الأشعة', + }, + RHEUMATOLOGY: { + en: 'Rheumatology', + ar: 'أمراض الروماتيزم', + }, + SURGERY: { + en: 'General Surgery', + ar: 'الجراحة العامة', + }, + UROLOGY: { + en: 'Urology', + ar: 'المسالك البولية', + }, + ANESTHESIOLOGY: { + en: 'Anesthesiology', + ar: 'التخدير', + }, + EMERGENCY_MEDICINE: { + en: 'Emergency Medicine', + ar: 'طب الطوارئ', + }, + FAMILY_MEDICINE: { + en: 'Family Medicine', + ar: 'طب الأسرة', + }, + PATHOLOGY: { + en: 'Pathology', + ar: 'علم الأمراض', + }, + PHYSICAL_THERAPY: { + en: 'Physical Therapy', + ar: 'العلاج الطبيعي', + }, + PLASTIC_SURGERY: { + en: 'Plastic Surgery', + ar: 'جراحة التجميل', + }, + SPORTS_MEDICINE: { + en: 'Sports Medicine', + ar: 'طب الرياضة', + }, + IMMUNOLOGY: { + en: 'Immunology', + ar: 'امراض المناعة', + }, +} as const; + +// Type for specialization keys +export type SpecializationKey = keyof typeof SPECIALIZATIONS; + +// Get all valid specialization keys +export const VALID_SPECIALIZATION_KEYS = Object.keys(SPECIALIZATIONS) as SpecializationKey[]; + +// Get all English specialization values +export const VALID_SPECIALIZATIONS_EN = Object.values(SPECIALIZATIONS).map(spec => spec.en); + +// Type for specialization english values +export type SpecializationEnglishValue = (typeof SPECIALIZATIONS)[keyof typeof SPECIALIZATIONS]['en']; + +// Get all Arabic specialization values +export const VALID_SPECIALIZATIONS_AR = Object.values(SPECIALIZATIONS).map(spec => spec.ar); + +// Type for specialization arabic values +export type SpecializationArabicValue = (typeof SPECIALIZATIONS)[keyof typeof SPECIALIZATIONS]['ar']; + +// Helper function to get specialization by key +export const getSpecialization = (key: SpecializationKey) => { + return SPECIALIZATIONS[key]; +}; + +// Helper function to validate specialization +export const isValidSpecialization = (value: string): boolean => { + return VALID_SPECIALIZATION_KEYS.includes(value as SpecializationKey); +}; + +// Helper function to get specialization key from English or Arabic value +export const getSpecializationKey = (value: string): SpecializationKey | null => { + const entry = Object.entries(SPECIALIZATIONS).find( + ([_, spec]) => spec.en === value || spec.ar === value + ); + return entry ? (entry[0] as SpecializationKey) : null; +}; diff --git a/src/controllers/admin.controller.ts b/src/controllers/admin.controller.ts new file mode 100644 index 0000000..f83cd4d --- /dev/null +++ b/src/controllers/admin.controller.ts @@ -0,0 +1,224 @@ + +import { NextFunction, Request, Response } from 'express'; +import { Container } from 'typedi'; +import { AdminService } from '@/services/admin.service'; +import { AddUserFromAdminDto } from '@/dtos/admins.dto'; +import { RequestWithLanguage } from '@/middlewares/language.middleware'; +import { formatSpecializationResponse } from '@/utils/specializationTransform'; +import { SpecializationKey } from '@/constants/specializations'; +import { createMultiLangMessage, SuccessResponseMessages } from '@/utils/responseMessages'; +import { HttpException } from '@/exceptions/HttpException'; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { ClinicService } from '@/services/clinic.service'; +import { ClinicActiveStatusResponseDto, ClinicResponseDto } from '@/dtos/clinics.dto'; + +export class AdminController { + public adminService = Container.get(AdminService); + public clinicService = Container.get(ClinicService); + + public addDoctor = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + const doctorData: AddUserFromAdminDto = req.body; + const newDoctor = await this.adminService.addDoctor(doctorData); + + const formattedNewDoctor = newDoctor.doctor ? { + ...newDoctor.doctor, + specialization: formatSpecializationResponse( + newDoctor.doctor.specialization as SpecializationKey, + req.language + ), + } : null; + + const doctorResponse = { + ...newDoctor, + doctor: formattedNewDoctor, + }; + const responseMessage = createMultiLangMessage(SuccessResponseMessages.DOCTOR_CREATED); + res.status(201).json({ + data: doctorResponse, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + }; + + public addNurse = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + const nurseData: AddUserFromAdminDto = req.body; + const newNurse = await this.adminService.addNurse(nurseData); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSE_CREATED); + res.status(201).json({ + data: newNurse, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + }; + + public getAllDoctors = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const doctors = await this.adminService.getAllDoctors(); + const language = req.language; + + // Format specializations based on language preference + const formattedDoctors = doctors.map(doctor => ({ + ...doctor, + doctor: doctor.doctor ? { + ...doctor.doctor, + specialization: formatSpecializationResponse( + doctor.doctor.specialization as SpecializationKey, + language + ), + } : null, + })); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.DOCTORS_RETRIEVED); + res.status(200).json({ + data: formattedDoctors, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + public getAllNurses = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const nurses = await this.adminService.getAllNurses(); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSES_RETRIEVED); + res.status(200).json({ + data: nurses, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + + public getDoctorById = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const doctorId = req.params.id; + const doctor = await this.adminService.getDoctorById(doctorId); + const language = req.language; + + // Format specialization based on language preference + const formattedDoctor = { + ...doctor, + doctor: doctor.doctor ? { + ...doctor.doctor, + specialization: formatSpecializationResponse( + doctor.doctor.specialization as SpecializationKey, + language + ), + } : null, + }; + const responseMessage = createMultiLangMessage(SuccessResponseMessages.DOCTOR_RETRIEVED); + + res.status(200).json({ + data: formattedDoctor, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + public getNurseById = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const nurseId = req.params.id; + const nurse = await this.adminService.getNurseById(nurseId); + if (!nurse) { + const error = createBilingualError(404, ErrorMessages.NURSE_DATA_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSE_RETRIEVED); + res.status(200).json({ + data: nurse, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + public getUnverifiedDoctors = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const unverifiedDoctors = await this.adminService.getUnverifiedDoctors(); + const language = req.language; + // Format specializations based on language preference + const formattedDoctors = unverifiedDoctors.map(doctor => ({ + ...doctor, + doctor: doctor.doctor ? { + ...doctor.doctor, + specialization: formatSpecializationResponse( + doctor.doctor.specialization as SpecializationKey, + language + ), + } : null, + })); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.UNVERIFIED_DOCTORS_RETRIEVED); + res.status(200).json({ + data: formattedDoctors, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + public getUnverifiedNurses = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const unverifiedNurses = await this.adminService.getUnverifiedNurses(); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.UNVERIFIED_NURSES_RETRIEVED); + res.status(200).json({ + data: unverifiedNurses, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + public updateDoctorVerificationStatus = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + + const doctorId = req.params.id; + const { isVerified } = req.body; + await this.adminService.updateDoctorVerificationStatus(doctorId, isVerified); + await this.adminService.sendVerificationStatusEmail(doctorId, isVerified); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.DOCTOR_VERIFICATION_STATUS_UPDATED); + res.status(200).json({ + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + public updateNurseVerificationStatus = async (req: RequestWithLanguage, res: Response, next: NextFunction): Promise => { + const nurseId = req.params.id; + const { isVerified } = req.body; + await this.adminService.updateNurseVerificationStatus(nurseId, isVerified); + await this.adminService.sendVerificationStatusEmail(nurseId, isVerified); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSE_VERIFICATION_STATUS_UPDATED); + res.status(200).json({ + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + + // Clinic Routes + public getAllClinics = async (req: Request, res: Response, next: NextFunction): Promise => { + + const clinics = await this.clinicService.getAllClinics(); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_RETRIEVED); + res.status(200).json({ + data: clinics, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + public getClinicById = async (req: Request, res: Response, next: NextFunction): Promise => { + const clinicId = req.params.id; + const clinic = await this.clinicService.getClinicById(clinicId); + if (!clinic) { + const error = createBilingualError(404, ErrorMessages.CLINIC_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_RETRIEVED); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr, data: clinic }); + } + public setClinicActiveStatus = async (req: Request, res: Response, next: NextFunction): Promise => { + const clinicId = req.params.id; + const { is_active } = req.body; + const updatedClinic: ClinicActiveStatusResponseDto = await this.clinicService.setClinicActiveStatus(clinicId, is_active); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_STATUS_UPDATED); + res.status(200).json({ + data: updatedClinic, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr, + }); + } + +} \ No newline at end of file diff --git a/src/controllers/ai_appointments.controller.ts b/src/controllers/ai_appointments.controller.ts new file mode 100644 index 0000000..5d07712 --- /dev/null +++ b/src/controllers/ai_appointments.controller.ts @@ -0,0 +1,72 @@ +import { HttpException } from "@/exceptions/HttpException"; +import { RequestWithUser, USER_ROLE } from "@/interfaces"; +import { AiAppointmentsService } from "@/services/ai_appointments.service"; +import { catchAsync } from "@/utils/catchAsync"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; +import { createMultiLangMessage, SuccessResponseMessages } from "@/utils/responseMessages"; +import e, { Request, Response, NextFunction } from "express"; +import Container from "typedi"; + +export class AiAppointmentsController { + public aiAppointmentsService = Container.get(AiAppointmentsService); + + public getUploadUrl = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const appointmentId = req.params.appointmentId; + const userType = req.query.userType as USER_ROLE.DOCTOR | USER_ROLE.PATIENT | "MIXED"; + + if (userType !== USER_ROLE.DOCTOR && userType !== USER_ROLE.PATIENT && userType !== "MIXED") { + const error = createBilingualError(400, ErrorMessages.INVALID_USER_TYPE); + throw new HttpException(error.status, error.message, error.messageAr); + } + const objectKey = `appointments/${appointmentId}/${userType}.webm`; + + const isAppointmentExist = await this.aiAppointmentsService.checkAppointmentExistence(appointmentId); + if (!isAppointmentExist) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const uploadUrl = await this.aiAppointmentsService.getUploadUrl(objectKey); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.UPLOAD_URL_GENERATED); + res.status(200).json({ + ...responseMessage, + data: { + uploadUrl, + objectKey + } + }); + }) + + public processAudioAI = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const appointmentId = req.params.appointmentId; + const { doctorKey, patientKey, mixedKey, prompt } = req.body; + + const isAppointmentExist = await this.aiAppointmentsService.checkAppointmentExistence(appointmentId); + if (!isAppointmentExist) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + let finalScript: string; + + if (doctorKey && patientKey) { + finalScript = await this.aiAppointmentsService.processSeparateAudioAI(doctorKey, patientKey); + } + + else if (mixedKey) { + finalScript = await this.aiAppointmentsService.processMixedAudioAI(mixedKey); + } + + else { + const error = createBilingualError(400, ErrorMessages.MISSING_AUDIO_KEYS); + throw new HttpException(error.status, error.message, error.messageAr); + } + const SOAP = await this.aiAppointmentsService.generateSOAP(finalScript, prompt); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.SOAP_GENERATED); + res.status(202).json({ + ...responseMessage, + data: { + SOAP + } + }); + }) +} \ No newline at end of file diff --git a/src/controllers/appointment.controller.ts b/src/controllers/appointment.controller.ts new file mode 100644 index 0000000..3ebea72 --- /dev/null +++ b/src/controllers/appointment.controller.ts @@ -0,0 +1,568 @@ +import { Request, Response, NextFunction } from "express"; +import { RequestWithUser } from "@/interfaces"; +import { HttpException } from "@/exceptions/HttpException"; +import { catchAsync } from '@/utils/catchAsync'; +import { AppointmentService } from "@/services/appointment.service" +import { AppointmentStatusChangedPayload } from "@/interfaces"; +import Container from "typedi"; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { SuccessResponseMessages, createMultiLangMessage } from '@/utils/responseMessages'; +import { SocketService } from "@/services/socket.service"; +import { Agora_APP_ID } from "@/config"; + +export class AppointmentController { + + public appointmentService = Container.get(AppointmentService); + public socketService = Container.get(SocketService); + + public getAvailableDays = catchAsync(async (req: Request, res: Response): Promise => { + const { doctorId } = req.params; + const { clinicId } = req.query; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const availableDays = await this.appointmentService.getAvailableDays(doctorId, clinicId as string || null) + const response = createMultiLangMessage(SuccessResponseMessages.AVAILABLE_DAYS_RETRIEVED); + res.status(200).json({ + data: availableDays, + ...response + }); + + }); + + public getAvailableSlots = catchAsync(async (req: Request, res: Response): Promise => { + const { doctorId } = req.params; + const { date, clinicId } = req.query; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!date) { + const error = createBilingualError(400, ErrorMessages.DATE_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // validate date format (YYYY-MM-DD) + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(date as string)) { + const error = createBilingualError(400, ErrorMessages.INVALID_DATE_FORMAT); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const requestedDate = new Date(date as string); + if (isNaN(requestedDate.getTime())) { + const error = createBilingualError(400, ErrorMessages.INVALID_DATE_FORMAT); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const availableSlots = await this.appointmentService.getAvailableSlots(doctorId, clinicId as string || null, date as string) + const response = createMultiLangMessage(SuccessResponseMessages.AVAILABLE_SLOTS_RETRIEVED); + res.status(200).json({ + data: availableSlots, + ...response + }); + + }); + + public bookAppointment = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const { doctorId, clinicId, scheduledTime } = req.body; + const scheduledDate = new Date(scheduledTime); + + if (isNaN(scheduledDate.getTime())) { + const error = createBilingualError(400, ErrorMessages.INVALID_SCHEDULED_TIME); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.appointmentService.bookAppointment(patientId, doctorId, clinicId || null, scheduledDate); + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_BOOKED_SUCCESSFULLY); + + res.status(201).json({ + ...response + }); + }); + + public getPatientAppointments = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + + if (!patientId) { + const error = createBilingualError(400, ErrorMessages.PATIENT_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const appointments = await this.appointmentService.getPatientAppointments(patientId); + const response = createMultiLangMessage(SuccessResponseMessages.PATIENT_APPOINTMENTS_RETRIEVED); + res.status(200).json({ + data: appointments, + ...response + }); + }); + + public getTodayAppointment = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + + if (!patientId) { + const error = createBilingualError(400, ErrorMessages.PATIENT_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const appointments = await this.appointmentService.getTodayAppointment(patientId); + const response = createMultiLangMessage(SuccessResponseMessages.PATIENT_TODAY_APPOINTMENT_RETRIEVED); + res.status(200).json({ + data: appointments, + ...response + }); + }); + + public getPatientSelectedAppointment = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const { appointmentId } = req.params; + + if (!patientId) { + const error = createBilingualError(400, ErrorMessages.PATIENT_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const appointment = await this.appointmentService.getPatientSelectedAppointment(appointmentId, patientId); + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_DETAILS_RETRIEVED); + res.status(200).json({ + data: appointment, + ...response + }); + }); + + public rescheduleAppointmentByPatient = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const { appointmentId } = req.params; + const { newScheduledTime } = req.body; + + const result = await this.appointmentService.rescheduleAppointmentByPatient(patientId, appointmentId, new Date(newScheduledTime)); + + const payload: AppointmentStatusChangedPayload = { + appointmentId, + newStatus: 'CONFIRMED', + doctorId: result.doctorId, + patientId: result.patientId, + patientName: result.patientName, + appointmentDate: result.appointmentDate, + startTime: result.startTime, + }; + await this.socketService.emitAppointmentStatusChanged(payload); + + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_RESCHEDULED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }); + + public cancelAppointment = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const userId = req.user.id; + const { appointmentId } = req.params; + + const result = await this.appointmentService.cancelAppointment(userId, appointmentId); + const payload: AppointmentStatusChangedPayload = { + appointmentId, + newStatus: 'CANCELLED', + doctorId: result.doctorId, + patientId: result.patientId, + patientName: result.patientName, + appointmentDate: result.appointmentDate, + startTime: result.startTime, + }; + await this.socketService.emitAppointmentStatusChanged(payload); + await this.socketService.emitQueueUpdatesToPatients(result.doctorId, new Date(result.appointmentDate)); + + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_CANCELLED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }); + + public rescheduleAppointmentByDoctor = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { appointmentId } = req.params; + const { minutes } = req.body; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (minutes > 60) { + const error = createBilingualError(400, ErrorMessages.MINUTES_EXCEEDED_LIMIT); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const affectedAppointments = await this.appointmentService.rescheduleAppointmentByDoctor(doctorId, appointmentId, minutes); + for (const appointment of affectedAppointments) { + const payload: AppointmentStatusChangedPayload = { + appointmentId: appointment.appointmentId, + newStatus: 'CONFIRMED', + doctorId: appointment.doctorId, + patientId: appointment.patientId, + patientName: appointment.patientName, + appointmentDate: appointment.appointmentDate, + startTime: appointment.startTime, + }; + await this.socketService.emitAppointmentStatusChanged(payload); + } + + if (affectedAppointments.length > 0) { + await this.socketService.emitQueueUpdatesToPatients(doctorId, new Date(affectedAppointments[0].appointmentDate)); + } + + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_RESCHEDULED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }); + + public getUpcommingDoctorSchedule = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const schedule = await this.appointmentService.getUpcommingDoctorSchedule(doctorId); + const response = createMultiLangMessage(SuccessResponseMessages.DOCTOR_SCHEDULE_RETRIEVED); + res.status(200).json({ + data: schedule, + ...response + }); + }); + + public getCurrentDoctorSchedule = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const schedule = await this.appointmentService.getCurrentDoctorSchedule(doctorId); + const response = createMultiLangMessage(SuccessResponseMessages.DOCTOR_SCHEDULE_RETRIEVED); + res.status(200).json({ + data: schedule, + ...response + }); + }); + + public getDoctorAppointmentContext = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { appointmentId } = req.params; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!appointmentId) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const context = await this.appointmentService.getDoctorAppointmentContext(doctorId, appointmentId); + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_DETAILS_RETRIEVED); + + res.status(200).json({ + data: context, + ...response, + }); + }); + + public enterDoctorSchedule = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { + clinicId, + workingDay, + startTime, + endTime, + slotDuration, + bufferTime, + isOnline, + } = req.body; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.appointmentService.enterDoctorSchedule(doctorId, clinicId || null, workingDay, startTime, endTime, slotDuration, bufferTime, isOnline); + + const response = createMultiLangMessage(SuccessResponseMessages.SCHEDULE_CREATED_SUCCESSFULLY); + res.status(201).json({ + ...response + }); + }); + + public getDoctorSchedule = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const schedules = await this.appointmentService.getDoctorSchedule(doctorId); + const response = createMultiLangMessage(SuccessResponseMessages.DOCTOR_SCHEDULE_RETRIEVED); + res.status(200).json({ + data: schedules, + ...response + }); + + }); + + public editDoctorSchedule = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const { scheduleId, workingDay, ...body } = req.body; + + // for resolving the mapping issue with the db + const updates = this.appointmentService.convertKeysToSnakeCase(body); + + if (workingDay !== undefined) { + updates.day_of_week = this.appointmentService.getDayOfWeek(workingDay); + } + await this.appointmentService.editDoctorSchedule(doctorId, scheduleId, updates); + const response = createMultiLangMessage(SuccessResponseMessages.SCHEDULE_UPDATED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + + }); + + public getScheduleByDate = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { date } = req.query; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!date) { + const error = createBilingualError(400, ErrorMessages.DATE_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(date as string)) { + const error = createBilingualError(400, ErrorMessages.INVALID_DATE_FORMAT); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const scheduleData = await this.appointmentService.getScheduleByDate(doctorId, date as string); + const response = createMultiLangMessage(SuccessResponseMessages.DOCTOR_SCHEDULE_RETRIEVED); + res.status(200).json({ + data: scheduleData, + ...response + }); + }); + + public checkConflictingAppointments = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { scheduleId, startDate, endDate } = req.query; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!scheduleId) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (startDate || endDate) { + if (!startDate || !endDate) { + const error = createBilingualError(400, ErrorMessages.VACATION_DATES_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(startDate as string) || !dateRegex.test(endDate as string)) { + const error = createBilingualError(400, ErrorMessages.INVALID_DATE_FORMAT); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const start = new Date(startDate as string); + const end = new Date(endDate as string); + + if (start >= end) { + const error = createBilingualError(400, ErrorMessages.INVALID_DATE_RANGE); + throw new HttpException(error.status, error.message, error.messageAr); + } + } + + const checkResult = await this.appointmentService.checkConflictingAppointments(doctorId, scheduleId as string, startDate as string | undefined, endDate as string | undefined); + + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENTS_CHECK_COMPLETED); + res.status(200).json({ + ...response, + data: checkResult + }); + }) + + public handleDoctorVacation = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { scheduleId, startDate, endDate } = req.body; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!scheduleId) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!startDate || !endDate) { + const error = createBilingualError(400, ErrorMessages.VACATION_DATES_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + + await this.appointmentService.handleDoctorVacation(doctorId, scheduleId, startDate, endDate); + const response = createMultiLangMessage(SuccessResponseMessages.VACATION_SET_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }) + + public deleteDoctorSchedule = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { scheduleId } = req.body; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!scheduleId) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.appointmentService.deleteDoctorSchedule(doctorId, scheduleId); + const response = createMultiLangMessage(SuccessResponseMessages.SCHEDULE_DELETED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }) + + public getDoctorVacations = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const doctorVacations = await this.appointmentService.getDoctorVacations(doctorId) + + const response = createMultiLangMessage(SuccessResponseMessages.DOCTOR_VACATIONS_RETRIEVED); + res.status(200).json({ + ...response, + data: doctorVacations + }); + }) + + public cancelDoctorVacation = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const { vacationId, scheduleId } = req.body; + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!scheduleId) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.appointmentService.cancelDoctorVacation(doctorId, vacationId, scheduleId); + const response = createMultiLangMessage(SuccessResponseMessages.VACATION_REMOVED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }); + + public getAppointmentsByDate = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const nurseId = req.user?.id; + const { doctorId, clinicId, date } = req.query; + + if (!nurseId) { + const error = createBilingualError(400, ErrorMessages.NURSE_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!doctorId) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!date) { + const error = createBilingualError(400, ErrorMessages.DATE_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(date as string)) { + const error = createBilingualError(400, ErrorMessages.INVALID_DATE_FORMAT); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const appointments = await this.appointmentService.getAppointmentsByDate(doctorId as string, clinicId as string, date as string); + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENTS_BY_NURSE_RETRIEVED); + res.status(200).json({ + data: appointments, + ...response + }); + }); + + public completeAppointment = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const nurseId = req.user?.id; + const { appointmentId } = req.params; + + if (!nurseId) { + const error = createBilingualError(400, ErrorMessages.NURSE_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.appointmentService.completeAppointment(appointmentId); + const response = createMultiLangMessage(SuccessResponseMessages.APPOINTMENT_COMPLETED_SUCCESSFULLY); + res.status(200).json({ + ...response + }); + }); + + public getAgoraToken = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const userId = req.user?.id; + const { appointmentId } = req.params; + + const token = await this.appointmentService.generateAgoraToken(appointmentId, userId); + const response = createMultiLangMessage(SuccessResponseMessages.AGORA_TOKEN_GENERATED_SUCCESSFULLY); + res.status(200).json({ + ...response, + data: { + token, + appId: Agora_APP_ID, + uid: userId + } + }); + }); +} \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..7e96069 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,159 @@ +import { NextFunction, Request, Response } from 'express'; +import { Container } from 'typedi'; +import { RequestWithUser } from '@interfaces/auth.interface'; +import { User } from '@interfaces/users.interface'; +import { AuthService } from '@services/auth.service'; +import { ChangePasswordDto, CompleteUserProfileDto, CreateUserDto, LoginUserDto, ResetPasswordDto } from '@/dtos/users.dto'; +import { catchAsync } from '@/utils/catchAsync'; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { HttpException } from '@/exceptions/HttpException'; +import { createMultiLangMessage, SuccessResponseMessages } from '@/utils/responseMessages'; + +export class AuthController { + public auth = Container.get(AuthService); + + public signUp = catchAsync(async (req: Request, res: Response, next: NextFunction): Promise => { + const userData: CreateUserDto = req.body; + const { createdUserData, cookies } = await this.auth.signup(userData); + + res.setHeader('Set-Cookie', cookies); + + await this.auth.sendEmailOtp(userData.email); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.SIGNED_UP_SUCCESSFULLY); + res.status(201).json({ + data: createdUserData, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public logIn = catchAsync(async (req: Request, res: Response, next: NextFunction): Promise => { + const userData: LoginUserDto = req.body; + const { cookies, findUser } = await this.auth.login(userData); + + res.setHeader('Set-Cookie', cookies); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.LOGGED_IN_SUCCESSFULLY); + res.status(200).json({ + data: findUser, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public logOut = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const userData: User = req.user; + const logOutUserData: User = await this.auth.logout(userData); + + res.setHeader('Set-Cookie', [ + 'Authorization=; HttpOnly; Max-Age=0; Path=/; SameSite=Lax', + 'RefreshToken=; HttpOnly; Max-Age=0; Path=/; SameSite=Lax' + ]); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.LOGGED_OUT_SUCCESSFULLY); + res.status(200).json({ + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public refresh = catchAsync(async (req: Request, res: Response, next: NextFunction): Promise => { + const refreshToken = req.cookies?.RefreshToken; + const { cookies, user, accessToken } = await this.auth.refreshAccessToken(refreshToken); + + cookies.forEach((cookie: string) => { + res.append('Set-Cookie', cookie); + }); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.TOKEN_REFRESHED_SUCCESSFULLY); + res.status(200).json({ + data: { + user, + accessToken: { + expiresIn: accessToken.expiresIn, + expiresAt: new Date(Date.now() + accessToken.expiresIn * 1000) + } + }, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public completeProfile = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const userData: User = req.user; + const profileData: CompleteUserProfileDto = req.body; + const updatedUserData: User = await this.auth.completeProfile(userData, profileData); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PROFILE_COMPLETED_SUCCESSFULLY); + res.status(200).json({ + data: updatedUserData, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public verifyOTP = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const email = await this.auth.getUserEmail(req) + const { otp } = req.body; + if (!otp) { + const error = createBilingualError(400, ErrorMessages.OTP_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + const isSuccessful = await this.auth.verifyEmailOtp(email, otp); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.OTP_VERIFIED_SUCCESSFULLY); + res.status(200).json({ + data: isSuccessful, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public forgetPassword = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const email = req.body.email; + if (!email) { + const error = createBilingualError(400, ErrorMessages.EMAIL_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + await this.auth.sendPasswordResetEmail(email); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PASSWORD_RESET_EMAIL_SENT_SUCCESSFULLY); + res.status(200).json({ + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public resetPassword = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const { token, newPassword }: ResetPasswordDto = req.body; + + await this.auth.resetPassword(token, newPassword); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PASSWORD_RESET_SUCCESSFULLY); + res.status(200).json({ + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public resendOTP = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const email = await this.auth.getUserEmail(req) + await this.auth.sendEmailOtp(email); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.OTP_RESENT_SUCCESSFULLY); + res.status(200).json({ + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public checkPassword = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const userId = req.user?.id; + const { password } = req.body; + const isMatch = await this.auth.checkPassword(userId, password); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PASSWORD_CHECK_SUCCESSFUL); + res.status(200).json({ data: { isMatch }, messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public changePassword = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const userId = req.user?.id; + const { newPassword }: ChangePasswordDto = req.body; + await this.auth.changePassword(userId, newPassword); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PASSWORD_CHANGED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } +} + diff --git a/src/controllers/clinic.controller.ts b/src/controllers/clinic.controller.ts new file mode 100644 index 0000000..f4620d3 --- /dev/null +++ b/src/controllers/clinic.controller.ts @@ -0,0 +1,149 @@ +import { CreateUpdateClinicRequestDto } from "@/dtos/clinics.dto"; +import { HttpException } from "@/exceptions/HttpException"; +import { RequestWithUser } from "@/interfaces"; +import { ClinicService } from "@/services/clinic.service"; +import { catchAsync } from "@/utils/catchAsync"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; +import { createMultiLangMessage, SuccessResponseMessages } from "@/utils/responseMessages"; +import { NextFunction, Request, Response } from "express"; +import Container from "typedi"; +import { Gender } from "@prisma/client"; + +export class ClinicController { + public clinicService = Container.get(ClinicService); + + public createClinic = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const clinicData: CreateUpdateClinicRequestDto = req.body; + + const isAllowedToCreateClinic = await this.clinicService.isDoctorAllowedToCreateClinic(req.user.id); + if (!isAllowedToCreateClinic) { + const error = createBilingualError(403, ErrorMessages.MAX_CLINICS_REACHED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const createdClinic = await this.clinicService.createClinic(req.user.id, clinicData); + + this.clinicService.linkDoctorToClinic(req.user.id, createdClinic, clinicData.fees); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_CREATED_SUCCESSFULLY); + + res.status(201).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public getClinicById = catchAsync(async (req: Request, res: Response, next: NextFunction): Promise => { + const clinicId = req.params.id; + + const clinic = await this.clinicService.getClinicById(clinicId); + if (!clinic) { + const error = createBilingualError(404, ErrorMessages.CLINIC_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_RETRIEVED); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr, data: clinic }); + }); + + public updateClinicById = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const clinicId = req.params.id; + const clinicUpdateData: CreateUpdateClinicRequestDto = req.body; + const isCreatingDoctor = await this.clinicService.isCreatingDoctorOfClinic(req.user.id, clinicId); + if (!isCreatingDoctor) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_CLINIC_UPDATE); + throw new HttpException(error.status, error.message, error.messageAr); + } + const isClinicUpdated = await this.clinicService.updateClinic(req.user.id, clinicId, clinicUpdateData); + + if (!isClinicUpdated) { + const error = createBilingualError(404, ErrorMessages.CLINIC_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_UPDATED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public deleteClinicById = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const clinicId = req.params.id; + + const isCreatingDoctor = await this.clinicService.isCreatingDoctorOfClinic(req.user.id, clinicId); + + if (!isCreatingDoctor) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_CLINIC_DELETION); + throw new HttpException(error.status, error.message, error.messageAr); + } + await this.clinicService.deleteClinic(clinicId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_DELETED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public getDoctorClinics = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction) => { + const doctorId = req.user?.id; + const clinics = await this.clinicService.getDoctorClinics(doctorId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_DOCTORS_RETRIEVED); + res.status(200).json({ + data: clinics, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + }); + + public getClinicDoctors = async (req: Request, res: Response, next: NextFunction) => { + const { clinicId } = req.params; + const { gender, minFees, maxFees } = req.query; + + let validGender: Gender | undefined = undefined; + if (gender && typeof gender === 'string') { + const upperGender = gender.toUpperCase(); + if (Object.values(Gender).includes(upperGender as Gender)) { + validGender = upperGender as Gender; + } + } + + const finalMinFees = minFees && typeof minFees === 'string' ? parseFloat(minFees) : undefined; + const finalMaxFees = maxFees && typeof maxFees === 'string' ? parseFloat(maxFees) : undefined; + + if (finalMinFees > finalMaxFees){ + const error = createBilingualError(404, ErrorMessages.INVALID_FEES_RANGE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const doctors = await this.clinicService.getClinicDoctors(clinicId, validGender, finalMinFees, finalMaxFees); + const response = createMultiLangMessage(SuccessResponseMessages.CLINIC_DOCTORS_RETRIEVED); + res.status(200).json({ + data: doctors, + ...response + }); + } + + public getActiveClinics = async (req: Request, res: Response, next: NextFunction): Promise => { + const { canPayOnline, lang } = req.query; + if (!lang || (lang !== 'en' && lang !== 'ar')) { + const error = createBilingualError(400, ErrorMessages.SPECIALIZATION_LANG); + throw new HttpException(400, error.message, error.messageAr); + } + + const payOnline = canPayOnline !== undefined ? canPayOnline === 'true' : undefined; + const clinics = await this.clinicService.getActiveClinics(lang as 'en' | 'ar', payOnline); + const response = createMultiLangMessage(SuccessResponseMessages.CLINICS_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: clinics, + ...response + }); + } + + public updateClinicFeesById = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const clinicId = req.params.id; + const { fees } = req.body; + const isDoctorLinkedToClinic = await this.clinicService.isDoctorLinkedToClinic(req.user.id, clinicId); + if (!isDoctorLinkedToClinic) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_CLINIC_UPDATE); + throw new HttpException(error.status, error.message, error.messageAr); + } + const isFeesUpdated = await this.clinicService.updateClinicFees(req.user.id, clinicId, fees); + + if (!isFeesUpdated) { + const error = createBilingualError(404, ErrorMessages.CLINIC_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const responseMessage = createMultiLangMessage(SuccessResponseMessages.CLINIC_FEES_UPDATED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); +} diff --git a/src/controllers/doctor.controller.ts b/src/controllers/doctor.controller.ts new file mode 100644 index 0000000..cc64cb6 --- /dev/null +++ b/src/controllers/doctor.controller.ts @@ -0,0 +1,191 @@ + +import { DoctorLoginRequestDto, DoctorSetPasswordRequestDto, DoctorSignupRequestDto, PostAnnouncementDto, EditAnnouncementDto } from "@/dtos/doctors.dto"; +import { RequestWithUser } from "@/interfaces"; +import { DoctorService } from "@/services/doctor.service"; +import { UserService } from "@/services/user.service"; +import { HttpException } from "@/exceptions/HttpException"; +import { createMultiLangMessage, SuccessResponseMessages } from "@/utils/responseMessages"; +import { NextFunction, Request, Response } from "express"; +import { Container } from "typedi"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; + + +export class DoctorController { + public doctorService = Container.get(DoctorService); + public userService = Container.get(UserService); + + public doctorSignup = async (req: Request, res: Response, next: NextFunction) => { + const doctorData: DoctorSignupRequestDto = req.body; + const doctorFiles = req.files as Express.Multer.File[]; + await this.doctorService.signup(doctorData, doctorFiles); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.DOCTOR_CREATED_WAITING_VERIFICATION); + res.status(201).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }; + + public doctorLogin = async (req: Request, res: Response, next: NextFunction) => { + const doctorLoginData: DoctorLoginRequestDto = req.body; + const loginResult = await this.doctorService.login(doctorLoginData); + + if (loginResult === false) { + // For testing purposes only - To Be CHANGED according to Frontend Link + res.redirect('/test') + } else if (typeof loginResult === 'object') { + const { cookies, doctorAccountData } = loginResult; + res.setHeader('Set-Cookie', cookies); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.DOCTOR_RETRIEVED); + res.status(200).json({ + data: doctorAccountData, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + } + } + + public doctorSetPassword = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const doctorId = req.user?.id; + const { password }: DoctorSetPasswordRequestDto = req.body; + await this.doctorService.setPassword(doctorId, password); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PASSWORD_SET_SUCCESSFULLY_BY_DOCTOR); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public getDoctors = async (req: Request, res: Response, next: NextFunction): Promise => { + const { gender, minFees, maxFees, isOnline, lang } = req.query; + + if (!lang || (lang !== 'en' && lang !== 'ar')) { + const error = createBilingualError(400, ErrorMessages.SPECIALIZATION_LANG); + throw new HttpException(400, error.message, error.messageAr); + } + + const finalIsOnline = isOnline !== undefined ? isOnline === 'true' : undefined; + const finalGender = gender as string | undefined; + + const finalMinFees = minFees && typeof minFees === 'string' ? parseFloat(minFees) : undefined; + const finalMaxFees = maxFees && typeof maxFees === 'string' ? parseFloat(maxFees) : undefined; + + if (finalMinFees > finalMaxFees) { + const error = createBilingualError(404, ErrorMessages.INVALID_FEES_RANGE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const doctors = await this.doctorService.getDoctors(lang as 'en' | 'ar', finalGender, finalMinFees, finalMaxFees, finalIsOnline); + + const response = createMultiLangMessage(SuccessResponseMessages.DOCTORS_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: doctors, + ...response + }); + } + + public postAnnouncement = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + const announcementData: PostAnnouncementDto = req.body; + + await this.doctorService.postAnnouncement(doctorId, announcementData); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ANNOUNCEMENT_CREATED_SUCCESSFULLY); + res.status(201).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public getDoctorAnnouncements = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + const announcements = await this.doctorService.getDoctorAnnouncements(doctorId); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ANNOUNCEMENTS_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: announcements, + ...responseMessage + }); + } + + public getAnnouncementApplicants = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + if (!doctorId) { + const error = createBilingualError(401, ErrorMessages.DOCTOR_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const { announcementId } = req.params; + const applicants = await this.doctorService.getAnnouncementApplicants(doctorId, announcementId); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.APPLICANTS_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: applicants, + ...responseMessage + }); + } + + public getWorkingNurses = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + if (!doctorId) { + const error = createBilingualError(401, ErrorMessages.DOCTOR_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const nurses = await this.doctorService.getWorkingNurses(doctorId); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSES_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: nurses, + ...responseMessage + }); + } + + public approveApplicant = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + const { applicantId } = req.params; + const { announcementId } = req.query; + if (!doctorId) { + const error = createBilingualError(401, ErrorMessages.DOCTOR_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.doctorService.approveApplicant(announcementId as string, applicantId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.APPLICANT_APPROVED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public rejectApplicant = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + const { applicantId } = req.params; + const { announcementId } = req.query; + + if (!doctorId) { + const error = createBilingualError(401, ErrorMessages.DOCTOR_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + await this.doctorService.rejectApplicant(announcementId as string, applicantId); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.APPLICANT_REJECTED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public deleteAnnouncement = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + const { announcementId } = req.params; + + if (!doctorId) { + const error = createBilingualError(401, ErrorMessages.DOCTOR_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.doctorService.deleteAnnouncement(doctorId, announcementId); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ANNOUNCEMENT_DELETED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public editAnnouncement = async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const doctorId = req.user?.id; + const { announcementId } = req.params; + const announcementData: EditAnnouncementDto = req.body; + + if (!doctorId) { + const error = createBilingualError(401, ErrorMessages.DOCTOR_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.doctorService.editAnnouncement(doctorId, announcementId, announcementData); + + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ANNOUNCEMENT_EDITED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } +} \ No newline at end of file diff --git a/src/controllers/fabric.controller.ts b/src/controllers/fabric.controller.ts new file mode 100644 index 0000000..67d077d --- /dev/null +++ b/src/controllers/fabric.controller.ts @@ -0,0 +1,87 @@ +import { NextFunction, Request, Response } from 'express'; +import FabricService from '@/services/fabric.service'; +import identityStorage from '@/services/identity-storage.service'; +import { FabricIdentityInput } from '@/interfaces/fabric-identity.interface'; +import { HttpException } from '@/exceptions/HttpException'; + +class FabricController { + public fabricService = new FabricService(); + + + public onboardIdentity = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const input: FabricIdentityInput = req.body; + + // Validate required fields + if (!input.clinicId || !input.mspId || !input.certificate || + !input.privateKey || !input.peerEndpoint || !input.peerHostAlias || + !input.tlsCertificate) { + throw new HttpException(400, 'Missing required fields: clinicId, mspId, certificate, privateKey, peerEndpoint, peerHostAlias, tlsCertificate'); + } + + const identity = await identityStorage.storeIdentity(input); + res.status(201).json({ + data: identity, + message: 'Identity onboarded successfully' + }); + } catch (error) { + next(error); + } + }; + + + public listIdentities = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const identities = await identityStorage.listIdentities(); + res.status(200).json({ data: identities, message: 'listIdentities' }); + } catch (error) { + next(error); + } + }; + + public deleteIdentity = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const clinicId = req.params.clinicId; + await this.fabricService.closeConnection(clinicId); + await identityStorage.deleteIdentity(clinicId); + res.status(200).json({ message: 'Identity deleted successfully' }); + } catch (error) { + next(error); + } + }; + + public getConnectionStats = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const stats = this.fabricService.getConnectionStats(); + res.status(200).json({ data: stats, message: 'connectionStats' }); + } catch (error) { + next(error); + } + }; + + public initLedger = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const clinicId = req.params.clinicId; + const backupData = Array.isArray(req.body?.backupData) ? req.body.backupData : []; + await this.fabricService.initLedger(clinicId, backupData); + res.status(200).json({ message: 'Ledger initialized', seeded: backupData.length }); + } catch (error) { + next(error); + } + }; + + public checkHealth = async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const stats = this.fabricService.getConnectionStats(); + res.status(200).json({ + status: 'OK', + message: 'Fabric service is healthy', + activeConnections: stats.total + }); + } catch (error) { + next(error); + } + }; +} + +export default FabricController; \ No newline at end of file diff --git a/src/controllers/googleAuth.controller.ts b/src/controllers/googleAuth.controller.ts new file mode 100644 index 0000000..0532741 --- /dev/null +++ b/src/controllers/googleAuth.controller.ts @@ -0,0 +1,65 @@ +import { AuthService } from "@/services/auth.service"; +import passport from "passport"; +import { Container } from "typedi"; +import { NextFunction, Request, Response } from "express"; +import { User, UserLoginData } from "@/interfaces/users.interface"; +import { RequestWithUser } from "@/interfaces"; +import { GoogleAuthService } from "@/services/googleAuth.service"; +import { catchAsync } from "@/utils/catchAsync"; +import { createMultiLangMessage, SuccessResponseMessages } from "@/utils/responseMessages"; + +export class GoogleAuthController { + public authService = Container.get(AuthService); + public googleAuthService = Container.get(GoogleAuthService); + + public googleOAuth = passport.authenticate('google', { + scope: ['profile', 'email'], + }); + + public googleOAuthCallback = (req: Request, res: Response, next: NextFunction) => { + passport.authenticate('google', { + failureRedirect: '/login', + }, async (err, user: User, info: { isNewUser?: boolean }) => { + if (err) { + return next(err); + } + if (!user) { + return res.redirect('/login'); + } + + try { + // Generate JWT tokens for Google OAuth user + const tokenResponse = await this.authService.createTokens(user, true); + const cookies = this.authService.createCookies(tokenResponse); + + // Set JWT cookies + res.setHeader('Set-Cookie', cookies); + + // Check if it's a new user from the info object + const isNewUser = info?.isNewUser || false; + + // Redirect based on whether it's first time or not + if (isNewUser) { + res.redirect(`${process.env.FRONTEND_URL}/api/auth/google-callback`); + } else { + res.redirect(`${process.env.FRONTEND_URL}/api/auth/google-callback`); + } + } catch (error) { + next(error); + } + })(req, res, next); + }; + + public updatePhoneNumber = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const phone: string = req.body.phone; + await this.googleAuthService.updatePhoneNumber(req.user.id, phone); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PHONE_NUMBER_UPDATED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public getGoogleUserData = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction): Promise => { + const googleUserData: UserLoginData = await this.googleAuthService.getGoogleUserData(req.user.id); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.GOOGLE_USER_DATA_RETRIEVED); + res.status(200).json({ data: googleUserData, messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); +} \ No newline at end of file diff --git a/src/controllers/medical-records.controller.ts b/src/controllers/medical-records.controller.ts new file mode 100644 index 0000000..8d37f1e --- /dev/null +++ b/src/controllers/medical-records.controller.ts @@ -0,0 +1,147 @@ +import { CreateDoctorRecordJsonDto, CreateMedicalRecordDto, CreatePatientMedicalHistoryDto, UpdatePatientMedicalHistoryDto } from '@/dtos/medical-records.dto'; +import { Request, Response } from 'express'; +import { RequestWithUser } from '@/interfaces/auth.interface'; +import { MedicalRecordService } from '@/services/medical-records.service'; +import { catchAsync } from '@/utils/catchAsync'; +import { RecordType } from '@/interfaces'; + +export class MedicalRecordController { + + private medicalRecordService = new MedicalRecordService(); + + + public getIpfsHealth = catchAsync(async (req: Request, res: Response): Promise => { + const result = await this.medicalRecordService.checkIpfsHealth(); + res.status(200).json(result); + }); + + public getPatientRecordsMetadata = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + + const records = await this.medicalRecordService.getPatientFiles(patientId); + + res.status(200).json({ + message: 'Medical records retrieved successfully', + data: records, + }); + }); + + public createDoctorRecord = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const clinicId = req.params.clinicId; + const patientId = req.params.patientId; + const doctorId = req.user.id; + const dto: CreateDoctorRecordJsonDto = req.body; + + const recordId = await this.medicalRecordService.addDoctorRecord( + clinicId, + patientId, + doctorId, + dto, + ); + + res.status(201).json({ + message: 'Medical record created successfully', + data: { recordId }, + }); + }); + + public createPatientMedicalHistory = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const dto: CreatePatientMedicalHistoryDto = req.body; + + const recordId = await this.medicalRecordService.addPatientMedicalHistory(patientId, dto); + + res.status(201).json({ + message: 'Medical history entry created successfully', + data: { recordId }, + }); + }); + + public updatePatientMedicalHistory = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const recordId = req.params.recordId; + const dto: UpdatePatientMedicalHistoryDto = req.body; + + await this.medicalRecordService.updatePatientMedicalHistory(patientId, recordId, dto); + + res.status(200).json({ + message: 'Medical history entry updated successfully', + }); + }); + + public deletePatientMedicalHistory = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const recordId = req.params.recordId; + + await this.medicalRecordService.deletePatientMedicalHistory(patientId, recordId); + + res.status(200).json({ + message: 'Medical history entry deleted successfully', + }); + }); + + public getPatientVisitSummaries = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + + const notes = await this.medicalRecordService.getSOAPNotesForPatient(patientId, RecordType.VISIT_SUMMARY); + + res.status(200).json({ + message: 'Visit summaries retrieved successfully', + data: notes, + }); + }); + + public getPatientMedicalHistory = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + + const history = await this.medicalRecordService.getMedicalHistory(patientId); + + res.status(200).json({ + message: 'Medical history retrieved successfully', + data: history, + }); + }); + + public getDoctorPatientVisitSummaries = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const patientId = req.params.patientId; + + const notes = await this.medicalRecordService.getVisitSummariesForDoctor(doctorId, patientId); + + res.status(200).json({ + message: 'Visit summaries retrieved successfully', + data: notes, + }); + }); + + public getDoctorPatientMedicalHistory = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const doctorId = req.user.id; + const patientId = req.params.patientId; + + const history = await this.medicalRecordService.getMedicalHistoryForDoctor(doctorId, patientId); + + res.status(200).json({ + message: 'Medical history retrieved successfully', + data: history, + }); + }); + + public grantPatientAccess = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const patientId = req.user.id; + const { targetClinicId } = req.body; + + await this.medicalRecordService.grantAccess(patientId, targetClinicId); + + res.status(200).json({ + message: 'Access granted successfully', + }); + }); + + public deleteAllRecords = catchAsync(async (_req: Request, res: Response): Promise => { + const result = await this.medicalRecordService.deleteAllRecords(); + res.status(200).json({ + message: `Deleted ${result.deleted} records from DB, IPFS, and blockchain`, + data: result, + }); + }); +} diff --git a/src/controllers/nurse.controller.ts b/src/controllers/nurse.controller.ts new file mode 100644 index 0000000..a6c3d18 --- /dev/null +++ b/src/controllers/nurse.controller.ts @@ -0,0 +1,93 @@ +import { Request, Response, NextFunction } from "express"; +import { RequestWithUser } from "@/interfaces"; +import { HttpException } from "@/exceptions/HttpException"; +import { catchAsync } from '@/utils/catchAsync'; +import { NurseService } from "@/services/nurse.service"; +import Container from "typedi"; +import { NurseSignupRequestDto, NurseLoginRequestDto, NurseSetPasswordRequestDto} from "@/dtos/nurses.dto"; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { SuccessResponseMessages, createMultiLangMessage } from '@/utils/responseMessages'; + + +export class NurseController { + private nurseService = Container.get(NurseService); + + public nurseSignup = catchAsync(async (req: Request, res: Response, next: NextFunction) => { + const nurseData: NurseSignupRequestDto = req.body; + const nurseFiles = req.files as Express.Multer.File[]; + await this.nurseService.nurseSignup(nurseData, nurseFiles); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSE_CREATED_WAITING_VERIFICATION); + res.status(201).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public nurseLogin = async (req: Request, res: Response, next: NextFunction) => { + const nurseLoginData: NurseLoginRequestDto = req.body; + const loginResult = await this.nurseService.nurseLogin(nurseLoginData); + + if (loginResult === false) { + res.redirect('/test') + } else if (typeof loginResult === 'object') { + const { cookies, NurseAccountData } = loginResult; + res.setHeader('Set-Cookie', cookies); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSE_RETRIEVED); + res.status(200).json({ + data: NurseAccountData, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + } + } + + public nurseSetPassword = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction) => { + const nurseId = req.user?.id; + const { password }: NurseSetPasswordRequestDto = req.body; + await this.nurseService.nurseSetPassword(nurseId, password); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PASSWORD_SET_SUCCESSFULLY_BY_NURSE); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public getAllAnnouncements = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction) => { + const nurseId = req.user?.id; + if (!nurseId) { + const error = createBilingualError(400, ErrorMessages.NURSE_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const announcements = await this.nurseService.getAllAnnouncements(nurseId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ANNOUNCEMENTS_RETRIEVED); + res.status(200).json({ data: announcements, messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public getNurseApplications = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction) => { + const nurseId = req.user?.id; + if (!nurseId) { + const error = createBilingualError(400, ErrorMessages.NURSE_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const applications = await this.nurseService.getNurseApplications(nurseId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.APPLICATIONS_RETRIEVED); + res.status(200).json({ data: applications, messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public applyToAnnouncement = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction) => { + const nurseId = req.user?.id; + const announcementId = req.params.announcementId; + if (!nurseId) { + const error = createBilingualError(400, ErrorMessages.NURSE_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + await this.nurseService.applyToAnnouncement(nurseId, announcementId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.APPLIED_TO_ANNOUNCEMENT_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); + + public getNurseSchedule = catchAsync(async (req: RequestWithUser, res: Response, next: NextFunction) => { + const nurseId = req.user?.id; + if (!nurseId) { + const error = createBilingualError(400, ErrorMessages.NURSE_ID_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const schedule = await this.nurseService.getNurseSchedule(nurseId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.NURSE_SCHEDULE_RETRIEVED); + res.status(200).json({ data: schedule, messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + }); +} \ No newline at end of file diff --git a/src/controllers/queue.controller.ts b/src/controllers/queue.controller.ts new file mode 100644 index 0000000..6787fff --- /dev/null +++ b/src/controllers/queue.controller.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from "express"; +import { RequestWithUser } from "@/interfaces"; +import { HttpException } from "@/exceptions/HttpException"; +import { catchAsync } from '@/utils/catchAsync'; +import Container from "typedi"; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { SuccessResponseMessages, createMultiLangMessage } from '@/utils/responseMessages'; + +import { QueueService } from "@/services/queue.service"; + +export class QueueController { + + public queueService = Container.get(QueueService); + + public getQueuePosition = catchAsync(async (req: RequestWithUser, res: Response): Promise => { + const appointmentId = req.params.appointmentId as string; + + if (!appointmentId) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ID_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const queuePosition = await this.queueService.getQueuePosition(appointmentId); + const response = createMultiLangMessage(SuccessResponseMessages.QUEUE_POSITION_RETRIEVED); + res.status(200).json({ + data: queuePosition, + ...response + }); + + }); +} \ No newline at end of file diff --git a/src/controllers/superAdmin.controller.ts b/src/controllers/superAdmin.controller.ts new file mode 100644 index 0000000..ad899ae --- /dev/null +++ b/src/controllers/superAdmin.controller.ts @@ -0,0 +1,42 @@ +import { AddAdminFromSuperAdminDto } from "@/dtos/superAdmins.dto"; +import { SuperAdminService } from "@/services/superAdmin.service"; +import { createMultiLangMessage, SuccessResponseMessages } from "@/utils/responseMessages"; +import { NextFunction, Response, Request } from "express"; +import Container from "typedi"; + +export class SuperAdminController { + + public superAdminService = Container.get(SuperAdminService); + + public addAdmin = async (req: Request, res: Response, next: NextFunction): Promise => { + const adminData: AddAdminFromSuperAdminDto = req.body; + const newAdmin = await this.superAdminService.addAdmin(adminData); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ADMIN_ADDED_SUCCESSFULLY); + res.status(201).json({ + data: newAdmin, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + } + + public getAllAdmins = async (req: Request, res: Response, next: NextFunction): Promise => { + const admins = await this.superAdminService.getAllAdmins(); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ADMINS_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: admins, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + } + + public getAdminById = async (req: Request, res: Response, next: NextFunction): Promise => { + const adminId: string = req.params.id; + const admin = await this.superAdminService.getAdminById(adminId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.ADMIN_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ + data: admin, + messageEn: responseMessage.messageEn, + messageAr: responseMessage.messageAr + }); + } +} \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..1bc1c87 --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,57 @@ +import { HttpException } from "@/exceptions/HttpException"; +import { RequestWithUser } from "@/interfaces"; +import { UserService } from "@/services/user.service"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; +import { createMultiLangMessage, SuccessResponseMessages } from "@/utils/responseMessages"; +import { NextFunction, Request, Response } from "express"; +import { Container } from "typedi"; + + +export class UsersController { + public userService = Container.get(UserService); + + public updateProfilePicture = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const userId = req.user?.id; + const userRole = req.user?.role; + const profilePictureFile = req.file; + + if (!profilePictureFile) { + const error = createBilingualError(400, ErrorMessages.NO_FILE_UPLOADED); + throw new HttpException(error.status, error.message, error.messageAr); + } + await this.userService.updateProfilePicture(userId, profilePictureFile.path, userRole); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PROFILE_PICTURE_UPDATED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public getProfilePicture = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const userId = req.user?.id; + const profilePictureUrl = await this.userService.getProfilePicture(userId); + if (!profilePictureUrl) { + const error = createBilingualError(404, ErrorMessages.NO_PROFILE_PICTURE); + throw new HttpException(error.status, error.message, error.messageAr); + } + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PROFILE_PICTURE_RETRIEVED_SUCCESSFULLY); + res.status(200).json({ data: { url: profilePictureUrl }, messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public deleteProfilePicture = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const userId = req.user?.id; + await this.userService.deleteProfilePicture(userId); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.PROFILE_PICTURE_DELETED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + + public updateUserProfile = async (req: RequestWithUser, res: Response, next: NextFunction) => { + const userId = req.user?.id; + const { name, phone, gender, dateOfBirth, availability_type } = req.body; + if (!name && !phone && !gender && !dateOfBirth) { + const error = createBilingualError(400, ErrorMessages.NO_PROFILE_DATA_PROVIDED); + throw new HttpException(error.status, error.message, error.messageAr); + } + await this.userService.updateUserProfile(userId, name, phone, gender, dateOfBirth, availability_type); + const responseMessage = createMultiLangMessage(SuccessResponseMessages.USER_PROFILE_UPDATED_SUCCESSFULLY); + res.status(200).json({ messageEn: responseMessage.messageEn, messageAr: responseMessage.messageAr }); + } + +} \ No newline at end of file diff --git a/src/dtos/admins.dto.ts b/src/dtos/admins.dto.ts new file mode 100644 index 0000000..b3d039b --- /dev/null +++ b/src/dtos/admins.dto.ts @@ -0,0 +1,71 @@ +import { DoctorAccountStatus, Gender, Role, NurseAccountStatus } from "@prisma/client"; +import { IsEmail, IsNotEmpty, IsString, IsInt, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +export class AddUserFromAdminDto { + @IsEmail() + @IsNotEmpty() + public email: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsNotEmpty() + public phone: string; + + @IsNotEmpty() + public date_of_birth: Date; + + @IsString() + public gender: Gender; + + @IsInt() + @Type(() => Number) + @IsOptional() + public years_of_experience: number; +} + +export class DoctorFromAdminResponseDto { + public name: string; + public email: string; + public username: string; + public phone: string; + public gender: Gender; + public date_of_birth: Date; + public role?: Role; + public isVerified: boolean; + public hasCompletedProfile?: boolean + public photoUrl?: string; + public doctor?: { + specialization: string; + avg_time?: Date; + account_status?: DoctorAccountStatus; + fellowshipCertificateUrl?: string; + graduationCertificateUrl?: string; + mastersCertificateUrl?: string; + membershipCardUrl?: string; + unionSpecializationCertificateUrl?: string; + professionalPracticeCardUrl?: string; + }; +} + +export class NurseFromAdminResponseDto { + public name: string; + public email: string; + public username: string; + public phone: string; + public gender: Gender; + public date_of_birth: Date; + public isVerified: boolean; + public hasCompletedProfile?: boolean + public photoUrl?: string; + public nurse?: { + account_status?: NurseAccountStatus; + years_of_experience?: number; + brief?: string; + nationalCardUrl?: string; + bonusFileUrl?: string; + }; +} \ No newline at end of file diff --git a/src/dtos/appointments.dto.ts b/src/dtos/appointments.dto.ts new file mode 100644 index 0000000..dba6774 --- /dev/null +++ b/src/dtos/appointments.dto.ts @@ -0,0 +1,145 @@ +import { IsBoolean, IsNotEmpty, IsDateString, IsOptional, IsUUID, IsNumber, ValidateIf, IsString, Min, IsInt, Max } from 'class-validator'; + + +export class BookAppointmentDto { + @IsUUID() + @IsNotEmpty() + doctorId: string; + + @IsUUID() + @IsOptional() + clinicId?: string; + + @IsDateString() + @IsNotEmpty() + scheduledTime: string; +} + + +export class GetAvailableDaysDto { + @IsUUID() + @IsNotEmpty() + doctorId: string; + + @IsUUID() + @IsOptional() + clinicId?: string; +} + +export class GetAvailableSlotsDto { + @IsUUID() + @IsNotEmpty() + doctorId: string; + + @IsDateString() + @IsNotEmpty() + date: string; + + @IsUUID() + @IsOptional() + clinicId?: string; +} + +export class RescheduleAppointmentDto { + @IsDateString() + @IsNotEmpty() + newScheduledTime: string; +} + +export class RescheduleAppointmentByDoctorDto { + @IsNumber() + minutes: number; +} + + +export class EnterDoctorScheduleDto { + @IsOptional() + clinicId?: string | null; + + @IsInt() + @Min(0) + @Max(6) + @IsNotEmpty() + workingDay: number; + + @IsString() + @IsNotEmpty() + startTime: string; + + @IsString() + @IsNotEmpty() + endTime: string; + + @IsInt() + @IsNotEmpty() + slotDuration: number; + + @IsInt() + @IsOptional() + bufferTime?: number = 0; + + @IsBoolean() + @IsNotEmpty() + isOnline: boolean; +} + +export class EditDoctorScheduleDto { + @IsUUID() + @IsNotEmpty() + scheduleId: string; + + @IsOptional() + clinicId?: string | null; + + @IsOptional() + @IsInt() + @Min(0) + @Max(6) + workingDay?: number; + + @IsOptional() + @IsString() + startTime?: string; + + @IsOptional() + @IsString() + endTime?: string; + + @IsOptional() + @IsInt() + slotDuration?: number; + + @IsOptional() + @IsInt() + bufferTime?: number; + + @IsOptional() + @IsBoolean() + isOnline?: boolean; + + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @IsOptional() + @IsString() + breakStart?: string; + + @IsOptional() + @IsString() + breakEnd?: string; +} + +export class HandleDoctorVacationDto { + @IsUUID() + @IsNotEmpty() + scheduleId: string; + + @IsDateString() + @IsNotEmpty() + startDate: string; + + @IsDateString() + @IsNotEmpty() + endDate: string; +} \ No newline at end of file diff --git a/src/dtos/clinics.dto.ts b/src/dtos/clinics.dto.ts new file mode 100644 index 0000000..5b7e144 --- /dev/null +++ b/src/dtos/clinics.dto.ts @@ -0,0 +1,65 @@ +import { IsBoolean, IsDate, IsNotEmpty, IsNumber, IsOptional, IsString, Matches } from "class-validator"; + +export class CreateUpdateClinicRequestDto { + @IsString() + @IsNotEmpty() + public name: string; + + @IsNotEmpty() + @IsString() + @Matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { + message: 'opening_at must be in HH:MM format (e.g., 13:00)' + }) + public opening_at: string; + + @IsNotEmpty() + @IsString() + @Matches(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, { + message: 'closing_at must be in HH:MM format (e.g., 13:00)' + }) + public closing_at: string; + + @IsString() + @IsNotEmpty() + public address: string; + + @IsOptional() + @IsString() + public address_maps_link?: string; + + @IsString() + @IsNotEmpty() + public phone: string; + + @IsOptional() + @IsBoolean() + public canPayOnline?: boolean; + + @IsNotEmpty() + @IsNumber() + fees: number; +} + +export class ClinicResponseDto { + public id: string; + public name: string; + public opening_at: string; + public closing_at: string; + public address: string; + public address_maps_link?: string; + public phone: string; + public canPayOnline?: boolean; + public is_active: boolean; +} + +export class ClinicActiveStatusResponseDto { + public id: string; + public name: string; + public is_active: boolean; +} + +export class ClinicUpdateFeesDto { + @IsNotEmpty() + @IsNumber() + fees: number; +} \ No newline at end of file diff --git a/src/dtos/doctors.dto.ts b/src/dtos/doctors.dto.ts new file mode 100644 index 0000000..d8e0e58 --- /dev/null +++ b/src/dtos/doctors.dto.ts @@ -0,0 +1,143 @@ +import { AvailabilityType, DayOfWeek, Gender } from "@prisma/client"; +import { IsString, IsNotEmpty, IsEmail, IsArray, IsOptional, IsInt, IsEnum, ValidateNested, Min } from "class-validator"; +import { UpdateUserProfileDto } from "./users.dto"; +import { Type } from "class-transformer"; + + +export class DoctorSignupRequestDto { + @IsString() + @IsNotEmpty() + public name: string; + + @IsEmail() + @IsNotEmpty() + public email: string; + + @IsString() + @IsNotEmpty() + public phone: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsString() + availability_type?: AvailabilityType; + + @IsString() + @IsNotEmpty() + public gender: Gender; + + @IsString() + public date_of_birth?: Date; + + graduationCertificate: Express.Multer.File; + membershipCard: Express.Multer.File; + professionalPracticeCard: Express.Multer.File; + + mastersCertificate: Express.Multer.File; + fellowshipCertificate: Express.Multer.File; + unionSpecializationCertificate: Express.Multer.File; + + +} + +export class DoctorLoginRequestDto { + @IsNotEmpty() + public emailOrUsername: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsString() + public rememberMe?: boolean; +} + +export class DoctorSetPasswordRequestDto { + @IsString() + @IsNotEmpty() + public password: string; +} + +export class DoctorProfilePictureRequestDto { + profilePicture: Express.Multer.File; +} + +export class DoctorUpdateProfileRequestDto extends UpdateUserProfileDto { + @IsString() + availability_type?: AvailabilityType; +} + +export class WorkingDayDto { + @IsEnum(DayOfWeek) + @IsNotEmpty() + public day_of_week: DayOfWeek; + + @IsString() + @IsNotEmpty() + public start_time: string; + + @IsString() + @IsNotEmpty() + public end_time: string; +} + +export class PostAnnouncementDto { + @IsString() + @IsNotEmpty() + public clinic_id: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => WorkingDayDto) + public working_days: WorkingDayDto[]; + + @IsOptional() + @IsEnum(Gender) + public gender?: Gender; + + @IsOptional() + @IsInt() + @Min(0) + public max_age?: number; + + @IsOptional() + @IsInt() + @Min(0) + public years_of_experience?: number; + + @IsOptional() + @IsString() + public notes?: string; +} + +export class EditAnnouncementDto { + @IsOptional() + @IsString() + public clinic_id: string; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => WorkingDayDto) + public working_days: WorkingDayDto[]; + + @IsOptional() + @IsEnum(Gender) + public gender?: Gender; + + @IsOptional() + @IsInt() + @Min(0) + public max_age?: number; + + @IsOptional() + @IsInt() + @Min(0) + public years_of_experience?: number; + + @IsOptional() + @IsString() + public notes?: string; +} \ No newline at end of file diff --git a/src/dtos/fabric-identity.dto.ts b/src/dtos/fabric-identity.dto.ts new file mode 100644 index 0000000..4738c84 --- /dev/null +++ b/src/dtos/fabric-identity.dto.ts @@ -0,0 +1,38 @@ +import { IsString, IsOptional, IsNotEmpty } from 'class-validator'; +export class OnboardIdentityDto { + @IsString() + @IsNotEmpty() + public clinicId: string; + + @IsString() + @IsNotEmpty() + public mspId: string; + + @IsString() + @IsNotEmpty() + public certificate: string; + + @IsString() + @IsNotEmpty() + public privateKey: string; + + @IsString() + @IsNotEmpty() + public peerEndpoint: string; + + @IsString() + @IsNotEmpty() + public peerHostAlias: string; + + @IsString() + @IsNotEmpty() + public tlsCertificate: string; + + @IsString() + @IsOptional() + public channelName?: string; + + @IsString() + @IsOptional() + public chaincodeName?: string; +} diff --git a/src/dtos/googleUsers.dto.ts b/src/dtos/googleUsers.dto.ts new file mode 100644 index 0000000..404b1ea --- /dev/null +++ b/src/dtos/googleUsers.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty, IsString, MaxLength } from "class-validator"; + +export class CreateGoogleUsersDto { + public email: string; + public name: string; + public isEmailVerified: boolean; +} + +export class UpdateGoogleUserPhoneDto { + @IsNotEmpty() + @MaxLength(15) + @IsString() + public phone: string; +} \ No newline at end of file diff --git a/src/dtos/medical-records.dto.ts b/src/dtos/medical-records.dto.ts new file mode 100644 index 0000000..6518e32 --- /dev/null +++ b/src/dtos/medical-records.dto.ts @@ -0,0 +1,62 @@ +import { IsString, IsEnum, IsOptional, IsUUID, IsObject } from "class-validator"; +import { RecordType } from "@/interfaces/enums.interface"; + + +// checks data when a patient uploads any MR +export class CreateMedicalRecordDto { + @IsString() + name: string; + + @IsEnum(RecordType) + type: RecordType; + + @IsOptional() + @IsUUID() + doctor_id?: string; + +} + +// checks data when a doctor submits a JSON-based medical record +export class CreateDoctorRecordJsonDto { + @IsString() + name: string; + + @IsEnum(RecordType) + type: RecordType; + + @IsObject() + content: Record; +} + +// checks data when a patient submits their own medical history entry +export class CreatePatientMedicalHistoryDto { + @IsString() + name: string; + + @IsObject() + content: Record; +} + +// checks data when a patient updates an existing medical history entry +export class UpdatePatientMedicalHistoryDto { + @IsOptional() + @IsString() + name?: string; + + @IsOptional() + @IsObject() + content?: Record; +} + +// permissions --> later + +// checks data when searching/filtering MR +export class GetMedicalRecordsDto { + @IsOptional() + @IsEnum(RecordType) + type?: RecordType; + + @IsOptional() + @IsUUID() + doctor_id?: string; +} \ No newline at end of file diff --git a/src/dtos/medicalRecord.dto.ts b/src/dtos/medicalRecord.dto.ts new file mode 100644 index 0000000..9a9f2c6 --- /dev/null +++ b/src/dtos/medicalRecord.dto.ts @@ -0,0 +1,43 @@ +import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; + +export class CreateMedicalRecordDto { + + @IsUUID() + @IsNotEmpty() + public patientId: string; + + @IsUUID() + @IsNotEmpty() + public recordId: string; + + @IsUUID() + @IsNotEmpty() + public doctorId: string; + + @IsString() + @IsNotEmpty() + public type: string; + + @IsString() + @IsNotEmpty() + public ipfsCidKey: string; +} + +export class UpdateMedicalRecordDto { + + @IsUUID() + @IsNotEmpty() + public recordId: string; + + @IsUUID() + @IsNotEmpty() + public doctorId: string; + + @IsString() + @IsNotEmpty() + public type: string; + + @IsString() + @IsOptional() + public ipfsCidKey?: string; +} diff --git a/src/dtos/nurses.dto.ts b/src/dtos/nurses.dto.ts new file mode 100644 index 0000000..f684fb2 --- /dev/null +++ b/src/dtos/nurses.dto.ts @@ -0,0 +1,62 @@ +import { Gender } from "@prisma/client"; +import { IsString, IsNotEmpty, IsEmail, IsInt, IsOptional } from "class-validator"; +import { Type } from "class-transformer"; + +export class NurseSignupRequestDto { + @IsString() + @IsNotEmpty() + public name: string; + + @IsEmail() + @IsNotEmpty() + public email: string; + + @IsString() + @IsNotEmpty() + public phone: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsInt() + @Type(() => Number) + @IsNotEmpty() + public years_of_experience: number; + + @IsString() + @IsOptional() + public brief?: string; + + @IsString() + @IsNotEmpty() + public gender: Gender; + + @IsString() + public date_of_birth?: Date; + + nationalCard: Express.Multer.File; + bonusFile: Express.Multer.File; +} + +export class NurseLoginRequestDto { + @IsNotEmpty() + public emailOrUsername: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsString() + public rememberMe?: boolean; +} + +export class NurseSetPasswordRequestDto { + @IsString() + @IsNotEmpty() + public password: string; +} + +export class NurseProfilePictureRequestDto { + profilePicture: Express.Multer.File; +} \ No newline at end of file diff --git a/src/dtos/superAdmins.dto.ts b/src/dtos/superAdmins.dto.ts new file mode 100644 index 0000000..ae9d965 --- /dev/null +++ b/src/dtos/superAdmins.dto.ts @@ -0,0 +1,41 @@ +import { Gender, Role } from "@prisma/client"; +import { IsEmail, IsNotEmpty, IsString } from "class-validator"; + +export class AddAdminFromSuperAdminDto { + @IsEmail() + @IsNotEmpty() + public email: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsNotEmpty() + public phone: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsString() + @IsNotEmpty() + public gender: Gender; + + @IsString() + @IsNotEmpty() + public date_of_birth: string; +} + +export class AdminFromSuperAdminResponseDto { + public email: string; + public name: string; + public username: string; + public phone: string; + public role: Role; + public gender: Gender; + public isVerified: boolean; + public hasCompletedProfile: boolean; + public date_of_birth: Date; + public photo_url?: string; +} \ No newline at end of file diff --git a/src/dtos/users.dto.ts b/src/dtos/users.dto.ts new file mode 100644 index 0000000..bd33d9e --- /dev/null +++ b/src/dtos/users.dto.ts @@ -0,0 +1,91 @@ +import { Gender } from '@prisma/client'; +import { IsEmail, IsString, IsNotEmpty, MinLength, MaxLength, IsDateString, IsBoolean } from 'class-validator'; + +export class CreateUserDto { + @IsEmail() + public email: string; + + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsNotEmpty() + public phone: string; + + @IsString() + @IsNotEmpty() + @MinLength(8) + @MaxLength(32) + public password: string; + + @IsBoolean() + public rememberMe: boolean; +} + +export class LoginUserDto { + @IsString() + @IsNotEmpty() + public emailOrUsername: string; + + @IsString() + @IsNotEmpty() + public password: string; + + @IsBoolean() + public rememberMe: boolean; +} + +export class UpdateUserDto { + @IsString() + @IsNotEmpty() + @MinLength(9) + @MaxLength(32) + public password: string; +} + +export class CompleteUserProfileDto { + + @IsString() + @IsNotEmpty() + public gender: Gender; + + @IsNotEmpty() + @IsDateString() + public date_of_birth: string; +} + +export class ResetPasswordDto { + @IsString() + @IsNotEmpty() + public token: string; + + @IsString() + @IsNotEmpty() + @MinLength(8) + @MaxLength(32) + public newPassword: string; +} + +export class UpdateUserProfileDto { + @IsString() + public name?: string; + @IsString() + public phone?: string; + @IsString() + public gender?: Gender; + @IsDateString() + public date_of_birth?: Date; +} + +export class PasswordCheckDto { + @IsString() + @IsNotEmpty() + public password: string; +} + +export class ChangePasswordDto { + @IsString() + @IsNotEmpty() + public newPassword: string; +} \ No newline at end of file diff --git a/src/exceptions/HttpException.ts b/src/exceptions/HttpException.ts new file mode 100644 index 0000000..553048b --- /dev/null +++ b/src/exceptions/HttpException.ts @@ -0,0 +1,12 @@ +export class HttpException extends Error { + public status: number; + public message: string; + public messageAr?: string; + + constructor(status: number, message: string, messageAr?: string) { + super(message); + this.status = status; + this.message = message; + this.messageAr = messageAr; + } +} diff --git a/src/http/auth.http b/src/http/auth.http new file mode 100644 index 0000000..2198991 --- /dev/null +++ b/src/http/auth.http @@ -0,0 +1,27 @@ +# baseURL +@baseURL = http://localhost:3000 + +### +# User Signup +POST {{ baseURL }}/signup +Content-Type: application/json + +{ + "email": "example@email.com", + "password": "password" +} + +### +# User Login +POST {{ baseURL }}/login +Content-Type: application/json + +{ + "email": "example@email.com", + "password": "password" +} + +### +# User Logout +POST {{ baseURL }}/logout +Content-Type: application/json diff --git a/src/http/users.http b/src/http/users.http new file mode 100644 index 0000000..13209f2 --- /dev/null +++ b/src/http/users.http @@ -0,0 +1,34 @@ +# baseURL +@baseURL = http://localhost:3000 + +### +# Find All Users +GET {{ baseURL }}/users + +### +# Find User By Id +GET {{ baseURL }}/users/1 + +### +# Create User +POST {{ baseURL }}/users +Content-Type: application/json + +{ + "email": "example@email.com", + "password": "password" +} + +### +# Modify User By Id +PUT {{ baseURL }}/users/1 +Content-Type: application/json + +{ + "email": "example@email.com", + "password": "password" +} + +### +# Delete User By Id +DELETE {{ baseURL }}/users/1 diff --git a/src/interfaces/appointments.interface.ts b/src/interfaces/appointments.interface.ts new file mode 100644 index 0000000..603da9b --- /dev/null +++ b/src/interfaces/appointments.interface.ts @@ -0,0 +1,173 @@ +import { User } from './users.interface'; +import { AppointmentStatus, DayOfWeek, VacationStatus, Gender } from '@prisma/client' + +export interface Appointment { + id: string; + patient_id: string; + doctor_id: string; + clinic_id: string | null; + scheduled_time: Date; + slot_duration: number; + end_time: Date; + is_online: boolean; + is_completed: boolean; + estimated_time?: number; + status: AppointmentStatus; + cancelled_by: string | null; + created_at: Date; + modified_at: Date; + deleted_at?: Date; + + patient: User; + doctor: User; +} + +export interface PatientAppointment { + id: string; + doctor_id: string; + clinic_id: string | null; + status: AppointmentStatus; + is_online: boolean; + slot_duration: number; + doctor_name: string; + doctor_profile_pic: string; + appointment_date: string; + start_time: string; + end_time: string; + clinic_name: string | null; + clinic_address: string | null; + address_maps_link: string | null; +} + +export interface PatientTodayAppointment { + id: string; + doctor_id: string; + clinic_id: string | null; + status: AppointmentStatus; + is_online: boolean; + slot_duration: number; + doctor_name: string; + doctor_profile_pic: string; + appointment_date: string; + start_time: string; + end_time: string; + clinic_name: string | null; + clinic_address: string | null; + address_maps_link: string | null; + position: number; + estimatedWaitMinutes: number; + patientsAhead: number; +} + +export interface DoctorAppointment { + id: string; + status: AppointmentStatus; + slot_duration: number; + patient_name: string; + appointment_date: string; + start_time: string; + end_time: string; + clinic_name: string | null; + clinic_address: string | null; +} + +export interface DoctorScheduleDay { + date: string; + displayDate: string; + appointments: DoctorAppointment[]; +} + +export interface AvailableDay { + date: string; + dayOfWeek: DayOfWeek; + displayDate: string; +} + +export interface TimeSlot { + start: string; + end: string; + available: boolean; + online: boolean; +} + +export interface DoctorSchedule { + id: string; + clinicId: string | null; + dayOfWeek: DayOfWeek; + startTime: string; + endTime: string; + slotDuration: number; + bufferTime: number; + isOnline: boolean; + isActive: boolean; + breakStart: string | null; + breakEnd: string | null; +} + +export interface checkExistingAppointments { + existing: boolean, + numOfAppointments?: number +} + +export interface ConflictingAppointment { + id: string; + scheduled_time: Date; +} + +export interface Vacations { + vacationId: string; + scheduleId: string; + clinicId: string | null; + clinicName: string | null; + clinicAddress: string | null; + dayOfWeek: DayOfWeek; + isOnline: boolean; + status: VacationStatus; + cancelledAppointments: number; +} + +export interface DoctorVacations { + breakStart: string; + breakEnd: string; + vacations: Vacations[]; +} + +export interface AppointmentData { + id: string; + clinic: { + id: string; + name: string; + address: string; + address_maps_link: string; + }; + patient: { + id: string; + name: string + gender: Gender; + phone: string; + }; + status: AppointmentStatus; + slot_duration: number; + appointment_date: string; + start_time: string; + end_time: string; +} + +export interface AppointmentStatusChangedPayload { + appointmentId: string; + newStatus: AppointmentStatus; + doctorId: string; + patientId: string; + patientName: string; + appointmentDate: string; + startTime: string; +} + +export interface AppointmentEventData { + appointmentId: string; + doctorId: string; + patientId: string; + patientName: string; + appointmentDate: string; + startTime: string; +} \ No newline at end of file diff --git a/src/interfaces/audit-logs.interface.ts b/src/interfaces/audit-logs.interface.ts new file mode 100644 index 0000000..0092c54 --- /dev/null +++ b/src/interfaces/audit-logs.interface.ts @@ -0,0 +1,12 @@ +import { User } from './users.interface'; +import { Action } from './enums.interface'; + +export interface AuditLog { + id: string; + user_id: string; + action: Action; + bc_hash: string; + created_at: Date; + + user: User; +} diff --git a/src/interfaces/auth.interface.ts b/src/interfaces/auth.interface.ts new file mode 100644 index 0000000..9ac00f2 --- /dev/null +++ b/src/interfaces/auth.interface.ts @@ -0,0 +1,33 @@ +import { Request } from 'express'; +import { User } from '@interfaces/users.interface'; +import { Role } from '@prisma/client'; + + +export interface DataStoredInToken { + id: string; + role:Role; +} + +export interface AccessTokenData { + token: string; + expiresIn: number; +} + +export interface RefreshTokenData { + token: string; + expiresIn: number; +} + +export interface TokenResponse { + accessToken: AccessTokenData; + refreshToken?: RefreshTokenData; +} + +export interface SocketStoredInToken { + id: string; + role: string; +} + +export interface RequestWithUser extends Request { + user: User; +} diff --git a/src/interfaces/clinics.interface.ts b/src/interfaces/clinics.interface.ts new file mode 100644 index 0000000..c0408ae --- /dev/null +++ b/src/interfaces/clinics.interface.ts @@ -0,0 +1,49 @@ +import { User, Doctor } from './users.interface'; +import { DoctorPersonalData } from './doctors.interface'; + +export interface Clinic { + id: string; + name: string; + phone: string; + canPayOnline?: boolean; + is_active: boolean; + opening_at: string; + closing_at: string; + address: string; + created_at: Date; + modified_at: Date; + deleted_at?: Date; + + clinic_nurses?: ClinicNurse[]; + clinic_doctors?: ClinicDoctor[]; +} + +export interface ClinicNurse { + id: string; + clinic_id: string; + nurse_id: string; + + clinic: Clinic; + nurse: User; +} + +export interface ClinicDoctor { + id: string; + clinic_id: string; + doctor_id: string; + + clinic: Clinic; + doctor: Doctor; +} + +export interface DoctorClinics { + id: string; + name: string; + phone: string; + canPayOnline: boolean; + opening_at: string; + closing_at: string; + address: string; + address_maps_link: string; + doctors?: Partial[]; +} \ No newline at end of file diff --git a/src/interfaces/doctor-schedule.interface.ts b/src/interfaces/doctor-schedule.interface.ts new file mode 100644 index 0000000..e0ee844 --- /dev/null +++ b/src/interfaces/doctor-schedule.interface.ts @@ -0,0 +1,16 @@ +import { DayOfWeek} from "@prisma/client"; + +export interface DoctorSchedule { + id: string; + doctor_id: string; + clinic_id: string | null; + day_of_week: DayOfWeek; + start_time: Date; + end_time: Date; + slot_duration: number; + buffer_time: number; + is_active: boolean; + created_at: Date; + modified_at: Date; + deleted_at: Date | null; +} diff --git a/src/interfaces/doctors.interface.ts b/src/interfaces/doctors.interface.ts new file mode 100644 index 0000000..07b5724 --- /dev/null +++ b/src/interfaces/doctors.interface.ts @@ -0,0 +1,65 @@ +import { DoctorAccountStatus, AvailabilityType, Gender, AnnouncementStatus, DayOfWeek} from "@prisma/client"; +import { DoctorClinics } from "./clinics.interface"; + +export interface Doctor { + id: string; + avg_time?: Date | null; + account_status: DoctorAccountStatus; + num_of_created_clinics: number; + availability_type: AvailabilityType; + present: boolean; +} + +export interface DoctorLoginData { + id: string, + name: string, + email: string, + username: string, + phone: string, + gender: string, + doctor: { + specialization: string, + account_status: DoctorAccountStatus + } +} + +export interface DoctorPersonalData { + id: string; + name: string; + gender: Gender; + age: number; + specialization: string; + phone: string; + fees: number; + profilePic: string; + is_online: boolean; + clinics?: DoctorClinics[] +} + +export interface WorkingDays { + day_of_week: DayOfWeek; + start_time: string; + end_time: string; +} + +export interface DoctorAnnouncements { + id: string; + doctor: { + id: string; + name: string; + gender: Gender; + profilePic: string; + }; + clinic: { + id: string; + name: string + address: string; + address_maps_link: string; + }; + working_days: WorkingDays[]; + status?: AnnouncementStatus; + gender?: Gender; + max_age?: number; + years_of_experience?: number; + notes?: string; +} \ No newline at end of file diff --git a/src/interfaces/enums.interface.ts b/src/interfaces/enums.interface.ts new file mode 100644 index 0000000..a517d87 --- /dev/null +++ b/src/interfaces/enums.interface.ts @@ -0,0 +1,54 @@ +export enum Period { + DAILY = 'DAILY', + WEEKLY = 'WEEKLY', + MONTHLY = 'MONTHLY', + YEARLY = 'YEARLY', +} + +export enum ScanLabType { + SCAN = 'SCAN', + LAB = 'LAB', +} + +export enum Action { + CREATE = 'CREATE', + UPDATE = 'UPDATE', + DELETE = 'DELETE', + READ = 'READ', + LOGIN = 'LOGIN', + LOGOUT = 'LOGOUT', +} + +// should be changed to visit, histroy and file +export enum RecordType { + LAB_RESULT = 'LAB_RESULT', + SCAN = 'SCAN', + DIAGNOSIS = 'DIAGNOSIS', + VISIT_SUMMARY = 'VISIT_SUMMARY', + SOAP_NOTE = 'SOAP_NOTE', + MEDICAL_HISTORY = 'MEDICAL_HISTORY', + FILE = 'FILE', + VISIT = 'VISIT', +} + +export enum DOCTOR_FILES { + GRADUATION_CERTIFICATE = 'graduationCertificate', + MEMBERSHIP_CARD = 'membershipCard', + PROFESSIONAL_PRACTICE_CARD = 'professionalPracticeCard', + MASTERS_CERTIFICATE = 'mastersCertificate', + FELLOWSHIP_CERTIFICATE = 'fellowshipCertificate', + UNION_SPECIALIZATION_CERTIFICATE = 'unionSpecializationCertificate', +} + +export enum NURSE_FILES { + NATIONAL_CARD = 'nationalCard', + BONUS_FILE = 'bonusFile', + +} + +export enum USER_ROLE { + DOCTOR = 'DOCTOR', + NURSE = 'NURSE', + PATIENT = 'PATIENT', + ADMIN = 'ADMIN', +} \ No newline at end of file diff --git a/src/interfaces/fabric-identity.interface.ts b/src/interfaces/fabric-identity.interface.ts new file mode 100644 index 0000000..6b1ea64 --- /dev/null +++ b/src/interfaces/fabric-identity.interface.ts @@ -0,0 +1,26 @@ + +export interface FabricIdentity { + clinicId: string; + mspId: string; + certificate: string; + privateKey: string; + peerEndpoint: string; + peerHostAlias: string; + tlsCertificate: string; + channelName: string; + chaincodeName: string; + createdAt: string; + updatedAt: string; +} + +export interface FabricIdentityInput { + clinicId: string; + mspId: string; + certificate: string; + privateKey: string; + peerEndpoint: string; + peerHostAlias: string; + tlsCertificate: string; + channelName?: string; + chaincodeName?: string; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..774949e --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,35 @@ +// Enums +export * from './enums.interface'; + +// Auth +export * from './auth.interface'; + +// Users +export * from './users.interface'; + +// Routes +export * from './routes.interface'; + +// Appointments +export * from './appointments.interface'; + +// Medications +export * from './medications.interface'; + +// Scans & Labs +export * from './scans-labs.interface'; + +// Clinics +export * from './clinics.interface'; + +// Audit Logs +export * from './audit-logs.interface'; + +// Medical Records +export * from './medical-records.interface'; + +// Appointments +// export * from './appointments.interface'; + +// Doctor Schedule +// export * from './doctor-schedule.interface'; diff --git a/src/interfaces/medical-records.interface.ts b/src/interfaces/medical-records.interface.ts new file mode 100644 index 0000000..e76a757 --- /dev/null +++ b/src/interfaces/medical-records.interface.ts @@ -0,0 +1,10 @@ +export interface MedicalRecord { + patientId: string; + recordId: string; + doctorId: string; + type: string; + ipfsCidKey: string; + ownerMsp?: string; + authorizedMsps?: string[]; + deleted?: boolean; +} diff --git a/src/interfaces/medicalRecords.interface.ts b/src/interfaces/medicalRecords.interface.ts new file mode 100644 index 0000000..8b57ad6 --- /dev/null +++ b/src/interfaces/medicalRecords.interface.ts @@ -0,0 +1,17 @@ +import { RecordType } from '@prisma/client'; + +export interface MedicalRecord { + id: string; + patient_id: string; + clinic_id: string; + doctor_id?: string; + appointment_id?: string; + name: string; + cid: string; + type: RecordType; + mime_type: string; +} + +export interface MedicalRecordFile extends MedicalRecord { + buffer: Buffer; +} diff --git a/src/interfaces/medications.interface.ts b/src/interfaces/medications.interface.ts new file mode 100644 index 0000000..f050fbc --- /dev/null +++ b/src/interfaces/medications.interface.ts @@ -0,0 +1,21 @@ +import { User } from './users.interface'; +import { Period } from './enums.interface'; + +export interface Medication { + id: string; + patient_id: string; + doctor_id: string; + treatment_name: string; + medication_end_date: Date; + medication_start_time: Date; + frequency: number; + period: Period; + description?: string; + category: string; + created_at: Date; + modified_at: Date; + deleted_at?: Date; + + patient?: User; + doctor?: User; +} diff --git a/src/interfaces/nurse.interface.ts b/src/interfaces/nurse.interface.ts new file mode 100644 index 0000000..84900b2 --- /dev/null +++ b/src/interfaces/nurse.interface.ts @@ -0,0 +1,89 @@ +import { NurseAccountStatus, Gender, AnnouncementStatus, AnnouncementNurseStatus } from "@prisma/client"; +import { WorkingDays } from "./doctors.interface"; + +export interface NurseLoginData { + id: string, + name: string, + email: string, + username: string, + phone: string, + gender: string, + nurse: { + account_status: NurseAccountStatus, + } +} + +export interface NurseData { + id: string; + name: string; + email: string; + phone: string; + gender: Gender; + age: number; + profilePic: string | null; + years_of_experience: number; + nationalCardUrl: string; + brief: string | null; + bonusFileUrl: string | null; +} + +export interface NurseApplications { + id: string; + application_status: AnnouncementNurseStatus; + doctor: { + id: string; + name: string; + gender: Gender; + profilePic: string; + }; + clinic: { + id: string; + name: string + address: string; + address_maps_link: string; + }; + working_days: WorkingDays[]; + status?: AnnouncementStatus; + gender?: Gender; + max_age?: number; + years_of_experience?: number; + notes?: string; +} + +export interface NurseSchedule { + id: string; + doctor: { + id: string; + name: string; + gender: Gender; + profilePic: string; + }; + clinic: { + id: string; + name: string + address: string; + address_maps_link: string; + }; + working_days: WorkingDays[]; +} + +export interface NurseFullDetails { + id: string; + name: string; + email: string; + phone: string; + gender: Gender; + age: number; + profilePic: string | null; + years_of_experience: number; + nationalCardUrl: string; + brief: string | null; + bonusFileUrl: string | null; + clinics: { + id: string; + name: string; + address: string; + address_maps_link: string; + working_days: WorkingDays[]; + }[]; +} \ No newline at end of file diff --git a/src/interfaces/queue.interface.ts b/src/interfaces/queue.interface.ts new file mode 100644 index 0000000..551e98c --- /dev/null +++ b/src/interfaces/queue.interface.ts @@ -0,0 +1,6 @@ +export interface QueuePosition { + position: number; + estimatedWaitMinutes: number; + patientsAhead: number; +} + diff --git a/src/interfaces/routes.interface.ts b/src/interfaces/routes.interface.ts new file mode 100644 index 0000000..0f7005c --- /dev/null +++ b/src/interfaces/routes.interface.ts @@ -0,0 +1,6 @@ +import { Router } from 'express'; + +export interface Routes { + path?: string; + router: Router; +} diff --git a/src/interfaces/scans-labs.interface.ts b/src/interfaces/scans-labs.interface.ts new file mode 100644 index 0000000..be17e8c --- /dev/null +++ b/src/interfaces/scans-labs.interface.ts @@ -0,0 +1,21 @@ +import { User } from './users.interface'; +import { Period, ScanLabType } from './enums.interface'; + +export interface ScanLab { + id: string; + patient_id: string; + doctor_id: string; + name: string; + scheduled_date?: Date; + scheduled_time?: Date; + frequency?: number; + period?: Period; + description?: string; + type: ScanLabType; + created_at: Date; + modified_at: Date; + deleted_at?: Date; + + patient?: User; + doctor?: User; +} diff --git a/src/interfaces/users.interface.ts b/src/interfaces/users.interface.ts new file mode 100644 index 0000000..305763b --- /dev/null +++ b/src/interfaces/users.interface.ts @@ -0,0 +1,85 @@ +import { Appointment } from './appointments.interface'; +import { Medication } from './medications.interface'; +import { ScanLab } from './scans-labs.interface'; +import { ClinicNurse, ClinicDoctor } from './clinics.interface'; +import { AuditLog } from './audit-logs.interface'; +import { DoctorAccountStatus, Gender, Role, NurseAccountStatus } from '@prisma/client'; + +export interface User { + id: string; + name: string; + email: string; + username: string; + phone: string; + gender: Gender; + date_of_birth: Date; + role: Role; + password_hash: string; + isVerified: boolean; + hasCompletedProfile: boolean; + created_at: Date; + modified_at: Date; + deleted_at?: Date; + photo_url?: string; + patient?: Patient; + doctor?: Partial; + nurse?: Partial; + appointments_as_patient?: Appointment[]; + appointments_as_doctor?: Appointment[]; + medications_as_patient?: Medication[]; + medications_as_doctor?: Medication[]; + scans_labs_as_patient?: ScanLab[]; + scans_labs_as_doctor?: ScanLab[]; + clinics_as_nurse?: ClinicNurse[]; + audit_logs?: AuditLog[]; + controlled_patients?: Patient[]; +} + +export interface Patient { + id: string; + bc_address: string; + consent: boolean; + controlling_nurse_id?: string; + + user: User; + controlling_nurse_user?: User; +} + +export interface Doctor { + id: string; + specialization: string; + avg_time?: Date; + account_status: DoctorAccountStatus; + + user: User; + clinic_doctors?: ClinicDoctor[]; +} + +export interface Nurse { + id: string; + account_status: NurseAccountStatus; + years_of_experience: number; + brief?: string; + + user: User; +} + +export interface UserLoginData { + name: string, + email: string, + username: string, + phone: string, + gender: Gender, + date_of_birth: Date, + role: Role, + isVerified: Boolean, + hasCompletedProfile: Boolean, + photo_url?: string; + doctor?: { + specialization: string; + account_status: DoctorAccountStatus; + } + nurse?: { + account_status: NurseAccountStatus; + } +} diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..80d0096 --- /dev/null +++ b/src/middlewares/auth.middleware.ts @@ -0,0 +1,66 @@ +import { PrismaClient, Role } from '@prisma/client'; +import { NextFunction, Response, Request } from 'express'; +import { verify } from 'jsonwebtoken'; +import { SECRET_KEY } from '@config'; +import { HttpException } from '@exceptions/HttpException'; +import { DataStoredInToken, RequestWithUser } from '@interfaces/auth.interface'; +import { User } from '@/interfaces'; +import { ErrorMessages, createBilingualError } from '@/utils/errorMessages'; +import prisma from '@/config/prisma'; + +const getAuthorization = (req: Request) => { + const cookie = req.cookies['Authorization']; + if (cookie) return cookie; + + const header = req.header('Authorization'); + if (header) { + return header.startsWith('Bearer ') ? header.slice(7) : header; + } + + return null; +}; + +export const AuthMiddleware = async (req: RequestWithUser, res: Response, next: NextFunction) => { + try { + // Check for JWT token + const Authorization = getAuthorization(req); + if (Authorization) { + const { id } = (await verify(Authorization, SECRET_KEY)) as DataStoredInToken; + const users = prisma.user; + const findUser: User = await users.findUnique({ where: { id } }); + + if (findUser) { + req.user = findUser; + next(); + } else { + const error = createBilingualError(401, ErrorMessages.WRONG_AUTHENTICATION_TOKEN); + next(new HttpException(error.status, error.message, error.messageAr)); + } + } else { + const error = createBilingualError(401, ErrorMessages.AUTHENTICATION_REQUIRED); + next(new HttpException(error.status, error.message, error.messageAr)); + } + } catch (error) { + const err = createBilingualError(401, ErrorMessages.WRONG_AUTHENTICATION_TOKEN); + next(new HttpException(err.status, err.message, err.messageAr)); + } +}; + +export const RoleMiddleware = (...allowedRoles: Role[]) => { + return (req: RequestWithUser, res: Response, next: NextFunction) => { + if (!req.user) { + const error = createBilingualError(401, ErrorMessages.AUTHENTICATION_REQUIRED); + return next(new HttpException(error.status, error.message, error.messageAr)); + } + + if (!allowedRoles.includes(req.user.role)) { + const error = createBilingualError(403, { + en: 'Access denied. Insufficient permissions.', + ar: 'تم رفض الوصول. صلاحيات غير كافية.' + }); + return next(new HttpException(error.status, error.message, error.messageAr)); + } + + next(); + }; +}; diff --git a/src/middlewares/error.middleware.ts b/src/middlewares/error.middleware.ts new file mode 100644 index 0000000..ff77a37 --- /dev/null +++ b/src/middlewares/error.middleware.ts @@ -0,0 +1,99 @@ +import { NextFunction, Request, Response } from 'express'; +import { HttpException } from '@exceptions/HttpException'; +import { logger } from '@utils/logger'; + +function mapGrpcCodeToHttp(code: number): number { + switch (code) { + case 0: // OK + return 200; + case 1: // CANCELLED + return 499; + case 3: // INVALID_ARGUMENT + return 400; + case 4: // DEADLINE_EXCEEDED + return 504; + case 5: // NOT_FOUND + return 404; + case 6: // ALREADY_EXISTS + return 409; + case 7: // PERMISSION_DENIED + return 403; + case 8: // RESOURCE_EXHAUSTED + return 429; + case 9: // FAILED_PRECONDITION + return 412; + case 10: // ABORTED + return 409; + case 11: // OUT_OF_RANGE + return 400; + case 12: // UNIMPLEMENTED + return 501; + case 13: // INTERNAL + return 500; + case 14: // UNAVAILABLE + return 503; + case 15: // DATA_LOSS + return 500; + case 16: // UNAUTHENTICATED + return 401; + default: + return 500; + } +} + +export const ErrorMiddleware = (error: any, req: Request, res: Response, next: NextFunction) => { + try { + let status = 500; + let message = 'Something went wrong'; + let messageAr = 'حدث خطأ ما'; + + // Preserve HttpException + if (error instanceof HttpException) { + status = error.status || 500; + message = error.message || message; + messageAr = error.messageAr || messageAr; + } else { + // Generic Error handling: try to map gRPC/Fabric errors to HTTP codes + message = error?.message || String(error); + + // Prefer numeric code property if available + let grpcCode: number | null = null; + if (typeof error?.code === 'number') grpcCode = error.code; + else if (typeof error?.status === 'number') grpcCode = error.status; + + // Try to parse numeric code from message like "status code 10" or "code: 9" or leading "9 FAILED_PRECONDITION" + if (grpcCode === null) { + const m1 = /status code\s*[:=]?\s*(\d+)/i.exec(message); + const m2 = /code\s*[:=]?\s*(\d+)/i.exec(message); + const m3 = /^\s*(\d+)\s+[A-Z_]+/i.exec(message); + const m = m1 || m2 || m3; + if (m) { + const parsed = parseInt(m[1], 10); + if (!isNaN(parsed)) grpcCode = parsed; + } + } + + if (grpcCode !== null) { + status = mapGrpcCodeToHttp(grpcCode); + } else { + // Fallback mapping from tokens + const t = (message || '').toUpperCase(); + if (t.includes('UNAUTHENTICATED')) status = 401; + else if (t.includes('PERMISSION_DENIED')) status = 403; + else if (t.includes('ENDORSEMENT_POLICY') || t.includes('FAILED_PRECONDITION')) status = 412; + else if (t.includes('NOT_FOUND')) status = 404; + else if (t.includes('ALREADY_EXISTS')) status = 409; + else if (t.includes('INVALID_ARGUMENT')) status = 400; + else status = 500; + } + } + + logger.error(`[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`); + res.status(status).json({ + messageEn: message, + messageAr, + }); + } catch (err) { + next(err); + } +}; diff --git a/src/middlewares/language.middleware.ts b/src/middlewares/language.middleware.ts new file mode 100644 index 0000000..155eca6 --- /dev/null +++ b/src/middlewares/language.middleware.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from 'express'; + +export interface RequestWithLanguage extends Request { + language?: 'en' | 'ar'; +} + +/** + * Middleware to extract language preference from request headers + * Checks for 'Accept-Language' or custom 'X-Language' header + */ +export const LanguageMiddleware = (req: RequestWithLanguage, res: Response, next: NextFunction) => { + // Check custom header first + const customLang = req.header('X-Language')?.toLowerCase(); + + if (customLang === 'ar' || customLang === 'arabic') { + req.language = 'ar'; + } else if (customLang === 'en' || customLang === 'english') { + req.language = 'en'; + } else { + // Check Accept-Language header + const acceptLang = req.header('Accept-Language')?.toLowerCase(); + + if (acceptLang?.includes('ar')) { + req.language = 'ar'; + } else { + req.language = 'en'; // Default to English + } + } + + next(); +}; diff --git a/src/middlewares/multer.middleware.ts b/src/middlewares/multer.middleware.ts new file mode 100644 index 0000000..9dce826 --- /dev/null +++ b/src/middlewares/multer.middleware.ts @@ -0,0 +1,63 @@ +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import { Request } from 'express'; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { HttpException } from '@/exceptions/HttpException'; + +const uploadDir = path.join(process.cwd(), 'uploads'); + +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + console.log(`Created uploads directory: ${uploadDir}`); +} + +// We use diskStorage so the file is saved to a 'temp' folder first. +const storage = multer.diskStorage({ + destination: (req: Request, file: Express.Multer.File, cb: (error: Error | null, destination: string) => void) => { + cb(null, 'uploads/'); + }, + filename: (req: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => { + // We create a unique name: "doctor-timestamp.jpg" + const uniqueSuffix = Math.round(Math.random() * 1E9); + cb(null, file.fieldname + path.extname(file.originalname)); + } +}); + +// 2. Filter to accept ONLY images +const imageFilter = (req: Request, file: Express.Multer.File, cb: (error: Error | null, acceptFile: boolean) => void) => { + if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') { + cb(null, true); // Accept file + } else { + const bilingualError = createBilingualError(400, ErrorMessages.UNSUPPORTED_IMAGE_FILE_FORMAT); + const error = new HttpException(bilingualError.status, bilingualError.message, bilingualError.messageAr); + cb(error, false); // Reject file + } +}; + +const pdfFilter = (req: Request, file: Express.Multer.File, cb: (error: Error | null, acceptFile: boolean) => void) => { + if (file.mimetype === 'application/pdf') { + cb(null, true); // Accept file + } else { + const bilingualError = createBilingualError(400, ErrorMessages.UNSUPPORTED_FILE_FORMAT_PDF); + const error = new HttpException(bilingualError.status, bilingualError.message, bilingualError.messageAr); + cb(error, false); // Reject file + } +}; + +// 3. Initialize Multer with limits +export const uploadImage = multer({ + storage: storage, + fileFilter: imageFilter, + limits: { + fileSize: 1024 * 1024 * 3 // Limit file size to 3MB + } +}); + +export const uploadPdf = multer({ + storage: storage, + fileFilter: pdfFilter, + limits: { + fileSize: 1024 * 1024 * 10 // Limit file size to 10MB + } +}); \ No newline at end of file diff --git a/src/middlewares/permissions.middleware.ts b/src/middlewares/permissions.middleware.ts new file mode 100644 index 0000000..0a1659c --- /dev/null +++ b/src/middlewares/permissions.middleware.ts @@ -0,0 +1,10 @@ +import { Request, Response, NextFunction } from 'express'; +import { PrismaClient } from '@prisma/client'; +import { HttpException } from '@/exceptions/HttpException'; + + +const prisma = new PrismaClient(); + +// check if user owns MR + +// check if user can view MR \ No newline at end of file diff --git a/src/middlewares/upload.middleware.ts b/src/middlewares/upload.middleware.ts new file mode 100644 index 0000000..c8b2950 --- /dev/null +++ b/src/middlewares/upload.middleware.ts @@ -0,0 +1,40 @@ +import multer, { FileFilterCallback } from "multer"; +import { Request } from 'express'; +import { HttpException } from "@/exceptions/HttpException"; + + +const storage = multer.memoryStorage(); +const AllowedFileTypes = [ + 'application/pdf', + 'image/jpeg', + 'image/jpg', + 'image/png', + 'image/gif', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'text/plain', + 'application/json', +]; + + +const fileFilter = (req: Request, file: Express.Multer.File, cb: FileFilterCallback) => { + if (AllowedFileTypes.includes(file.mimetype)) { + cb(null, true); + } + else { + cb(new HttpException(400, `file type not allowed`)) + } +}; + + +const upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 300 * 1024 * 1024, + }, +}); + + +export const uploadSingleFile = upload.single('file'); +export const uploadMultipleFiles = upload.array('files', 3); \ No newline at end of file diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts new file mode 100644 index 0000000..8bdf82a --- /dev/null +++ b/src/middlewares/validation.middleware.ts @@ -0,0 +1,52 @@ +import { plainToInstance } from 'class-transformer'; +import { validateOrReject, ValidationError } from 'class-validator'; +import { NextFunction, Request, Response } from 'express'; +import { HttpException } from '@exceptions/HttpException'; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; + +/** + * @name ValidationMiddleware + * @description Allows use of decorator and non-decorator based validation + * @param type dto (pass null to skip DTO validation when only validating file upload) + * @param skipMissingProperties When skipping missing properties + * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined + * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present + * @param requireFile When true, validates that a file has been uploaded via multer (checks req.file or req.files) + */ +export const ValidationMiddleware = ( + type: any = null, + skipMissingProperties = false, + whitelist = false, + forbidNonWhitelisted = false, + requireFile = false, +) => { + return (req: Request, res: Response, next: NextFunction) => { + // Validate file upload if required + if (requireFile) { + const hasFile = req.file || (req.files && (Array.isArray(req.files) ? req.files.length > 0 : Object.keys(req.files).length > 0)); + if (!hasFile) { + const error = createBilingualError(400, ErrorMessages.NO_FILE_UPLOADED) + return next(new HttpException(400, error.message, error.messageAr)); + } + } + + // Skip DTO validation if type is null (file-only validation) + if (type === null) { + return next(); + } + + const dto = plainToInstance(type, req.body); + validateOrReject(dto, { skipMissingProperties, whitelist, forbidNonWhitelisted }) + .then(() => { + req.body = dto; + next(); + }) + .catch((errors: ValidationError[]) => { + const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', '); + // For validation errors, we keep the detailed message in English and provide a generic Arabic message + // since validation constraints are typically defined in English + const messageAr = 'خطأ في التحقق من صحة البيانات المدخلة'; + next(new HttpException(400, message, messageAr)); + }); + }; +}; diff --git a/src/prisma/migrations/20251031175320_init_schema/migration.sql b/src/prisma/migrations/20251031175320_init_schema/migration.sql new file mode 100644 index 0000000..4ac770a --- /dev/null +++ b/src/prisma/migrations/20251031175320_init_schema/migration.sql @@ -0,0 +1,231 @@ +-- CreateEnum +CREATE TYPE "ScanLabType" AS ENUM ('SCAN', 'LAB'); + +-- CreateEnum +CREATE TYPE "Action" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'READ', 'LOGIN', 'LOGOUT'); + +-- CreateEnum +CREATE TYPE "Period" AS ENUM ('DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY'); + +-- CreateEnum +CREATE TYPE "Gender" AS ENUM ('MALE', 'FEMALE'); + +-- CreateTable +CREATE TABLE "Users" ( + "id" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + "email" VARCHAR(255) NOT NULL, + "username" VARCHAR(255) NOT NULL, + "phone" VARCHAR(20) NOT NULL, + "password_hash" VARCHAR(255) NOT NULL, + "gender" "Gender" NOT NULL, + "date_of_birth" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Doctor" ( + "id" TEXT NOT NULL, + "specialization" VARCHAR(255) NOT NULL, + "avg_time" TIME(0), + + CONSTRAINT "Doctor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Patient" ( + "id" TEXT NOT NULL, + "bc_address" VARCHAR(255) NOT NULL, + "consent" BOOLEAN NOT NULL DEFAULT false, + "controlling_nurse_id" TEXT, + + CONSTRAINT "Patient_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Appointments" ( + "id" TEXT NOT NULL, + "patient_id" TEXT, + "doctor_id" TEXT, + "scheduled_time" TIMESTAMP(3) NOT NULL, + "is_online" BOOLEAN NOT NULL DEFAULT false, + "is_completed" BOOLEAN NOT NULL DEFAULT false, + "estimated_time" DOUBLE PRECISION, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Appointments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Medications" ( + "id" TEXT NOT NULL, + "patient_id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "treatment_name" VARCHAR(255) NOT NULL, + "category" VARCHAR(100) NOT NULL, + "medication_end_date" TIMESTAMP(3) NOT NULL, + "medication_start_time" TIME(0) NOT NULL, + "frequency" INTEGER NOT NULL, + "period" "Period" NOT NULL, + "description" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Medications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Scans_Labs" ( + "id" TEXT NOT NULL, + "patient_id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "name" VARCHAR(255) NOT NULL, + "scheduled_date" TIMESTAMP(3), + "scheduled_time" TIME(0), + "frequency" INTEGER, + "period" "Period", + "description" TEXT, + "type" "ScanLabType" NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Scans_Labs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Clinic" ( + "id" TEXT NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "opening_at" TIME(0) NOT NULL, + "closing_at" TIME(0) NOT NULL, + "address" VARCHAR(300) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Clinic_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ClinicNurse" ( + "id" TEXT NOT NULL, + "clinic_id" TEXT NOT NULL, + "nurse_id" TEXT NOT NULL, + + CONSTRAINT "ClinicNurse_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ClinicDoctor" ( + "id" TEXT NOT NULL, + "clinic_id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + + CONSTRAINT "ClinicDoctor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLogs" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "action" "Action" NOT NULL, + "bc_hash" VARCHAR(255) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLogs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Users_email_key" ON "Users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Users_username_key" ON "Users"("username"); + +-- CreateIndex +CREATE INDEX "Appointments_patient_id_idx" ON "Appointments"("patient_id"); + +-- CreateIndex +CREATE INDEX "Appointments_doctor_id_idx" ON "Appointments"("doctor_id"); + +-- CreateIndex +CREATE INDEX "Appointments_scheduled_time_idx" ON "Appointments"("scheduled_time"); + +-- CreateIndex +CREATE INDEX "Medications_patient_id_idx" ON "Medications"("patient_id"); + +-- CreateIndex +CREATE INDEX "Medications_doctor_id_idx" ON "Medications"("doctor_id"); + +-- CreateIndex +CREATE INDEX "Scans_Labs_patient_id_idx" ON "Scans_Labs"("patient_id"); + +-- CreateIndex +CREATE INDEX "Scans_Labs_doctor_id_idx" ON "Scans_Labs"("doctor_id"); + +-- CreateIndex +CREATE INDEX "ClinicNurse_nurse_id_idx" ON "ClinicNurse"("nurse_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "ClinicNurse_clinic_id_nurse_id_key" ON "ClinicNurse"("clinic_id", "nurse_id"); + +-- CreateIndex +CREATE INDEX "ClinicDoctor_doctor_id_idx" ON "ClinicDoctor"("doctor_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "ClinicDoctor_clinic_id_doctor_id_key" ON "ClinicDoctor"("clinic_id", "doctor_id"); + +-- CreateIndex +CREATE INDEX "AuditLogs_user_id_idx" ON "AuditLogs"("user_id"); + +-- CreateIndex +CREATE INDEX "AuditLogs_created_at_idx" ON "AuditLogs"("created_at"); + +-- AddForeignKey +ALTER TABLE "Doctor" ADD CONSTRAINT "Doctor_id_fkey" FOREIGN KEY ("id") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Patient" ADD CONSTRAINT "Patient_id_fkey" FOREIGN KEY ("id") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Patient" ADD CONSTRAINT "Patient_controlling_nurse_id_fkey" FOREIGN KEY ("controlling_nurse_id") REFERENCES "Users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Appointments" ADD CONSTRAINT "Appointments_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Appointments" ADD CONSTRAINT "Appointments_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Medications" ADD CONSTRAINT "Medications_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Medications" ADD CONSTRAINT "Medications_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scans_Labs" ADD CONSTRAINT "Scans_Labs_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Scans_Labs" ADD CONSTRAINT "Scans_Labs_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ClinicNurse" ADD CONSTRAINT "ClinicNurse_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ClinicNurse" ADD CONSTRAINT "ClinicNurse_nurse_id_fkey" FOREIGN KEY ("nurse_id") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ClinicDoctor" ADD CONSTRAINT "ClinicDoctor_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ClinicDoctor" ADD CONSTRAINT "ClinicDoctor_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AuditLogs" ADD CONSTRAINT "AuditLogs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20251031214538_refresh_token_table/migration.sql b/src/prisma/migrations/20251031214538_refresh_token_table/migration.sql new file mode 100644 index 0000000..59ff378 --- /dev/null +++ b/src/prisma/migrations/20251031214538_refresh_token_table/migration.sql @@ -0,0 +1,24 @@ +-- CreateTable +CREATE TABLE "RefreshTokens" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "token_hash" VARCHAR(255) NOT NULL, + "expires_at" TIMESTAMP(3) NOT NULL, + "is_revoked" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "revoked_at" TIMESTAMP(3), + + CONSTRAINT "RefreshTokens_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "RefreshTokens_user_id_idx" ON "RefreshTokens"("user_id"); + +-- CreateIndex +CREATE INDEX "RefreshTokens_token_hash_idx" ON "RefreshTokens"("token_hash"); + +-- CreateIndex +CREATE INDEX "RefreshTokens_expires_at_idx" ON "RefreshTokens"("expires_at"); + +-- AddForeignKey +ALTER TABLE "RefreshTokens" ADD CONSTRAINT "RefreshTokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20251103213136_otp_schema/migration.sql b/src/prisma/migrations/20251103213136_otp_schema/migration.sql new file mode 100644 index 0000000..9b5c3eb --- /dev/null +++ b/src/prisma/migrations/20251103213136_otp_schema/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Users" ADD COLUMN "email_OTP" VARCHAR(6), +ADD COLUMN "email_OTP_expires_at" TIMESTAMP(3), +ADD COLUMN "isVerified" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/prisma/migrations/20251103223535_reset_password_schema/migration.sql b/src/prisma/migrations/20251103223535_reset_password_schema/migration.sql new file mode 100644 index 0000000..465f898 --- /dev/null +++ b/src/prisma/migrations/20251103223535_reset_password_schema/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Users" ADD COLUMN "password_reset_token" VARCHAR(255), +ADD COLUMN "password_reset_token_expires_at" TIMESTAMP(3); diff --git a/src/prisma/migrations/20251111114052_add_medical_records_table/migration.sql b/src/prisma/migrations/20251111114052_add_medical_records_table/migration.sql new file mode 100644 index 0000000..4cb91f7 --- /dev/null +++ b/src/prisma/migrations/20251111114052_add_medical_records_table/migration.sql @@ -0,0 +1,32 @@ +-- CreateEnum +CREATE TYPE "RecordType" AS ENUM ('LAB_RESULT', 'SCAN', 'DIAGNOSIS', 'VISIT_SUMMARY'); + +-- CreateTable +CREATE TABLE "MedicalRecords" ( + "id" TEXT NOT NULL, + "patient_id" TEXT NOT NULL, + "doctor_id" TEXT, + "name" VARCHAR(255) NOT NULL, + "cid" VARCHAR(255) NOT NULL, + "type" "RecordType" NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "MedicalRecords_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "MedicalRecords_cid_key" ON "MedicalRecords"("cid"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_patient_id_idx" ON "MedicalRecords"("patient_id"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_doctor_id_idx" ON "MedicalRecords"("doctor_id"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_cid_idx" ON "MedicalRecords"("cid"); + +-- AddForeignKey +ALTER TABLE "MedicalRecords" ADD CONSTRAINT "MedicalRecords_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20251111224133_added_user_role/migration.sql b/src/prisma/migrations/20251111224133_added_user_role/migration.sql new file mode 100644 index 0000000..c2ea822 --- /dev/null +++ b/src/prisma/migrations/20251111224133_added_user_role/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('ADMIN', 'DOCTOR', 'NURSE', 'PATIENT'); + +-- AlterTable +ALTER TABLE "Users" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'PATIENT'; diff --git a/src/prisma/migrations/20251112181411_added_has_completed_profile/migration.sql b/src/prisma/migrations/20251112181411_added_has_completed_profile/migration.sql new file mode 100644 index 0000000..6f3ee04 --- /dev/null +++ b/src/prisma/migrations/20251112181411_added_has_completed_profile/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Users" ADD COLUMN "hasCompletedProfile" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/prisma/migrations/20251212114202_added_doctor_info/migration.sql b/src/prisma/migrations/20251212114202_added_doctor_info/migration.sql new file mode 100644 index 0000000..b44e870 --- /dev/null +++ b/src/prisma/migrations/20251212114202_added_doctor_info/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - Added the required column `phone` to the `Clinic` table without a default value. This is not possible if the table is not empty. + - Added the required column `fees` to the `ClinicDoctor` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Clinic" ADD COLUMN "address_maps_link" VARCHAR(500), +ADD COLUMN "canPayOnline" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "phone" VARCHAR(20) NOT NULL; + +-- AlterTable +ALTER TABLE "ClinicDoctor" ADD COLUMN "fees" DOUBLE PRECISION NOT NULL; + +-- AlterTable +ALTER TABLE "Users" ADD COLUMN "photo_public_id" VARCHAR(500), +ADD COLUMN "photo_url" VARCHAR(500); diff --git a/src/prisma/migrations/20251212135524_added_super_admin_role/migration.sql b/src/prisma/migrations/20251212135524_added_super_admin_role/migration.sql new file mode 100644 index 0000000..9704e58 --- /dev/null +++ b/src/prisma/migrations/20251212135524_added_super_admin_role/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Role" ADD VALUE 'SUPER_ADMIN'; diff --git a/src/prisma/migrations/20251213100731_merging_with_latest_dev/migration.sql b/src/prisma/migrations/20251213100731_merging_with_latest_dev/migration.sql new file mode 100644 index 0000000..79f2843 --- /dev/null +++ b/src/prisma/migrations/20251213100731_merging_with_latest_dev/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the `MedicalRecords` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."MedicalRecords" DROP CONSTRAINT "MedicalRecords_patient_id_fkey"; + +-- DropTable +DROP TABLE "public"."MedicalRecords"; + +-- DropEnum +DROP TYPE "public"."RecordType"; diff --git a/src/prisma/migrations/20251213101242_readding_the_medical_record_table/migration.sql b/src/prisma/migrations/20251213101242_readding_the_medical_record_table/migration.sql new file mode 100644 index 0000000..4cb91f7 --- /dev/null +++ b/src/prisma/migrations/20251213101242_readding_the_medical_record_table/migration.sql @@ -0,0 +1,32 @@ +-- CreateEnum +CREATE TYPE "RecordType" AS ENUM ('LAB_RESULT', 'SCAN', 'DIAGNOSIS', 'VISIT_SUMMARY'); + +-- CreateTable +CREATE TABLE "MedicalRecords" ( + "id" TEXT NOT NULL, + "patient_id" TEXT NOT NULL, + "doctor_id" TEXT, + "name" VARCHAR(255) NOT NULL, + "cid" VARCHAR(255) NOT NULL, + "type" "RecordType" NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "MedicalRecords_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "MedicalRecords_cid_key" ON "MedicalRecords"("cid"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_patient_id_idx" ON "MedicalRecords"("patient_id"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_doctor_id_idx" ON "MedicalRecords"("doctor_id"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_cid_idx" ON "MedicalRecords"("cid"); + +-- AddForeignKey +ALTER TABLE "MedicalRecords" ADD CONSTRAINT "MedicalRecords_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20251213134456_added_doctor_account_status_enum/migration.sql b/src/prisma/migrations/20251213134456_added_doctor_account_status_enum/migration.sql new file mode 100644 index 0000000..ddede4c --- /dev/null +++ b/src/prisma/migrations/20251213134456_added_doctor_account_status_enum/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "DoctorAccountStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "account_status" "DoctorAccountStatus" NOT NULL DEFAULT 'PENDING'; diff --git a/src/prisma/migrations/20260124222051_clinic_update_schema/migration.sql b/src/prisma/migrations/20260124222051_clinic_update_schema/migration.sql new file mode 100644 index 0000000..8239d22 --- /dev/null +++ b/src/prisma/migrations/20260124222051_clinic_update_schema/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - Added the required column `created_by` to the `Clinic` table without a default value. This is not possible if the table is not empty. + - Added the required column `name` to the `Clinic` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Clinic" ADD COLUMN "created_by" VARCHAR(255) NOT NULL, +ADD COLUMN "name" VARCHAR(255) NOT NULL; + +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "num_of_created_clinics" INTEGER NOT NULL DEFAULT 0; diff --git a/src/prisma/migrations/20260124224056_adjusted_time_to_string/migration.sql b/src/prisma/migrations/20260124224056_adjusted_time_to_string/migration.sql new file mode 100644 index 0000000..61c18ae --- /dev/null +++ b/src/prisma/migrations/20260124224056_adjusted_time_to_string/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Changed the type of `opening_at` on the `Clinic` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `closing_at` on the `Clinic` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "Clinic" DROP COLUMN "opening_at", +ADD COLUMN "opening_at" VARCHAR(12) NOT NULL, +DROP COLUMN "closing_at", +ADD COLUMN "closing_at" VARCHAR(12) NOT NULL; diff --git a/src/prisma/migrations/20260125212701_appointments_modifications/migration.sql b/src/prisma/migrations/20260125212701_appointments_modifications/migration.sql new file mode 100644 index 0000000..c71c689 --- /dev/null +++ b/src/prisma/migrations/20260125212701_appointments_modifications/migration.sql @@ -0,0 +1,71 @@ +/* + Warnings: + + - Added the required column `end_time` to the `Appointments` table without a default value. This is not possible if the table is not empty. + - Added the required column `slot_duration` to the `Appointments` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "AvailabilityType" AS ENUM ('UNSET', 'ONLINE', 'OFFLINE', 'BOTH'); + +-- CreateEnum +CREATE TYPE "DayOfWeek" AS ENUM ('SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'); + +-- CreateEnum +CREATE TYPE "AppointmentStatus" AS ENUM ('CONFIRMED', 'COMPLETED', 'CANCELLED', 'NO_SHOW'); + +-- AlterTable +ALTER TABLE "Appointments" ADD COLUMN "cancelled_by" TEXT, +ADD COLUMN "clinic_id" TEXT, +ADD COLUMN "end_time" TIMESTAMP(3) NOT NULL, +ADD COLUMN "slot_duration" TIMESTAMP(3) NOT NULL, +ADD COLUMN "status" "AppointmentStatus" NOT NULL DEFAULT 'CONFIRMED'; + +-- AlterTable +ALTER TABLE "ClinicDoctor" ADD COLUMN "is_accepting" BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "availability_type" "AvailabilityType" NOT NULL DEFAULT 'UNSET', +ADD COLUMN "present" BOOLEAN NOT NULL DEFAULT true; + +-- CreateTable +CREATE TABLE "DoctorSchedules" ( + "id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "clinic_id" TEXT, + "day_of_week" "DayOfWeek" NOT NULL, + "start_time" TIME(0) NOT NULL, + "end_time" TIME(0) NOT NULL, + "slot_duration" INTEGER NOT NULL, + "buffer_time" INTEGER NOT NULL DEFAULT 0, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "DoctorSchedules_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "DoctorSchedules_doctor_id_idx" ON "DoctorSchedules"("doctor_id"); + +-- CreateIndex +CREATE INDEX "DoctorSchedules_clinic_id_idx" ON "DoctorSchedules"("clinic_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "DoctorSchedules_doctor_id_clinic_id_day_of_week_key" ON "DoctorSchedules"("doctor_id", "clinic_id", "day_of_week"); + +-- CreateIndex +CREATE INDEX "Appointments_clinic_id_idx" ON "Appointments"("clinic_id"); + +-- CreateIndex +CREATE INDEX "Appointments_status_idx" ON "Appointments"("status"); + +-- AddForeignKey +ALTER TABLE "Appointments" ADD CONSTRAINT "Appointments_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DoctorSchedules" ADD CONSTRAINT "DoctorSchedules_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DoctorSchedules" ADD CONSTRAINT "DoctorSchedules_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20260127172048_fix_slot_duration_column/migration.sql b/src/prisma/migrations/20260127172048_fix_slot_duration_column/migration.sql new file mode 100644 index 0000000..50499f2 --- /dev/null +++ b/src/prisma/migrations/20260127172048_fix_slot_duration_column/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Changed the type of `slot_duration` on the `Appointments` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- AlterTable +ALTER TABLE "Appointments" DROP COLUMN "slot_duration", +ADD COLUMN "slot_duration" INTEGER NOT NULL; diff --git a/src/prisma/migrations/20260129164535_doctor_verification_files_urls/migration.sql b/src/prisma/migrations/20260129164535_doctor_verification_files_urls/migration.sql new file mode 100644 index 0000000..ba68ecd --- /dev/null +++ b/src/prisma/migrations/20260129164535_doctor_verification_files_urls/migration.sql @@ -0,0 +1,13 @@ +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "fellowshipCertificatePublicId" VARCHAR(500), +ADD COLUMN "fellowshipCertificateUrl" VARCHAR(500), +ADD COLUMN "graduationCertificatePublicId" VARCHAR(500), +ADD COLUMN "graduationCertificateUrl" VARCHAR(500), +ADD COLUMN "mastersCertificatePublicId" VARCHAR(500), +ADD COLUMN "mastersCertificateUrl" VARCHAR(500), +ADD COLUMN "membershipCardPublicId" VARCHAR(500), +ADD COLUMN "membershipCardUrl" VARCHAR(500), +ADD COLUMN "professionalPracticeCardPublicId" VARCHAR(500), +ADD COLUMN "professionalPracticeCardUrl" VARCHAR(500), +ADD COLUMN "unionSpecializationCertificatePublicId" VARCHAR(500), +ADD COLUMN "unionSpecializationCertificateUrl" VARCHAR(500); diff --git a/src/prisma/migrations/20260129185802_merging_verify_files_with_dev/migration.sql b/src/prisma/migrations/20260129185802_merging_verify_files_with_dev/migration.sql new file mode 100644 index 0000000..ddffcd4 --- /dev/null +++ b/src/prisma/migrations/20260129185802_merging_verify_files_with_dev/migration.sql @@ -0,0 +1,54 @@ +/* + Warnings: + + - You are about to drop the column `cancelled_by` on the `Appointments` table. All the data in the column will be lost. + - You are about to drop the column `clinic_id` on the `Appointments` table. All the data in the column will be lost. + - You are about to drop the column `end_time` on the `Appointments` table. All the data in the column will be lost. + - You are about to drop the column `slot_duration` on the `Appointments` table. All the data in the column will be lost. + - You are about to drop the column `status` on the `Appointments` table. All the data in the column will be lost. + - You are about to drop the column `is_accepting` on the `ClinicDoctor` table. All the data in the column will be lost. + - You are about to drop the column `availability_type` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the column `present` on the `Doctor` table. All the data in the column will be lost. + - You are about to drop the `DoctorSchedules` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."Appointments" DROP CONSTRAINT "Appointments_clinic_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."DoctorSchedules" DROP CONSTRAINT "DoctorSchedules_clinic_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."DoctorSchedules" DROP CONSTRAINT "DoctorSchedules_doctor_id_fkey"; + +-- DropIndex +DROP INDEX "public"."Appointments_clinic_id_idx"; + +-- DropIndex +DROP INDEX "public"."Appointments_status_idx"; + +-- AlterTable +ALTER TABLE "Appointments" DROP COLUMN "cancelled_by", +DROP COLUMN "clinic_id", +DROP COLUMN "end_time", +DROP COLUMN "slot_duration", +DROP COLUMN "status"; + +-- AlterTable +ALTER TABLE "ClinicDoctor" DROP COLUMN "is_accepting"; + +-- AlterTable +ALTER TABLE "Doctor" DROP COLUMN "availability_type", +DROP COLUMN "present"; + +-- DropTable +DROP TABLE "public"."DoctorSchedules"; + +-- DropEnum +DROP TYPE "public"."AppointmentStatus"; + +-- DropEnum +DROP TYPE "public"."AvailabilityType"; + +-- DropEnum +DROP TYPE "public"."DayOfWeek"; diff --git a/src/prisma/migrations/20260131133344_add_queue_parameters/migration.sql b/src/prisma/migrations/20260131133344_add_queue_parameters/migration.sql new file mode 100644 index 0000000..d22ef98 --- /dev/null +++ b/src/prisma/migrations/20260131133344_add_queue_parameters/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Appointments" ADD COLUMN "patients_ahead" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "position" INTEGER NOT NULL DEFAULT 0; diff --git a/src/prisma/migrations/20260203141848_fix_off_time_and_timezone/migration.sql b/src/prisma/migrations/20260203141848_fix_off_time_and_timezone/migration.sql new file mode 100644 index 0000000..845dda1 --- /dev/null +++ b/src/prisma/migrations/20260203141848_fix_off_time_and_timezone/migration.sql @@ -0,0 +1,74 @@ +/* + Warnings: + + - Added the required column `end_time` to the `Appointments` table without a default value. This is not possible if the table is not empty. + - Added the required column `slot_duration` to the `Appointments` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "AvailabilityType" AS ENUM ('UNSET', 'ONLINE', 'OFFLINE', 'BOTH'); + +-- CreateEnum +CREATE TYPE "DayOfWeek" AS ENUM ('SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'); + +-- CreateEnum +CREATE TYPE "AppointmentStatus" AS ENUM ('CONFIRMED', 'COMPLETED', 'CANCELLED', 'NO_SHOW'); + +-- AlterTable +ALTER TABLE "Appointments" ADD COLUMN "cancelled_by" TEXT, +ADD COLUMN "clinic_id" TEXT, +ADD COLUMN "end_time" TIMESTAMP(3) NOT NULL, +ADD COLUMN "slot_duration" INTEGER NOT NULL, +ADD COLUMN "status" "AppointmentStatus" NOT NULL DEFAULT 'CONFIRMED'; + +-- AlterTable +ALTER TABLE "ClinicDoctor" ADD COLUMN "is_accepting" BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE "Doctor" ADD COLUMN "availability_type" "AvailabilityType" NOT NULL DEFAULT 'UNSET', +ADD COLUMN "present" BOOLEAN NOT NULL DEFAULT true; + +-- CreateTable +CREATE TABLE "DoctorSchedules" ( + "id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "clinic_id" TEXT, + "day_of_week" "DayOfWeek" NOT NULL, + "start_time" TEXT NOT NULL, + "end_time" TEXT NOT NULL, + "slot_duration" INTEGER NOT NULL, + "buffer_time" INTEGER NOT NULL DEFAULT 0, + "is_online" BOOLEAN NOT NULL DEFAULT true, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "break_start" TEXT, + "break_end" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "DoctorSchedules_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "DoctorSchedules_doctor_id_idx" ON "DoctorSchedules"("doctor_id"); + +-- CreateIndex +CREATE INDEX "DoctorSchedules_clinic_id_idx" ON "DoctorSchedules"("clinic_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "DoctorSchedules_doctor_id_clinic_id_day_of_week_key" ON "DoctorSchedules"("doctor_id", "clinic_id", "day_of_week"); + +-- CreateIndex +CREATE INDEX "Appointments_clinic_id_idx" ON "Appointments"("clinic_id"); + +-- CreateIndex +CREATE INDEX "Appointments_status_idx" ON "Appointments"("status"); + +-- AddForeignKey +ALTER TABLE "Appointments" ADD CONSTRAINT "Appointments_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DoctorSchedules" ADD CONSTRAINT "DoctorSchedules_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DoctorSchedules" ADD CONSTRAINT "DoctorSchedules_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20260206200942_fix_remove_unique_combination_in_doctor_schedule/migration.sql b/src/prisma/migrations/20260206200942_fix_remove_unique_combination_in_doctor_schedule/migration.sql new file mode 100644 index 0000000..9a361f8 --- /dev/null +++ b/src/prisma/migrations/20260206200942_fix_remove_unique_combination_in_doctor_schedule/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "public"."DoctorSchedules_doctor_id_clinic_id_day_of_week_key"; diff --git a/src/prisma/migrations/20260207120541_add_vacations_table/migration.sql b/src/prisma/migrations/20260207120541_add_vacations_table/migration.sql new file mode 100644 index 0000000..7796282 --- /dev/null +++ b/src/prisma/migrations/20260207120541_add_vacations_table/migration.sql @@ -0,0 +1,35 @@ +-- CreateEnum +CREATE TYPE "VacationStatus" AS ENUM ('UPCOMING', 'CURRENT', 'ENDED'); + +-- CreateTable +CREATE TABLE "Vacations" ( + "id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "schedule_id" TEXT NOT NULL, + "start_date" TEXT NOT NULL, + "end_date" TEXT NOT NULL, + "status" "VacationStatus" NOT NULL DEFAULT 'UPCOMING', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "Vacations_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Vacations_doctor_id_idx" ON "Vacations"("doctor_id"); + +-- CreateIndex +CREATE INDEX "Vacations_schedule_id_idx" ON "Vacations"("schedule_id"); + +-- CreateIndex +CREATE INDEX "Vacations_start_date_idx" ON "Vacations"("start_date"); + +-- CreateIndex +CREATE INDEX "Vacations_end_date_idx" ON "Vacations"("end_date"); + +-- AddForeignKey +ALTER TABLE "Vacations" ADD CONSTRAINT "Vacations_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Vacations" ADD CONSTRAINT "Vacations_schedule_id_fkey" FOREIGN KEY ("schedule_id") REFERENCES "DoctorSchedules"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20260217131027_add_nurse_announcements_tables/migration.sql b/src/prisma/migrations/20260217131027_add_nurse_announcements_tables/migration.sql new file mode 100644 index 0000000..62e861e --- /dev/null +++ b/src/prisma/migrations/20260217131027_add_nurse_announcements_tables/migration.sql @@ -0,0 +1,160 @@ +-- CreateEnum +CREATE TYPE "NurseAccountStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "AnnouncementStatus" AS ENUM ('POSTED', 'PENDING', 'EXPIRED'); + +-- CreateEnum +CREATE TYPE "AnnouncementNurseStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- CreateTable +CREATE TABLE "Nurse" ( + "id" TEXT NOT NULL, + "account_status" "NurseAccountStatus" NOT NULL DEFAULT 'PENDING', + "years_of_experience" INTEGER NOT NULL, + "national_id_url" VARCHAR(500), + "national_id_public_id" VARCHAR(500), + "bonus_file_url" VARCHAR(500), + "bonus_file_public_id" VARCHAR(500), + "brief" TEXT, + + CONSTRAINT "Nurse_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "NurseSchedules" ( + "id" TEXT NOT NULL, + "nurse_id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "clinic_id" TEXT, + "day_of_week" "DayOfWeek" NOT NULL, + "start_time" VARCHAR(12) NOT NULL, + "end_time" VARCHAR(12) NOT NULL, + "is_online" BOOLEAN NOT NULL DEFAULT false, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + "userId" TEXT, + + CONSTRAINT "NurseSchedules_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Announcements" ( + "id" TEXT NOT NULL, + "doctor_id" TEXT NOT NULL, + "clinic_id" TEXT NOT NULL, + "status" "AnnouncementStatus" NOT NULL DEFAULT 'PENDING', + "gender" "Gender", + "max_age" INTEGER, + "years_of_experience" INTEGER, + "notes" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + "userId" TEXT, + + CONSTRAINT "Announcements_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AnnouncementDays" ( + "id" TEXT NOT NULL, + "announcement_id" TEXT NOT NULL, + "day_of_week" "DayOfWeek" NOT NULL, + "start_time" TEXT NOT NULL, + "end_time" TEXT NOT NULL, + + CONSTRAINT "AnnouncementDays_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AnnouncementNurses" ( + "id" TEXT NOT NULL, + "announcement_id" TEXT NOT NULL, + "nurse_id" TEXT NOT NULL, + "status" "AnnouncementNurseStatus" NOT NULL DEFAULT 'PENDING', + "doctor_id" TEXT, + "clinic_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "userId" TEXT, + + CONSTRAINT "AnnouncementNurses_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "NurseSchedules_nurse_id_idx" ON "NurseSchedules"("nurse_id"); + +-- CreateIndex +CREATE INDEX "NurseSchedules_doctor_id_idx" ON "NurseSchedules"("doctor_id"); + +-- CreateIndex +CREATE INDEX "NurseSchedules_clinic_id_idx" ON "NurseSchedules"("clinic_id"); + +-- CreateIndex +CREATE INDEX "Announcements_doctor_id_idx" ON "Announcements"("doctor_id"); + +-- CreateIndex +CREATE INDEX "Announcements_clinic_id_idx" ON "Announcements"("clinic_id"); + +-- CreateIndex +CREATE INDEX "Announcements_status_idx" ON "Announcements"("status"); + +-- CreateIndex +CREATE INDEX "AnnouncementDays_announcement_id_idx" ON "AnnouncementDays"("announcement_id"); + +-- CreateIndex +CREATE INDEX "AnnouncementNurses_announcement_id_idx" ON "AnnouncementNurses"("announcement_id"); + +-- CreateIndex +CREATE INDEX "AnnouncementNurses_nurse_id_idx" ON "AnnouncementNurses"("nurse_id"); + +-- CreateIndex +CREATE INDEX "AnnouncementNurses_doctor_id_idx" ON "AnnouncementNurses"("doctor_id"); + +-- CreateIndex +CREATE INDEX "AnnouncementNurses_clinic_id_idx" ON "AnnouncementNurses"("clinic_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "AnnouncementNurses_announcement_id_nurse_id_key" ON "AnnouncementNurses"("announcement_id", "nurse_id"); + +-- AddForeignKey +ALTER TABLE "Nurse" ADD CONSTRAINT "Nurse_id_fkey" FOREIGN KEY ("id") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NurseSchedules" ADD CONSTRAINT "NurseSchedules_nurse_id_fkey" FOREIGN KEY ("nurse_id") REFERENCES "Nurse"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NurseSchedules" ADD CONSTRAINT "NurseSchedules_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "NurseSchedules" ADD CONSTRAINT "NurseSchedules_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Announcements" ADD CONSTRAINT "Announcements_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Announcements" ADD CONSTRAINT "Announcements_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Announcements" ADD CONSTRAINT "Announcements_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnnouncementDays" ADD CONSTRAINT "AnnouncementDays_announcement_id_fkey" FOREIGN KEY ("announcement_id") REFERENCES "Announcements"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnnouncementNurses" ADD CONSTRAINT "AnnouncementNurses_announcement_id_fkey" FOREIGN KEY ("announcement_id") REFERENCES "Announcements"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnnouncementNurses" ADD CONSTRAINT "AnnouncementNurses_nurse_id_fkey" FOREIGN KEY ("nurse_id") REFERENCES "Nurse"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnnouncementNurses" ADD CONSTRAINT "AnnouncementNurses_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnnouncementNurses" ADD CONSTRAINT "AnnouncementNurses_clinic_id_fkey" FOREIGN KEY ("clinic_id") REFERENCES "Clinic"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AnnouncementNurses" ADD CONSTRAINT "AnnouncementNurses_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20260217152715_modify_file_fields_for_nurse/migration.sql b/src/prisma/migrations/20260217152715_modify_file_fields_for_nurse/migration.sql new file mode 100644 index 0000000..c8256a2 --- /dev/null +++ b/src/prisma/migrations/20260217152715_modify_file_fields_for_nurse/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `bonus_file_public_id` on the `Nurse` table. All the data in the column will be lost. + - You are about to drop the column `bonus_file_url` on the `Nurse` table. All the data in the column will be lost. + - You are about to drop the column `national_id_public_id` on the `Nurse` table. All the data in the column will be lost. + - You are about to drop the column `national_id_url` on the `Nurse` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Nurse" DROP COLUMN "bonus_file_public_id", +DROP COLUMN "bonus_file_url", +DROP COLUMN "national_id_public_id", +DROP COLUMN "national_id_url", +ADD COLUMN "bonusFilePublicId" VARCHAR(500), +ADD COLUMN "bonusFileUrl" VARCHAR(500), +ADD COLUMN "nationalCardPublicId" VARCHAR(500), +ADD COLUMN "nationalCardUrl" VARCHAR(500); diff --git a/src/prisma/migrations/20260224215850_add_doctor_relation_for_nurse_schedule/migration.sql b/src/prisma/migrations/20260224215850_add_doctor_relation_for_nurse_schedule/migration.sql new file mode 100644 index 0000000..aa37129 --- /dev/null +++ b/src/prisma/migrations/20260224215850_add_doctor_relation_for_nurse_schedule/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - The values [POSTED] on the enum `AnnouncementStatus` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "AnnouncementStatus_new" AS ENUM ('PENDING', 'EXPIRED'); +ALTER TABLE "public"."Announcements" ALTER COLUMN "status" DROP DEFAULT; +ALTER TABLE "Announcements" ALTER COLUMN "status" TYPE "AnnouncementStatus_new" USING ("status"::text::"AnnouncementStatus_new"); +ALTER TYPE "AnnouncementStatus" RENAME TO "AnnouncementStatus_old"; +ALTER TYPE "AnnouncementStatus_new" RENAME TO "AnnouncementStatus"; +DROP TYPE "public"."AnnouncementStatus_old"; +ALTER TABLE "Announcements" ALTER COLUMN "status" SET DEFAULT 'PENDING'; +COMMIT; + +-- AlterTable +ALTER TABLE "AnnouncementNurses" ADD COLUMN "deleted_at" TIMESTAMP(3); + +-- AddForeignKey +ALTER TABLE "NurseSchedules" ADD CONSTRAINT "NurseSchedules_doctor_id_fkey" FOREIGN KEY ("doctor_id") REFERENCES "Doctor"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20260301221257_update_medical_records_and_encryption/migration.sql b/src/prisma/migrations/20260301221257_update_medical_records_and_encryption/migration.sql new file mode 100644 index 0000000..daf6cd5 --- /dev/null +++ b/src/prisma/migrations/20260301221257_update_medical_records_and_encryption/migration.sql @@ -0,0 +1,55 @@ +/* + Warnings: + + - Added the required column `clinic_id` to the `MedicalRecords` table without a default value. This is not possible if the table is not empty. + - Added the required column `key_id` to the `MedicalRecords` table without a default value. This is not possible if the table is not empty. + - Added the required column `mime_type` to the `MedicalRecords` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "RecordType" ADD VALUE 'SOAP_NOTE'; +ALTER TYPE "RecordType" ADD VALUE 'MEDICAL_HISTORY'; + +-- AlterTable +ALTER TABLE "MedicalRecords" ADD COLUMN "appointment_id" TEXT, +ADD COLUMN "clinic_id" TEXT NOT NULL, +ADD COLUMN "key_id" TEXT NOT NULL, +ADD COLUMN "mime_type" VARCHAR(100) NOT NULL; + +-- CreateTable +CREATE TABLE "EncryptionKeys" ( + "id" TEXT NOT NULL, + "patient_id" TEXT NOT NULL, + "encrypted_key" VARCHAR(500) NOT NULL, + "algorithm" VARCHAR(50) NOT NULL DEFAULT 'AES-256-GCM', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMP(3) NOT NULL, + "deleted_at" TIMESTAMP(3), + + CONSTRAINT "EncryptionKeys_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "EncryptionKeys_patient_id_key" ON "EncryptionKeys"("patient_id"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_clinic_id_idx" ON "MedicalRecords"("clinic_id"); + +-- CreateIndex +CREATE INDEX "MedicalRecords_appointment_id_idx" ON "MedicalRecords"("appointment_id"); + +-- AddForeignKey +ALTER TABLE "MedicalRecords" ADD CONSTRAINT "MedicalRecords_appointment_id_fkey" FOREIGN KEY ("appointment_id") REFERENCES "Appointments"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicalRecords" ADD CONSTRAINT "MedicalRecords_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "EncryptionKeys"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "EncryptionKeys" ADD CONSTRAINT "EncryptionKeys_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/prisma/migrations/20260307000000_move_key_to_blockchain/migration.sql b/src/prisma/migrations/20260307000000_move_key_to_blockchain/migration.sql new file mode 100644 index 0000000..b02ec0e --- /dev/null +++ b/src/prisma/migrations/20260307000000_move_key_to_blockchain/migration.sql @@ -0,0 +1,5 @@ +-- Drop the foreign key constraint and the key_id column from MedicalRecords +-- since encryption keys are now stored on the Hyperledger Fabric blockchain. + +ALTER TABLE "MedicalRecords" DROP CONSTRAINT IF EXISTS "MedicalRecords_key_id_fkey"; +ALTER TABLE "MedicalRecords" DROP COLUMN IF EXISTS "key_id"; diff --git a/src/prisma/migrations/20260312145020_record_type/migration.sql b/src/prisma/migrations/20260312145020_record_type/migration.sql new file mode 100644 index 0000000..f81b2b8 --- /dev/null +++ b/src/prisma/migrations/20260312145020_record_type/migration.sql @@ -0,0 +1,10 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "RecordType" ADD VALUE 'VISIT'; +ALTER TYPE "RecordType" ADD VALUE 'FILE'; diff --git a/src/prisma/migrations/migration_lock.toml b/src/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/src/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma new file mode 100644 index 0000000..7c73d9e --- /dev/null +++ b/src/prisma/schema.prisma @@ -0,0 +1,534 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + name String @db.VarChar(255) + email String @unique @db.VarChar(255) + username String @unique @db.VarChar(255) + phone String @db.VarChar(20) + password_hash String @db.VarChar(255) + gender Gender + date_of_birth DateTime + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + email_OTP String? @db.VarChar(6) + email_OTP_expires_at DateTime? + isVerified Boolean @default(false) + password_reset_token String? @db.VarChar(255) + password_reset_token_expires_at DateTime? + role Role @default(PATIENT) + hasCompletedProfile Boolean @default(false) + photo_public_id String? @db.VarChar(500) + photo_url String? @db.VarChar(500) + + announcementNurses AnnouncementNurse[] + announcements Announcement[] + appointments_as_doctor Appointment[] @relation("DoctorAppointments") + appointments_as_patient Appointment[] @relation("PatientAppointments") + audit_logs AuditLog[] @relation("UserAuditLogs") + clinics_as_nurse ClinicNurse[] @relation("NurseClinics") + doctor Doctor? @relation("UserAsDoctor") + medications_as_doctor Medication[] @relation("DoctorMedications") + medications_as_patient Medication[] @relation("PatientMedications") + nurse Nurse? @relation("UserAsNurse") + nurseSchedules NurseSchedule[] + controlled_patients Patient[] @relation("ControllingNurse") + patient Patient? @relation("UserAsPatient") + refresh_tokens RefreshToken[] @relation("UserRefreshTokens") + scans_labs_as_doctor ScanLab[] @relation("DoctorScansLabs") + scans_labs_as_patient ScanLab[] @relation("PatientScansLabs") + medical_records_as_patient MedicalRecord[] @relation("PatientMedicalRecords") + encryption_key EncryptionKey? @relation("PatientEncryptionKey") + + @@map("Users") +} + +model Doctor { + id String @id @default(uuid()) + specialization String @db.VarChar(255) + avg_time DateTime? @db.Time(0) + account_status DoctorAccountStatus @default(PENDING) + num_of_created_clinics Int @default(0) + fellowshipCertificatePublicId String? @db.VarChar(500) + fellowshipCertificateUrl String? @db.VarChar(500) + graduationCertificatePublicId String? @db.VarChar(500) + graduationCertificateUrl String? @db.VarChar(500) + mastersCertificatePublicId String? @db.VarChar(500) + mastersCertificateUrl String? @db.VarChar(500) + membershipCardPublicId String? @db.VarChar(500) + membershipCardUrl String? @db.VarChar(500) + professionalPracticeCardPublicId String? @db.VarChar(500) + professionalPracticeCardUrl String? @db.VarChar(500) + unionSpecializationCertificatePublicId String? @db.VarChar(500) + unionSpecializationCertificateUrl String? @db.VarChar(500) + availability_type AvailabilityType @default(UNSET) + present Boolean @default(true) + announcementNurses AnnouncementNurse[] @relation("AnnouncementNurseDoctor") + announcements Announcement[] @relation("AnnouncementDoctor") + clinic_doctors ClinicDoctor[] + user User @relation("UserAsDoctor", fields: [id], references: [id], onDelete: Cascade) + doctorSchedules DoctorSchedule[] + nurseSchedules NurseSchedule[] @relation("NurseScheduleDoctor") + vacations Vacation[] + + @@map("Doctor") +} + +model Patient { + id String @id @default(uuid()) + bc_address String @db.VarChar(255) + consent Boolean @default(false) + controlling_nurse_id String? + controlling_nurse_user User? @relation("ControllingNurse", fields: [controlling_nurse_id], references: [id]) + user User @relation("UserAsPatient", fields: [id], references: [id], onDelete: Cascade) + + @@map("Patient") +} + +model Nurse { + id String @id @default(uuid()) + account_status NurseAccountStatus @default(PENDING) + years_of_experience Int + brief String? + bonusFilePublicId String? @db.VarChar(500) + bonusFileUrl String? @db.VarChar(500) + nationalCardPublicId String? @db.VarChar(500) + nationalCardUrl String? @db.VarChar(500) + announcement_nurses AnnouncementNurse[] @relation("AnnouncementNurseNurse") + user User @relation("UserAsNurse", fields: [id], references: [id], onDelete: Cascade) + nurse_schedules NurseSchedule[] @relation("NurseScheduleNurse") + + @@map("Nurse") +} + +model Appointment { + id String @id @default(uuid()) + patient_id String? + doctor_id String? + scheduled_time DateTime + is_online Boolean @default(false) + is_completed Boolean @default(false) + estimated_time Float? + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + patients_ahead Int @default(0) + position Int @default(0) + cancelled_by String? + clinic_id String? + end_time DateTime + slot_duration Int + status AppointmentStatus @default(CONFIRMED) + clinic Clinic? @relation(fields: [clinic_id], references: [id]) + doctor User? @relation("DoctorAppointments", fields: [doctor_id], references: [id], onDelete: Restrict) + patient User? @relation("PatientAppointments", fields: [patient_id], references: [id], onDelete: Restrict) + medicalRecords MedicalRecord[] + + @@index([patient_id]) + @@index([doctor_id]) + @@index([scheduled_time]) + @@index([clinic_id]) + @@index([status]) + @@map("Appointments") +} + +model Medication { + id String @id @default(uuid()) + patient_id String + doctor_id String + treatment_name String @db.VarChar(255) + category String @db.VarChar(100) + medication_end_date DateTime + medication_start_time DateTime @db.Time(0) + frequency Int + period Period + description String? + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + doctor User @relation("DoctorMedications", fields: [doctor_id], references: [id]) + patient User @relation("PatientMedications", fields: [patient_id], references: [id]) + + @@index([patient_id]) + @@index([doctor_id]) + @@map("Medications") +} + +model ScanLab { + id String @id @default(uuid()) + patient_id String + doctor_id String + name String @db.VarChar(255) + scheduled_date DateTime? + scheduled_time DateTime? @db.Time(0) + frequency Int? + period Period? + description String? + type ScanLabType + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + doctor User @relation("DoctorScansLabs", fields: [doctor_id], references: [id]) + patient User @relation("PatientScansLabs", fields: [patient_id], references: [id]) + + @@index([patient_id]) + @@index([doctor_id]) + @@map("Scans_Labs") +} + +model Clinic { + id String @id @default(uuid()) + is_active Boolean @default(true) + address String @db.VarChar(300) + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + address_maps_link String? @db.VarChar(500) + canPayOnline Boolean @default(false) + phone String @db.VarChar(20) + created_by String @db.VarChar(255) + name String @db.VarChar(255) + opening_at String @db.VarChar(12) + closing_at String @db.VarChar(12) + announcementNurses AnnouncementNurse[] @relation("AnnouncementNurseClinic") + announcements Announcement[] @relation("AnnouncementClinic") + appointments Appointment[] + clinic_doctors ClinicDoctor[] + clinic_nurses ClinicNurse[] + doctorSchedules DoctorSchedule[] + nurseSchedules NurseSchedule[] + + @@map("Clinic") +} + +model ClinicNurse { + id String @id @default(uuid()) + clinic_id String + nurse_id String + clinic Clinic @relation(fields: [clinic_id], references: [id], onDelete: Cascade) + nurse User @relation("NurseClinics", fields: [nurse_id], references: [id], onDelete: Cascade) + + @@unique([clinic_id, nurse_id]) + @@index([nurse_id]) + @@map("ClinicNurse") +} + +model ClinicDoctor { + id String @id @default(uuid()) + clinic_id String + doctor_id String + fees Float + is_accepting Boolean @default(true) + clinic Clinic @relation(fields: [clinic_id], references: [id], onDelete: Cascade) + doctor Doctor @relation(fields: [doctor_id], references: [id], onDelete: Cascade) + + @@unique([clinic_id, doctor_id]) + @@index([doctor_id]) + @@map("ClinicDoctor") +} + +model AuditLog { + id String @id @default(uuid()) + user_id String + action Action + bc_hash String @db.VarChar(255) + created_at DateTime @default(now()) + user User @relation("UserAuditLogs", fields: [user_id], references: [id]) + + @@index([user_id]) + @@index([created_at]) + @@map("AuditLogs") +} + +model MedicalRecord { + id String @id @default(uuid()) + patient_id String + clinic_id String + doctor_id String? + appointment_id String? + name String @db.VarChar(255) + cid String @unique @db.VarChar(255) + mime_type String @db.VarChar(100) + type RecordType + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + + patient User @relation("PatientMedicalRecords", fields: [patient_id], references: [id]) + appointment Appointment? @relation(fields: [appointment_id], references: [id]) + + @@index([patient_id]) + @@index([clinic_id]) + @@index([doctor_id]) + @@index([appointment_id]) + @@index([cid]) + @@map("MedicalRecords") +} + +model EncryptionKey { + id String @id @default(uuid()) + patient_id String @unique + encrypted_key String @db.VarChar(500) + algorithm String @default("AES-256-GCM") @db.VarChar(50) + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + + patient User @relation("PatientEncryptionKey", fields: [patient_id], references: [id], onDelete: Cascade) + + @@map("EncryptionKeys") +} + +model RefreshToken { + id String @id @default(uuid()) + user_id String + token_hash String @db.VarChar(255) + expires_at DateTime + is_revoked Boolean @default(false) + created_at DateTime @default(now()) + revoked_at DateTime? + user User @relation("UserRefreshTokens", fields: [user_id], references: [id], onDelete: Cascade) + + @@index([user_id]) + @@index([token_hash]) + @@index([expires_at]) + @@map("RefreshTokens") +} + +model DoctorSchedule { + id String @id @default(uuid()) + doctor_id String + clinic_id String? + day_of_week DayOfWeek + start_time String + end_time String + slot_duration Int + buffer_time Int @default(0) + is_online Boolean @default(true) + is_active Boolean @default(true) + break_start String? + break_end String? + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + clinic Clinic? @relation(fields: [clinic_id], references: [id], onDelete: Cascade) + doctor Doctor @relation(fields: [doctor_id], references: [id], onDelete: Cascade) + vacations Vacation[] + + @@index([doctor_id]) + @@index([clinic_id]) + @@map("DoctorSchedules") +} + +model NurseSchedule { + id String @id @default(uuid()) + nurse_id String + doctor_id String + clinic_id String? + day_of_week DayOfWeek + start_time String @db.VarChar(12) + end_time String @db.VarChar(12) + is_online Boolean @default(false) + is_active Boolean @default(true) + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + userId String? + clinic Clinic? @relation(fields: [clinic_id], references: [id], onDelete: Cascade) + doctor Doctor @relation("NurseScheduleDoctor", fields: [doctor_id], references: [id], onDelete: Cascade) + nurse Nurse @relation("NurseScheduleNurse", fields: [nurse_id], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) + + @@index([nurse_id]) + @@index([doctor_id]) + @@index([clinic_id]) + @@map("NurseSchedules") +} + +model Vacation { + id String @id @default(uuid()) + doctor_id String + schedule_id String + start_date String + end_date String + status VacationStatus @default(UPCOMING) + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + doctor Doctor @relation(fields: [doctor_id], references: [id], onDelete: Cascade) + schedule DoctorSchedule @relation(fields: [schedule_id], references: [id], onDelete: Cascade) + + @@index([doctor_id]) + @@index([schedule_id]) + @@index([start_date]) + @@index([end_date]) + @@map("Vacations") +} + +model Announcement { + id String @id @default(uuid()) + doctor_id String + clinic_id String + status AnnouncementStatus @default(PENDING) + gender Gender? + max_age Int? + years_of_experience Int? + notes String? + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + deleted_at DateTime? + userId String? + working_days AnnouncementDay[] + announcement_nurses AnnouncementNurse[] @relation("AnnouncementNurses") + clinic Clinic @relation("AnnouncementClinic", fields: [clinic_id], references: [id], onDelete: Cascade) + doctor Doctor @relation("AnnouncementDoctor", fields: [doctor_id], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) + + @@index([doctor_id]) + @@index([clinic_id]) + @@index([status]) + @@map("Announcements") +} + +model AnnouncementDay { + id String @id @default(uuid()) + announcement_id String + day_of_week DayOfWeek + start_time String + end_time String + announcement Announcement @relation(fields: [announcement_id], references: [id], onDelete: Cascade) + + @@index([announcement_id]) + @@map("AnnouncementDays") +} + +model AnnouncementNurse { + id String @id @default(uuid()) + announcement_id String + nurse_id String + status AnnouncementNurseStatus @default(PENDING) + doctor_id String? + clinic_id String? + created_at DateTime @default(now()) + modified_at DateTime @updatedAt + userId String? + deleted_at DateTime? + announcement Announcement @relation("AnnouncementNurses", fields: [announcement_id], references: [id], onDelete: Cascade) + clinic Clinic? @relation("AnnouncementNurseClinic", fields: [clinic_id], references: [id]) + doctor Doctor? @relation("AnnouncementNurseDoctor", fields: [doctor_id], references: [id]) + nurse Nurse @relation("AnnouncementNurseNurse", fields: [nurse_id], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id]) + + @@unique([announcement_id, nurse_id]) + @@index([announcement_id]) + @@index([nurse_id]) + @@index([doctor_id]) + @@index([clinic_id]) + @@map("AnnouncementNurses") +} + +enum VacationStatus { + UPCOMING + CURRENT + ENDED +} + +enum ScanLabType { + SCAN + LAB +} + +enum Action { + CREATE + UPDATE + DELETE + READ + LOGIN + LOGOUT +} + +enum Period { + DAILY + WEEKLY + MONTHLY + YEARLY +} + +enum Gender { + MALE + FEMALE +} + +enum Role { + ADMIN + DOCTOR + NURSE + PATIENT + SUPER_ADMIN +} + +enum RecordType { + LAB_RESULT + SCAN + DIAGNOSIS + VISIT_SUMMARY + SOAP_NOTE + MEDICAL_HISTORY + VISIT + FILE +} + +enum DoctorAccountStatus { + PENDING + APPROVED + REJECTED +} + +enum AvailabilityType { + UNSET + ONLINE + OFFLINE + BOTH +} + +enum DayOfWeek { + SUNDAY + MONDAY + TUESDAY + WEDNESDAY + THURSDAY + FRIDAY + SATURDAY +} + +enum AppointmentStatus { + CONFIRMED + COMPLETED + CANCELLED + NO_SHOW +} + +enum NurseAccountStatus { + PENDING + APPROVED + REJECTED +} + +enum AnnouncementStatus { + PENDING + EXPIRED +} + +enum AnnouncementNurseStatus { + PENDING + APPROVED + REJECTED +} diff --git a/src/routes/admin.route.ts b/src/routes/admin.route.ts new file mode 100644 index 0000000..d55cd4e --- /dev/null +++ b/src/routes/admin.route.ts @@ -0,0 +1,586 @@ +import { Router } from 'express'; +import { AdminController } from '@/controllers/admin.controller'; +import { AddUserFromAdminDto } from '@/dtos/admins.dto'; +import { Routes } from '@/interfaces'; +import { AuthMiddleware, RoleMiddleware } from '@/middlewares/auth.middleware'; +import { LanguageMiddleware } from '@/middlewares/language.middleware'; +import { ValidationMiddleware } from '@/middlewares/validation.middleware'; +import { Role } from '@prisma/client'; + +export class AdminRoute implements Routes { + public path = '/admin'; + public router = Router(); + public adminController = new AdminController(); + + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.post( + '/admin/doctors', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Doctor data', + required: true, + schema: { + $email: 'doctor@example.com', + $name: 'Dr. Smith', + $phone: '1234567890', + $gender: 'MALE or FEMALE', + $date_of_birth: '1990-01-01', + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[201] = { + description: 'Doctor added successfully', + schema: { + data: { email: 'doctor@example.com', name: 'Dr. Smith', role: 'DOCTOR', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, hasCompletedProfile: false, + doctor: { specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null, account_status: 'PENDING' }, photoUrl: null }, + messageEn: "Doctor account created successfully.", + messageAr: ".تم إنشاء حساب الطبيب بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + LanguageMiddleware, + ValidationMiddleware(AddUserFromAdminDto), + this.adminController.addDoctor, + ); + + this.router.post( + '/admin/nurses', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Nurse data', + required: true, + schema: { + $email: 'nurse@example.com', + $name: 'Nurse Jane', + $phone: '1234567890', + $gender: 'MALE or FEMALE', + $date_of_birth: '1995-06-15', + years_of_experience: 3 + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[201] = { + description: 'Nurse added successfully', + schema: { + data: { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + role: 'NURSE', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + isVerified: true, + hasCompletedProfile: false, + photo_url: null, + nurse: { + account_status: 'APPROVED', + years_of_experience: 3, + brief: null, + nationalCardUrl: null, + bonusFileUrl: null + } + }, + messageEn: 'Nurse account created successfully.', + messageAr: '.تم إنشاء حساب الممرض بنجاح' + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + ValidationMiddleware(AddUserFromAdminDto), + this.adminController.addNurse, + ); + + this.router.get( + '/admin/doctors', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Doctors retrieved successfully', + schema: { + data:[ { id: '1' , email: 'doctor@example.com', name: 'Dr. Smith', role: 'DOCTOR', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, hasCompletedProfile: false, + photoUrl: null, + doctor: { + specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null , account_status: 'APPROVED', + mastersCertificateUrl: '', graduationCertificateUrl: '', fellowshipCertificateUrl: '', professionalPracticeCardUrl: '', membershipCardUrl: '', unionSpecializationCertificateUrl: '' + }}] , + messageEn: 'Doctors retrieved successfully', + messageAr: "تم استرجاع بيانات الأطباء بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + LanguageMiddleware, + this.adminController.getAllDoctors, + ); + + this.router.get( + '/admin/nurses', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Nurses retrieved successfully', + schema: { + data: [ + { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + role: 'NURSE', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + isVerified: true, + hasCompletedProfile: true, + photo_url: null, + nurse: { + account_status: 'APPROVED', + years_of_experience: 3, + brief: 'Experienced nurse in ICU', + nationalCardUrl: '', + bonusFileUrl: '' + } + } + ], + messageEn: 'Nurses retrieved successfully', + messageAr: "تم استرجاع بيانات الممرضين بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.getAllNurses, + ); + + this.router.get( + '/admin/doctors/unverified', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Unverified doctors retrieved successfully', + schema: { + data:[ { id: '1' , email: 'doctor@example.com', name: 'Dr. Smith', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, date_of_birth: '1990-01-01', photoUrl: null, + doctor: { + specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null , account_status: 'APPROVED', + mastersCertificateUrl: '', graduationCertificateUrl: '', fellowshipCertificateUrl: '', professionalPracticeCardUrl: '', membershipCardUrl: '', unionSpecializationCertificateUrl: '' + } + } + ], + messageEn: 'Unverified doctors retrieved successfully', + messageAr: "تم استرجاع بيانات الأطباء غير المعتمدين بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + LanguageMiddleware, + this.adminController.getUnverifiedDoctors, + ); + + this.router.get( + '/admin/nurses/unverified', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Unverified nurses retrieved successfully', + schema: { + data: [ + { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + isVerified: false, + hasCompletedProfile: false, + photo_url: null, + nurse: { + account_status: 'PENDING', + years_of_experience: 3, + brief: 'Experienced nurse in ICU', + nationalCardUrl: '', + bonusFileUrl: '' + } + } + ], + messageEn: 'Unverified nurses retrieved successfully', + messageAr: "تم استرجاع بيانات الممرضين غير المعتمدين بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.getUnverifiedNurses, + ); + + + this.router.patch( + '/admin/doctors/verify/:id', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Doctor ID', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Verification status', + required: true, + schema: { + $isApproved: true + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie or Authorization header)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Doctor verification status updated successfully', + schema: { + messageEn: 'Doctor verification status updated successfully', + messageAr: "تم تحديث حالة اعتماد الطبيب بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.updateDoctorVerificationStatus, + ); + + this.router.patch( + '/admin/nurses/verify/:id', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Nurse ID', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Verification status', + required: true, + schema: { + $isVerified: true + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie or Authorization header)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Nurse verification status updated successfully', + schema: { + messageEn: 'Nurse verification status updated successfully', + messageAr: "تم تحديث حالة اعتماد الممرض بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.updateNurseVerificationStatus, + ); + + this.router.get( + '/admin/doctors/:id', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Doctor ID', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Doctor retrieved successfully', + schema: { + data: { email: 'doctor@example.com', name: 'Dr. Smith', role: 'DOCTOR', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, hasCompletedProfile: false, + photoUrl: null , + doctor: { + specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null , account_status: 'APPROVED', + mastersCertificateUrl: '', graduationCertificateUrl: '', fellowshipCertificateUrl: '', professionalPracticeCardUrl: '', membershipCardUrl: '', unionSpecializationCertificateUrl: '' + } }, + messageEn: 'Doctor retrieved successfully', + messageAr: "تم استرجاع بيانات الطبيب بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + LanguageMiddleware, + this.adminController.getDoctorById, + ); + + this.router.get( + '/admin/nurses/:id', + /* + #swagger.tags = ['Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Nurse ID', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Nurse retrieved successfully', + schema: { + data: { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + role: 'NURSE', + isVerified: true, + hasCompletedProfile: true, + photo_url: null, + nurse: { + account_status: 'APPROVED', + years_of_experience: 3, + brief: 'Experienced nurse in ICU', + nationalCardUrl: '', + bonusFileUrl: '' + } + }, + messageEn: 'Nurse retrieved successfully', + messageAr: "تم استرجاع بيانات الممرض بنجاح." + } + } + #swagger.responses[404] = { + description: 'Nurse not found', + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.getNurseById, + ); + + // Clinic routes + this.router.get( + `${this.path}/clinics`, + /* + #swagger.path = '/admin/clinics' + #swagger.method = 'get' + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Get clinics successful', + schema: { + data: [ + { + id: 'clinic-uuid', + name: 'Clinic Name', + is_active: false, + opening_at: '08:00', + closing_at: '16:00', + address: '123 Main St, City, Country', + address_maps_link: 'https://maps.google.com/?q=123+Main+St,+City,+Country', + phone: '1234567890', + canPayOnline: true + } + ], + messageEn: 'Clinics retrieved successfully', + messageAr: "تم استرجاع بيانات العيادات بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.getAllClinics + ); + this.router.get( + `${this.path}/clinics/:id`, + /* + #swagger.path = '/admin/clinics/{id}' + #swagger.method = 'get' + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['id'] = { + in: 'path', + description: 'The unique identifier of the clinic to retrieve', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Clinic retrieved successfully', + schema: { + data: { + id: 'clinic-uuid', + name: 'Clinic Name', + is_active: false, + opening_at: '08:00', + closing_at: '16:00', + address: '123 Main St, City, Country', + address_maps_link: 'https://maps.google.com/?q=123+Main+St,+City,+Country', + phone: '1234567890', + canPayOnline: true + }, + messageEn: 'Clinic retrieved successfully', + messageAr: "تم استرجاع بيانات العيادة بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.getClinicById + ); + this.router.patch( + `${this.path}/clinics/:id/set-active-status`, + /* + #swagger.path = '/admin/clinics/{id}/set-active-status' + #swagger.method = 'patch' + #swagger.tags = ['Admin'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['id'] = { + in: 'path', + description: 'The unique identifier of the clinic to set active status', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'set active status', + required: true, + schema: { + is_active: true + } + } + #swagger.responses[200] = { + description: 'Clinic active status toggled successfully', + schema: { + data: { + id: 'clinic-uuid', + name: 'Clinic Name', + is_active: true + }, + messageEn: 'Clinic active status toggled successfully', + messageAr: "تم تبديل حالة العيادة بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.ADMIN), + this.adminController.setClinicActiveStatus + ); + } +} \ No newline at end of file diff --git a/src/routes/ai_appointments.route.ts b/src/routes/ai_appointments.route.ts new file mode 100644 index 0000000..491a117 --- /dev/null +++ b/src/routes/ai_appointments.route.ts @@ -0,0 +1,124 @@ +import { AiAppointmentsController } from "@/controllers/ai_appointments.controller"; +import { Routes } from "@/interfaces"; +import { AuthMiddleware } from "@/middlewares/auth.middleware"; +import { Router } from "express"; + +export class AiAppointmentsRoute implements Routes { + public path: string = "" + public router: Router = Router() + private aiAppointmentsController = new AiAppointmentsController(); + + constructor() { + this.initializeRoutes() + }; + + private initializeRoutes(): void { + this.router.get(`${this.path}/:appointmentId/upload-url`, + /* + #swagger.path = '/{appointmentId}/upload-url' + #swagger.method = 'get' + #swagger.tags = ['AI Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.description = 'Get a pre-signed upload URL for uploading audio files to S3' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'The ID of the appointment', + required: true, + type: 'string' + } + #swagger.parameters['userType'] = { + in: 'query', + description: 'Type of user recording the audio (DOCTOR, PATIENT, or MIXED)', + required: true, + type: 'string', + enum: ['DOCTOR', 'PATIENT', 'MIXED'] + } + #swagger.responses[200] = { + description: 'Upload URL generated successfully', + schema: { + message: 'Upload URL generated successfully', + messageAr: 'تم إنشاء رابط التحميل بنجاح', + data: { + uploadUrl: 'string', + objectKey: 'string' + } + } + } + #swagger.responses[400] = { + description: 'Bad request - invalid userType parameter' + } + #swagger.responses[401] = { + description: 'Unauthorized - missing or invalid token' + } + #swagger.responses[404] = { + description: 'Appointment not found' + } + */ + AuthMiddleware, + this.aiAppointmentsController.getUploadUrl + ) + + this.router.post(`${this.path}/:appointmentId/process-audio-ai`, + /* + #swagger.path = '/{appointmentId}/process-audio-ai' + #swagger.method = 'post' + #swagger.tags = ['AI Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.description = 'Process audio recordings using AI to generate SOAP notes. Accepts either separate doctor/patient audio keys or a single mixed audio key' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'The ID of the appointment', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Audio file keys for processing. Provide either (doctorKey AND patientKey) OR mixedKey', + required: true, + schema: { + $doctorKey: 'appointments/appointmentId/DOCTOR.webm', + $patientKey: 'appointments/appointmentId/PATIENT.webm', + $mixedKey: 'appointments/appointmentId/MIXED.webm', + $prompt: 'string' + } + } + #swagger.responses[202] = { + description: 'SOAP notes generated successfully', + schema: { + message: 'SOAP generated successfully', + messageAr: 'تم إنشاء ملاحظات SOAP بنجاح', + data: { + SOAP: { + subjective: 'string', + objective: 'string', + assessment: 'string', + plan: 'string' + } + } + } + } + #swagger.responses[400] = { + description: 'Bad request - missing required audio keys' + } + #swagger.responses[401] = { + description: 'Unauthorized - missing or invalid token' + } + #swagger.responses[404] = { + description: 'Appointment not found' + } + */ + AuthMiddleware, + this.aiAppointmentsController.processAudioAI + ) + } +} \ No newline at end of file diff --git a/src/routes/appointment.route.ts b/src/routes/appointment.route.ts new file mode 100644 index 0000000..d070ddc --- /dev/null +++ b/src/routes/appointment.route.ts @@ -0,0 +1,1348 @@ +import { Routes } from "@/interfaces"; +import { Router } from "express"; +import { ClinicController } from "@/controllers/clinic.controller"; +import { DoctorController } from "@/controllers/doctor.controller"; +import { AppointmentController } from "@/controllers/appointment.controller"; +import { ValidationMiddleware } from "@/middlewares/validation.middleware"; +import { AuthMiddleware, RoleMiddleware } from "@/middlewares/auth.middleware"; +import { BookAppointmentDto, RescheduleAppointmentDto, RescheduleAppointmentByDoctorDto, EnterDoctorScheduleDto, EditDoctorScheduleDto, HandleDoctorVacationDto } from "@/dtos/appointments.dto"; +import { AiAppointmentsRoute } from "./ai_appointments.route"; +import { Role } from "@prisma/client"; + +export class AppointmentRoute implements Routes { + public path = '/appointments'; + public router = Router(); + clinicController = new ClinicController(); + doctorController = new DoctorController(); + appointmentController = new AppointmentController(); + private _aiRouter = new AiAppointmentsRoute(); + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.use(`${this.path}/ai`, this._aiRouter.router); + + this.router.get( + `${this.path}/doctors`, + /* + #swagger.path = '/appointments/doctors' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.description = 'Get all doctors available for booking appointments' + #swagger.parameters['lang'] = { + in: 'query', + description: 'Required language for specialization', + required: true, + type: 'string' + } + #swagger.parameters['gender'] = { + in: 'query', + description: 'Filter doctors by gender (MALE or FEMALE)', + required: false, + type: 'string' + } + #swagger.parameters['minFees'] = { + in: 'query', + description: 'Minimum fees filter', + required: false, + type: 'number' + } + #swagger.parameters['maxFees'] = { + in: 'query', + description: 'Maximum fees filter', + required: false, + type: 'number' + } + #swagger.parameters['isOnline'] = { + in: 'query', + description: 'Filter for online availability (true for online, false for offline)', + required: false, + type: 'boolean' + } + #swagger.responses[200] = { + description: 'Doctors retrieved successfully', + schema: { + data: [ + { + id: 'doctor-uuid', + name: 'John Doe', + gender: 'MALE', + age: 45, + specialization: 'IMMUNOLOGY', + phone: '+1234567890', + fees: 200, + is_online: true, + profilePic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + clinics: [ + { + id: 'clinic-uuid', + name: 'New Cairo Medical Clinic', + phone: '+1234567890', + canPayOnline: true, + opening_at: '09:00', + closing_at: '17:00', + address: '123 Main Street, Medical Park', + address_maps_link: 'https://maps.google.com/?q=123+Main+Street' + } + ] + } + ], + messageEn: 'Doctors retrieved successfully', + messageAr: 'تم استرجاع الأطباء بنجاح' + } + } + #swagger.responses[400] = { + description: 'Bad request' + } + */ + this.doctorController.getDoctors + ); + + // get all clinics + this.router.get( + `${this.path}/clinics`, + /* + #swagger.path = '/appointments/clinics' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.description = 'Get all active clinics available for booking appointments' + #swagger.parameters['lang'] = { + in: 'query', + description: 'Required language for specialization', + required: true, + type: 'string' + } + #swagger.parameters['canPayOnline'] = { + in: 'query', + description: 'Filter clinics that support online payment (true) or not (false)', + required: false, + type: 'boolean' + } + #swagger.responses[200] = { + description: 'Clinics retrieved successfully', + schema: { + data: [ + { + id: 'clinic-uuid', + name: 'New Cairo Medical Clinic', + phone: '+1234567890', + canPayOnline: true, + opening_at: '09:00', + closing_at: '17:00', + address: '123 Main Street, Medical Park', + address_maps_link: 'https://maps.google.com/?q=123+Main+Street', + doctors: [ + { + id: 'doctor-uuid', + name: 'John Doe', + gender: 'MALE', + age: 45, + specialization: 'IMMUNOLOGY', + phone: '+1234567890', + fees: 200, + is_online: true, + profilePic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg' + }, + { + id: 'doctor-uuid2', + name: 'House', + gender: 'MALE', + age: 45, + phone: '+1234567890', + fees: 200, + profilePic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg' + } + ] + } + ], + messageEn: 'Clinics retrieved successfully', + messageAr: 'تم استرجاع العيادات بنجاح' + } + } + #swagger.responses[400] = { + description: 'Bad request' + } + */ + this.clinicController.getActiveClinics + ); + + // get all doctors in a clinic + this.router.get( + `${this.path}/clinic/:clinicId/doctors`, + /* + #swagger.path = '/appointments/clinic/{clinicId}/doctors' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.description = 'Get all doctors in a specific clinic' + #swagger.parameters['clinicId'] = { + in: 'path', + description: 'Clinic ID', + required: true, + type: 'string' + } + #swagger.parameters['gender'] = { + in: 'query', + description: 'Filter doctors by gender (MALE or FEMALE)', + required: false, + type: 'string' + } + #swagger.parameters['minFees'] = { + in: 'query', + description: 'Minimum fees filter', + required: false, + type: 'number' + } + #swagger.parameters['maxFees'] = { + in: 'query', + description: 'Maximum fees filter', + required: false, + type: 'number' + } + #swagger.responses[200] = { + description: 'Clinic doctors retrieved successfully', + schema: { + data: [ + { + id: 'doctor-uuid', + name: 'John Doe', + gender: 'MALE', + age: 45, + specialization: 'IMMUNOLOGY', + phone: '+1234567890', + fees: 200, + profilePic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + } + ], + messageEn: 'Clinic doctors retrieved successfully', + messageAr: 'تم استرجاع أطباء العيادة بنجاح' + } + } + #swagger.responses[400] = { + description: 'Bad request' + } + */ + this.clinicController.getClinicDoctors + ); + + // get available days + this.router.get( + `${this.path}/doctor/:doctorId/available-days`, + /* + #swagger.path = '/appointments/doctor/{doctorId}/available-days' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.description = 'Get available days for booking with a specific doctor (up to 30 days ahead)' + #swagger.parameters['doctorId'] = { + in: 'path', + description: 'Doctor ID', + required: true, + type: 'string' + } + #swagger.parameters['clinicId'] = { + in: 'query', + description: 'Clinic ID (required for offline appointments)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Available days retrieved successfully', + schema: { + data: [ + { + date: '2026-02-03', + dayOfWeek: 'MONDAY', + displayDate: 'Monday, February 3, 2026' + } + ], + message: 'Available days retrieved successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - missing doctor ID or invalid parameters' + } + */ + this.appointmentController.getAvailableDays + ); + + // get all available slots + this.router.get( + `${this.path}/doctor/:doctorId/available-slots`, + /* + #swagger.path = '/appointments/doctor/{doctorId}/available-slots' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.description = 'Get available time slots for a specific doctor on a given date' + #swagger.parameters['doctorId'] = { + in: 'path', + description: 'Doctor ID', + required: true, + type: 'string' + } + #swagger.parameters['date'] = { + in: 'query', + description: 'Date in YYYY-MM-DD format', + required: true, + type: 'string' + } + #swagger.parameters['clinicId'] = { + in: 'query', + description: 'Clinic ID (required for offline appointments)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Available slots retrieved successfully', + schema: { + data: [ + { + start: '09:00', + end: '09:20', + available: true, + online: true + }, + { + start: '09:30', + end: '09:50', + available: false, + online: true + } + ], + message: 'Available slots retrieved successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - missing date, invalid format, or past date' + } + */ + this.appointmentController.getAvailableSlots + ); + + // book appointment + this.router.post( + `${this.path}/book`, + /* + #swagger.path = '/appointments/book' + #swagger.method = 'post' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.description = 'Book a new appointment with a doctor' + #swagger.security = [{ + bearerAuth: [] + }] + #swagger.parameters['body'] = { + in: 'body', + description: 'Appointment booking details', + required: true, + schema: { + doctorId: 'doctor-uuid', + clinicId: 'clinic-uuid', + scheduledTime: '2026-02-03T09:00:00.000Z' + } + } + #swagger.responses[201] = { + description: 'Appointment booked successfully', + schema: { + message: 'Appointment booked successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - invalid data or slot not available', + schema: { + message: 'Error message describing the issue' + } + } + #swagger.responses[401] = { + description: 'Unauthorized - user not authenticated' + } + */ + AuthMiddleware, + ValidationMiddleware(BookAppointmentDto), + this.appointmentController.bookAppointment + ); + + this.router.get( + `${this.path}/patient/appointments`, + /* + #swagger.path = '/appointments/patient/appointments' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a patient)', + required: true, + type: 'string' + } + #swagger.description = 'Get all appointments for the patient' + #swagger.responses[200] = { + description: 'Patient appointments retrieved successfully', + schema: { + data: [ + { + id: 'appointment-uuid', + doctorId: 'doctor-uuid', + clinicId: 'clinic-uuid', + status: 'CONFIRMED', + slot_duration: 30, + doctor_name: 'Dr. House', + doctor_profile_pic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + appointment_date: '2026-02-03', + start_time: '09:00', + end_time: '09:30', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park', + address_maps_link: 'https://maps.app.goo.gl/iKB7dwMcneuUaULYA', + }, + { + id: 'appointment-uuid', + doctorId: 'doctor-uuid', + clinicId: null, + status: 'CONFIRMED', + slot_duration: 20, + doctor_name: 'Dr. House', + doctor_profile_pic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + appointment_date: '2026-03-03', + start_time: '09:00', + end_time: '09:20', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park', + address_maps_link: 'https://maps.app.goo.gl/iKB7dwMcneuUaULYA', + } + ] + } + } + #swagger.responses[400] = { + description: 'Bad request - patient ID missing' + } + #swagger.responses[401] = { + description: 'Unauthorized - patient not authenticated' + } + */ + AuthMiddleware, + this.appointmentController.getPatientAppointments + ); + + this.router.get( + `${this.path}/patient/today-appointment`, + /* + #swagger.path = '/appointments/patient/today-appointment' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.summary = 'Get all appointments for the patient today' + #swagger.description = 'Returns all appointments scheduled for today for the patient' + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (patient role required)', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Today\'s appointments retrieved successfully', + schema: { + success: true, + data: [ + { + id: 'appointment-uuid-1', + doctorId: 'doctor-uuid', + clinicId: 'clinic-uuid', + status: 'CONFIRMED', + is_online: true, + slot_duration: 30, + doctor_name: 'Dr. House', + doctor_profile_pic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + appointment_date: '2026-02-05', + start_time: '09:00', + end_time: '09:30', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park', + address_maps_link: 'https://maps.app.goo.gl/iKB7dwMcneuUaULYA', + position: 5, + estimatedWaitMinutes: 60, + patientsAhead: 3 + }, + { + id: 'appointment-uuid-2', + doctorId: 'doctor-uuid', + clinicId: 'clinic-uuid', + status: 'CONFIRMED', + is_online: false, + slot_duration: 20, + doctor_name: 'Dr. Wilson', + doctor_profile_pic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + appointment_date: '2026-02-05', + start_time: '14:30', + end_time: '14:50', + clinic_name: 'Downtown Clinic', + clinic_address: '456 Nile Corniche', + address_maps_link: 'https://maps.app.goo.gl/iKB7dwMcneuUaULYA', + position: null, + estimatedWaitMinutes: null, + patientsAhead: null + } + ] + } + } + #swagger.responses[400] = { + description: 'Bad request (invalid authentication or missing required fields)', + } + #swagger.responses[401] = { + description: 'Unauthorized - missing or invalid authentication token', + } + #swagger.responses[403] = { + description: 'Forbidden - user is not authorized as a patient', + } + */ + AuthMiddleware, + this.appointmentController.getTodayAppointment + ); + + this.router.get( + `${this.path}/patient/:appointmentId`, + /* + #swagger.path = '/appointments/patient/{appointmentId}' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a patient)', + required: true, + type: 'string' + } + #swagger.description = 'Get details of a specific appointment for the patient' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'Appointment ID', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Appointment details retrieved successfully', + schema: { + data: { + id: 'appointment-uuid', + doctorId: 'doctor-uuid', + clinicId: 'clinic-uuid', + status: 'CONFIRMED', + slot_duration: 30, + doctor_name: 'Dr. House', + doctor_profile_pic: 'https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg', + appointment_date: '2026-02-03', + start_time: '09:00', + end_time: '09:30', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park', + address_maps_link: 'https://maps.app.goo.gl/iKB7dwMcneuUaULYA', + } + } + } + #swagger.responses[400] = { + description: 'Bad request - patient ID missing' + } + #swagger.responses[401] = { + description: 'Unauthorized - patient not authenticated' + } + #swagger.responses[404] = { + description: 'Appointment not found or does not belong to the patient' + } + */ + AuthMiddleware, + this.appointmentController.getPatientSelectedAppointment + ); + + this.router.patch( + `${this.path}/patient/:appointmentId/reschedule`, + /* + #swagger.path = '/appointments/patient/{appointmentId}/reschedule' + #swagger.method = 'patch' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a patient)', + required: true, + type: 'string' + } + #swagger.description = 'Reschedule an appointment to a new time by the patient' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'Appointment ID to reschedule', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'New scheduled time for the appointment', + required: true, + schema: { + newScheduledTime: '2026-02-10T10:30:00.000Z' + } + } + #swagger.responses[200] = { + description: 'Appointment rescheduled successfully', + schema: { + message: 'Appointment rescheduled successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - missing or invalid parameters (patient ID, new scheduled time, etc.)' + } + #swagger.responses[401] = { + description: 'Unauthorized - patient not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - appointment does not belong to the authenticated patient' + } + #swagger.responses[404] = { + description: 'Appointment not found or time slot not available' + } + */ + AuthMiddleware, + ValidationMiddleware(RescheduleAppointmentDto), + this.appointmentController.rescheduleAppointmentByPatient + ); + + this.router.delete( + `${this.path}/:appointmentId/cancel`, + /* + #swagger.path = '/appointments/{appointmentId}/cancel' + #swagger.method = 'delete' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.description = 'Cancel an appointment (soft delete). Can be cancelled by either patient or doctor.' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'Appointment ID to cancel', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Appointment cancelled successfully', + schema: { + success: true, + message: 'Appointment cancelled successfully', + data: { + appointmentId: 'appointment-uuid', + cancelledAt: '2026-01-29T12:00:00.000Z' + } + } + } + #swagger.responses[400] = { + description: 'Bad request - appointment already cancelled, completed, or too late to cancel', + schema: { + message: 'Error message describing the issue' + } + } + #swagger.responses[401] = { + description: 'Unauthorized - user not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - user is not the patient or doctor of this appointment' + } + #swagger.responses[404] = { + description: 'Appointment not found' + } + */ + AuthMiddleware, + this.appointmentController.cancelAppointment + ); + + this.router.patch( + `${this.path}/doctor/:appointmentId/reschedule`, + /* + #swagger.path = '/appointments/doctor/{appointmentId}/reschedule' + #swagger.method = 'patch' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Reschedule an appointment by adding minutes (delay) as a doctor' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'Appointment ID to reschedule', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Minutes to add (max 60)', + required: true, + schema: { + minutes: 30 + } + } + #swagger.responses[200] = { + description: 'Appointment rescheduled successfully', + schema: { + message: 'Appointment rescheduled successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - missing minutes, exceeds limit, or invalid parameters' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - appointment does not belong to the doctor' + } + #swagger.responses[404] = { + description: 'Appointment not found' + } + */ + AuthMiddleware, + ValidationMiddleware(RescheduleAppointmentByDoctorDto), + this.appointmentController.rescheduleAppointmentByDoctor + ); + + this.router.get( + `${this.path}/doctor/upcomming-schedule`, + /* + #swagger.path = '/appointments/doctor/upcomming-schedule' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Get the doctor's complete schedule with all appointments grouped by date' + #swagger.responses[200] = { + description: 'Doctor schedule retrieved successfully', + schema: { + data: [ + { + date: '2026-02-03', + displayDate: 'Monday, February 3, 2026', + appointments: [ + { + id: 'appointment-uuid-1', + status: 'CONFIRMED', + slot_duration: 30, + patient_name: 'John Doe', + appointment_date: '2026-02-03', + start_time: '09:00', + end_time: '09:30', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park' + }, + { + id: 'appointment-uuid-2', + status: 'CONFIRMED', + slot_duration: 30, + patient_name: 'Jane Smith', + appointment_date: '2026-02-03', + start_time: '10:00', + end_time: '10:30', + clinic_name: null, + clinic_address: null + } + ] + }, + { + date: '2026-02-05', + displayDate: 'Wednesday, February 5, 2026', + appointments: [ + { + id: 'appointment-uuid-3', + status: 'CONFIRMED', + slot_duration: 45, + patient_name: 'Bob Johnson', + appointment_date: '2026-02-05', + start_time: '14:00', + end_time: '14:45', + clinic_name: 'Downtown Health Center', + clinic_address: '456 Oak Avenue' + } + ] + } + ] + } + } + #swagger.responses[400] = { + description: 'Bad request - doctor ID missing or invalid' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + */ + AuthMiddleware, + this.appointmentController.getUpcommingDoctorSchedule + ); + + this.router.post( + `${this.path}/doctor/schedule`, + /* + #swagger.path = '/appointments/doctor/schedule' + #swagger.method = 'post' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Enter or update a doctor's schedule for a specific day' + #swagger.parameters['body'] = { + in: 'body', + description: 'Schedule details', + required: true, + schema: { + clinicId: 'clinic-uuid (optional)', + workingDay: 1, + startTime: '09:00', + endTime: '17:00', + slotDuration: 30, + bufferTime: 5, + isOnline: true + } + } + #swagger.responses[201] = { + description: 'Schedule created successfully', + schema: { + message: 'Schedule created successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - invalid parameters or doctor ID missing' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + */ + AuthMiddleware, + ValidationMiddleware(EnterDoctorScheduleDto), + this.appointmentController.enterDoctorSchedule + ); + + this.router.get( + `${this.path}/doctor/schedule`, + /* + #swagger.path = '/appointments/doctor/schedule' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Get the doctor\'s own schedule' + #swagger.responses[200] = { + description: 'Doctor schedule retrieved successfully', + schema: { + data: [ + { + id: 'schedule-uuid', + clinicId: 'clinic-uuid (optional)', + dayOfWeek: 'MONDAY', + startTime: '09:00', + endTime: '17:00', + slotDuration: 30, + bufferTime: 5, + isOnline: true, + isActive: true, + breakStart: null, + breakEnd: null + } + ], + message: 'Doctor schedule retrieved successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - doctor ID missing' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + */ + AuthMiddleware, + this.appointmentController.getDoctorSchedule + ); + + this.router.patch( + `${this.path}/doctor/schedule`, + /* + #swagger.path = '/appointments/doctor/schedule' + #swagger.method = 'patch' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Edit a specific entry in the doctor\'s schedule' + #swagger.parameters['body'] = { + in: 'body', + description: 'Schedule edit details (all fields optional except scheduleId)', + required: true, + schema: { + scheduleId: 'schedule-uuid', + clinicId: 'clinic-uuid (optional)', + workingDay: 1, + startTime: '09:00', + endTime: '17:00', + slotDuration: 30, + bufferTime: 5, + isOnline: true, + isActive: false, + breakStart: '2026-02-01', + breakEnd: '2026-02-22' + } + } + #swagger.responses[200] = { + description: 'Schedule updated successfully', + schema: { + message: 'Schedule updated successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - invalid parameters or conflict' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - schedule does not belong to the doctor' + } + #swagger.responses[404] = { + description: 'Schedule not found' + } + */ + AuthMiddleware, + ValidationMiddleware(EditDoctorScheduleDto), + this.appointmentController.editDoctorSchedule + ); + + this.router.get( + `${this.path}/doctor/current-schedule`, + /* + #swagger.path = '/appointments/doctor/current-schedule' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Get all confirmed appointments for doctor today' + #swagger.responses[200] = { + description: 'Today\'s appointments retrieved successfully', + schema: { + data: [ + { + id: 'appointment-uuid', + status: 'CONFIRMED', + slot_duration: 30, + patient_name: 'John Doe', + appointment_date: '2026-02-03', + start_time: '09:00', + end_time: '09:30', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park' + }, + { + id: 'appointment-uuid-2', + status: 'CONFIRMED', + slot_duration: 20, + patient_name: 'Jane Smith', + appointment_date: '2026-02-03', + start_time: '10:15', + end_time: '10:35', + clinic_name: null, + clinic_address: null + } + ], + message: { + en: "Doctor's schedule retrieved successfully", + ar: "تم استرجاع جدول الطبيب بنجاح" + } + } + } + #swagger.responses[401] = { + description: 'Unauthorized - invalid or missing token' + } + #swagger.responses[400] = { + description: 'Bad request' + } + */ + AuthMiddleware, + this.appointmentController.getCurrentDoctorSchedule + ); + + this.router.get( + `${this.path}/doctor/:appointmentId/context`, + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + this.appointmentController.getDoctorAppointmentContext, + ); + + this.router.get( + `${this.path}/doctor/daily-schedule`, + /* + #swagger.path = '/appointments/doctor/daily-schedule' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Get all doctor appointments for a specific date' + #swagger.parameters['date'] = { + in: 'query', + description: 'Date in YYYY-MM-DD format', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Daily schedule retrieved successfully', + schema: { + data: [ + { + id: 'appointment-uuid', + status: 'CONFIRMED', + slot_duration: 30, + patient_name: 'John Sink', + appointment_date: '2026-02-05', + start_time: '09:00', + end_time: '09:30', + clinic_name: 'New Cairo Medical Clinic', + clinic_address: '123 Main Street, Medical Park' + }, + { + id: 'appointment-uuid-2', + status: 'COMPLETED', + slot_duration: 20, + patient_name: 'Jane Hopper', + appointment_date: '2026-02-05', + start_time: '10:15', + end_time: '10:35', + clinic_name: null, + clinic_address: null + } + ], + message: { + en: "Doctor's schedule retrieved successfully", + ar: "تم استرجاع جدول الطبيب بنجاح" + } + } + } + #swagger.responses[400] = { + description: 'Bad request - missing or invalid date parameter, or invalid date format' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + */ + AuthMiddleware, + this.appointmentController.getScheduleByDate + ); + + this.router.get( + `${this.path}/doctor/schedule/check-appointments`, + /* + #swagger.path = '/appointments/doctor/schedule/check-appointments' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Check for existing confirmed appointments in a doctor schedule' + #swagger.parameters['scheduleId'] = { + in: 'query', + description: 'Schedule ID to check', + required: true, + type: 'string' + } + #swagger.parameters['startDate'] = { + in: 'query', + description: 'Optional for vacation. format: YYYY-MM-DD', + required: false, + type: 'string' + } + #swagger.parameters['endDate'] = { + in: 'query', + description: 'Optional for vacation. format: YYYY-MM-DD', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Check completed successfully', + schema: { + data: { + existing: true, + numOfAppointments: 3 + }, + message: 'Check completed successfully' + } + } + #swagger.responses[400] = { + description: 'Bad request - missing/invalid parameters' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - schedule does not belong to the doctor' + } + #swagger.responses[404] = { + description: 'Schedule not found' + } + */ + AuthMiddleware, + this.appointmentController.checkConflictingAppointments + ); + + this.router.patch( + `${this.path}/doctor/vacation`, + /* + #swagger.path = '/appointments/doctor/vacation' + #swagger.method = 'patch' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Set vacation period for a specific doctor schedule. This will automatically cancel any existing confirmed appointments in the period (use vacation-check first to warn the doctor)' + #swagger.parameters['body'] = { + in: 'body', + description: 'Vacation details', + required: true, + schema: { + scheduleId: 'schedule-uuid', + startDate: '2026-03-01', + endDate: '2026-03-15' + } + } + #swagger.responses[200] = { + description: 'Vacation set successfully (any conflicting appointments cancelled)' + } + #swagger.responses[400] = { + description: 'Bad request - invalid dates or date range' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - schedule does not belong to the doctor' + } + #swagger.responses[404] = { + description: 'Schedule not found' + } + */ + AuthMiddleware, + ValidationMiddleware(HandleDoctorVacationDto), + this.appointmentController.handleDoctorVacation + ); + + this.router.delete( + `${this.path}/doctor/schedule/delete`, + /* + #swagger.path = '/appointments/doctor/schedule/delete' + #swagger.method = 'delete' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Delete a doctor\'s schedule. If there are any appointments linked to this schedule, they will be automatically cancelled' + #swagger.parameters['body'] = { + in: 'body', + description: 'Schedule deletion payload', + required: true, + schema: { + scheduleId: 'schedule-uuid' + } + } + #swagger.responses[200] = { + description: 'Schedule successfully deleted (any associated confirmed appointments were cancelled)', + schema: { + success: true, + message: { + en: "Schedule deleted successfully", + ar: "تم حذف الجدول بنجاح" + } + } + } + #swagger.responses[400] = { + description: 'Bad request - missing scheduleId in body or invalid request' + } + #swagger.responses[401] = { + description: 'Unauthorized - missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden - schedule does not belong to the authenticated doctor' + } + #swagger.responses[404] = { + description: 'Schedule not found' + } + */ + AuthMiddleware, + this.appointmentController.deleteDoctorSchedule + ); + + this.router.get( + `${this.path}/doctor/vacation`, + /* + #swagger.path = '/appointments/doctor/vacation' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Get all vacation periods for the doctor, grouped by schedule with details including affected appointments' + #swagger.responses[200] = { + description: 'Doctor vacations retrieved successfully', + schema: { + data: [ + { + breakStart: '2026-03-01', + breakEnd: '2026-03-15', + vacations: [ + { + vacationId: 'vacation-uuid', + scheduleId: 'schedule-uuid', + clinicId: 'clinic-uuid', + clinicName: 'New Cairo Medical Clinic', + clinicAddress: '123 Main Street, Medical Park', + dayOfWeek: 'MONDAY', + isOnline: false, + status: 'ACTIVE', + cancelledAppointments: 5 + }, + { + vacationId: 'vacation-uuid-2', + scheduleId: 'schedule-uuid-2', + clinicId: null, + clinicName: null, + clinicAddress: null, + dayOfWeek: 'WEDNESDAY', + isOnline: true, + status: 'ACTIVE', + cancelledAppointments: 2 + } + ] + } + ], + message: { + en: "Doctor's vacations retrieved successfully", + ar: "تم استرجاع إجازات الطبيب بنجاح" + } + } + } + #swagger.responses[400] = { + description: 'Bad request - doctor ID missing or invalid' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + */ + AuthMiddleware, + this.appointmentController.getDoctorVacations + ); + + this.router.patch( + `${this.path}/doctor/vacation/cancel`, + /* + #swagger.path = '/appointments/doctor/vacation/cancel' + #swagger.method = 'patch' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a doctor)', + required: true, + type: 'string' + } + #swagger.description = 'Cancel a specific vacation period for a doctor schedule' + #swagger.parameters['body'] = { + in: 'body', + description: 'Vacation cancellation details', + required: true, + schema: { + vacationId: 'vacation-uuid', + scheduleId: 'schedule-uuid' + } + } + #swagger.responses[200] = { + description: 'Vacation removed successfully', + } + #swagger.responses[400] = { + description: 'Bad request - missing vacationId or scheduleId, or invalid parameters' + } + #swagger.responses[401] = { + description: 'Unauthorized - doctor not authenticated' + } + #swagger.responses[403] = { + description: 'Forbidden - vacation or schedule does not belong to the doctor' + } + #swagger.responses[404] = { + description: 'Vacation or schedule not found' + } + */ + AuthMiddleware, + this.appointmentController.cancelDoctorVacation + ); + + this.router.get( + `${this.path}/:appointmentId/agora-token`, + /* + #swagger.path = '/appointments/{appointmentId}/agora-token' + #swagger.method = 'get' + #swagger.tags = ['Appointments'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (patient or doctor of the appointment)', + required: true, + type: 'string' + } + #swagger.description = 'Get Agora token and channel name for a specific appointment' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'The ID of the appointment', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Agora token and channel name retrieved successfully', + schema: { + message: 'Agora token retrieved successfully', + messageAr: 'تم استرجاع توكن أجورا بنجاح', + data: { + token: 'string', + appId: 'string' + }, + message: 'Agora token retrieved successfully' + } + } + */ + AuthMiddleware, + this.appointmentController.getAgoraToken + ); + } +} diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts new file mode 100644 index 0000000..c67618a --- /dev/null +++ b/src/routes/auth.route.ts @@ -0,0 +1,410 @@ +import { Router } from 'express'; +import { AuthController } from '@controllers/auth.controller'; +import { ChangePasswordDto, CompleteUserProfileDto, CreateUserDto, LoginUserDto, PasswordCheckDto, ResetPasswordDto } from '@dtos/users.dto'; +import { Routes } from '@interfaces/routes.interface'; +import { AuthMiddleware } from '@middlewares/auth.middleware'; +import { GoogleAuthController } from '@/controllers/googleAuth.controller'; +import { ValidationMiddleware } from '@middlewares/validation.middleware'; +import { UpdateGoogleUserPhoneDto } from '@/dtos/googleUsers.dto'; +import { errorWrapper } from '@/utils/errorWrapper'; + + +export class AuthRoute implements Routes { + public path = '/auth'; + public router = Router(); + public auth = new AuthController(); + public googleAuth = new GoogleAuthController(); + + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.post( + `/auth/signup`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'User signup data', + required: true, + schema: { + $email: 'user@example.com', + $name: 'John Doe', + $phone: '1234567890', + $password: 'password123', + $rememberMe: false + } + } + #swagger.responses[201] = { + description: 'User successfully created', + schema: { + data: { + id: 1, + email: 'user@example.com', + name: 'John Doe', + phone: '1234567890', + isEmailVerified: false, + hasCompletedProfile: false, + gender: null, + date_of_birth: null, + role: 'PATIENT', + photoUrl: null + }, + messageEn: 'Signed Up Successfully', + messageAr: "تم انشاء الحساب بنجاح" + } + } + */ + ValidationMiddleware(CreateUserDto), + this.auth.signUp, + ); + + this.router.post( + `/auth/login`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'User login data', + required: true, + schema: { + $emailOrUsername: 'user@example.com', + $password: 'password123', + rememberMe: false + } + } + #swagger.responses[200] = { + description: 'Login successful', + schema: { + data: { id: 1, email: 'user@example.com', name: 'John Doe', role: 'PATIENT', photo_url: 'https://example.com/photo.jpg', doctor: { specialization: 'Cardiology', account_status: 'APPROVED' } }, + messageEn: 'Logged In Successfully', + messageAr: "تم تسجيل الدخول بنجاح" + } + } + */ + ValidationMiddleware(LoginUserDto), + this.auth.logIn, + ); + + this.router.post( + `/auth/logout`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Logout successful', + schema: { messageEn: 'Logged Out Successfully', messageAr: "تم تسجيل الخروج بنجاح" } + } + */ + AuthMiddleware, + this.auth.logOut, + ); + + this.router.post( + `/auth/refresh`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['RefreshToken'] = { + in: 'header', + description: 'Refresh token (sent via RefreshToken cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Token refreshed successfully', + schema: { + data: { user: {}, accessToken: { expiresIn: 3600, expiresAt: '2025-12-12T12:00:00.000Z' } }, + messageEn: 'Token Refreshed Successfully', + messageAr: "تم تحديث الرمز بنجاح" + } + } + */ + this.auth.refresh, + ); + + this.router.patch( + `/auth/complete-profile-info`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Complete user profile', + required: true, + schema: { + $gender: 'Male', + $date_of_birth: '1990-01-01' + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Profile completed successfully', + schema: { + data: { id: 1, hasCompletedProfile: true }, + messageEn: 'Profile Completed Successfully', + messageAr: "تم إكمال الملف الشخصي بنجاح" + } + } + */ + ValidationMiddleware(CompleteUserProfileDto), + /* + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token', + required: true, + type: 'string' + } + */ + AuthMiddleware, + this.auth.completeProfile, + ); + + this.router.patch( + `/auth/verify-otp`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Verify OTP', + required: true, + schema: { $otp: '123456' } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie or)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'OTP verified successfully', + schema: { + data: true, + messageEn: 'OTP Verified Successfully', + messageAr: "تم التحقق من رمز التحقق بنجاح" + } + } + */ + AuthMiddleware, + this.auth.verifyOTP, + ); + + this.router.post( + `/auth/forget-password`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Request password reset', + required: true, + schema: { $email: 'user@example.com' } + } + #swagger.responses[200] = { + description: 'Password reset email sent', + schema: { messageEn: 'Password Reset Email Sent Successfully', messageAr: "تم إرسال بريد إعادة تعيين كلمة المرور بنجاح" } + } + */ + this.auth.forgetPassword, + ); + + this.router.post( + `/auth/reset-password`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Reset password', + required: true, + schema: { + $token: 'reset-token', + $newPassword: 'newPassword123' + } + } + #swagger.responses[200] = { + description: 'Password reset successfully', + schema: { messageEn: 'Password Reset Successfully', messageAr: "تم إعادة تعيين كلمة المرور بنجاح" } + } + */ + ValidationMiddleware(ResetPasswordDto), + this.auth.resetPassword, + ); + + this.router.post( + `/auth/resend-otp`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cooki)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'OTP resent successfully', + schema: { messageEn: 'OTP Resent Successfully', messageAr: "تم إعادة إرسال رمز التحقق بنجاح" } + } + */ + AuthMiddleware, + this.auth.resendOTP, + ); + + this.router.get( + `/auth/google`, + /* + #swagger.tags = ['Auth'] + #swagger.responses[302] = { + description: 'Redirects to Google OAuth consent page' + } + */ + this.googleAuth.googleOAuth, + ); + + this.router.get( + `/auth/google/callback`, + /* + #swagger.tags = ['Auth'] + #swagger.responses[302] = { + description: 'Redirects after Google authentication' + } + */ + this.googleAuth.googleOAuthCallback, + ); + + this.router.patch( + `/auth/google/update-phone`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Update Google user phone', + required: true, + schema: { $phone: '1234567890' } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie or)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Phone number updated successfully', + schema: { + data: { phone: '1234567890' }, + messageEn: 'Phone number updated successfully', + messageAr: "تم تحديث رقم الهاتف بنجاح" + } + } + */ + ValidationMiddleware(UpdateGoogleUserPhoneDto), + /* + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token', + required: true, + type: 'string' + } + */ + AuthMiddleware, + this.googleAuth.updatePhoneNumber, + ); + + this.router.get( + `/auth/google/userData`, + /* + #swagger.tags = ['Auth'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'User data retrieved successfully', + schema: { + data: { email: 'user@example.com', name: 'John Doe', username: 'johndoe', phone: '1234567890', gender: 'MALE' , date_of_birth: '1990-01-01', isVerified: false, hasCompletedProfile: false }, + messageEn: 'User data retrieved successfully', + messageAr: "تم استرجاع بيانات المستخدم بنجاح" + } + } + */ + AuthMiddleware, + this.googleAuth.getGoogleUserData, + ); + + this.router.post( + `${this.path}/check-password`, + /* + #swagger.path = '/auth/check-password' + #swagger.tags = ['Auth'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'User current password', + required: true, + schema: { + password: 'current_password123' + } + } + #swagger.responses[200] = { + description: 'Password check successful', + schema: { + data: { + isMatch: true + }, + messageEn: 'Password is correct', + messageAr: "كلمة المرور صحيحة" + } + } + */ + AuthMiddleware, + ValidationMiddleware(PasswordCheckDto), + errorWrapper(this.auth.checkPassword) + ); + + this.router.patch( + `${this.path}/change-password`, + /* + #swagger.path = '/auth/change-password' + #swagger.tags = ['Auth'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'User password change data', + required: true, + schema: { + newPassword: 'new_password123' + } + } + #swagger.responses[200] = { + description: 'Password changed successfully', + schema: { + messageEn: 'Password changed successfully', + messageAr: "تم تغيير كلمة المرور بنجاح" + } + } + */ + AuthMiddleware, + ValidationMiddleware(ChangePasswordDto), + errorWrapper(this.auth.changePassword) + ); + } +} diff --git a/src/routes/clinic.route.ts b/src/routes/clinic.route.ts new file mode 100644 index 0000000..a77ce19 --- /dev/null +++ b/src/routes/clinic.route.ts @@ -0,0 +1,261 @@ +import { ClinicController } from "@/controllers/clinic.controller"; +import { ClinicUpdateFeesDto, CreateUpdateClinicRequestDto } from "@/dtos/clinics.dto"; +import { Routes } from "@/interfaces"; +import { AuthMiddleware, RoleMiddleware } from "@/middlewares/auth.middleware"; +import { ValidationMiddleware } from "@/middlewares/validation.middleware"; +import { Role } from "@prisma/client"; +import { Router } from "express"; + +export class ClinicRoute implements Routes { + public path = '/clinics' + public router = Router(); + public clinicController = new ClinicController(); + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.post( + `${this.path}`, + /* + #swagger.path = '/clinics' + #swagger.method = 'post' + #swagger.tags = ['Clinics'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Clinic creation data', + required: true, + schema: { + $name: 'Downtown Medical Clinic', + $opening_at: '09:00', + $closing_at: '17:00', + $address: '123 Main Street, City Center', + address_maps_link: 'https://maps.google.com/?q=123+Main+Street', + $phone: '+1234567890', + canPayOnline: true, + $fees: 100 + } + } + #swagger.responses[201] = { + description: 'Clinic created successfully', + schema: { + messageEn: 'Clinic created successfully', + messageAr: "تم إنشاء العيادة بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + ValidationMiddleware(CreateUpdateClinicRequestDto), + this.clinicController.createClinic + ); + + this.router.get( + `${this.path}/:id`, + /* + #swagger.path = '/clinics/{id}' + #swagger.method = 'get' + #swagger.tags = ['Clinics'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['id'] = { + in: 'path', + description: 'The unique identifier of the clinic', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Clinic details retrieved successfully', + schema: { + data: { + id: 'clinic-uuid-123', + name: 'Downtown Medical Clinic', + is_active: true, + opening_at: '09:00', + closing_at: '17:00', + address: '123 Main Street, City Center', + address_maps_link: 'https://maps.google.com/?q=123+Main+Street', + phone: '+1234567890', + canPayOnline: true, + created_at: '2024-01-01T00:00:00.000Z' + }, + messageEn: 'Clinic retrieved successfully', + messageAr: "تم استرجاع بيانات العيادة بنجاح" + } + } + */ + AuthMiddleware, // To be Discussed: Should patients be able to view clinic details? + this.clinicController.getClinicById + ); + + this.router.patch( + `${this.path}/:id`, + /* + #swagger.path = '/clinics/{id}' + #swagger.method = 'patch' + #swagger.tags = ['Clinics'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['id'] = { + in: 'path', + description: 'The unique identifier of the clinic to update', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Clinic update data (all fields are optional)', + required: true, + schema: { + name: 'Downtown Medical Clinic - Updated', + opening_at: '08:00', + closing_at: '18:00', + address: '456 New Street, City Center', + address_maps_link: 'https://maps.google.com/?q=456+New+Street', + phone: '+1234567891', + canPayOnline: false, + fees: 150, + } + } + #swagger.responses[200] = { + description: 'Clinic updated successfully', + schema: { + messageEn: 'Clinic updated successfully', + messageAr: "تم تحديث العيادة بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + ValidationMiddleware(CreateUpdateClinicRequestDto, true), + this.clinicController.updateClinicById + ); + + this.router.patch( + `${this.path}/:id/fees`, + /* + #swagger.path = '/clinics/{id}/fees' + #swagger.method = 'patch' + #swagger.tags = ['Clinics'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['id'] = { + in: 'path', + description: 'The unique identifier of the clinic to update fees for', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Clinic fees update data', + required: true, + schema: { + fees: 200 + } + } + #swagger.responses[200] = { + description: 'Clinic fees updated successfully', + schema: { + messageEn: 'Clinic fees updated successfully', + messageAr: "تم تحديث رسوم العيادة بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + ValidationMiddleware(ClinicUpdateFeesDto), + this.clinicController.updateClinicFeesById + ); + + this.router.delete( + `${this.path}/:id`, + /* + #swagger.path = '/clinics/{id}' + #swagger.method = 'delete' + #swagger.tags = ['Clinics'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['id'] = { + in: 'path', + description: 'The unique identifier of the clinic to delete', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Clinic deleted successfully', + schema: { + messageEn: 'Clinic deleted successfully', + messageAr: "تم حذف العيادة بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + this.clinicController.deleteClinicById + ); + + this.router.get( + `${this.path}`, + /* + #swagger.path = '/clinics' + #swagger.method = 'get' + #swagger.tags = ['Clinics'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Get doctor clinics successful', + schema: { + data: [ + { + id: 'clinic-uuid', + name: 'Clinic Name', + address: '123 Main St, City, Country', + address_maps_link: 'https://maps.google.com/?q=123+Main+St,+City,+Country', + phone: '1234567890', + opening_at: '09:00', + closing_at: '17:00', + canPayOnline: true, + is_active: true, + created_at: '2024-01-01T00:00:00.000Z', + fees: 100, + created_by: 'doctor-uuid', + isOwner: true + } + ], + messageEn: "Doctor's clinics retrieved successfully", + messageAr: "تم استرجاع عيادات الطبيب بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + this.clinicController.getDoctorClinics + ); + } +} \ No newline at end of file diff --git a/src/routes/doctors.route.ts b/src/routes/doctors.route.ts new file mode 100644 index 0000000..23052a9 --- /dev/null +++ b/src/routes/doctors.route.ts @@ -0,0 +1,640 @@ +import { DoctorController } from "@/controllers/doctor.controller"; +import { DoctorLoginRequestDto, DoctorSetPasswordRequestDto, DoctorSignupRequestDto, PostAnnouncementDto, EditAnnouncementDto } from "@/dtos/doctors.dto"; +import { Routes } from "@/interfaces"; +import { ValidationMiddleware } from "@/middlewares/validation.middleware"; +import { Router } from "express"; +import { errorWrapper } from "@/utils/errorWrapper"; +import { AuthMiddleware, RoleMiddleware } from "@/middlewares/auth.middleware"; +import { Role } from "@prisma/client"; +import { uploadPdf } from "@/middlewares/multer.middleware"; + + +export class DoctorsRoute implements Routes { + public path = '/doctors' + public router = Router(); + public doctorsController = new DoctorController(); + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + + // Doctor Signup Route + this.router.post( + `/doctors/signup`, + /* + #swagger.tags = ['Doctors'] + #swagger.consumes = ['multipart/form-data'] + #swagger.parameters['email'] = { + in: 'formData', + description: 'Doctor email address', + required: true, + type: 'string' + } + #swagger.parameters['name'] = { + in: 'formData', + description: 'Doctor full name', + required: true, + type: 'string' + } + #swagger.parameters['phone'] = { + in: 'formData', + description: 'Doctor phone number', + required: true, + type: 'string' + } + #swagger.parameters['password'] = { + in: 'formData', + description: 'Doctor password', + required: true, + type: 'string' + } + #swagger.parameters['gender'] = { + in: 'formData', + description: 'Doctor gender (MALE or FEMALE)', + required: true, + type: 'string', + enum: ['MALE', 'FEMALE'] + } + #swagger.parameters['availability_type'] = { + in: 'formData', + description: 'availability type of the doctor', + required: false, + type: 'string', + enum: ['UNSET', 'ONLINE', 'OFFLINE', 'BOTH'] + } + #swagger.parameters['date_of_birth'] = { + in: 'formData', + description: 'Doctor date of birth (YYYY-MM-DD)', + required: true, + type: 'string' + } + #swagger.parameters['graduationCertificate'] = { + in: 'formData', + description: 'Graduation certificate PDF', + required: true, + type: 'file' + } + #swagger.parameters['membershipCard'] = { + in: 'formData', + description: 'Membership card PDF', + required: true, + type: 'file' + } + #swagger.parameters['professionalPracticeCard'] = { + in: 'formData', + description: 'Professional practice card PDF', + required: true, + type: 'file' + } + #swagger.parameters['mastersCertificate'] = { + in: 'formData', + description: 'Masters certificate PDF', + required: true, + type: 'file' + } + #swagger.parameters['fellowshipCertificate'] = { + in: 'formData', + description: 'Fellowship certificate PDF', + required: true, + type: 'file' + } + #swagger.parameters['unionSpecializationCertificate'] = { + in: 'formData', + description: 'Union specialization certificate PDF', + required: true, + type: 'file' + } + #swagger.responses[201] = { + description: 'Doctor signup successful', + schema: { + messageEn: 'Doctor registered successfully', + messageAr: "تم تسجيل الطبيب بنجاح" + } + } + */ + uploadPdf.fields([ + { name: 'graduationCertificate', maxCount: 1 }, + { name: 'membershipCard', maxCount: 1 }, + { name: 'professionalPracticeCard', maxCount: 1 }, + { name: 'mastersCertificate', maxCount: 1 }, + { name: 'fellowshipCertificate', maxCount: 1 }, + { name: 'unionSpecializationCertificate', maxCount: 1 }, + ]), + ValidationMiddleware(DoctorSignupRequestDto, false, false, false, true), + errorWrapper(this.doctorsController.doctorSignup) + ); + + // Doctor Login Route + this.router.post( + `/doctors/login`, + /* + #swagger.tags = ['Doctors'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Doctor login data', + required: true, + schema: { + $emailOrUsername: 'doctor@example.com', + $password: 'SecurePassword123', + $rememberMe: "true" + } + } + #swagger.responses[200] = { + description: 'Doctor login successful', + schema: { + data: { + id: 1, + email: 'test@example.com', + name: 'Dr. Smith', + username: 'drsmith', + phone: '1234567890', + gender: 'MALE', + doctor: { + specialization: 'CARDIOLOGY', + account_status: 'APPROVED' + } + }, + messageEn: 'Doctor logged in successfully', + messageAr: "تم تسجيل دخول الطبيب بنجاح" + } + } + */ + ValidationMiddleware(DoctorLoginRequestDto), + errorWrapper(this.doctorsController.doctorLogin) + ); + + // Doctor Set Password Route + this.router.patch( + `/doctors/set-password`, + /* + #swagger.tags = ['Doctors'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'New password data', + required: true, + schema: { + $password: 'NewSecurePassword123' + } + } + #swagger.responses[200] = { + description: 'Password set successfully', + schema: { + messageEn: 'Password updated successfully', + messageAr: "تم تحديث كلمة المرور بنجاح" + } + } + */ + ValidationMiddleware(DoctorSetPasswordRequestDto), + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + errorWrapper(this.doctorsController.doctorSetPassword) + ); + + this.router.post( + `/doctors/announcement`, + /* + #swagger.path = '/doctors/announcement' + #swagger.method = 'post' + #swagger.tags = ['Doctors'] + #swagger.description = 'Allows doctor to post a nurse hiring announcement' + #swagger.parameters['body'] = { + in: 'body', + description: 'Announcement data', + required: true, + schema: { + $clinic_id: 'uuid-of-the-clinic', + $working_days: [ + { + $day_of_week: 'MONDAY', + $start_time: '09:00', + $end_time: '17:00' + }, + { + $day_of_week: 'TUESDAY', + $start_time: '10:00', + $end_time: '17:00' + } + ], + gender: 'FEMALE', + max_age: 40, + years_of_experience: 3, + notes: 'Looking for an experienced nurse' + } + } + #swagger.responses[201] = { + description: 'Announcement posted successfully', + schema: { + messageEn: 'Announcement created successfully', + messageAr: 'تم نشر الإعلان بنجاح' + } + } + #swagger.responses[403] = { + description: 'Doctor account not approved (PENDING or REJECTED)' + } + #swagger.responses[404] = { + description: 'Doctor not found or does not belong to the specified clinic' + } + */ + AuthMiddleware, + ValidationMiddleware(PostAnnouncementDto), + this.doctorsController.postAnnouncement + ) + + this.router.get( + `/doctors/announcements`, + /* + #swagger.path = '/doctors/announcements' + #swagger.method = 'get' + #swagger.tags = ['Doctors'] + #swagger.description = 'Retrieves all nurse hiring announcements posted by the doctor' + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Announcements retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + doctor: { + id: 'uuid-string', + name: 'Dr. Ahmed Ali', + gender: 'MALE', + profilePic: 'https://res.cloudinary.com/example/image.jpg' + }, + clinic: { + id: 'uuid-string', + name: 'Al Salam Clinic', + address: '123 Main St, Cairo', + address_maps_link: 'https://maps.google.com/?q=...' + }, + working_days: [ + { + day_of_week: 'MONDAY', + start_time: '09:00', + end_time: '17:00' + } + ], + status: 'PENDING', + gender: 'FEMALE', + max_age: 40, + years_of_experience: 3, + notes: 'Looking for an experienced nurse' + } + ], + messageEn: 'Announcements retrieved successfully', + messageAr: 'تم استرجاع الإعلانات بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[404] = { + description: 'Doctor not found' + } + */ + AuthMiddleware, + this.doctorsController.getDoctorAnnouncements + + ) + + this.router.get( + `/doctors/announcements/:announcementId/applicants`, + /* + #swagger.path = '/doctors/announcements/{announcementId}/applicants' + #swagger.method = 'get' + #swagger.tags = ['Doctors'] + #swagger.description = 'Retrieves all PENDING nurse applicants for a specific announcement' + #swagger.parameters['announcementId'] = { + in: 'path', + description: 'ID of the announcement to retrieve applicants for', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Applicants retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + name: 'Max Mustermann', + email: 'max.mustermann@example.com', + gender: 'FEMALE', + phone: '+201234567890', + age: 28, + profilePic: 'https://res.cloudinary.com/example/photo.jpg', + years_of_experience: 5, + nationalCardUrl: 'https://res.cloudinary.com/example/national_card.pdf', + brief: 'Experienced ICU nurse with 5 years in critical care', + bonusFileUrl: 'https://res.cloudinary.com/example/bonus.pdf' + } + ], + messageEn: 'Applicants retrieved successfully', + messageAr: 'تم استرجاع المتقدمين بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden – announcement does not belong to this doctor' + } + #swagger.responses[404] = { + description: 'Announcement not found' + } + */ + AuthMiddleware, + this.doctorsController.getAnnouncementApplicants + ) + this.router.get( + `/doctors/nurses`, + /* + #swagger.path = '/doctors/nurses' + #swagger.method = 'get' + #swagger.tags = ['Doctors'] + #swagger.description = 'Retrieves all nurses working with the doctor' + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Nurses retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + name: 'Max Mustermann', + email: 'max.mustermann@example.com', + gender: 'FEMALE', + phone: '+201234567890', + age: 25, + profilePic: 'https://res.cloudinary.com/example/photo.jpg', + years_of_experience: 2, + nationalCardUrl: 'https://res.cloudinary.com/example/national_card.pdf', + bonusFileUrl: 'https://res.cloudinary.com/example/bonus.pdf', + brief: 'Experienced ICU nurse with 5 years in critical care', + clinics: [ + { + id: 'uuid-string', + name: 'Al Salam Clinic', + address: '123 Main St, Cairo', + address_maps_link: 'https://maps.google.com/?q=...', + working_days: [ + { + day_of_week: 'SUNDAY', + start_time: '14:00', + end_time: '17:00' + } + ] + }, + { + id: 'uuid-string', + name: 'Medical Park Clinic', + address: '123 Main St, New Cairo', + address_maps_link: 'https://maps.google.com/?q=...', + working_days: [ + { + day_of_week: 'MONDAY', + start_time: '10:00', + end_time: '17:00' + }, + { + day_of_week: 'TUESDAY', + start_time: '10:00', + end_time: '17:00' + } + ] + } + ] + } + ], + messageEn: 'Nurses retrieved successfully', + messageAr: 'تم استرجاع الممرضين بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + */ + + AuthMiddleware, + this.doctorsController.getWorkingNurses + ) + + this.router.patch( + `/doctors/announcements/:applicantId/approve`, + /* + #swagger.path = '/doctors/announcements/{applicantId}/approve' + #swagger.method = 'patch' + #swagger.tags = ['Doctors'] + #swagger.description = 'Approves a nurse applicant for a specific announcement' + #swagger.parameters['applicantId'] = { + in: 'path', + description: 'ID of the nurse applicant to approve', + required: true, + type: 'string' + } + #swagger.parameters['announcementId'] = { + in: 'query', + description: 'ID of the announcement the applicant applied to', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Applicant approved successfully', + schema: { + messageEn: 'Applicant approved successfully', + messageAr: 'تم قبول المتقدم بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden – announcement does not belong to this doctor' + } + #swagger.responses[404] = { + description: 'Applicant or announcement not found' + } + */ + AuthMiddleware, + this.doctorsController.approveApplicant + ) + + this.router.patch( + `/doctors/announcements/:applicantId/reject`, + /* + #swagger.path = '/doctors/announcements/{applicantId}/reject' + #swagger.method = 'patch' + #swagger.tags = ['Doctors'] + #swagger.description = 'Rejects a nurse applicant for a specific announcement' + #swagger.parameters['applicantId'] = { + in: 'path', + description: 'ID of the nurse applicant to reject', + required: true, + type: 'string' + } + #swagger.parameters['announcementId'] = { + in: 'query', + description: 'ID of the announcement the applicant applied to', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Applicant rejected successfully', + schema: { + messageEn: 'Applicant rejected successfully', + messageAr: 'تم رفض المتقدم بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden – announcement does not belong to this doctor' + } + #swagger.responses[404] = { + description: 'Applicant or announcement not found' + } + */ + AuthMiddleware, + this.doctorsController.rejectApplicant + ) + + this.router.delete( + `/doctors/announcements/:announcementId`, + /* + #swagger.path = '/doctors/announcements/{announcementId}' + #swagger.method = 'delete' + #swagger.tags = ['Doctors'] + #swagger.description = 'Deletes a specific nurse hiring announcement' + #swagger.parameters['announcementId'] = { + in: 'path', + description: 'ID of the announcement to delete', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Announcement deleted successfully', + schema: { + messageEn: 'Announcement deleted successfully', + messageAr: 'تم حذف الإعلان بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden – announcement does not belong to this doctor' + } + #swagger.responses[404] = { + description: 'Announcement not found' + } + */ + AuthMiddleware, + this.doctorsController.deleteAnnouncement + ) + + this.router.patch( + `/doctors/announcements/:announcementId`, + /* + #swagger.path = '/doctors/announcements/{announcementId}' + #swagger.method = 'patch' + #swagger.tags = ['Doctors'] + #swagger.description = 'Edits a specific nurse hiring announcement (only if it is still PENDING)' + #swagger.parameters['announcementId'] = { + in: 'path', + description: 'ID of the announcement to edit', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'Updated announcement data (only include fields to be updated)', + required: true, + schema: { + $clinic_id: 'uuid-of-the-clinic', + $working_days: [ + { + $day_of_week: 'MONDAY', + $start_time: '09:00', + $end_time: '17:00' + }, + { + $day_of_week: 'TUESDAY', + $start_time: '10:00', + $end_time: '17:00' + } + ], + gender: 'FEMALE', + max_age: 40, + years_of_experience: 3, + notes: 'Looking for an experienced nurse' + } + } + #swagger.responses[200] = { + description: 'Announcement updated successfully', + schema: { + messageEn: 'Announcement updated successfully', + messageAr: 'تم تحديث الإعلان بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden – announcement does not belong to this doctor or is not PENDING' + } + #swagger.responses[404] = { + description: 'Announcement not found' + } + */ + AuthMiddleware, + ValidationMiddleware(EditAnnouncementDto), + this.doctorsController.editAnnouncement + ) + + } +} \ No newline at end of file diff --git a/src/routes/fabric.route.ts b/src/routes/fabric.route.ts new file mode 100644 index 0000000..e7cdc31 --- /dev/null +++ b/src/routes/fabric.route.ts @@ -0,0 +1,90 @@ +import { Router } from 'express'; +import FabricContoller from '@/controllers/fabric.controller'; +import { CreateMedicalRecordDto, UpdateMedicalRecordDto } from '@/dtos/medicalRecord.dto'; +import { OnboardIdentityDto } from '@/dtos/fabric-identity.dto'; +import { ValidationMiddleware } from '@middlewares/validation.middleware'; +import { Routes } from '@interfaces/routes.interface'; + +export class FabricRoute implements Routes { + public path = '/records'; + public router = Router(); + public fabricController = new FabricContoller(); + + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + // Identity Management Routes + this.router.post( + '/fabric/onboard', + /* + #swagger.tags = ['FabricIdentity'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Identity onboarding data', + required: true, + schema: { + $clinicId: 'clinic-uuid-here', + $mspId: 'Org1MSP', + $certificate: 'PEM certificate', + $privateKey: 'PEM private key', + $peerEndpoint: 'localhost:7051', + $peerHostAlias: 'peer0.org1.example.com', + $tlsCertificate: 'PEM TLS certificate', + channelName: 'mychannel', + chaincodeName: 'test' + } + } + */ + ValidationMiddleware(OnboardIdentityDto), + this.fabricController.onboardIdentity, + ); + this.router.get( + '/fabric/identities', + /* #swagger.tags = ['FabricIdentity'] */ + this.fabricController.listIdentities, + ); + this.router.delete( + '/fabric/identities/:clinicId', + /* #swagger.tags = ['FabricIdentity'] */ + this.fabricController.deleteIdentity, + ); + this.router.get( + '/fabric/connections', + /* #swagger.tags = ['FabricIdentity'] */ + this.fabricController.getConnectionStats, + ); + this.router.post( + '/fabric/init-ledger', + /* + #swagger.tags = ['FabricIdentity'] + #swagger.description = 'Initialize the ledger, optionally seeding it with backup records' + #swagger.parameters['body'] = { + in: 'body', + description: 'Optional backup data to seed the ledger', + required: false, + schema: { + backupData: [ + { + patientId: 'patient-uuid', + recordId: 'record-uuid', + doctorId: 'doctor-uuid', + type: 'LAB_RESULT', + ownerMsp: 'Org1MSP', + authorizedMsps: [] + } + ] + } + } + */ + this.fabricController.initLedger, + ); + this.router.get( + '/records/health', + /* #swagger.tags = ['MedicalRecords'] */ + this.fabricController.checkHealth, + ); + } +} + diff --git a/src/routes/medical-record.route.ts b/src/routes/medical-record.route.ts new file mode 100644 index 0000000..5b7659b --- /dev/null +++ b/src/routes/medical-record.route.ts @@ -0,0 +1,366 @@ +import { Routes } from '@/interfaces'; +import { ValidationMiddleware } from '@/middlewares/validation.middleware'; +import { Router } from 'express'; +import { MedicalRecordController } from '@/controllers/medical-records.controller'; +import { AuthMiddleware, RoleMiddleware } from '@/middlewares/auth.middleware'; +import { Role } from '@prisma/client'; +import { + CreateDoctorRecordJsonDto, + CreateMedicalRecordDto, + CreatePatientMedicalHistoryDto, + UpdatePatientMedicalHistoryDto, +} from '@/dtos/medical-records.dto'; +import { uploadSingleFile } from '@/middlewares/upload.middleware'; + +export class MedicalRecordRoute implements Routes { + public path = '/medical-records'; + public router = Router(); + public medicalRecordController = new MedicalRecordController(); + + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.get( + `${this.path}/health/ipfs`, + /* + #swagger.path = '/medical-records/health/ipfs' + #swagger.method = 'get' + #swagger.tags = ['Medical Records - Public'] + #swagger.description = 'Checks connectivity to the IPFS (Pinata) service' + #swagger.responses[200] = { + description: 'IPFS connection is healthy', + schema: { status: 'ok', message: 'IPFS connection is healthy' } + } + #swagger.responses[503] = { + description: 'IPFS service is unreachable' + } + */ + this.medicalRecordController.getIpfsHealth, + ); + + this.router.post( + `${this.path}/clinics/:clinicId/patients/:patientId/visit-summaries`, + /* + #swagger.path = '/medical-records/clinics/{clinicId}/patients/{patientId}/visit-summaries' + #swagger.method = 'post' + #swagger.tags = ['Medical Records - Doctor'] + #swagger.description = 'Doctor creates a JSON-based medical record for a patient. Validates the doctor works at the clinic. Content is encrypted and stored on IPFS.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.parameters['clinicId'] = { + in: 'path', + description: 'UUID of the clinic', + required: true, + type: 'string' + } + + #swagger.parameters['patientId'] = { + in: 'path', + description: 'UUID of the patient', + required: true, + type: 'string' + } + + #swagger.parameters['body'] = { + in: 'body', + description: 'Medical record payload', + required: true, + schema: { + name: 'SOAP Note 2026-03-08', + type: 'SOAP_NOTE', + content: { + subjective: 'Patient reports headache', + objective: 'BP 120/80', + assessment: 'Tension headache', + plan: 'Ibuprofen 400mg' + } + } + } + + #swagger.responses[201] = { + description: 'Medical record created successfully', + schema: { message: 'Medical record created successfully', data: { recordId: 'uuid-string' } } + } + #swagger.responses[400] = { + description: 'Validation failed' + } + #swagger.responses[403] = { + description: 'Doctor is not associated with this clinic' + } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + ValidationMiddleware(CreateDoctorRecordJsonDto), + this.medicalRecordController.createDoctorRecord, + ); + + this.router.get( + `${this.path}/patient/visit-summaries`, + /* + #swagger.path = '/medical-records/patient/visit-summaries' + #swagger.method = 'get' + #swagger.tags = ['Medical Records - Patient'] + #swagger.description = 'Patient retrieves all their VISIT_SUMMARY records, decrypted and authorized across all clinics on-chain.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Visit summaries retrieved successfully', + schema: { + message: 'Visit summaries retrieved successfully', + data: [{ recordId: 'uuid-string', content: {} }] + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + */ + AuthMiddleware, + RoleMiddleware(Role.PATIENT), + this.medicalRecordController.getPatientVisitSummaries, + ); + + this.router.post( + `${this.path}/patient/medical-history`, + /* + #swagger.path = '/medical-records/patient/medical-history' + #swagger.method = 'post' + #swagger.tags = ['Medical Records - Patient'] + #swagger.description = 'Patient adds a new MEDICAL_HISTORY entry. Type is fixed — only name and content are required.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.parameters['body'] = { + in: 'body', + required: true, + schema: { + name: 'Previous Surgeries', + content: { conditions: ['hypertension'], surgeries: ['appendectomy'] } + } + } + + #swagger.responses[201] = { + description: 'Medical history entry created successfully', + schema: { message: 'Medical history entry created successfully', data: { recordId: 'uuid-string' } } + } + #swagger.responses[400] = { description: 'Validation failed' } + #swagger.responses[401] = { description: 'Unauthorized' } + */ + AuthMiddleware, + RoleMiddleware(Role.PATIENT), + ValidationMiddleware(CreatePatientMedicalHistoryDto), + this.medicalRecordController.createPatientMedicalHistory, + ); + + this.router.patch( + `${this.path}/patient/medical-history/:recordId`, + /* + #swagger.path = '/medical-records/patient/medical-history/{recordId}' + #swagger.method = 'patch' + #swagger.tags = ['Medical Records - Patient'] + #swagger.description = 'Patient updates an existing MEDICAL_HISTORY record they own. At least one of name or content must be provided. If content changes, the file is re-encrypted and re-uploaded to IPFS.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.parameters['recordId'] = { + in: 'path', + description: 'UUID of the record to update', + required: true, + type: 'string' + } + + #swagger.parameters['body'] = { + in: 'body', + required: true, + schema: { + name: 'Updated History Title', + content: { conditions: ['hypertension'], surgeries: ['appendectomy'] } + } + } + + #swagger.responses[200] = { + description: 'Medical history entry updated successfully', + schema: { message: 'Medical history entry updated successfully' } + } + #swagger.responses[400] = { description: 'Validation failed' } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[404] = { description: 'Record not found or not owned by patient' } + */ + AuthMiddleware, + RoleMiddleware(Role.PATIENT), + ValidationMiddleware(UpdatePatientMedicalHistoryDto), + this.medicalRecordController.updatePatientMedicalHistory, + ); + + this.router.delete( + `${this.path}/patient/medical-history/:recordId`, + /* + #swagger.path = '/medical-records/patient/medical-history/{recordId}' + #swagger.method = 'delete' + #swagger.tags = ['Medical Records - Patient'] + #swagger.description = 'Patient soft-deletes one of their own MEDICAL_HISTORY records. Also removes it from blockchain and IPFS.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.parameters['recordId'] = { + in: 'path', + description: 'UUID of the record to delete', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Medical history entry deleted successfully', + schema: { message: 'Medical history entry deleted successfully' } + } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[404] = { description: 'Record not found or not owned by patient' } + */ + AuthMiddleware, + RoleMiddleware(Role.PATIENT), + this.medicalRecordController.deletePatientMedicalHistory, + ); + + this.router.get( + `${this.path}/patient/medical-history`, + /* + #swagger.path = '/medical-records/patient/medical-history' + #swagger.method = 'get' + #swagger.tags = ['Medical Records - Patient'] + #swagger.description = 'Patient retrieves all their MEDICAL_HISTORY records, decrypted and authorized across all clinics on-chain.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Medical history retrieved successfully', + schema: { + message: 'Medical history retrieved successfully', + data: [{ recordId: 'uuid-string', content: {} }] + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + */ + AuthMiddleware, + RoleMiddleware(Role.PATIENT), + this.medicalRecordController.getPatientMedicalHistory, + ); + + this.router.get( + `${this.path}/:patientId/visit-summaries`, + /* + #swagger.path = '/medical-records/{patientId}/visit-summaries' + #swagger.method = 'get' + #swagger.tags = ['Medical Records - Doctor'] + #swagger.description = 'Doctor retrieves VISIT_SUMMARY records for a patient. Only returns records the doctor\'s clinic(s) are authorized to access on-chain.' + #swagger.parameters['patientId'] = { in: 'path', required: true, type: 'string', description: 'UUID of the patient' } + #swagger.responses[200] = { description: 'Visit summaries retrieved successfully', schema: { message: 'Visit summaries retrieved successfully', data: [{ recordId: 'uuid-string', content: {} }] } } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[403] = { description: 'Access denied' } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + this.medicalRecordController.getDoctorPatientVisitSummaries, + ); + + this.router.get( + `${this.path}/:patientId/medical-history`, + /* + #swagger.path = '/medical-records/{patientId}/medical-history' + #swagger.method = 'get' + #swagger.tags = ['Medical Records - Doctor'] + #swagger.description = 'Doctor retrieves MEDICAL_HISTORY records for a patient. Only returns records the doctor\'s clinic(s) are authorized to access on-chain.' + #swagger.parameters['patientId'] = { in: 'path', required: true, type: 'string', description: 'UUID of the patient' } + #swagger.responses[200] = { description: 'Medical history retrieved successfully', schema: { message: 'Medical history retrieved successfully', data: [{ recordId: 'uuid-string', content: {} }] } } + #swagger.responses[401] = { description: 'Unauthorized' } + #swagger.responses[403] = { description: 'Access denied' } + */ + AuthMiddleware, + RoleMiddleware(Role.DOCTOR), + this.medicalRecordController.getDoctorPatientMedicalHistory, + ); + + this.router.post( + `${this.path}/grant-access`, + /* + #swagger.path = '/medical-records/grant-access' + #swagger.method = 'post' + #swagger.tags = ['Medical Records - Patient'] + #swagger.description = 'Patient grants a target clinic access to ALL their medical records across all owner clinics.' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.parameters['body'] = { + in: 'body', + required: true, + schema: { targetClinicId: 'uuid-string' } + } + + #swagger.responses[200] = { + description: 'Access granted successfully', + schema: { message: 'Access granted successfully' } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + */ + AuthMiddleware, + RoleMiddleware(Role.PATIENT), + this.medicalRecordController.grantPatientAccess, + ); + + this.router.delete( + `${this.path}/dev/all`, + /* + #swagger.path = '/record/dev/all' + #swagger.method = 'delete' + #swagger.tags = ['Medical Records'] + #swagger.description = 'DEV ONLY — hard-deletes every medical record from DB, IPFS, and blockchain. No authentication required.' + #swagger.responses[200] = { + description: 'All records deleted', + schema: { message: 'Deleted 5 records from DB, IPFS, and blockchain', data: { deleted: 5 } } + } + */ + this.medicalRecordController.deleteAllRecords, + ); + } +} diff --git a/src/routes/nurse.route.ts b/src/routes/nurse.route.ts new file mode 100644 index 0000000..e6c5300 --- /dev/null +++ b/src/routes/nurse.route.ts @@ -0,0 +1,561 @@ +import { Routes } from "@/interfaces"; +import { ValidationMiddleware } from "@/middlewares/validation.middleware"; +import { Router } from "express"; +import { NurseController } from "@/controllers/nurse.controller"; +import { AppointmentController } from "@/controllers/appointment.controller"; +import { NurseLoginRequestDto, NurseSetPasswordRequestDto, NurseSignupRequestDto } from "@/dtos/nurses.dto"; +import { AuthMiddleware, RoleMiddleware } from "@/middlewares/auth.middleware"; +import { Role } from "@prisma/client"; +import { uploadPdf } from "@/middlewares/multer.middleware"; + +export class NurseRoute implements Routes { + public path = '/nurses' + public router = Router(); + public nursesController = new NurseController(); + public appointmentController = new AppointmentController(); + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.post( + `${this.path}/signup`, + /* + #swagger.path = '/nurses/signup' + #swagger.method = 'post' + #swagger.tags = ['Nurses'] + #swagger.description = 'Creates a new nurse account. Requires national ID card upload and optional bonus file' + #swagger.consumes = ['multipart/form-data'] + + #swagger.parameters['name'] = { + in: 'formData', + description: 'name of the nurse', + required: true, + type: 'string', + } + #swagger.parameters['email'] = { + in: 'formData', + description: 'Email address', + required: true, + type: 'string', + } + #swagger.parameters['phone'] = { + in: 'formData', + description: 'Phone number', + required: true, + type: 'string', + } + #swagger.parameters['password'] = { + in: 'formData', + description: 'Initial password for the account', + required: true, + type: 'string', + } + #swagger.parameters['years_of_experience'] = { + in: 'formData', + description: 'Number of years of professional nursing experience', + required: true, + type: 'integer', + } + #swagger.parameters['gender'] = { + in: 'formData', + description: 'Gender (must match Prisma enum: MALE or FEMALE)', + required: true, + type: 'string', + } + #swagger.parameters['date_of_birth'] = { + in: 'formData', + description: 'Date of birth (format YYYY-MM-DD)', + required: true, + type: 'string', + } + #swagger.parameters['brief'] = { + in: 'formData', + description: 'Short professional summary / bio (optional)', + required: false, + type: 'string', + } + #swagger.parameters['nationalCard'] = { + in: 'formData', + description: 'National ID card or passport scan (PDF only)', + required: true, + type: 'file' + } + #swagger.parameters['bonusFile'] = { + in: 'formData', + description: 'Additional document: nursing license, experience certificate, etc.', + required: false, + type: 'file' + } + + #swagger.responses[201] = { + description: 'Account created successfully – awaiting admin approval', + schema: { + message_en: "Nurse account created successfully. Please wait for verification.", + message_ar: "تم إنشاء حساب الممرضة بنجاح. يرجى الانتظار للموافقة عليه.", + } + } + #swagger.responses[400] = { + description: 'Validation failed (missing fields, wrong file type, invalid date format, etc.)' + } + #swagger.responses[500] = { + description: 'Server error during file upload or database transaction' + } + */ + + uploadPdf.fields([ + { name: 'nationalCard', maxCount: 1 }, + { name: 'bonusFile', maxCount: 1 }, + ]), + ValidationMiddleware(NurseSignupRequestDto), + this.nursesController.nurseSignup, + ) + + this.router.post( + `${this.path}/login`, + /* + #swagger.path = '/nurses/login' + #swagger.method = 'post' + #swagger.tags = ['Nurses'] + #swagger.description = 'Authenticates nurse credentials' + #swagger.parameters['body'] = { + in: 'body', + description: 'Nurse login data', + required: true, + schema: { + $emailOrUsername: 'nurse@example.com', + $password: 'SecurePassword123', + $rememberMe: "true" + } + } + + #swagger.responses[200] = { + description: 'Login successful – approved nurse with completed profile', + schema: { + data: { + id: 'uuid-string', + name: 'Maxine Lee', + email: 'maxine.lee@example.com', + username: 'maxine.lee', + phone: '+201234567890', + gender: 'FEMALE', + nurse: { account_status: 'APPROVED' } + }, + messageEn: 'Nurse retrieved successfully', + messageAr: 'تم استرجاع بيانات الممرض بنجاح' + } + } + #swagger.responses[401] = { + description: 'Invalid credentials (wrong email/username or password)' + } + #swagger.responses[403] = { + description: 'Account not approved (PENDING or REJECTED)' + } + */ + ValidationMiddleware(NurseLoginRequestDto), + this.nursesController.nurseLogin + ); + + this.router.patch( + `${this.path}/set-password`, + /* + #swagger.path = '/nurses/set-password' + #swagger.method = 'patch' + #swagger.tags = ['Nurses'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'New password data', + required: true, + schema: { + $password: 'NewSecurePassword123' + } + } + #swagger.responses[200] = { + description: 'Password set successfully', + schema: { + messageEn: 'Password updated successfully', + messageAr: "تم تحديث كلمة المرور بنجاح" + } + } + #swagger.responses[400] = { + description: 'Password already set / validation error' + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden – user is not a nurse role' + } + #swagger.responses[404] = { + description: 'Nurse user not found' + } + + */ + ValidationMiddleware(NurseSetPasswordRequestDto), + AuthMiddleware, + RoleMiddleware(Role.NURSE), + this.nursesController.nurseSetPassword + ); + + this.router.get( + `${this.path}/announcements`, + /* + #swagger.path = '/nurses/announcements' + #swagger.method = 'get' + #swagger.tags = ['Nurses'] + #swagger.description = 'Retrieves all active announcements for the nurse' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Announcements retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + doctor: { + id: 'uuid-string', + name: 'Dr. House', + gender: 'MALE', + profilePic: 'https://res.cloudinary.com/example/image.jpg' + }, + clinic: { + id: 'uuid-string', + name: 'Al Salam Clinic', + address: '123 Main St, Cairo', + address_maps_link: 'https://maps.google.com/?q=...' + }, + working_days: [ + { + day_of_week: 'MONDAY', + start_time: '09:00', + end_time: '17:00' + } + ], + status: 'PENDING', + gender: 'FEMALE', + max_age: 40, + years_of_experience: 3, + notes: 'Looking for an experienced nurse' + } + ], + messageEn: 'Announcements retrieved successfully', + messageAr: 'تم استرجاع الإعلانات بنجاح' + } + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[404] = { + description: 'Nurse account not approved (PENDING or REJECTED)' + } + */ + AuthMiddleware, + this.nursesController.getAllAnnouncements + ); + + this.router.get( + `${this.path}/applications`, + /* + #swagger.path = '/nurses/applications' + #swagger.method = 'get' + #swagger.tags = ['Nurses'] + #swagger.description = 'get all announcements the nurse has applied to' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Applications retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + application_status: 'PENDING', + doctor: { + id: 'uuid-string', + name: 'Dr. House', + gender: 'MALE', + profilePic: 'https://res.cloudinary.com/example/image.jpg' + }, + clinic: { + id: 'uuid-string', + name: 'Al Salam Clinic', + address: '123 Main St, Cairo', + address_maps_link: 'https://maps.google.com/?q=...' + }, + working_days: [ + { + day_of_week: 'MONDAY', + start_time: '09:00', + end_time: '17:00' + } + ], + status: 'POSTED', + gender: 'FEMALE', + max_age: 40, + years_of_experience: 3, + notes: 'Looking for an experienced nurse' + } + ], + messageEn: 'Applications retrieved successfully', + messageAr: 'تم استرجاع الطلبات بنجاح' + } + } + #swagger.responses[400] = { + description: 'Nurse ID not found in token' + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[404] = { + description: 'Nurse account not approved (PENDING or REJECTED)' + } + */ + AuthMiddleware, + this.nursesController.getNurseApplications + ); + + this.router.post( + `${this.path}/announcements/:announcementId/apply`, + /* + #swagger.path = '/nurses/announcements/{announcementId}/apply' + #swagger.method = 'post' + #swagger.tags = ['Nurses'] + #swagger.description = 'Apply to a specific announcement' + + #swagger.parameters['announcementId'] = { + in: 'path', + description: 'ID of the announcement to apply for', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Application submitted successfully', + schema: { + messageEn: 'Applied to announcement successfully', + messageAr: 'تم التقديم على الإعلان بنجاح' + } + } + #swagger.responses[400] = { + description: 'Invalid announcement ID / already applied / validation error' + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[404] = { + description: 'Announcement not found / Nurse account not approved (PENDING or REJECTED)' + } + */ + AuthMiddleware, + this.nursesController.applyToAnnouncement + ) + + this.router.get( + `${this.path}/schedule`, + /* + #swagger.path = '/nurses/schedule' + #swagger.method = 'get' + #swagger.tags = ['Nurses'] + #swagger.description = 'get the schedule for the nurse' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Schedule retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + doctor: { + id: 'uuid-string', + name: 'Dr. House', + gender: 'MALE', + profilePic: 'https://res.cloudinary.com/example/image.jpg' + }, + clinic: { + id: 'uuid-string', + name: 'Al Salam Clinic', + address: '123 Main St, Cairo', + address_maps_link: 'https://maps.google.com/?q=...' + }, + working_days: [ + { + day_of_week: 'MONDAY', + start_time: '09:00', + end_time: '17:00' + } + ] + } + ], + messageEn: 'Nurse schedule retrieved successfully', + messageAr: 'تم استرجاع جدول الممرضة بنجاح' + } + } + #swagger.responses[400] = { + description: 'Nurse ID not found in token' + } + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + #swagger.responses[404] = { + description: 'Nurse account not approved or no schedule assigned' + } + */ + AuthMiddleware, + this.nursesController.getNurseSchedule + ); + + this.router.get( + `${this.path}/appointments`, + /* + #swagger.path = '/nurses/appointments' + #swagger.method = 'get' + #swagger.tags = ['Nurses'] + #swagger.description = 'Get all appointments for a specific doctor on a given date' + + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + + #swagger.parameters['doctorId'] = { + in: 'query', + description: 'The ID of the doctor whose appointments are being retrieved', + required: true, + type: 'string' + } + + #swagger.parameters['clinicId'] = { + in: 'query', + description: 'The ID of the clinic to filter appointments by', + required: false, + type: 'string' + } + + #swagger.parameters['date'] = { + in: 'query', + description: 'The date to retrieve appointments for, in YYYY-MM-DD format', + required: true, + type: 'string' + } + + #swagger.responses[200] = { + description: 'Appointments retrieved successfully', + schema: { + data: [ + { + id: 'uuid-string', + clinic: { + id: 'uuid-string', + name: 'Al Salam Clinic', + address: '123 Main St, Cairo', + address_maps_link: 'https://maps.google.com/?q=...' + }, + patient: { + id: 'uuid-string', + name: 'Ahmed Hassan', + gender: 'MALE', + phone: '+201012345678' + }, + status: 'CONFIRMED', + slot_duration: 30, + appointment_date: '2025-03-15', + start_time: '09:00 AM', + end_time: '09:30 AM' + } + ], + messageEn: 'Appointments retrieved successfully', + messageAr: 'تم استرجاع المواعيد بنجاح' + } + } + + #swagger.responses[400] = { + description: 'Bad request – missing or invalid parameters (nurseId, doctorId, date)' + } + + #swagger.responses[401] = { + description: 'Unauthorized – missing or invalid token' + } + + #swagger.responses[500] = { + description: 'Internal server error' + } + */ + AuthMiddleware, + this.appointmentController.getAppointmentsByDate + ); + + this.router.patch( + `${this.path}/appointments/:appointmentId/complete`, + /* + #swagger.path = '/nurses/appointments/{appointmentId}/complete' + #swagger.method = 'patch' + #swagger.tags = ['Nurses'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication (must be a nurse)', + required: true, + type: 'string' + } + #swagger.description = 'Mark an appointment as completed. Only accessible by authenticated nurses' + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'UUID of the appointment to complete', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Appointment marked as completed successfully', + schema: { + messageEn: 'Appointment completed successfully', + messageAr: 'تم إكمال الموعد بنجاح' + } + } + #swagger.responses[400] = { + description: 'Bad request - missing nurse ID or invalid appointment' + } + #swagger.responses[401] = { + description: 'Unauthorized - missing or invalid token' + } + #swagger.responses[403] = { + description: 'Forbidden - appointment does not belong to the authenticated user' + } + #swagger.responses[404] = { + description: 'Appointment not found' + } + */ + AuthMiddleware, + this.appointmentController.completeAppointment + ) + } +} \ No newline at end of file diff --git a/src/routes/queue.route.ts b/src/routes/queue.route.ts new file mode 100644 index 0000000..aa64038 --- /dev/null +++ b/src/routes/queue.route.ts @@ -0,0 +1,62 @@ +import { Routes } from "@/interfaces"; +import { Router } from "express"; +import { QueueController } from "@/controllers/queue.controller"; +import { AuthMiddleware } from "@/middlewares/auth.middleware"; + + +export class QueueRoute implements Routes { + public path = '/queue'; + public router = Router(); + public queueController = new QueueController(); + + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + this.router.get( + `${this.path}/position/:appointmentId`, + /* + #swagger.path = '/queue/position/{appointmentId}' + #swagger.method = 'get' + #swagger.tags = ['Queue'] + #swagger.parameters['appointmentId'] = { + in: 'path', + description: 'Appointment ID to get its queue position', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.description = 'Get queue position, number of patients ahead, and estimated waiting time for a specific appointment' + #swagger.responses[200] = { + description: 'Queue position retrieved successfully', + schema: { + data: { + position: 3, + patientsAhead: 2, + estimatedWaitMinutes: 60 + }, + message: 'Queue position retrieved successfully', + } + } + #swagger.responses[400] = { + description: 'Appointment ID is required' + } + #swagger.responses[404] = { + description: 'Appointment not found or doctor not working on this day' + } + #swagger.responses[401] = { + description: 'Unauthorized' + } + */ + + AuthMiddleware, + this.queueController.getQueuePosition + ); + } +} \ No newline at end of file diff --git a/src/routes/superAdmin.route.ts b/src/routes/superAdmin.route.ts new file mode 100644 index 0000000..32de727 --- /dev/null +++ b/src/routes/superAdmin.route.ts @@ -0,0 +1,508 @@ +import { AdminController } from "@/controllers/admin.controller"; +import { SuperAdminController } from "@/controllers/superAdmin.controller"; +import { AddUserFromAdminDto } from "@/dtos/admins.dto"; +import { AddAdminFromSuperAdminDto } from "@/dtos/superAdmins.dto"; +import { Routes } from "@/interfaces"; +import { AuthMiddleware, RoleMiddleware } from "@/middlewares/auth.middleware"; +import { LanguageMiddleware } from "@/middlewares/language.middleware"; +import { ValidationMiddleware } from "@/middlewares/validation.middleware"; +import { Role } from "@prisma/client"; +import { Router } from "express"; + + +export class SuperAdminRoute implements Routes { + public path = '/super-admin'; + public router = Router(); + public superAdminController = new SuperAdminController(); + public adminController = new AdminController(); + + constructor() { + this.initializeRoutes(); + } + private initializeRoutes() { + + // ADMIN ROUTES + this.router.post( + '/super-admin/admins', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Admin data', + required: true, + schema: { + $email: 'admin@example.com', + $name: 'Jane Smith', + $password: 'SecurePass123!', + $phone: '1234567890', + $gender: 'MALE or FEMALE', + $date_of_birth: '1990-01-01' + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[201] = { + description: 'Admin added successfully', + schema: { + data: { + email: 'admin@example.com', + name: 'Jane Smith', + role: 'ADMIN', + username: 'janesmith', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1990-01-01T00:00:00.000Z', + photo_url: null, + isVerified: true, + hasCompletedProfile: true + }, + messageEn: 'Admin added successfully', + messageAr: "تم إضافة المسؤول بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + ValidationMiddleware(AddAdminFromSuperAdminDto), + this.superAdminController.addAdmin, + ); + + this.router.get( + '/super-admin/admins', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Admins retrieved successfully', + schema: { + data: [{ + id: '1', + email: 'admin@example.com', + name: 'Jane Smith', + role: 'ADMIN', + username: 'janesmith', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1990-01-01T00:00:00.000Z', + photo_url: null, + isVerified: true, + hasCompletedProfile: true + }], + messageEn: 'Admins retrieved successfully', + messageAr: "تم استرجاع المسؤولين بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + this.superAdminController.getAllAdmins, + ); + + this.router.get( + '/super-admin/admins/:id', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Admin ID', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Admin retrieved successfully', + schema: { + data: { + email: 'admin@example.com', + name: 'Jane Smith', + role: 'ADMIN', + username: 'janesmith', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1990-01-01T00:00:00.000Z', + photo_url: null, + isVerified: true, + hasCompletedProfile: true + }, + messageEn: 'Admin retrieved successfully', + messageAr: "تم استرجاع المسؤول بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + this.superAdminController.getAdminById, + ) + + // DOCTOR ROUTES + this.router.post( + '/super-admin/doctors', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Doctor data', + required: true, + schema: { + $email: 'doctor@example.com', + $name: 'Dr. John Doe', + $phone: '1234567890', + $gender: 'MALE or FEMALE', + $date_of_birth: '1990-01-01', + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[201] = { + description: 'Doctor added successfully', + schema: { + data: { email: 'doctor@example.com', name: 'Dr. Smith', role: 'DOCTOR', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, hasCompletedProfile: false, + doctor: { specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null }, photoUrl: null }, + messageEn: 'Doctor added successfully', + messageAr: "تم إضافة الطبيب بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + LanguageMiddleware, + ValidationMiddleware(AddUserFromAdminDto), + this.adminController.addDoctor, + ); + + this.router.post( + '/super-admin/nurses', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['body'] = { + in: 'body', + description: 'Nurse data', + required: true, + schema: { + $email: 'nurse@example.com', + $name: 'Nurse Jane', + $phone: '1234567890', + $gender: 'MALE or FEMALE', + $date_of_birth: '1995-06-15', + years_of_experience: 3 + } + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[201] = { + description: 'Nurse added successfully', + schema: { + data: { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + role: 'NURSE', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + isVerified: true, + hasCompletedProfile: false, + photo_url: null, + nurse: { + account_status: 'APPROVED', + years_of_experience: 3, + brief: null, + nationalCardUrl: null, + bonusFileUrl: null + } + }, + messageEn: 'Nurse account created successfully.', + messageAr: '.تم إنشاء حساب الممرض بنجاح' + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + ValidationMiddleware(AddUserFromAdminDto), + this.adminController.addNurse, + ); + + this.router.get( + '/super-admin/doctors', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Doctors retrieved successfully', + schema: { + data: [{ id: '1', email: 'doctor@example.com', name: 'Dr. Smith', role: 'DOCTOR', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, hasCompletedProfile: false, + photoUrl: null , + doctor: { + specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null , account_status: 'APPROVED', + mastersCertificateUrl: '', graduationCertificateUrl: '', fellowshipCertificateUrl: '', professionalPracticeCardUrl: '', membershipCardUrl: '', unionSpecializationCertificateUrl: '' + }}], + messageEn: 'Doctors retrieved successfully', + messageAr: "تم استرجاع الأطباء بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + LanguageMiddleware, + this.adminController.getAllDoctors, + ); + + this.router.get( + '/super-admin/nurses', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Nurses retrieved successfully', + schema: { + data: [ + { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + role: 'NURSE', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + isVerified: true, + hasCompletedProfile: true, + photo_url: null, + nurse: { + account_status: 'APPROVED', + years_of_experience: 3, + brief: 'Experienced nurse in ICU', + nationalCardUrl: '', + bonusFileUrl: '' + } + } + ], + messageEn: 'Nurses retrieved successfully', + messageAr: "تم استرجاع بيانات الممرضين بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + this.adminController.getAllNurses, + ); + + this.router.get( + '/super-admin/doctors/:id', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Doctor ID', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.parameters['accept-language'] = { + in: 'header', + description: 'Language preference (en or ar)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Doctor retrieved successfully', + schema: { + data: { email: 'doctor@example.com', name: 'Dr. Smith', role: 'DOCTOR', + username: 'smith', phone : '1234567890', gender: 'MALE', isVerified: false, hasCompletedProfile: false, + photoUrl: null , + doctor: { + specialization: {key: 'CARDIOLOGY' , value: 'Cardiology'} , avg_time: null , account_status: 'APPROVED', + mastersCertificateUrl: '', graduationCertificateUrl: '', fellowshipCertificateUrl: '', professionalPracticeCardUrl: '', membershipCardUrl: '', unionSpecializationCertificateUrl: '' + } }, + messageEn: 'Doctor retrieved successfully', + messageAr: "تم استرجاع الطبيب بنجاح" + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + LanguageMiddleware, + this.adminController.getDoctorById, + ) + + this.router.get( + '/super-admin/nurses/:id', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Nurse ID', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'header', + description: 'Bearer access token (sent via Authorization cookie)', + required: false, + type: 'string' + } + #swagger.responses[200] = { + description: 'Nurse retrieved successfully', + schema: { + data: { + id: '1', + email: 'nurse@example.com', + name: 'Nurse Jane', + username: 'jane', + phone: '1234567890', + gender: 'FEMALE', + date_of_birth: '1995-06-15', + role: 'NURSE', + isVerified: true, + hasCompletedProfile: true, + photo_url: null, + nurse: { + account_status: 'APPROVED', + years_of_experience: 3, + brief: 'Experienced nurse in ICU', + nationalCardUrl: '', + bonusFileUrl: '' + } + }, + messageEn: 'Nurse retrieved successfully', + messageAr: "تم استرجاع بيانات الممرض بنجاح." + } + } + #swagger.responses[404] = { + description: 'Nurse not found', + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + this.adminController.getNurseById, + ); + + // CLINIC ROUTES + this.router.get( + '/super-admin/clinics', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Get clinics successful', + schema: { + data: [ + { + id: 'clinic-uuid', + name: 'Clinic Name', + is_active: false, + opening_at: '08:00', + closing_at: '16:00', + address: '123 Main St, City, Country', + address_maps_link: 'https://maps.google.com/?q=123+Main+St,+City,+Country', + phone: '1234567890', + canPayOnline: true + } + ], + messageEn: 'Clinics retrieved successfully', + messageAr: "تم استرجاع بيانات العيادات بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + this.adminController.getAllClinics, + ); + this.router.get( + '/super-admin/clinics/:id', + /* + #swagger.tags = ['Super Admin'] + #swagger.parameters['id'] = { + in: 'path', + description: 'Clinic ID', + required: true, + type: 'string' + } + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Clinic retrieved successfully', + schema: { + data: { + id: 'clinic-uuid', + name: 'Clinic Name', + is_active: false, + opening_at: '08:00', + closing_at: '16:00', + address: '123 Main St, City, Country', + address_maps_link: 'https://maps.google.com/?q=123+Main+St,+City,+Country', + phone: '1234567890', + canPayOnline: true + }, + messageEn: 'Clinic retrieved successfully', + messageAr: "تم استرجاع بيانات العيادة بنجاح." + } + } + */ + AuthMiddleware, + RoleMiddleware(Role.SUPER_ADMIN), + this.adminController.getClinicById, + ); + } +} \ No newline at end of file diff --git a/src/routes/user.route.ts b/src/routes/user.route.ts new file mode 100644 index 0000000..a12756f --- /dev/null +++ b/src/routes/user.route.ts @@ -0,0 +1,135 @@ +import { UsersController } from "@/controllers/user.controller"; +import { PasswordCheckDto, UpdateUserProfileDto } from "@/dtos/users.dto"; +import { Routes } from "@/interfaces"; +import { AuthMiddleware } from "@/middlewares/auth.middleware"; +import { uploadImage } from "@/middlewares/multer.middleware"; +import { ValidationMiddleware } from "@/middlewares/validation.middleware"; +import { errorWrapper } from "@/utils/errorWrapper"; +import { Router } from "express"; + +export class UsersRoute implements Routes { + public path = '/users' + public router = Router(); + public usersController = new UsersController(); + constructor() { + this.initializeRoutes(); + } + + private initializeRoutes() { + // User Profile Picture Routes + this.router.patch( + `${this.path}/profile-picture`, + /* + #swagger.path = '/users/profile-picture' + #swagger.tags = ['Users'] + #swagger.consumes = ['multipart/form-data'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['profilePicture'] = { + in: 'formData', + type: 'file', + required: true, + description: 'Profile picture file' + } + #swagger.responses[200] = { + description: 'Profile picture updated successfully', + schema: { + messageEn: 'Profile picture updated successfully', + messageAr: "تم تحديث صورة الملف الشخصي بنجاح" + } + } + */ + AuthMiddleware, + uploadImage.single('profilePicture'), + ValidationMiddleware(null, false, false, false, true), + errorWrapper(this.usersController.updateProfilePicture) + ); + this.router.get( + `${this.path}/profile-picture`, + /* + #swagger.path = '/users/profile-picture' + #swagger.tags = ['Users'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Get profile picture successful', + schema: { + data: { + url: 'https://res.cloudinary.com/your-cloud-name/image/upload/v1696543210/doctors/profile_pictures/doctor_1_profile_picture_1696543210.jpg' + }, + messageEn: 'Profile picture retrieved successfully', + messageAr: "تم استرجاع صورة الملف الشخصي بنجاح" + } + } + */ + AuthMiddleware, + errorWrapper(this.usersController.getProfilePicture) + ) + this.router.delete( + `${this.path}/profile-picture`, + /* + #swagger.path = '/users/profile-picture' + #swagger.tags = ['Users'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.responses[200] = { + description: 'Profile picture deleted successfully', + schema: { + messageEn: 'Profile picture deleted successfully', + messageAr: "تم حذف صورة الملف الشخصي بنجاح" + } + } + */ + AuthMiddleware, + errorWrapper(this.usersController.deleteProfilePicture) + ); + + this.router.patch( + `${this.path}/update-profile`, + /* + #swagger.path = '/users/update-profile' + #swagger.tags = ['Users'] + #swagger.parameters['Authorization'] = { + in: 'cookie', + description: 'Bearer token for authentication', + required: true, + type: 'string' + } + #swagger.parameters['body'] = { + in: 'body', + description: 'User profile update data', + required: false, + schema: { + name: 'John Doe', + phone: '1234567890', + gender: 'MALE or FEMALE', + dateOfBirth: '1990-01-01', + availability_type: 'ONLINE, OFFLINE, BOTH or UNSET' + } + } + #swagger.responses[200] = { + description: 'Profile updated successfully', + schema: { + messageEn: 'Profile updated successfully', + messageAr: "تم تحديث الملف الشخصي بنجاح" + } + } + */ + AuthMiddleware, + ValidationMiddleware(UpdateUserProfileDto, false, false, true), + errorWrapper(this.usersController.updateUserProfile) + ); + } +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..4158f9e --- /dev/null +++ b/src/server.ts @@ -0,0 +1,37 @@ +import { App } from '@/app'; +import { AuthRoute } from '@routes/auth.route'; +import { AdminRoute } from '@routes/admin.route'; +import { ValidateEnv } from '@utils/validateEnv'; +import { FabricRoute } from '@routes/fabric.route'; +import { SuperAdminRoute } from './routes/superAdmin.route'; +import { DoctorsRoute } from './routes/doctors.route'; +import { ClinicRoute } from './routes/clinic.route'; +import { AppointmentRoute } from './routes/appointment.route'; +import { QueueRoute } from './routes/queue.route'; +import { NurseRoute } from './routes/nurse.route'; +import { UsersRoute } from './routes/user.route'; +import { MedicalRecordRoute } from './routes/medical-record.route'; +import { logger } from '@utils/logger'; + +import { AiAppointmentsRoute } from './routes/ai_appointments.route'; +ValidateEnv(); + +// Prevent the process from crashing on unhandled async errors +process.on('unhandledRejection', (reason: any) => { + logger.error(`⚠️ Unhandled Promise Rejection: ${reason?.message || reason}`); + if (reason?.stack) logger.error(reason.stack); +}); + +process.on('uncaughtException', (err: Error) => { + logger.error(`⚠️ Uncaught Exception: ${err.message}`); + if (err.stack) logger.error(err.stack); +}); + +const app = new App( + [ + new AuthRoute(), new FabricRoute(), new AdminRoute(), + new SuperAdminRoute(), new DoctorsRoute(), new ClinicRoute(), new AppointmentRoute(), new QueueRoute(), + new UsersRoute(), new NurseRoute(),new MedicalRecordRoute(), new AiAppointmentsRoute() + ]); + +app.listen(); diff --git a/src/services/admin.service.ts b/src/services/admin.service.ts new file mode 100644 index 0000000..798d6b9 --- /dev/null +++ b/src/services/admin.service.ts @@ -0,0 +1,470 @@ +import { DoctorAccountStatus, NurseAccountStatus, PrismaClient, Role } from '@prisma/client'; +import { hash } from 'bcrypt'; +import { Service } from 'typedi'; +import { AddUserFromAdminDto, DoctorFromAdminResponseDto, NurseFromAdminResponseDto } from '@/dtos/admins.dto'; +import { HttpException } from '@/exceptions/HttpException'; +import { ErrorMessages, createBilingualError } from '@/utils/errorMessages'; +import { User } from '@/interfaces'; +import { SENDER_EMAIL } from '@/config'; +import { transporter } from '@/utils/nodeMailerService'; +import { ClinicResponseDto } from '@/dtos/clinics.dto'; + +// TO BE CHANGED +const prisma = new PrismaClient(); + +@Service() +export class AdminService { + public async addDoctor(doctorData: AddUserFromAdminDto): Promise { + // Check if email already exists + const existingUser = await prisma.user.findUnique({ + where: { email: doctorData.email } + }); + + if (existingUser) { + const error = createBilingualError(409, ErrorMessages.EMAIL_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Generate username from email + const username = doctorData.email.split('@')[0]; + + // Check if username exists + const existingUsername = await prisma.user.findUnique({ + where: { username } + }); + + if (existingUsername) { + const error = createBilingualError(409, ErrorMessages.USERNAME_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Generate default password (doctor can change it later) + const defaultPassword = 'doctor123'; // Should be changed on first login + const hashedPassword = await hash(defaultPassword, 10); + + // Create user with doctor role + const createdUser = await prisma.user.create({ + data: { + email: doctorData.email, + name: doctorData.name, + username, + phone: doctorData.phone, + gender: doctorData.gender, + date_of_birth: new Date(doctorData.date_of_birth), + password_hash: hashedPassword, + role: Role.DOCTOR, + isVerified: true, + hasCompletedProfile: false, + }, + }); + + // Create doctor profile + await prisma.doctor.create({ + data: { + id: createdUser.id, + specialization: "IMMUNOLOGY", // This is now the KEY (e.g., "IMMUNOLOGY") + account_status: DoctorAccountStatus.APPROVED, + } + }); + const createdDoctor = await prisma.user.findUnique({ + where: { id: createdUser.id }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + role: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + doctor: { + select: { + specialization: true, + } + }, + } + }); + + return createdDoctor; + + } + + public async addNurse(nurseData: AddUserFromAdminDto): Promise { + const existingUser = await prisma.user.findUnique({ + where: { + email: nurseData.email + } + }); + + if (existingUser) { + const error = createBilingualError(409, ErrorMessages.EMAIL_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const username = nurseData.email.split('@')[0]; + + const existingUsername = await prisma.user.findUnique({ + where: { + username + } + }); + + if (existingUsername) { + const error = createBilingualError(409, ErrorMessages.USERNAME_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const defaultPassword = 'nurse123'; + const hashedPassword = await hash(defaultPassword, 10); + + const createdUser = await prisma.user.create({ + data: { + email: nurseData.email, + name: nurseData.name, + username, + phone: nurseData.phone, + gender: nurseData.gender, + date_of_birth: new Date(nurseData.date_of_birth), + password_hash: hashedPassword, + role: Role.NURSE, + isVerified: true, + hasCompletedProfile: false, + }, + }); + + await prisma.nurse.create({ + data: { + id: createdUser.id, + years_of_experience: nurseData.years_of_experience, + account_status: NurseAccountStatus.APPROVED, + } + }); + const createdNurse = await prisma.user.findUnique({ + where: { + id: createdUser.id + }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + role: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + nurse: { + select: { + years_of_experience: true, + } + }, + } + }); + + return createdNurse; + + } + + public async getAllDoctors(): Promise { + + const doctors = await prisma.user.findMany({ + where: { role: Role.DOCTOR }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + role: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + doctor: { + select: { + specialization: true, + avg_time: true, + account_status: true, + fellowshipCertificateUrl: true, + graduationCertificateUrl: true, + mastersCertificateUrl: true, + membershipCardUrl: true, + unionSpecializationCertificateUrl: true, + professionalPracticeCardUrl: true, + } + }, + } + }); + + return doctors; + + } + + public async getAllNurses(): Promise { + const nurses = await prisma.user.findMany({ + where: { + role: Role.NURSE + }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + role: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + nurse: { + select: { + account_status: true, + years_of_experience: true, + brief: true, + nationalCardUrl: true, + bonusFileUrl: true, + } + }, + } + }); + + return nurses; + + } + + public async getDoctorById(id: string): Promise { + + const doctor = await prisma.user.findUnique({ + where: { id, role: Role.DOCTOR }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + role: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + doctor: { + select: { + specialization: true, + avg_time: true, + account_status: true, + fellowshipCertificateUrl: true, + graduationCertificateUrl: true, + mastersCertificateUrl: true, + membershipCardUrl: true, + unionSpecializationCertificateUrl: true, + professionalPracticeCardUrl: true, + } + }, + } + }); + + if (!doctor) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + return doctor; + } + + public async getNurseById(id: string): Promise { + const nurse = await prisma.user.findUnique({ + where: { id, role: Role.NURSE }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + role: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + nurse: { + select: { + account_status: true, + years_of_experience: true, + brief: true, + nationalCardUrl: true, + bonusFileUrl: true, + } + }, + } + }); + + if (!nurse) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + return nurse; + } + + public async getUnverifiedDoctors(): Promise { + + const unverifiedDoctors = await prisma.user.findMany({ + where: { role: Role.DOCTOR, doctor: { account_status: DoctorAccountStatus.PENDING } }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + isVerified: true, + photo_url: true, + doctor: { + select: { + specialization: true, + avg_time: true, + account_status: true, + fellowshipCertificateUrl: true, + graduationCertificateUrl: true, + mastersCertificateUrl: true, + membershipCardUrl: true, + unionSpecializationCertificateUrl: true, + professionalPracticeCardUrl: true, + } + }, + }, + }); + return unverifiedDoctors + + } + + public async getUnverifiedNurses(): Promise { + + const unverifiedNurses = await prisma.user.findMany({ + where: { + role: Role.NURSE, + nurse: { + account_status: NurseAccountStatus.PENDING + } + }, + select: { + id: true, + name: true, + email: true, + username: true, + phone: true, + gender: true, + date_of_birth: true, + isVerified: true, + hasCompletedProfile: true, + photo_url: true, + nurse: { + select: { + account_status: true, + years_of_experience: true, + brief: true, + nationalCardUrl: true, + bonusFileUrl: true, + } + }, + }, + }); + return unverifiedNurses + + } + + public async updateDoctorVerificationStatus(doctorId: string, isApproved: boolean | null): Promise { + + const doctor = await prisma.user.findUnique({ + where: { id: doctorId, role: Role.DOCTOR }, + }); + if (!doctor) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + let accountStatus: DoctorAccountStatus; + if (isApproved === true) { + accountStatus = DoctorAccountStatus.APPROVED; + } else if (isApproved === false) { + accountStatus = DoctorAccountStatus.REJECTED; + } else { + accountStatus = DoctorAccountStatus.PENDING; + } + + await prisma.user.update({ + where: { id: doctorId }, + data: { + doctor: { + update: { + account_status: accountStatus, + } + } + } + }); + } + + public async sendVerificationStatusEmail(userId: string, isApproved: boolean): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { email: true, name: true } + }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const mailOptions = { + from: SENDER_EMAIL, + to: user.email, + subject: isApproved ? 'User Account Approved - HoloCura' : 'User Account Rejected - HoloCura', + html: ` +

Dear ${user.name},

+

Your account has been ${isApproved ? 'approved' : 'rejected'}.

+

Thank you for using our platform.

+

Best regards,
HoloCura Team

+ ` + }; + + await transporter.sendMail(mailOptions); + } + + public async updateNurseVerificationStatus(nurseId: string, isApproved: boolean | null): Promise { + + const nurse = await prisma.user.findUnique({ + where: { id: nurseId, role: Role.NURSE }, + }); + if (!nurse) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + let accountStatus: NurseAccountStatus; + if (isApproved === true) { + accountStatus = NurseAccountStatus.APPROVED; + } else if (isApproved === false) { + accountStatus = NurseAccountStatus.REJECTED; + } else { + accountStatus = NurseAccountStatus.PENDING; + } + + await prisma.user.update({ + where: { id: nurseId }, + data: { + nurse: { + update: { + account_status: accountStatus, + } + } + } + }); + } +} \ No newline at end of file diff --git a/src/services/ai_appointments.service.ts b/src/services/ai_appointments.service.ts new file mode 100644 index 0000000..f53257b --- /dev/null +++ b/src/services/ai_appointments.service.ts @@ -0,0 +1,198 @@ +import { B2_BUCKET_NAME, GROQ_API_KEY } from "@/config"; +import prisma from "@/config/prisma"; +import s3Client from "@/config/storage"; +import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import Groq, { toFile } from "groq-sdk"; +import { FileLike } from "groq-sdk/uploads"; +import { Service } from "typedi"; + + +@Service() +export class AiAppointmentsService { + private appointments = prisma.appointment; + private groq = new Groq({ + apiKey: GROQ_API_KEY + }); + + public async checkAppointmentExistence(appointmentId: string): Promise { + const appointment = await this.appointments.findUnique({ + where: { id: appointmentId } + }); + return appointment !== null; + } + + public async getUploadUrl(objectKey: string): Promise { + + const command = new PutObjectCommand({ + Bucket: B2_BUCKET_NAME, + Key: objectKey, + ContentType: 'audio/webm', + }); + + const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); // URL valid for 1 hour + + return uploadUrl; + } + + public async processSeparateAudioAI(doctorKey: string, patientKey: string): Promise { + const [doctorAudio, patientAudio] = await Promise.all([ + this.getFromB2(doctorKey), + this.getFromB2(patientKey) + ]); + + const [doctorTranscription, patientTranscription] = await Promise.all([ + this.transcribeAudio(doctorAudio), + this.transcribeAudio(patientAudio) + ]); + + const finalScript = await this.mergeTranscriptions(doctorTranscription, patientTranscription); + return finalScript; + } + + public async processMixedAudioAI(mixedKey: string): Promise { + const mixedAudio = await this.getFromB2(mixedKey); + const rawTranscript = await this.transcribeAudio(mixedAudio); + const finalScript = await this.formatMixedAudioScript(rawTranscript.text); + return finalScript; + } + + public async generateSOAP(finalScript: string, prompt: string): Promise { + const chatCompletion = await this.groq.chat.completions.create({ + messages: [ + { + role: "system", + content: prompt ? prompt : `You are an expert clinical AI scribe specializing in rheumatology and autoimmune diseases. +Your task is to analyze the provided doctor-patient consultation transcript and generate a highly professional, concise medical SOAP note. + +CRITICAL INSTRUCTIONS: +1. You must output ONLY a valid JSON object. +2. The JSON MUST contain exactly these four keys: "subjective", "objective", "assessment", and "plan". +3. The input transcript may contain Egyptian Arabic, English, or a mix of both. You MUST translate all clinical findings into standard professional medical English. + +CLINICAL GUIDELINES: +- Subjective: Focus on the chief complaint, history of present illness, family medical history, pain levels, and specific autoimmune symptoms (e.g., duration of morning stiffness, fatigue). +- Objective: Extract any physical examination findings mentioned by the doctor (e.g., synovitis, swollen MCP/PIP joints, range of motion) and any lab/imaging results discussed. +- Assessment: State the suspected or confirmed diagnosis (e.g., Rheumatoid Arthritis flare, SLE) based on the context. +- Plan: List the treatment strategy clearly, including medication changes (e.g., Methotrexate, NSAIDs, Biologics), ordered labs (e.g., CRP, ESR, Anti-CCP), and follow-up instructions.` + }, + { + role: "user", + content: `Here is the consultation transcript:\n\n${finalScript}` + } + ], + model: "llama-3.3-70b-versatile", // 70B is highly recommended for complex medical reasoning + temperature: 0.1, // Low temperature ensures factual consistency and strict JSON compliance + response_format: { type: "json_object" } // FORCES the output to be strictly JSON + }); + + // Extract the JSON string from the LLM response + const jsonString = chatCompletion.choices[0]?.message?.content; + + // Parse it into a native JavaScript object + const soapNote = JSON.parse(jsonString); + + return soapNote; + } + + private async getFromB2(objectKey: string): Promise { + const getCommand = new GetObjectCommand({ + Bucket: B2_BUCKET_NAME, + Key: objectKey, + }); + + const b2Response = await s3Client.send(getCommand); + + const audioStream = await toFile(b2Response.Body as ReadableStream, 'audio.webm'); + return audioStream; + } + + private async transcribeAudio(audioFile: FileLike): Promise { + const result = await this.groq.audio.transcriptions.create({ + file: audioFile, + model: "whisper-large-v3", + response_format: "verbose_json", + language: "ar" + }); + return result; + } + + private async mergeTranscriptions(doctorTranscription: any, patientTranscription: any): Promise { + const doctorSegments = doctorTranscription?.segments || []; + const patientSegments = patientTranscription?.segments || []; + + // Tag every segment with the correct speaker + const taggedDoctor = doctorSegments.map(seg => ({ + speaker: "DOCTOR", + start: seg.start, + text: seg.text.trim() + })); + + const taggedPatient = patientSegments.map(seg => ({ + speaker: "PATIENT", + start: seg.start, + text: seg.text.trim() + })); + + // Combine both arrays and sort them chronologically by the 'start' time + const combinedSegments = [...taggedDoctor, ...taggedPatient].sort((a, b) => a.start - b.start); + + // Build the final script string, grouping continuous speech + let finalScript = ""; + let currentSpeaker = null; + + for (const segment of combinedSegments) { + // Ignore empty segments + if (!segment.text) continue; + + if (segment.speaker !== currentSpeaker) { + // The speaker changed. Start a new line with the timestamp and name. + finalScript += `\n[${this.formatTime(segment.start)}] ${segment.speaker}: ${segment.text}`; + currentSpeaker = segment.speaker; + } else { + // The same person is still talking. Just append the text to the current line. + finalScript += ` ${segment.text}`; + } + } + + return finalScript.trim(); // Remove leading/trailing whitespace + } + + // Helper function to convert 65.5 seconds into "01:05" format + private formatTime = (seconds) => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + }; + + private async formatMixedAudioScript(rawTranscript) { + const chatCompletion = await this.groq.chat.completions.create({ + messages: [ + { + role: "system", + content: `You are an expert clinical transcriber specializing in rheumatology and autoimmune diseases. +I will provide you with a raw, continuous audio transcript from a single microphone in a clinic. It contains both the doctor and the patient speaking, but the text is mixed together. + +Your EXACT job is to separate this text into a chronological script using context clues. +- The DOCTOR typically asks clinical questions, prescribes, and uses medical terminology. +- The PATIENT typically describes symptoms (e.g., joint pain, stiffness), answers questions, and speaks colloquially. + +Rules: +1. You must output the conversation using exactly two tags: [DOCTOR]: and [PATIENT]: +2. Do not summarize. Preserve the exact words spoken. +3. Do not add any introductory or concluding text. Output ONLY the script. +4. If the language is Arabic or a mix of Arabic/English, keep the original language intact in the script.` + }, + { + role: "user", + content: rawTranscript + } + ], + model: "llama-3.3-70b-versatile", + temperature: 0.1, // Keep it very low so it doesn't hallucinate new words + max_tokens: 4000 + }); + + return chatCompletion.choices[0]?.message?.content || ""; + } +} \ No newline at end of file diff --git a/src/services/appointment.service.ts b/src/services/appointment.service.ts new file mode 100644 index 0000000..67cd59c --- /dev/null +++ b/src/services/appointment.service.ts @@ -0,0 +1,1748 @@ +import prisma from '@/config/prisma'; +import { DayOfWeek } from '@prisma/client'; +import { AvailableDay } from '@/interfaces'; +import { Service, Container } from 'typedi'; +import { TimeSlot } from '@/interfaces'; +import { HttpException } from "@/exceptions/HttpException"; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { PatientTodayAppointment, DoctorAppointment, DoctorScheduleDay, PatientAppointment, DoctorSchedule, checkExistingAppointments, ConflictingAppointment, DoctorVacations, Vacations, AppointmentData, AppointmentEventData } from '@/interfaces/appointments.interface'; +import { QueueService } from './queue.service'; +import { start } from 'repl'; +import { RtcRole, RtcTokenBuilder } from 'agora-token'; +import { Agora_APP_CERTIFICATE, Agora_APP_ID } from '@/config'; + +@Service() +export class AppointmentService { + + private queueService = new QueueService(); + + public async getAvailableDays(doctorId: string, clinicId: string | null): Promise { + const daysAhead = 30 + const availableDays: AvailableDay[] = []; + + const schedules = await prisma.doctorSchedule.findMany({ + where: { + doctor_id: doctorId, + clinic_id: clinicId, + deleted_at: null + }, + select: { + day_of_week: true, + start_time: true, + end_time: true, + is_online: true, + slot_duration: true, + buffer_time: true, + is_active: true, + break_start: true, + break_end: true, + + } + }); + + if (schedules.length === 0) { + return []; + } + + /* + creates a map --> avoid searching through schedules every time we need to check if a doctor works on a specific day + key = day of week , value = the schedule object for that day + + instead of --> { day_of_week: 'MONDAY', start_time: '09:00', end_time: '17:00', slot_duration: 20, buffer_time: 10 } + will be --> 'MONDAY' => { start_time: '09:00', end_time: '17:00', ... } + */ + const scheduleMap = new Map(); + schedules.forEach(schedule => { + scheduleMap.set(schedule.day_of_week, schedule); + }); + + const now = this.getNowInEgypt(); + const today = this.getTodayBoundaries(now).start; + + for (let i = 0; i < daysAhead; i++) { + // create a copy from today --> if we used today directly it will be modified to today + 1 --> tomorrow date + const currentDate = new Date(today); + currentDate.setUTCDate(today.getUTCDate() + i); // current day now is = today + 1 + + const dayOfWeek = this.getDayOfWeek(currentDate.getUTCDay()); + const schedule = scheduleMap.get(dayOfWeek); + + // skip if doctor doesnt work on this day + if (!schedule) { + continue; + } + + + if (!schedule.is_online && !clinicId) { + const error = createBilingualError(400, ErrorMessages.CLINIC_REQUIRED_FOR_OFFLINE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!schedule.is_active) { + if (schedule.break_start && schedule.break_end) { + const breakStart = new Date(schedule.break_start); + breakStart.setUTCHours(0, 0, 0, 0); + + const breakEnd = new Date(schedule.break_end); + breakEnd.setUTCHours(23, 59, 59, 999); + + const currentDateOnly = new Date(currentDate); + currentDateOnly.setUTCHours(0, 0, 0, 0); + + if (currentDateOnly >= breakStart && currentDateOnly <= breakEnd) { + continue; + } + } + } + + const availableSlots = await this.getAvailableSlots(doctorId, clinicId, this.formatDate(currentDate)); + const hasAvailableSlots = availableSlots.length > 0; + + + if (hasAvailableSlots) { + availableDays.push({ + date: this.formatDate(currentDate), + dayOfWeek: dayOfWeek, + displayDate: this.formatDisplayDate(currentDate) + }); + } + } + return availableDays; + } + + + public async getAvailableSlots(doctorId: string, clinicId: string | null, date: string): Promise { + const requestedDate = new Date(date); + const dayOfWeek = this.getDayOfWeek(requestedDate.getUTCDay()); + + const now = this.getNowInEgypt(); + const { start: today } = this.getTodayBoundaries(now); + const { start: requestedDateOnly } = this.getTodayBoundaries(requestedDate); + + if (requestedDateOnly < today) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_IN_PAST); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const schedules = await prisma.doctorSchedule.findMany({ + where: { + day_of_week: dayOfWeek, + doctor_id: doctorId, + clinic_id: clinicId, + deleted_at: null, + }, + select: { + start_time: true, + end_time: true, + slot_duration: true, + buffer_time: true, + is_online: true, + }, + + orderBy: { + start_time: 'asc' + } + }); + + + if (schedules.length === 0) { + return []; + } + const startOfDay = new Date(requestedDate); + startOfDay.setUTCHours(0, 0, 0, 0); + + const endOfDay = new Date(requestedDate); + endOfDay.setUTCHours(23, 59, 59, 999); + + const existingAppointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + scheduled_time: { + gte: startOfDay, + lte: endOfDay, + }, + status: { + in: ['CONFIRMED', 'COMPLETED'] + }, + deleted_at: null, + }, + select: { + scheduled_time: true, + end_time: true, + is_online: true, + } + }); + + const allSlots: TimeSlot[] = []; + + + for (const schedule of schedules) { + const isOnline = schedule.is_online; + + if (!schedule.is_online && !clinicId) { + const error = createBilingualError(400, ErrorMessages.CLINIC_REQUIRED_FOR_OFFLINE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const thisScheduleSlots = this.generateTimeSlots(schedule.start_time, schedule.end_time, schedule.slot_duration, schedule.buffer_time, isOnline); + + const finalSlots = thisScheduleSlots.map(slot => { + const slotStart = this.parseTimeToDate(requestedDate, slot.start); + const slotEnd = this.parseTimeToDate(requestedDate, slot.end); + + const isBooked = existingAppointments.some(appt => { + const apptStart = new Date(appt.scheduled_time); + const apptEnd = new Date(appt.end_time); + return this.doesSlotOverlap(slotStart, slotEnd, apptStart, apptEnd); + }); + + const isInPast = slotStart <= now; + + return { + start: slot.start, + end: slot.end, + available: !isBooked && !isInPast, + online: isOnline + } satisfies TimeSlot; + }); + + allSlots.push(...finalSlots) + } + + allSlots.sort((a, b) => a.start.localeCompare(b.start)); + return allSlots; + + } + + public async bookAppointment(patientId: string, doctorId: string, clinicId: string | null, scheduledTime: Date): Promise { + const existingAppointment = await prisma.appointment.findFirst({ + where: { + patient_id: patientId, + doctor_id: doctorId, + clinic_id: clinicId, + scheduled_time: scheduledTime, + deleted_at: null, + } + }) + + if (existingAppointment) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ALREADY_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const schedule = await prisma.doctorSchedule.findFirst({ + where: { + doctor_id: doctorId, + clinic_id: clinicId, + is_active: true, + deleted_at: null, + day_of_week: this.getDayOfWeek(scheduledTime.getUTCDay()), + }, + select: { + slot_duration: true, + is_online: true, + } + }); + + if (!schedule) { + const error = createBilingualError(400, ErrorMessages.DAY_OUTSIDE_SCHEDULE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const isOnline = schedule.is_online; + + if (!isOnline && !clinicId) { + const error = createBilingualError(400, ErrorMessages.CLINIC_REQUIRED_FOR_OFFLINE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const endTime = new Date(scheduledTime.getTime() + schedule.slot_duration * 60000); + + + await prisma.appointment.create({ + data: { + patient_id: patientId, + doctor_id: doctorId, + clinic_id: isOnline ? null : clinicId, + scheduled_time: scheduledTime, + status: 'CONFIRMED', + slot_duration: schedule.slot_duration, + end_time: endTime, + is_online: isOnline, + estimated_time: schedule.slot_duration, + } + }); + } + + public async getPatientAppointments(patientId: string): Promise { + const appointments = await prisma.appointment.findMany({ + where: { + patient_id: patientId, + }, + select: { + id: true, + scheduled_time: true, + status: true, + is_online: true, + slot_duration: true, + end_time: true, + doctor: { + select: { + id: true, + name: true, + photo_url: true, + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + } + }, + orderBy: { + scheduled_time: 'asc', + } + }); + return appointments.map(appointment => ({ + id: appointment.id, + doctor_id: appointment.doctor.id, + clinic_id: appointment.clinic ? appointment.clinic.id : null, + status: appointment.status, + is_online: appointment.is_online, + slot_duration: appointment.slot_duration, + doctor_name: appointment.doctor.name, + doctor_profile_pic: appointment.doctor.photo_url, + appointment_date: this.formatDate(appointment.scheduled_time), + start_time: this.formatTime(appointment.scheduled_time), + end_time: this.formatTime(appointment.end_time), + clinic_name: appointment.clinic ? appointment.clinic.name : null, + clinic_address: appointment.clinic ? appointment.clinic.address : null, + address_maps_link: appointment.clinic ? appointment.clinic.address_maps_link : null, + })); + } + + public async getPatientSelectedAppointment(appointmentId: string, patientId: string): Promise { + const appointment = await prisma.appointment.findFirst({ + where: { + id: appointmentId, + patient_id: patientId, + }, + select: { + id: true, + scheduled_time: true, + status: true, + is_online: true, + slot_duration: true, + end_time: true, + doctor: { + select: { + id: true, + name: true, + photo_url: true, + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + } + } + }); + if (!appointment) { + return null; + } + + return { + id: appointment.id, + doctor_id: appointment.doctor.id, + clinic_id: appointment.clinic ? appointment.clinic.id : null, + status: appointment.status, + is_online: appointment.is_online, + slot_duration: appointment.slot_duration, + doctor_name: appointment.doctor.name, + doctor_profile_pic: appointment.doctor.photo_url, + appointment_date: this.formatDate(appointment.scheduled_time), + start_time: this.formatTime(appointment.scheduled_time), + end_time: this.formatTime(appointment.end_time), + clinic_name: appointment.clinic ? appointment.clinic.name : null, + clinic_address: appointment.clinic ? appointment.clinic.address : null, + address_maps_link: appointment.clinic ? appointment.clinic.address_maps_link : null, + }; + } + + public async getTodayAppointment(patientId: string): Promise { + const result: PatientTodayAppointment[] = []; + + const now = this.getNowInEgypt(); + const { start: today, end: endOfToday } = this.getTodayBoundaries(now); + + const appointments = await prisma.appointment.findMany({ + where: { + patient_id: patientId, + scheduled_time: { + gte: today, + lte: endOfToday, + }, + status: { in: ['CONFIRMED', 'COMPLETED'] }, + }, + select: { + id: true, + scheduled_time: true, + status: true, + is_online: true, + slot_duration: true, + end_time: true, + position: true, + estimated_time: true, + patients_ahead: true, + doctor: { + select: { + id: true, + name: true, + photo_url: true, + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + } + }, + orderBy: { + scheduled_time: 'asc' + } + }); + + if (appointments.length === 0) { + return []; + } + for (const appointment of appointments) { + const queueParameters = await this.queueService.getQueuePosition(appointment.id); + + result.push({ + id: appointment.id, + doctor_id: appointment.doctor.id, + clinic_id: appointment.clinic ? appointment.clinic.id : null, + status: appointment.status, + is_online: appointment.is_online, + slot_duration: appointment.slot_duration, + doctor_name: appointment.doctor.name, + doctor_profile_pic: appointment.doctor.photo_url, + appointment_date: this.formatDate(appointment.scheduled_time), + start_time: this.formatTime(appointment.scheduled_time), + end_time: this.formatTime(appointment.end_time), + clinic_name: appointment.clinic ? appointment.clinic.name : null, + clinic_address: appointment.clinic ? appointment.clinic.address : null, + address_maps_link: appointment.clinic ? appointment.clinic.address_maps_link : null, + position: queueParameters.position, + estimatedWaitMinutes: queueParameters.estimatedWaitMinutes, + patientsAhead: queueParameters.patientsAhead + }); + } + + return result; + } + + public async rescheduleAppointmentByPatient(patientId: string, appointmentId: string, newScheduledTime: Date): Promise { + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId + }, + select: { + id: true, + patient_id: true, + doctor_id: true, + scheduled_time: true, + deleted_at: true, + patient: { + select: { + name: true + } + } + } + }); + + const slotDuration = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + patient_id: patientId, + }, + select: { + slot_duration: true, + }, + }); + const newEndTime = new Date(newScheduledTime.getTime() + slotDuration.slot_duration * 60000); + + await prisma.appointment.update({ + where: { + id: appointmentId, + patient_id: patientId, + }, + data: { + scheduled_time: newScheduledTime, + end_time: newEndTime, + } + }); + + return { + appointmentId: appointment.id, + doctorId: appointment.doctor_id, + patientId: appointment.patient_id, + patientName: appointment.patient.name, + appointmentDate: this.formatDate(appointment.scheduled_time), + startTime: this.formatTime(appointment.scheduled_time), + }; + } + + public async rescheduleAppointmentByDoctor(doctorId: string, appointmentId: string, minutes: number): Promise { + const appointment = await this.getAndValidateAppointment(appointmentId, doctorId); + const appointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + status: "CONFIRMED", + deleted_at: null, + scheduled_time: { + gte: appointment.scheduled_time + } + }, + select: { + id: true, + patient_id: true, + doctor_id: true, + scheduled_time: true, + patient: { + select: { + name: true, + } + } + } + }) + + for (const { id: appointmentId } of appointments) { + await this.rescheduleSingleAppointment(doctorId, appointmentId, minutes); + } + + return appointments.map(appointment => { + const newScheduledTime = new Date(appointment.scheduled_time.getTime() + minutes * 60000); + return { + appointmentId: appointment.id, + doctorId: appointment.doctor_id, + patientId: appointment.patient_id, + patientName: appointment.patient.name, + appointmentDate: this.formatDate(newScheduledTime), + startTime: this.formatTime(newScheduledTime), + }; + }); + + } + + public async enterDoctorSchedule(doctorId: string, clinicId: string | null, workingDay: number, startTime: string, endTime: string, slotDuration: number, bufferTime: number, isOnline: boolean): Promise { + if (clinicId) { + const clinic = await prisma.clinic.findUnique({ + where: { id: clinicId }, + select: { id: true } + }); + + if (!clinic) { + const error = createBilingualError(404, ErrorMessages.CLINIC_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const clinicDoctor = await prisma.clinicDoctor.findUnique({ + where: { + clinic_id_doctor_id: { + clinic_id: clinicId, + doctor_id: doctorId + } + } + }); + + if (!clinicDoctor) { + const error = createBilingualError(403, ErrorMessages.DOCTOR_NOT_ASSOCIATED_WITH_CLINIC); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (isOnline) { + const error = createBilingualError(403, ErrorMessages.EITHER_ONLINE_OR_OFFLINE); + throw new HttpException(error.status, error.message, error.messageAr); + } + } + + if (!clinicId && !isOnline) { + const error = createBilingualError(403, ErrorMessages.EITHER_ONLINE_OR_OFFLINE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const startMinutes = this.timeStringToMinutes(startTime); + const endMinutes = this.timeStringToMinutes(endTime); + const dayOfWeek = this.getDayOfWeek(workingDay); + + if (startMinutes >= endMinutes) { + const error = createBilingualError(400, ErrorMessages.END_TIME_BEFORE_START_TIME); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // prevent time overlap in the same clinic (on different days) + if (clinicId) { + const overlappingClinics = await prisma.doctorSchedule.findMany({ + where: { + doctor_id: doctorId, + clinic_id: { not: clinicId }, + day_of_week: dayOfWeek, + deleted_at: null + }, + select: { + start_time: true, + end_time: true, + } + }); + + for (const schedule of overlappingClinics) { + const existingStartMins = this.timeStringToMinutes(schedule.start_time); + const existingEndMins = this.timeStringToMinutes(schedule.end_time); + + const hasTimeOverlap = (startMinutes < existingEndMins && endMinutes > existingStartMins); + if (hasTimeOverlap) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_CONFLICT_DIFFERENT_CLINIC); + throw new HttpException(error.status, error.message, error.messageAr); + } + } + } + + const sameDaySchedules = await prisma.doctorSchedule.findMany({ + where: { + doctor_id: doctorId, + day_of_week: dayOfWeek, + deleted_at: null + }, + select: { + start_time: true, + end_time: true, + is_online: true, + clinic_id: true, + } + }); + + for (const schedule of sameDaySchedules) { + if (schedule.is_online === isOnline) { + continue; + } + + const existingStartMinutes = this.timeStringToMinutes(schedule.start_time); + const existingEndMinutes = this.timeStringToMinutes(schedule.end_time); + + const hasTimeOverlap = (startMinutes < existingEndMinutes && endMinutes > existingStartMinutes); + + if (hasTimeOverlap) { + const error = createBilingualError(400, ErrorMessages.ONLINE_OFFLINE_CONFLICT); + throw new HttpException(error.status, error.message, error.messageAr); + } + } + + + const existingSchedule = await prisma.doctorSchedule.findFirst({ + where: { + doctor_id: doctorId, + clinic_id: clinicId, + day_of_week: dayOfWeek, + deleted_at: null, + } + }); + + if (existingSchedule) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_ALREADY_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.doctorSchedule.create({ + data: { + doctor_id: doctorId, + clinic_id: clinicId, + day_of_week: dayOfWeek, + start_time: startTime, + end_time: endTime, + slot_duration: slotDuration, + buffer_time: bufferTime, + is_online: isOnline, + is_active: true + } + }); + } + + public async cancelAppointment(userId: string, appointmentId: string): Promise { + // see whether the user is patient or doctor + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId + }, + select: { + id: true, + patient_id: true, + doctor_id: true, + scheduled_time: true, + deleted_at: true, + patient: { + select: { + name: true + } + } + } + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (appointment.deleted_at) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ALREADY_DELETED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (appointment.patient_id !== userId && appointment.doctor_id !== userId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_APPOINTMENT_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.appointment.update({ + where: { + id: appointmentId, + }, + data: { + cancelled_by: appointment.patient_id === userId ? 'PATIENT' : 'DOCTOR', + deleted_at: new Date(), + modified_at: new Date(), + status: 'CANCELLED', + } + }); + + return { + appointmentId: appointment.id, + doctorId: appointment.doctor_id, + patientId: appointment.patient_id, + patientName: appointment.patient.name, + appointmentDate: this.formatDate(appointment.scheduled_time), + startTime: this.formatTime(appointment.scheduled_time), + }; + + // penalty to be added later + }; + + public async getUpcommingDoctorSchedule(doctorId: string): Promise { + const now = this.getNowInEgypt(); + + const appointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + scheduled_time: { + gte: now, + }, + status: 'CONFIRMED', + deleted_at: null, + }, + orderBy: { + scheduled_time: 'asc', + }, + select: { + id: true, + scheduled_time: true, + end_time: true, + slot_duration: true, + status: true, + clinic_id: true, + patient: { + select: { + name: true, + } + }, + clinic: { + select: { + name: true, + address: true, + } + } + } + }); + + const groupedByDate = new Map(); + + appointments.forEach(app => { + const dateKey = this.formatDate(app.scheduled_time); + + const doctorAppointment: DoctorAppointment = { + id: app.id, + status: app.status, + slot_duration: app.slot_duration, + patient_name: app.patient.name, + appointment_date: dateKey, + start_time: this.formatTime(app.scheduled_time), + end_time: this.formatTime(app.end_time), + clinic_name: app.clinic ? app.clinic.name : null, + clinic_address: app.clinic ? app.clinic.address : null, + }; + + if (!groupedByDate.has(dateKey)) { + groupedByDate.set(dateKey, []); + } + groupedByDate.get(dateKey).push(doctorAppointment); + }); + + const schedule: DoctorScheduleDay[] = []; + groupedByDate.forEach((appointments, dateKey) => { + const date = new Date(dateKey + 'T00:00:00.000Z'); + schedule.push({ + date: dateKey, + displayDate: this.formatDisplayDate(date), + appointments: appointments, + }); + }); + + return schedule; + } + + public async getScheduleByDate(doctorId: string, date: string): Promise { + const requestedDate = new Date(date); + + const startOfDay = new Date(requestedDate); + startOfDay.setUTCHours(0, 0, 0, 0); + + const endOfDay = new Date(requestedDate); + endOfDay.setUTCHours(23, 59, 59, 999); + + const appointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + scheduled_time: { + gte: startOfDay, + lte: endOfDay + }, + }, + select: { + id: true, + scheduled_time: true, + end_time: true, + slot_duration: true, + status: true, + clinic_id: true, + patient: { + select: { + name: true, + } + }, + clinic: { + select: { + name: true, + address: true, + } + } + }, + orderBy: { + scheduled_time: 'asc' + } + }); + + return appointments.map(appointment => ({ + id: appointment.id, + status: appointment.status, + slot_duration: appointment.slot_duration, + patient_name: appointment.patient.name, + appointment_date: this.formatDate(new Date(appointment.scheduled_time)), + start_time: this.formatTime(new Date(appointment.scheduled_time)), + end_time: this.formatTime(new Date(appointment.end_time)), + clinic_name: appointment.clinic?.name || null, + clinic_address: appointment.clinic?.address || null, + })); + + } + + public async getAppointmentsByDate(doctorId: string, clinicId: string, date: string): Promise { + const requestedDate = new Date(date); + + const startOfDay = new Date(requestedDate); + startOfDay.setUTCHours(0, 0, 0, 0); + + const endOfDay = new Date(requestedDate); + endOfDay.setUTCHours(23, 59, 59, 999); + + const appointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + clinic_id: clinicId, + scheduled_time: { + gte: startOfDay, + lte: endOfDay + }, + status: { + in: ['CONFIRMED', 'COMPLETED'] + }, + deleted_at: null, + }, + select: { + id: true, + scheduled_time: true, + end_time: true, + slot_duration: true, + status: true, + patient: { + select: { + id: true, + name: true, + gender: true, + phone: true, + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + } + }, + orderBy: { + scheduled_time: 'asc' + } + }); + + return appointments.map(appointment => ({ + id: appointment.id, + patient: { + id: appointment.patient.id, + name: appointment.patient.name, + gender: appointment.patient.gender, + phone: appointment.patient.phone, + }, + clinic: { + id: appointment.clinic.id, + name: appointment.clinic.name, + address: appointment.clinic.address, + address_maps_link: appointment.clinic.address_maps_link, + }, + status: appointment.status, + slot_duration: appointment.slot_duration, + appointment_date: this.formatDate(new Date(appointment.scheduled_time)), + start_time: this.formatTime(new Date(appointment.scheduled_time)), + end_time: this.formatTime(new Date(appointment.end_time)), + })); + } + + public async completeAppointment(appointmentId: string): Promise { + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + }, + select: { + doctor_id: true, + scheduled_time: true, + status: true, + } + }); + + await this.getAndValidateAppointment(appointmentId, appointment.doctor_id); + + if (appointment.status === 'COMPLETED') { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ALREADY_COMPLETED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const now = this.getNowInEgypt(); + + if (now < appointment.scheduled_time) { + const error = createBilingualError(400, ErrorMessages.CANNOT_BE_COMPLETED_BEFORE_SCHEDULED_TIME); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.appointment.update({ + where: { + id: appointmentId, + deleted_at: null, + }, + data: { + status: 'COMPLETED', + is_completed: true, + deleted_at: new Date(), + } + }) + } + + public async cancelDoctorVacation(doctorId: string, vacationId: string, scheduleId: string): Promise { + const schedule = await prisma.doctorSchedule.findUnique({ + where: { + doctor_id: doctorId, + id: scheduleId + }, + }); + + if (!schedule) { + const error = createBilingualError(404, ErrorMessages.SCHEDULE_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (schedule.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_SCHEDULE_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.doctorSchedule.update({ + where: { + id: scheduleId + }, + data: { + is_active: true, + break_start: null, + break_end: null, + modified_at: new Date(), + } + }); + + await prisma.vacation.update({ + where: { + id: vacationId + }, + data: { + deleted_at: new Date(), + status: 'ENDED', + } + }); + } + + public async getDoctorVacations(doctorId: string): Promise { + const inActiveSchedules = await prisma.doctorSchedule.findMany({ + where: { + doctor_id: doctorId, + is_active: false, + break_start: { + not: null, + }, + break_end: { + not: null, + }, + deleted_at: null, + }, + select: { + id: true, + day_of_week: true, + is_online: true, + break_start: true, + break_end: true + }, + orderBy: { + break_start: 'asc' + } + }); + + const vacationGroupsMap = new Map(); + for (const schedule of inActiveSchedules) { + const key = `${schedule.break_start}_${schedule.break_end}`; + if (!vacationGroupsMap.has(key)) { + vacationGroupsMap.set(key, []); + } + vacationGroupsMap.get(key)!.push(schedule); + } + + + const doctorVacations: DoctorVacations[] = []; + + for (const [key, schedules] of vacationGroupsMap.entries()) { + const representativeSchedule = schedules[0]; + const allVacations: Vacations[] = []; + + const vacations = await prisma.vacation.findMany({ + where: { + doctor_id: doctorId, + start_date: representativeSchedule.break_start, + end_date: representativeSchedule.break_end, + deleted_at: null, + }, + select: { + id: true, + doctor_id: true, + schedule_id: true, + start_date: true, + end_date: true, + status: true, + schedule: { + select: { + is_online: true, + day_of_week: true, + clinic_id: true, + clinic: { + select: { + name: true, + address: true, + } + } + } + } + } + }); + + for (const vacation of vacations) { + const breakStartDate = new Date(vacation.start_date); + breakStartDate.setUTCHours(0, 0, 0, 0); + + const breakEndDate = new Date(vacation.end_date); + breakEndDate.setUTCHours(23, 59, 59, 999); + + const cancelledAppointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + status: 'CANCELLED', + cancelled_by: 'DOCTOR', + is_online: vacation.schedule.is_online, + scheduled_time: { + gte: breakStartDate, + lte: breakEndDate, + }, + deleted_at: { + not: null + } + }, + select: { + scheduled_time: true, + } + }); + const filteredCancelled = cancelledAppointments.filter(appointment => { + const apptDay = this.getDayOfWeek(appointment.scheduled_time.getUTCDay()); + return apptDay === vacation.schedule.day_of_week; + }); + + allVacations.push({ + vacationId: vacation.id, + scheduleId: vacation.schedule_id, + clinicId: vacation.schedule.clinic_id, + clinicName: vacation.schedule.clinic?.name || null, + clinicAddress: vacation.schedule.clinic?.address || null, + dayOfWeek: vacation.schedule.day_of_week, + isOnline: vacation.schedule.is_online, + status: vacation.status, + cancelledAppointments: filteredCancelled.length, + }) + } + + doctorVacations.push({ + breakStart: representativeSchedule.break_start, + breakEnd: representativeSchedule.break_end, + vacations: allVacations + }); + + } + return doctorVacations; + } + + public async getCurrentDoctorSchedule(doctorId: string): Promise { + const now = this.getNowInEgypt(); + const { start: startOfDay, end: endOfDay } = this.getTodayBoundaries(now); + + const appointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + scheduled_time: { + gte: startOfDay, + lte: endOfDay + }, + }, + orderBy: { + scheduled_time: 'asc', + }, + select: { + id: true, + scheduled_time: true, + end_time: true, + slot_duration: true, + status: true, + clinic_id: true, + patient: { + select: { + name: true, + } + }, + clinic: { + select: { + name: true, + address: true, + } + } + } + }); + + return appointments.map(app => ({ + id: app.id, + status: app.status, + slot_duration: app.slot_duration, + patient_name: app.patient.name, + appointment_date: this.formatDate(app.scheduled_time), + start_time: this.formatTime(app.scheduled_time), + end_time: this.formatTime(app.end_time), + clinic_name: app.clinic ? app.clinic.name : null, + clinic_address: app.clinic ? app.clinic.address : null, + })); + + } + + public async getDoctorAppointmentContext( + doctorId: string, + appointmentId: string, + ): Promise<{ appointmentId: string; clinicId: string; patientId: string }> { + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + }, + select: { + id: true, + doctor_id: true, + clinic_id: true, + patient_id: true, + deleted_at: true, + }, + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (appointment.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_APPOINTMENT_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (appointment.deleted_at) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ALREADY_DELETED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!appointment.clinic_id) { + const error = createBilingualError(400, ErrorMessages.CLINIC_REQUIRED_FOR_OFFLINE); + throw new HttpException(error.status, error.message, error.messageAr); + } + + return { + appointmentId: appointment.id, + clinicId: appointment.clinic_id, + patientId: appointment.patient_id, + }; + } + + public async getAppointmentOwners(appointmentId: string): Promise<{ doctorId: string; scheduledTime: Date; }> { + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + deleted_at: null, + }, + select: { + doctor_id: true, + patient_id: true, + scheduled_time: true, + }, + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + return { + doctorId: appointment.doctor_id, + scheduledTime: appointment.scheduled_time, + }; + } + + public async getDoctorSchedule(doctorId: string): Promise { + const schedules = await prisma.doctorSchedule.findMany({ + where: { + doctor_id: doctorId, + deleted_at: null, + }, + select: { + id: true, + clinic_id: true, + day_of_week: true, + start_time: true, + end_time: true, + slot_duration: true, + buffer_time: true, + is_online: true, + is_active: true, + break_start: true, + break_end: true, + } + }); + + return schedules.map(schedule => ({ + id: schedule.id, + clinicId: schedule.clinic_id, + dayOfWeek: schedule.day_of_week, + startTime: schedule.start_time, + endTime: schedule.end_time, + slotDuration: schedule.slot_duration, + bufferTime: schedule.buffer_time, + isOnline: schedule.is_online, + isActive: schedule.is_active, + breakStart: schedule.break_start, + breakEnd: schedule.break_end, + })); + } + + public async editDoctorSchedule(doctorId: string, scheduleId: string, updates: any): Promise { + const schedule = await prisma.doctorSchedule.findUnique({ + where: { + id: scheduleId + }, + }); + + if (!schedule) { + const error = createBilingualError(404, ErrorMessages.SCHEDULE_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (schedule.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_SCHEDULE_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.doctorSchedule.update({ + where: { + id: scheduleId + }, + data: { + ...updates, + modified_at: new Date(), + } + }); + } + + public async checkConflictingAppointments(doctorId: string, scheduleId: string, breakStart?: string, breakEnd?: string): Promise { + const conflictingAppointments = await this.getConflictingAppointments(doctorId, scheduleId, breakStart, breakEnd); + + return conflictingAppointments.length > 0 + ? { existing: true, numOfAppointments: conflictingAppointments.length } + : { existing: false }; + } + + + public async handleDoctorVacation(doctorId: string, scheduleId: string, breakStart: string, breakEnd: string): Promise { + const conflictingAppointments = await this.getConflictingAppointments(doctorId, scheduleId, breakStart, breakEnd) + const idsToCancel = conflictingAppointments.map(appointment => appointment.id); + + await prisma.appointment.updateMany({ + where: { + id: { in: idsToCancel }, + }, + data: { + status: 'CANCELLED', + cancelled_by: 'DOCTOR', + deleted_at: new Date(), + modified_at: new Date(), + }, + }); + + await prisma.doctorSchedule.update({ + where: { + id: scheduleId, + }, + data: { + is_active: false, + break_start: breakStart, + break_end: breakEnd, + modified_at: new Date(), + }, + }); + + await prisma.vacation.create({ + data: { + doctor_id: doctorId, + schedule_id: scheduleId, + start_date: breakStart, + end_date: breakEnd, + } + + }) + } + + + public async deleteDoctorSchedule(doctorId: string, scheduleId: string): Promise { + const conflictingAppointments = await this.getConflictingAppointments(doctorId, scheduleId) + const idsToCancel = conflictingAppointments.map(appointment => appointment.id); + + await prisma.appointment.updateMany({ + where: { + id: { in: idsToCancel }, + }, + data: { + status: 'CANCELLED', + cancelled_by: 'DOCTOR', + deleted_at: new Date(), + modified_at: new Date(), + }, + }); + + await prisma.doctorSchedule.update({ + where: { + id: scheduleId + }, + data: { + is_active: false, + deleted_at: new Date(), + } + }) + } + + public async getNurseAppointmentsToday(nurseId: string): Promise { + const crrentDate = this.getNowInEgypt(); + const today = this.formatDate(crrentDate); + const dayOfWeek = this.getDayOfWeek(crrentDate.getUTCDay()); + + const nurseSchedules = await prisma.nurseSchedule.findMany({ + where: { + nurse_id: nurseId, + day_of_week: dayOfWeek, + is_active: true, + deleted_at: null, + }, + select: { + doctor_id: true, + clinic_id: true + } + }); + + if (!nurseSchedules.length) { + return []; + } + + const allAppointments = await Promise.all( + nurseSchedules.map(schedule => + this.getAppointmentsByDate(schedule.doctor_id, schedule.clinic_id, today) + ) + ); + + return allAppointments.flat(); + } + + public async getAppointmentsForDay(doctorId: string, date: Date): Promise<{ id: string; patient_id: string }[]> { + const { start: startOfDay, end: endOfDay } = this.getTodayBoundaries(date); + + return prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + scheduled_time: { + gte: startOfDay, + lte: endOfDay + }, + status: { in: ['CONFIRMED'] }, + deleted_at: null, + }, + select: { + id: true, + patient_id: true + + }, + }); + } + + private async getConflictingAppointments(doctorId: string, scheduleId: string, breakStart?: string, breakEnd?: string): Promise { + const schedule = await prisma.doctorSchedule.findUnique({ + where: { + id: scheduleId + }, + select: { + doctor_id: true, + deleted_at: true, + is_online: true, + day_of_week: true + } + }); + + if (!schedule) { + const error = createBilingualError(404, ErrorMessages.SCHEDULE_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (schedule.deleted_at) { + const error = createBilingualError(400, ErrorMessages.SCHEDULE_ALREADY_DELETED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + let existingAppointments: { id: string; scheduled_time: Date }[]; + + if (breakStart && breakEnd) { + const vacationStart = new Date(breakStart); + vacationStart.setUTCHours(0, 0, 0, 0); + + const vacationEnd = new Date(breakEnd); + vacationEnd.setUTCHours(23, 59, 59, 999); + + existingAppointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + status: 'CONFIRMED', + deleted_at: null, + is_online: schedule.is_online, + scheduled_time: { + gte: vacationStart, + lte: vacationEnd, + } + }, + select: { + id: true, + scheduled_time: true + } + }); + } + else { + existingAppointments = await prisma.appointment.findMany({ + where: { + doctor_id: doctorId, + status: 'CONFIRMED', + deleted_at: null, + is_online: schedule.is_online, + }, + select: { + id: true, + scheduled_time: true + } + }); + } + + const confilctingAppointments = existingAppointments.filter((appointment) => { + const apptDay = this.getDayOfWeek(appointment.scheduled_time.getUTCDay()); + return apptDay === schedule.day_of_week; + + }) + return confilctingAppointments; + } + + private generateTimeSlots(startTime: string, endTime: string, slotDuration: number, bufferTime: number, isOnline: boolean): Omit[] { + const slots: Omit[] = []; + + const startMinutes = this.timeStringToMinutes(startTime); + const endMinutes = this.timeStringToMinutes(endTime); + + let currentMinutes = startMinutes; + + while (currentMinutes < endMinutes) { + const slotEndMinutes = currentMinutes + slotDuration; + if (slotEndMinutes <= endMinutes) { + slots.push({ + start: this.minutesToTimeString(currentMinutes), + end: this.minutesToTimeString(slotEndMinutes), + online: isOnline + }); + } + // move to next slot (slot duration + buffer time) + currentMinutes += (slotDuration + bufferTime); + } + + return slots; + } + + // converts js representation of days (0-6) to prisma's enum + public getDayOfWeek(jsDay: number): DayOfWeek { + const days: DayOfWeek[] = [ + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY, + ]; + return days[jsDay]; + } + + public convertKeysToSnakeCase>(obj: T): Record { + return Object.entries(obj).reduce((acc, [key, value]) => { + if (value !== undefined) { + acc[this.camelToSnakeCase(key)] = value; + } + return acc; + }, {} as Record); + } + + // format date as YYYY-MM-DD + private formatDate(date: Date): string { + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, '0'); + const day = String(date.getUTCDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + // format date for display (sun, jan20, 2026) + private formatDisplayDate(date: Date): string { + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'UTC' + }; + // later --> for arabic ar-EG + return date.toLocaleDateString('en-EG', options); + } + + // extract time from date / ex: 1970-01-01T09:00:00.000Z --> 09:00 + private formatTime(date: Date): string { + const hours = String(date.getUTCHours()).padStart(2, '0'); + const minutes = String(date.getUTCMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; + } + + // date: 2026-01-27, time string: 10:30 --> 2026-01-27 10:30:00 + private parseTimeToDate(date: Date, timeStr: string): Date { + const [hours, minutes] = timeStr.split(':').map(Number); + const result = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0)); + result.setUTCHours(hours, minutes, 0, 0); + return result; + } + + // ex: "10:30" --> 630 + private timeStringToMinutes(timeStr: string): number { + const [hours, minutes] = timeStr.split(':').map(Number); + return hours * 60 + minutes; + } + + // "HH:MM" format + private minutesToTimeString(minutes: number): string { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; + } + + private doesSlotOverlap(slotStart: Date, slotEnd: Date, appointmentStart: Date, appointmentEnd: Date): boolean { + return (slotStart < appointmentEnd && slotEnd > appointmentStart); + } + + private async rescheduleSingleAppointment(doctorId: string, appointmentId: string, minutes: number): Promise { + const appointment = await this.getAndValidateAppointment(appointmentId, doctorId); + + let updatedScheduledTime: Date; + let updatedEndTime: Date; + + updatedScheduledTime = new Date(appointment.scheduled_time.getTime() + minutes * 60000); + updatedEndTime = new Date(appointment.end_time.getTime() + minutes * 60000); + + await prisma.appointment.update({ + where: { + id: appointmentId, + }, + data: { + scheduled_time: updatedScheduledTime, + end_time: updatedEndTime, + modified_at: new Date(), + } + }); + } + + private async getAndValidateAppointment(appointmentId: string, doctorId: string) { + const appointment = await prisma.appointment.findUnique({ + where: { id: appointmentId }, + select: { + id: true, + patient_id: true, + doctor_id: true, + clinic_id: true, + scheduled_time: true, + end_time: true, + slot_duration: true, + status: true, + deleted_at: true, + patient: { + select: { + name: true, + } + } + } + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (appointment.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_APPOINTMENT_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (appointment.deleted_at) { + const error = createBilingualError(400, ErrorMessages.APPOINTMENT_ALREADY_DELETED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + return appointment; + } + + private camelToSnakeCase(str: string): string { + return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); + } + + private getNowInEgypt(): Date { + const now = new Date(); + + const cairoFormatter = new Intl.DateTimeFormat('en-CA', { + timeZone: 'Africa/Cairo', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); + + const cairoDateTimeStr = cairoFormatter.format(now).replace(', ', 'T'); + return new Date(`${cairoDateTimeStr}Z`); + } + + private getTodayBoundaries(date: Date): { start: Date; end: Date } { + const year = date.getUTCFullYear(); + const month = date.getUTCMonth(); + const day = date.getUTCDate(); + + const start = new Date(Date.UTC(year, month, day, 0, 0, 0, 0)); + const end = new Date(Date.UTC(year, month, day, 23, 59, 59, 999)); + + return { start, end }; + } + + + public async generateAgoraToken(appointmentId: string, userId: string): Promise { + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + } + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const appId = Agora_APP_ID; + const appCertificate = Agora_APP_CERTIFICATE; + + if (!appId || !appCertificate) { + const error = createBilingualError(500, ErrorMessages.AGORA_CREDENTIALS_NOT_CONFIGURED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const channelName = appointmentId; + const role = RtcRole.PUBLISHER; + const expirationTimeInSeconds = 3600; // 1 hour + const currentTimestamp = Math.floor(Date.now() / 1000); + const privilegeExpiredTs = currentTimestamp + expirationTimeInSeconds; + + const token = RtcTokenBuilder.buildTokenWithUserAccount(appId, appCertificate, channelName, userId, role, privilegeExpiredTs, privilegeExpiredTs); + return token; + } +} \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..e764389 --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,479 @@ +import { DoctorAccountStatus, Role , NurseAccountStatus} from '@prisma/client'; +import { compare, hash } from 'bcrypt'; +import { sign, verify } from 'jsonwebtoken'; +import { Service } from 'typedi'; +import { SECRET_KEY, REFRESH_TOKEN_SECRET, REFRESH_TOKEN_EXPIRY, ACCESS_TOKEN_EXPIRY, FRONTEND_URL, SENDER_EMAIL } from '@config'; +import { CompleteUserProfileDto, CreateUserDto, LoginUserDto } from '@dtos/users.dto'; +import { HttpException } from '@exceptions/HttpException'; +import { DataStoredInToken, AccessTokenData, RefreshTokenData, TokenResponse, RequestWithUser } from '@interfaces/auth.interface'; +import { UserLoginData, User } from '@interfaces/users.interface'; +import { transporter } from '@/utils/nodeMailerService'; +import { ErrorMessages, createBilingualError } from '@/utils/errorMessages'; +import crypto from 'crypto'; +import prisma from '@/config/prisma'; + +@Service() +export class AuthService { + public users = prisma.user; + public patients = prisma.patient; + public refreshTokens = prisma.refreshToken; + + public async signup(userData: CreateUserDto): Promise<{ createdUserData: User; cookies: string[] }> { + const findUserSameEmail: User = await this.users.findUnique({ where: { email: userData.email } }); + if (findUserSameEmail) { + const error = createBilingualError(409, ErrorMessages.EMAIL_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const emailHandle = userData.email.split('@')[0]; + const findUserSameUsername: User = await this.users.findUnique({ where: { username: emailHandle } }); + if (findUserSameUsername) { + const error = createBilingualError(409, ErrorMessages.USERNAME_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const hashedPassword = await hash(userData.password, 10); + const username = emailHandle; + const { rememberMe, password, ...userDataWithoutPassword } = userData; + const createdUserData: User = await this.users.create({ + data: { + ...userDataWithoutPassword, username, password_hash: hashedPassword, + role: Role.PATIENT, + gender: "MALE", date_of_birth: new Date("2000-01-01") + } + }); + + await this.patients.create({ + data: { + id: createdUserData.id, + bc_address: '', + consent: false, + } + }); + + const tokenResponse = await this.createTokens(createdUserData, userData.rememberMe); + const cookies = this.createCookies(tokenResponse); + + return { createdUserData, cookies }; + } + + public async login(userData: LoginUserDto): Promise<{ cookies: string[]; findUser: UserLoginData }> { + const findUser: User = await this.users.findFirst({ + where: { + OR: [ + { email: userData.emailOrUsername }, + { username: userData.emailOrUsername } + ] + }, + include: { + doctor: true, + nurse: true, + } + }); + if (!findUser) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND_CREDENTIALS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const isPasswordMatching: boolean = await compare(userData.password, findUser.password_hash); + if (!isPasswordMatching) { + const error = createBilingualError(404, ErrorMessages.PASSWORD_NOT_MATCHING); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const { name, gender, date_of_birth, email, isVerified, username, phone, role, hasCompletedProfile, doctor, nurse, photo_url } = findUser; + const userLoginData: UserLoginData = { + name, + email, + username, + phone, + gender, + date_of_birth, + role, + isVerified, + hasCompletedProfile, + photo_url, + doctor: doctor ? { + specialization: doctor.specialization, + account_status: doctor.account_status + } : undefined, + nurse: nurse ? { + account_status: nurse.account_status + } : undefined + }; + + if (userLoginData.doctor && userLoginData.doctor.account_status !== DoctorAccountStatus.APPROVED) { + const error = createBilingualError(403, ErrorMessages.DOCTOR_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (userLoginData.nurse && userLoginData.nurse.account_status !== NurseAccountStatus.APPROVED) { + const error = createBilingualError(403, ErrorMessages.NURSE_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const tokenResponse = await this.createTokens(findUser, userData.rememberMe); + const cookies = this.createCookies(tokenResponse); + + return { cookies, findUser: userLoginData }; + } + + public async logout(userData: User): Promise { + const findUser: User = await this.users.findFirst({ where: { email: userData.email, password_hash: userData.password_hash } }); + if (!findUser) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_EXIST); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Revoke all refresh tokens for this user + await this.refreshTokens.updateMany({ + where: { user_id: findUser.id, is_revoked: false }, + data: { is_revoked: true, revoked_at: new Date() }, + }); + + return findUser; + } + + public async completeProfile(userData: User, profileData: CompleteUserProfileDto): Promise { + const findUser: User = await this.users.findUnique({ where: { id: userData.id } }); + if (!findUser) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_EXIST); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const updatedUserData: User = await this.users.update({ + where: { id: userData.id }, + data: { + gender: profileData.gender, + date_of_birth: new Date(profileData.date_of_birth), + hasCompletedProfile: true, + }, + }); + + return updatedUserData; + } + + + public async createTokens(user: Partial, rememberMe: boolean = false): Promise { + const accessToken = this.createAccessToken(user); + + if (rememberMe) { + const refreshToken = await this.createRefreshToken(user); + return { accessToken, refreshToken }; + } + + return { accessToken }; + } + + public createAccessToken(user: Partial): AccessTokenData { + const dataStoredInToken: DataStoredInToken = { id: user.id , role: user.role}; + const secretKey: string = SECRET_KEY; + const expiresIn: number = this.parseTimeToSeconds(ACCESS_TOKEN_EXPIRY); + + return { expiresIn, token: sign(dataStoredInToken, secretKey, { expiresIn }) }; + } + + public async createRefreshToken(user: Partial): Promise { + const dataStoredInToken: DataStoredInToken = { id: user.id , role: user.role}; + const secretKey: string = REFRESH_TOKEN_SECRET; + const expiresIn: number = this.parseTimeToSeconds(REFRESH_TOKEN_EXPIRY); + + const token = sign(dataStoredInToken, secretKey, { expiresIn }); + + // Hash the token before storing + const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); + + // Store refresh token in database + await this.refreshTokens.create({ + data: { + user_id: user.id, + token_hash: tokenHash, + expires_at: new Date(Date.now() + expiresIn * 1000), + }, + }); + + return { expiresIn, token }; + } + + public createCookies(tokenResponse: TokenResponse): string[] { + const cookies: string[] = []; + + // Access token cookie + cookies.push(`Authorization=${tokenResponse.accessToken.token}; HttpOnly; Max-Age=${tokenResponse.accessToken.expiresIn}; Path=/; SameSite=Lax`); + + // Refresh token cookie (if exists) + if (tokenResponse.refreshToken) { + cookies.push(`RefreshToken=${tokenResponse.refreshToken.token}; HttpOnly; Max-Age=${tokenResponse.refreshToken.expiresIn}; Path=/; SameSite=Lax`); + } + return cookies; + } + + public async refreshAccessToken(refreshToken: string): Promise<{ cookies: string[]; user: User; accessToken: AccessTokenData }> { + if (!refreshToken) { + const error = createBilingualError(401, ErrorMessages.REFRESH_TOKEN_NOT_PROVIDED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Verify the refresh token + const secretKey: string = REFRESH_TOKEN_SECRET; + let decoded: DataStoredInToken; + + try { + decoded = verify(refreshToken, secretKey) as DataStoredInToken; + } catch (error) { + const err = createBilingualError(401, ErrorMessages.INVALID_REFRESH_TOKEN); + throw new HttpException(err.status, err.message, err.messageAr); + } + + // Hash the token to compare with stored hash + const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex'); + + // Check if refresh token exists and is not revoked + const storedToken = await this.refreshTokens.findFirst({ + where: { + token_hash: tokenHash, + user_id: decoded.id, + is_revoked: false, + expires_at: { gt: new Date() }, + }, + }); + + if (!storedToken) { + const error = createBilingualError(401, ErrorMessages.INVALID_REFRESH_TOKEN); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Get user + const user = await this.users.findUnique({ + where: { id: decoded.id }, + include: { doctor: true } // Include doctor relation if needed + }); + + if (!user) { + const error = createBilingualError(401, ErrorMessages.USER_NOT_EXIST); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Revoke the old refresh token (token rotation for security) + await this.refreshTokens.update({ + where: { id: storedToken.id }, + data: { + is_revoked: true, + revoked_at: new Date() + } + }); + + // Create new access token + const accessToken = this.createAccessToken(user); + + // Create new refresh token (token rotation) + const newRefreshToken = await this.createRefreshToken(user); + + // Create cookies with both tokens + const cookies = this.createCookies({ + accessToken, + refreshToken: newRefreshToken + }); + + return { cookies, user, accessToken }; + } + + // public async revokeRefreshToken(refreshToken: string): Promise { + // const tokenHash = crypto.createHash('sha256').update(refreshToken).digest('hex'); + + // await this.refreshTokens.updateMany({ + // where: { token_hash: tokenHash, is_revoked: false }, + // data: { is_revoked: true, revoked_at: new Date() }, + // }); + // } + + private parseTimeToSeconds(timeString: string): number { + const unit = timeString.slice(-1); + const value = parseInt(timeString.slice(0, -1)); + + switch (unit) { + case 's': return value; + case 'm': return value * 60; + case 'h': return value * 60 * 60; + case 'd': return value * 24 * 60 * 60; + default: return 3600; // Default 1 hour + } + } + + public async sendEmailOtp(email: string): Promise { + + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const expiryDate = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes + + const userInfo: Partial = await this.users.update({ + where: { email }, + data: { + email_OTP: otp, + email_OTP_expires_at: expiryDate, + }, + select: { email: true } + }); + + const mailOptions = { + from: SENDER_EMAIL, + to: userInfo.email, + subject: 'Your Email Verification Code', + html: ` +
+

Email Verification

+

Hi there,

+

Thank you for registering. Please use the following code to verify your email address:

+

+ ${otp} +

+

This code will expire in 10 minutes.

+

If you did not request this, please ignore this email.

+
+ ` + }; + + await transporter.sendMail(mailOptions); + } + + public async getUserEmail(req: RequestWithUser): Promise { + const email = await this.users.findUnique({ + where: { id: req.user.id }, + select: { email: true } + }); + if (!email) { + const error = createBilingualError(404, ErrorMessages.USER_EMAIL_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + return email.email; + } + + public async verifyEmailOtp(email: string, otp: string): Promise { + const user = await this.users.findUnique({ where: { email } }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_EXIST); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (user.email_OTP !== otp) { + const error = createBilingualError(400, ErrorMessages.INVALID_OTP); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (user.email_OTP_expires_at && user.email_OTP_expires_at < new Date()) { + const error = createBilingualError(400, ErrorMessages.OTP_EXPIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Clear OTP fields after successful verification + await this.users.update({ + where: { email }, + data: { + email_OTP: null, + email_OTP_expires_at: null, + isVerified: true, + }, + }); + + return true; + } + + public async sendPasswordResetEmail(email: string): Promise { + const user = await this.users.findUnique({ where: { email } }); + if (!user) { + const error = createBilingualError(200, ErrorMessages.EMAIL_SENT_IF_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const resetPasswordToken = crypto.randomBytes(32).toString('hex'); + const resetPasswordTokenExpiry = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes + + await this.users.update({ + where: { email }, + data: { + password_reset_token: resetPasswordToken, + password_reset_token_expires_at: resetPasswordTokenExpiry, + }, + }); + + const resetLink = `${FRONTEND_URL}/reset-password?token=${resetPasswordToken}`; + const mailOptions = { + from: SENDER_EMAIL, + to: user.email, + subject: 'Your Password Reset Request', + html: ` +
+

Password Reset Request

+

You are receiving this email because you (or someone else) requested a password reset for your account.

+

Please click the button below to reset your password:

+ + Reset Your Password + +

If you did not request this, please ignore this email. This link is valid for 10 minutes.

+
+ ` + }; + await transporter.sendMail(mailOptions); + } + + public async resetPassword(token: string, newPassword: string): Promise { + const user = await this.users.findFirst({ + where: { + password_reset_token: token, + password_reset_token_expires_at: { gt: new Date() }, + }, + }); + + if (!user) { + const error = createBilingualError(400, ErrorMessages.INVALID_PASSWORD_RESET_TOKEN); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const hashedPassword = await hash(newPassword, 10); + + await this.users.update({ + where: { id: user.id }, + data: { + password_hash: hashedPassword, + password_reset_token: null, + password_reset_token_expires_at: null, + }, + }); + } + + public async checkPassword(userId: string, password: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { password_hash: true } + }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const isMatch = await compare(password, user.password_hash); + return isMatch; + } + + public async changePassword(userId: string, newPassword: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const hashedPassword = await hash(newPassword, 10); + await prisma.user.update({ + where: { id: userId }, + data: { password_hash: hashedPassword } + }); + } + + // Keep old methods for backward compatibility + public createToken(user: User): AccessTokenData { + return this.createAccessToken(user); + } + + public createCookie(tokenData: AccessTokenData): string { + return `Authorization=${tokenData.token}; HttpOnly; Max-Age=${tokenData.expiresIn};`; + } +} diff --git a/src/services/backup.service.ts b/src/services/backup.service.ts new file mode 100644 index 0000000..88c7614 --- /dev/null +++ b/src/services/backup.service.ts @@ -0,0 +1,164 @@ +import fs from 'fs'; +import path from 'path'; +import { MedicalRecord } from '@/interfaces/medical-records.interface'; + +export class BackupService { + private backupFilePath: string; + private keysFilePath: string; + + constructor() { + const dataDir = path.join(__dirname, '../../data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + this.backupFilePath = path.join(dataDir, 'backup_records.json'); + this.keysFilePath = path.join(dataDir, 'backup_keys.json'); + if (!fs.existsSync(this.backupFilePath)) { + fs.writeFileSync(this.backupFilePath, JSON.stringify([])); + } + if (!fs.existsSync(this.keysFilePath)) { + fs.writeFileSync(this.keysFilePath, JSON.stringify({})); + } + } + + private readBackup(): MedicalRecord[] { + try { + const data = fs.readFileSync(this.backupFilePath, 'utf8'); + const records: MedicalRecord[] = JSON.parse(data).filter((r: MedicalRecord) => !r.deleted); + return records; + } catch (error) { + console.error('Error reading backup file:', error); + return []; + } + } + + private writeBackup(records: MedicalRecord[]): void { + try { + fs.writeFileSync(this.backupFilePath, JSON.stringify(records, null, 2)); + } catch (error) { + console.error('Error writing to backup file:', error); + } + } + + private readKeys(): Record { + try { + const data = fs.readFileSync(this.keysFilePath, 'utf8'); + return JSON.parse(data); + } catch (error) { + console.error('Error reading keys file:', error); + return {}; + } + } + + private writeKeys(keys: Record): void { + try { + fs.writeFileSync(this.keysFilePath, JSON.stringify(keys, null, 2)); + } catch (error) { + console.error('Error writing to keys file:', error); + } + } + + public addRecord(record: MedicalRecord, ownerMsp: string): void { + const records = this.readBackup(); + const existingIndex = records.findIndex(r => r.recordId === record.recordId && r.patientId === record.patientId); + if (existingIndex === -1) { + records.push({ + ...record, + ownerMsp, + authorizedMsps: record.authorizedMsps || [], + }); + this.writeBackup(records); + } + } + + public getRecordsByPatient(patientId: string, clientMspId: string): MedicalRecord[] { + const records = this.readBackup(); + return records.filter(r => { + if (r.patientId !== patientId) return false; + if (r.deleted) { + return false; + } + if (clientMspId === 'admin' || r.ownerMsp === clientMspId) { + return true; + } + + // Check authorized list + if (r.authorizedMsps && r.authorizedMsps.includes(clientMspId)) { + return true; + } + + return false; + }); + } + + public getAllRecords(): MedicalRecord[] { + return this.readBackup(); + } + + public updateRecord(patientId: string, payload: Omit): void { + const records = this.readBackup(); + const index = records.findIndex(r => r.recordId === payload.recordId && r.patientId === patientId); + if (index !== -1) { + records[index] = { ...records[index], ...payload }; + this.writeBackup(records); + } + } + public deleteRecord(patientId: string, recordId: string): void { + const records = this.readBackup(); + const index = records.findIndex(r => r.recordId === recordId && r.patientId === patientId); + if (index !== -1) { + records[index].deleted = true; + this.writeBackup(records); + } + } + + public grantAccess(patientId: string, clientMspId: string, targetMsp: string): void { + const records = this.readBackup(); + let updated = false; + for (const record of records) { + if (record.patientId === patientId && record.ownerMsp === clientMspId) { + if (!record.authorizedMsps) { + record.authorizedMsps = []; + } + if (!record.authorizedMsps.includes(targetMsp)) { + record.authorizedMsps.push(targetMsp); + updated = true; + } + } + } + if (updated) { + this.writeBackup(records); + } + } + + public storeRecordKey(patientId: string, recordId: string, encryptedDEK: string): void { + const keys = this.readKeys(); + keys[`${patientId}:${recordId}`] = encryptedDEK; + this.writeKeys(keys); + } + + public getRecordKey(patientId: string, recordId: string): string { + const keys = this.readKeys(); + const key = keys[`${patientId}:${recordId}`]; + if (!key) { + throw new Error(`DEK not found in backup for patient ${patientId} and record ${recordId}`); + } + return key; + } + + public recordKeyExists(patientId: string, recordId: string): boolean { + const keys = this.readKeys(); + return !!keys[`${patientId}:${recordId}`]; + } + + public deleteAllRecords(): void { + try { + this.writeBackup([]); + this.writeKeys({}); + } catch (error) { + console.error('Error clearing backup file:', error); + } + } +} + +export const backupService = new BackupService(); diff --git a/src/services/clinic.service.ts b/src/services/clinic.service.ts new file mode 100644 index 0000000..3061ca5 --- /dev/null +++ b/src/services/clinic.service.ts @@ -0,0 +1,455 @@ +import { ClinicActiveStatusResponseDto, ClinicResponseDto, CreateUpdateClinicRequestDto } from "@/dtos/clinics.dto"; +import { Service } from "typedi"; +import prisma from "@/config/prisma"; +import { Clinic } from "@/interfaces"; +import { DoctorPersonalData } from "@/interfaces/doctors.interface"; +import { DoctorClinics } from "@/interfaces/clinics.interface" +import { Doctor, Gender } from "@prisma/client"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; +import { HttpException } from "@/exceptions/HttpException"; +import { UserService } from "./user.service"; +import { DoctorAccountStatus } from "@prisma/client"; +import { formatSpecializationResponse } from "@/utils/specializationTransform"; +import { SpecializationKey } from "@/constants/specializations"; + +@Service() +export class ClinicService { + private userService = new UserService(); + private MAX_CLINICS_PER_DOCTOR = 3; + + public async isDoctorAllowedToCreateClinic(doctorId: string): Promise { + const doctor = await prisma.doctor.findUnique({ + where: { + id: doctorId, + }, + select: { + num_of_created_clinics: true, + } + }); + if (!doctor) { + return false; + } + return doctor.num_of_created_clinics <= this.MAX_CLINICS_PER_DOCTOR; + } + + public async createClinic(doctorId: string, clinicData: CreateUpdateClinicRequestDto): Promise { + + const createdClinic = await prisma.clinic.create({ + data: { + name: clinicData.name, + opening_at: clinicData.opening_at, + closing_at: clinicData.closing_at, + address: clinicData.address, + address_maps_link: clinicData.address_maps_link, + phone: clinicData.phone, + canPayOnline: clinicData.canPayOnline, + created_by: doctorId, + is_active: false, + }, + select: { + id: true, + } + }); + + return createdClinic.id; + } + + public async linkDoctorToClinic(doctorId: string, clinicId: string, fees: number): Promise { + await prisma.clinicDoctor.create({ + data: { + doctor_id: doctorId, + clinic_id: clinicId, + fees, + } + }); + + await prisma.doctor.update({ + where: { + id: doctorId, + }, + data: { + num_of_created_clinics: { + increment: 1, + } + } + }); + } + + public async getClinicById(clinicId: string): Promise { + const clinic = await prisma.clinic.findUnique({ + where: { + id: clinicId, + }, + select: { + id: true, + name: true, + is_active: true, + opening_at: true, + closing_at: true, + address: true, + address_maps_link: true, + phone: true, + canPayOnline: true, + created_at: true, + } + }); + + return clinic; + } + + public async updateClinic(doctorId: string, clinicId: string, clinicData: CreateUpdateClinicRequestDto): Promise { + const { fees, ...clinicUpdateData } = clinicData; + const updatedClinic = await prisma.clinic.update({ + where: { + id: clinicId, + }, + data: { + ...clinicUpdateData, + }, + }); + const clinicDoctor = await prisma.clinicDoctor.update({ + where: { + clinic_id_doctor_id: { + clinic_id: clinicId, + doctor_id: doctorId, + } + }, + data: { + fees, + } + }); + return (updatedClinic && clinicDoctor) !== null; + } + + public async isCreatingDoctorOfClinic(doctorId: string, clinicId: string): Promise { + const clinic = await prisma.clinic.findUnique({ + where: { + id: clinicId, + }, + select: { + created_by: true, + } + }); + if (!clinic) { + return false; + } + return clinic.created_by === doctorId; + } + + public async deleteClinic(clinicId: string): Promise { + const clinicDoctors = await prisma.clinicDoctor.findMany({ + where: { + clinic_id: clinicId, + }, + select: { + doctor_id: true, + } + }); + const deletedClinic = await prisma.$transaction(async (tx) => { + await tx.clinicDoctor.deleteMany({ + where: { + clinic_id: clinicId, + }, + }); + await tx.clinicNurse.deleteMany({ + where: { + clinic_id: clinicId, + } + }); + await tx.clinic.delete({ + where: { + id: clinicId, + } + }); + await Promise.all(clinicDoctors.map(async (cd) => { + await tx.doctor.update({ + where: { + id: cd.doctor_id, + }, + data: { + num_of_created_clinics: { + decrement: 1, + } + } + }); + })); + }); + } + + public async getDoctorClinics(doctorId: string): Promise[]> { + const clinics = await prisma.clinicDoctor.findMany({ + where: { + doctor_id: doctorId + }, + select: { + clinic: { + select: { + id: true, + name: true, + opening_at: true, + closing_at: true, + address: true, + address_maps_link: true, + phone: true, + is_active: true, + canPayOnline: true, + created_at: true, + created_by: true, + } + }, + fees: true + } + }); + return clinics.map(c => { + let isOwner = true; + if (c.clinic.created_by !== doctorId) { + isOwner = false; + } + return { + ...c.clinic, + fees: c.fees, + isOwner + } + }); + } + + public async getClinicDoctors(clinicId: string, gender?: Gender, minFees?: number, maxFees?: number): Promise[]> { + const doctors = await prisma.clinicDoctor.findMany({ + where: { + clinic_id: clinicId, + is_accepting: true, + fees: { + ...(minFees !== undefined && { gte: minFees }), + ...(maxFees !== undefined && { lte: maxFees }) + }, + doctor: { + account_status: 'APPROVED', + present: true, + availability_type: { + in: ['OFFLINE', 'BOTH'] + }, + user: { + ...(gender && { gender }), + } + + }, + }, + select: { + fees: true, + doctor: { + select: { + specialization: true, + user: { + select: { + id: true, + name: true, + gender: true, + date_of_birth: true, + phone: true, + photo_url: true, + }, + }, + }, + }, + }, + }); + + const results = await Promise.all( + doctors.map(async (doc) => { + const user = doc.doctor.user; + const age = await this.userService.calculateUserAge(user.date_of_birth); + + const doctorData = { + id: user.id, + name: user.name, + gender: user.gender, + age, + specialization: doc.doctor.specialization, + phone: user.phone, + fees: doc.fees, + profilePic: user.photo_url, + } satisfies Partial; + + return doctorData; + }) + ); + + return results; + } + + public async getAllClinics(): Promise { + const clinics = await prisma.clinic.findMany({ + select: { + id: true, + name: true, + address: true, + phone: true, + is_active: true, + opening_at: true, + closing_at: true, + canPayOnline: true, + address_maps_link: true, + } + }); + return clinics; + } + public async setClinicActiveStatus(clinicId: string, is_active: boolean): Promise { + const updatedClinic = await prisma.clinic.update({ + where: { + id: clinicId, + }, + data: { + is_active + }, + select: { + id: true, + name: true, + is_active: true, + } + }); + if (!updatedClinic) { + const error = createBilingualError(404, ErrorMessages.CLINIC_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + return updatedClinic; + } + + public async isDoctorLinkedToClinic(doctorId: string, clinicId: string): Promise { + const clinicDoctor = await prisma.clinicDoctor.findUnique({ + where: { + clinic_id_doctor_id: { + clinic_id: clinicId, + doctor_id: doctorId, + } + } + }); + if (clinicDoctor === null) { + return false; + } + return true; + } + + public async updateClinicFees(doctorId: string, clinicId: string, fees: number): Promise { + const clinicDoctor = await prisma.clinicDoctor.update({ + where: { + clinic_id_doctor_id: { + clinic_id: clinicId, + doctor_id: doctorId, + } + }, + data: { + fees, + } + }); + if (clinicDoctor === null) { + return false; + } + return true; + } + + public async getActiveClinics(lang: 'en' | 'ar', payOnline?: boolean): Promise { + + const clinicDoctors = await prisma.clinicDoctor.findMany({ + where: { + is_accepting: true, + doctor: { + account_status: DoctorAccountStatus.APPROVED, + }, + clinic: { + ...(payOnline !== undefined && { canPayOnline: payOnline }), + } + }, + include: { + doctor: { + select: { + availability_type: true, + specialization: true, + user: { + select: { + id: true, + name: true, + gender: true, + phone: true, + date_of_birth: true, + photo_url: true, + }, + }, + }, + }, + clinic: { + select: { + id: true, + name: true, + phone: true, + canPayOnline: true, + opening_at: true, + closing_at: true, + address: true, + address_maps_link: true, + }, + }, + }, + }); + + const clinicsGroupsMap = new Map(); + for (const docClinic of clinicDoctors) { + const clinicId = docClinic.clinic.id; + if (!clinicId) continue; + + if (!clinicsGroupsMap.has(clinicId)) { + clinicsGroupsMap.set(clinicId, []); + } + clinicsGroupsMap.get(clinicId)!.push(docClinic); + } + + const clinicsData: DoctorClinics[] = []; + + for (const [clinicId, doctorRecords] of clinicsGroupsMap.entries()) { + const representativeRecord = doctorRecords[0]; + const clinic = representativeRecord.clinic; + const user = representativeRecord.doctor.user; + + if (!user) continue; + + + const allDoctors: Partial[] = []; + + for (const record of doctorRecords) { + const age = await this.userService.calculateUserAge(record.doctor.user.date_of_birth); + let isOnline = false; + if (record.doctor.availability_type == 'ONLINE' || record.doctor.availability_type == 'BOTH') { + isOnline = true; + } + const specResponse = formatSpecializationResponse(record.doctor.specialization as SpecializationKey, lang); + const specialization = specResponse.value; + + allDoctors.push({ + id: record.doctor.user.id, + name: record.doctor.user.name, + gender: record.doctor.user.gender, + age, + specialization, + phone: record.doctor.user.phone, + fees: representativeRecord.fees, + is_online: isOnline, + profilePic: record.doctor.user.photo_url, + }); + } + + clinicsData.push({ + id: clinic.id, + name: clinic.name, + phone: clinic.phone, + canPayOnline: clinic.canPayOnline, + opening_at: clinic.opening_at, + closing_at: clinic.closing_at, + address: clinic.address, + address_maps_link: clinic.address_maps_link || "", + doctors: allDoctors + }); + } + + return clinicsData; + + } +} \ No newline at end of file diff --git a/src/services/cron.service.ts b/src/services/cron.service.ts new file mode 100644 index 0000000..4f56b2c --- /dev/null +++ b/src/services/cron.service.ts @@ -0,0 +1,120 @@ +import cron from 'node-cron'; +import prisma from '@/config/prisma'; +import { Service, Container } from 'typedi'; + +@Service() +export class VacationCronService { + + private static async runScheduledTasks() { + try { + await this.updateVacationStatuses(); + await this.reactivateEndedSchedules(); + } catch (e) { + console.error('error running cron service:', e); + } + } + + static startCronJobs() { + // run every min + cron.schedule('* * * * *', async () => { + await this.runScheduledTasks(); + }); + // run every hour + // cron.schedule('0 * * * *', async () => { + // await this.runScheduledTasks(); + // }); + + this.runScheduledTasks(); + } + + private static async updateVacationStatuses() { + try { + // YYYY-MM-DD format + const today = new Date().toISOString().split('T')[0]; + + await prisma.vacation.updateMany({ + where: { + status: 'UPCOMING', + start_date: { + lte: today, + }, + end_date: { + gte: today, + }, + }, + data: { + status: 'CURRENT', + }, + }); + + await prisma.vacation.updateMany({ + where: { + status: 'CURRENT', + end_date: { + lt: today, + }, + }, + data: { + status: 'ENDED', + }, + }); + } + catch (e) { + console.error('vacation status Update Error', e); + throw e; + } + } + + + private static async reactivateEndedSchedules() { + try { + const today = new Date().toISOString().split('T')[0]; + + const endedSchedules = await prisma.doctorSchedule.findMany({ + where: { + is_active: false, + break_end: { + not: null, + lte: today, + }, + }, + include: { + vacations: { + where: { + end_date: { + lt: today, + }, + }, + }, + }, + }); + + for (const schedule of endedSchedules) { + await prisma.doctorSchedule.update({ + where: { id: schedule.id }, + data: { + is_active: true, + break_start: null, + break_end: null, + }, + }); + + await prisma.vacation.updateMany({ + where: { + schedule_id: schedule.id, + status: 'ENDED', + deleted_at: null, + }, + data: { + deleted_at: new Date() + }, + }); + + } + } + catch (e) { + console.error('schedule eeactivation error]', e); + throw e; + } + } +} \ No newline at end of file diff --git a/src/services/doctor.service.ts b/src/services/doctor.service.ts new file mode 100644 index 0000000..bd2d2b0 --- /dev/null +++ b/src/services/doctor.service.ts @@ -0,0 +1,918 @@ +import { DoctorLoginRequestDto, DoctorSignupRequestDto, PostAnnouncementDto, EditAnnouncementDto } from "@/dtos/doctors.dto"; +import { Service } from "typedi"; +import { HttpException } from "@/exceptions/HttpException"; +import { ErrorMessages, createBilingualError } from "@/utils/errorMessages"; +import { Doctor, DoctorAccountStatus, PrismaClient, Role } from "@prisma/client"; +import { hash, compare } from "bcrypt"; +import { DoctorLoginData, DoctorPersonalData, DoctorAnnouncements } from "@/interfaces/doctors.interface"; +import { NurseData, NurseFullDetails } from "@/interfaces/nurse.interface"; +import { AuthService } from "./auth.service"; +import prisma from "@/config/prisma"; +import cloudinary from "@/utils/cloudinary"; +import { DOCTOR_FILES } from "@/interfaces"; +import fs from "fs"; +import { AvailabilityType, Gender } from "@prisma/client"; +import { UserService } from "./user.service"; +import { DoctorClinics } from "@/interfaces"; +import { formatSpecializationResponse } from "@/utils/specializationTransform"; +import { SpecializationKey } from "@/constants/specializations"; + +const authService = new AuthService(); + +@Service() +export class DoctorService { + + private userService = new UserService(); + + public async signup(doctorData: DoctorSignupRequestDto, doctorFiles: {}): Promise { + // Check if email already exists + const existingUser = await prisma.user.findUnique({ + where: { email: doctorData.email } + }); + + if (existingUser) { + const error = createBilingualError(409, ErrorMessages.EMAIL_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + // Generate username from email + const username = doctorData.email.split('@')[0]; + + // Check if username exists + const existingUsername = await prisma.user.findUnique({ + where: { username } + }); + + if (existingUsername) { + const error = createBilingualError(409, ErrorMessages.USERNAME_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const hashedPassword = await hash(doctorData.password, 10); + + // Create user and doctor in a transaction + const createdUserId = await prisma.$transaction(async (tx) => { + const createdUser = await tx.user.create({ + data: { + email: doctorData.email, + name: doctorData.name, + username, + phone: doctorData.phone, + gender: doctorData.gender, + date_of_birth: new Date(doctorData.date_of_birth), + password_hash: hashedPassword, + role: Role.DOCTOR, + isVerified: true, + hasCompletedProfile: true, + }, + }); + + await tx.doctor.create({ + data: { + id: createdUser.id, + specialization: "IMMUNOLOGY", + account_status: DoctorAccountStatus.PENDING, + availability_type: doctorData.availability_type, + }, + }); + return createdUser.id; + }); + + // Upload files and update doctor record with files urls + if (doctorFiles && Object.keys(doctorFiles).length > 0) { + const doctorFilesArray = Object.values(doctorFiles).flat() as Express.Multer.File[]; + + await this._uploadFiles(doctorFilesArray, createdUserId); + } + } + + + public async login(doctorLoginData: DoctorLoginRequestDto): Promise<{ cookies: string[]; doctorAccountData: DoctorLoginData } | boolean> { + + // Find user by email or username + const doctorUserData = await prisma.user.findFirst({ + where: { + OR: [ + { email: doctorLoginData.emailOrUsername }, + { username: doctorLoginData.emailOrUsername } + ] + }, + select: { + id: true, + email: true, + username: true, + name: true, + phone: true, + gender: true, + hasCompletedProfile: true, + password_hash: true, + doctor: { + select: { + specialization: true, + account_status: true + } + } + } + }); + + // Check if user exists and password matches + if (!doctorUserData) { + const error = createBilingualError(401, ErrorMessages.USER_NOT_FOUND_CREDENTIALS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const isPasswordMatching = await compare(doctorLoginData.password, doctorUserData.password_hash); + + if (!isPasswordMatching) { + const error = createBilingualError(401, ErrorMessages.USER_NOT_FOUND_CREDENTIALS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (doctorUserData.doctor?.account_status !== DoctorAccountStatus.APPROVED) { + const error = createBilingualError(403, ErrorMessages.DOCTOR_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!doctorUserData.hasCompletedProfile) { + return false; + } + + const doctorAccountData: DoctorLoginData = + { + id: doctorUserData.id, + name: doctorUserData.name, + email: doctorUserData.email, + username: doctorUserData.username, + phone: doctorUserData.phone, + gender: doctorUserData.gender, + doctor: { + specialization: doctorUserData.doctor?.specialization, + account_status: doctorUserData.doctor?.account_status + } + } + + const token = await authService.createTokens(doctorUserData, doctorLoginData.rememberMe); + const cookies = authService.createCookies(token); + + return { cookies, doctorAccountData }; + } + + public async setPassword(doctorId: string, password: string): Promise { + const hashedPassword = await hash(password, 10); + const doctorUserData = await prisma.user.findUnique({ + where: { id: doctorId }, + select: { hasCompletedProfile: true } + }); + if (!doctorUserData) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + if (doctorUserData.hasCompletedProfile) { + const error = createBilingualError(400, ErrorMessages.DOCTOR_PASSWORD_ALREADY_SET); + throw new HttpException(error.status, error.message, error.messageAr); + } + await prisma.user.update({ + where: { id: doctorId }, + data: { + password_hash: hashedPassword, + hasCompletedProfile: true + } + }); + } + + + private async _uploadFiles(files: Express.Multer.File[], doctorId: string): Promise { + const uploadedFiles: { public_id: string }[] = []; + + try { + // Validate all fieldnames before uploading + for (const file of files) { + if (!Object.values(DOCTOR_FILES).includes(file.fieldname as any)) { + const error = createBilingualError(400, ErrorMessages.UNKNOWN_FILE_FIELDNAME); + throw new HttpException(error.status, error.message, error.messageAr); + } + } + + // Upload all files to Cloudinary in parallel + const uploadResults = await Promise.all( + files.map(file => + cloudinary.uploader.upload(file.path, { + folder: `DOCTORS/documents/${doctorId}`, + overwrite: false, + public_id: `DOCTOR_${doctorId}_${file.fieldname}_${Date.now()}` + }) + ) + ); + + // Track uploaded files for potential rollback + uploadedFiles.push(...uploadResults.map(r => ({ public_id: r.public_id }))); + + // Map file fields to database columns + const updateData: any = {}; + files.forEach((file, index) => { + const uploadResult = uploadResults[index]; + + switch (file.fieldname) { + case DOCTOR_FILES.GRADUATION_CERTIFICATE: + updateData.graduationCertificateUrl = uploadResult.secure_url; + updateData.graduationCertificatePublicId = uploadResult.public_id; + break; + case DOCTOR_FILES.MEMBERSHIP_CARD: + updateData.membershipCardUrl = uploadResult.secure_url; + updateData.membershipCardPublicId = uploadResult.public_id; + break; + case DOCTOR_FILES.PROFESSIONAL_PRACTICE_CARD: + updateData.professionalPracticeCardUrl = uploadResult.secure_url; + updateData.professionalPracticeCardPublicId = uploadResult.public_id; + break; + case DOCTOR_FILES.MASTERS_CERTIFICATE: + updateData.mastersCertificateUrl = uploadResult.secure_url; + updateData.mastersCertificatePublicId = uploadResult.public_id; + break; + case DOCTOR_FILES.FELLOWSHIP_CERTIFICATE: + updateData.fellowshipCertificateUrl = uploadResult.secure_url; + updateData.fellowshipCertificatePublicId = uploadResult.public_id; + break; + case DOCTOR_FILES.UNION_SPECIALIZATION_CERTIFICATE: + updateData.unionSpecializationCertificateUrl = uploadResult.secure_url; + updateData.unionSpecializationCertificatePublicId = uploadResult.public_id; + break; + } + console.log(`Deleting ${file.path}`); + + fs.unlinkSync(file.path); // Delete local file after upload + }); + + // Update database with all URLs in a single operation + await prisma.doctor.update({ + where: { id: doctorId }, + data: updateData + }); + + } catch (error) { + // Rollback: Delete all uploaded files from Cloudinary + if (uploadedFiles.length > 0) { + await Promise.all( + uploadedFiles.map(f => cloudinary.uploader.destroy(f.public_id).catch(() => { })) + ); + + } + + // Delete local files in case of error (only if they still exist) + files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + throw error; + } + } + + public async getDoctors(lang: 'en' | 'ar', gender?: string, minFees?: number, maxFees?: number, isOnline?: boolean): Promise { + const WhereClause: any = { + is_accepting: true, + doctor: { + account_status: DoctorAccountStatus.APPROVED, + } + }; + + if (isOnline !== undefined) { + WhereClause.doctor = { + ...(WhereClause.doctor || {}), + availability_type: isOnline + ? { in: [AvailabilityType.ONLINE, AvailabilityType.BOTH] } + : { in: [AvailabilityType.OFFLINE, AvailabilityType.BOTH] }, + }; + } + + if (minFees !== undefined || maxFees !== undefined) { + WhereClause.fees = {}; + + if (minFees !== undefined) { + WhereClause.fees.gte = minFees; + } + if (maxFees !== undefined) { + WhereClause.fees.lte = maxFees; + } + } + + if (gender) { + const normalized = gender.toUpperCase(); + if (normalized === 'MALE' || normalized === 'FEMALE') { + WhereClause.doctor.user = { + gender: normalized as Gender + }; + } + } + + const doctorClinics = await prisma.clinicDoctor.findMany({ + where: WhereClause, + include: { + doctor: { + select: { + availability_type: true, + specialization: true, + user: { + select: { + id: true, + name: true, + gender: true, + phone: true, + date_of_birth: true, + photo_url: true, + + }, + }, + }, + }, + clinic: { + select: { + id: true, + name: true, + phone: true, + canPayOnline: true, + opening_at: true, + closing_at: true, + address: true, + address_maps_link: true, + }, + }, + }, + }); + + const doctorGroupsMap = new Map(); + for (const docClinic of doctorClinics) { + const doctorId = docClinic.doctor.user?.id; + if (!doctorId) continue; + + if (!doctorGroupsMap.has(doctorId)) { + doctorGroupsMap.set(doctorId, []); + } + doctorGroupsMap.get(doctorId)!.push(docClinic); + } + + const doctorPersonalData: DoctorPersonalData[] = []; + + for (const [doctorId, clinicRecords] of doctorGroupsMap.entries()) { + const representativeRecord = clinicRecords[0]; + const doctor = representativeRecord.doctor; + const user = doctor.user; + + if (!user) continue; + + const age = await this.userService.calculateUserAge(user.date_of_birth); + let canWorkOnline = false; + if (doctor.availability_type == 'ONLINE' || doctor.availability_type == 'BOTH') { + canWorkOnline = true; + } + const allClinics: DoctorClinics[] = []; + + if (!isOnline) { + for (const record of clinicRecords) { + allClinics.push({ + id: record.clinic.id, + name: record.clinic.name, + phone: record.clinic.phone, + canPayOnline: record.clinic.canPayOnline, + opening_at: record.clinic.opening_at, + closing_at: record.clinic.closing_at, + address: record.clinic.address, + address_maps_link: record.clinic.address_maps_link || "", + }); + } + } + const specResponse = formatSpecializationResponse(doctor.specialization as SpecializationKey, lang); + + const specialization = specResponse.value; + + doctorPersonalData.push({ + id: user.id, + name: user.name, + gender: user.gender, + age, + specialization, + phone: user.phone, + fees: representativeRecord.fees, + profilePic: user.photo_url, + is_online: canWorkOnline, + clinics: allClinics, + }); + } + + return doctorPersonalData; + } + + public async getDoctorAnnouncements(doctorId: string): Promise { + const doctor = await prisma.doctor.findUnique({ + where: { + id: doctorId + }, + select: { + account_status: true + } + }); + + if (!doctor) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (doctor.account_status !== DoctorAccountStatus.APPROVED) { + const error = createBilingualError(403, ErrorMessages.DOCTOR_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const announcements = await prisma.announcement.findMany({ + where: { + doctor_id: doctorId + }, + select: { + id: true, + doctor: { + select: { + user: { + select: { + id: true, + name: true, + gender: true, + photo_url: true, + } + } + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + }, + working_days: { + select: { + day_of_week: true, + start_time: true, + end_time: true, + } + }, + status: true, + gender: true, + max_age: true, + years_of_experience: true, + notes: true, + + } + }); + return announcements.map(announcement => ({ + id: announcement.id, + doctor: { + id: announcement.doctor.user.id, + name: announcement.doctor.user.name, + gender: announcement.doctor.user.gender, + profilePic: announcement.doctor.user.photo_url, + }, + clinic: { + id: announcement.clinic.id, + name: announcement.clinic.name, + address: announcement.clinic.address, + address_maps_link: announcement.clinic.address_maps_link, + }, + working_days: announcement.working_days.map(wd => ({ + day_of_week: wd.day_of_week, + start_time: wd.start_time, + end_time: wd.end_time, + })), + status: announcement.status, + gender: announcement.gender || undefined, + max_age: announcement.max_age || undefined, + years_of_experience: announcement.years_of_experience || undefined, + notes: announcement.notes || undefined, + })); + } + + public async approveApplicant(announcementId: string, nurseId: string): Promise { + const application = await prisma.announcementNurse.findUnique({ + where: { + announcement_id_nurse_id: { + announcement_id: announcementId, + nurse_id: nurseId, + } + }, + select: { + status: true, + announcement: { + select: { + doctor_id: true, + clinic_id: true, + status: true, + working_days: { + select: { + day_of_week: true, + start_time: true, + end_time: true, + } + } + } + } + } + }); + + if (!application) { + const error = createBilingualError(404, ErrorMessages.APPLICATION_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (application.status !== 'PENDING') { + const error = createBilingualError(409, ErrorMessages.APPLICATION_ALREADY_PROCESSED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (application.announcement.status === 'EXPIRED') { + const error = createBilingualError(400, ErrorMessages.ANNOUNCEMENT_EXPIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.$transaction(async (tx) => { + await tx.announcementNurse.update({ + where: { + announcement_id_nurse_id: { + announcement_id: announcementId, + nurse_id: nurseId, + } + }, + data: { + status: 'APPROVED', + doctor_id: application.announcement.doctor_id, + clinic_id: application.announcement.clinic_id, + } + }); + + // await tx.announcementNurse.updateMany({ + // where: { + // announcement_id: announcementId, + // nurse_id: { not: nurseId }, + // status: 'PENDING' + // }, + // data: { + // status: 'DISABLED' + // } + // }); + + await tx.announcement.update({ + where: { + id: announcementId + }, + data: { + status: 'EXPIRED', + deleted_at: new Date() + } + }); + await tx.nurseSchedule.createMany({ + data: application.announcement.working_days.map(workDay => ({ + nurse_id: nurseId, + doctor_id: application.announcement.doctor_id, + clinic_id: application.announcement.clinic_id, + day_of_week: workDay.day_of_week, + start_time: workDay.start_time, + end_time: workDay.end_time, + is_online: !application.announcement.clinic_id, + is_active: true, + })) + }); + }); + } + + public async rejectApplicant(announcementId: string, nurseId: string): Promise { + const application = await prisma.announcementNurse.findUnique({ + where: { + announcement_id_nurse_id: { + announcement_id: announcementId, + nurse_id: nurseId, + } + }, + select: { + status: true, + announcement: { + select: { + doctor_id: true, + status: true, + } + } + } + }); + + if (!application) { + const error = createBilingualError(404, ErrorMessages.APPLICATION_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (application.status !== 'PENDING') { + const error = createBilingualError(409, ErrorMessages.APPLICATION_ALREADY_PROCESSED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (application.announcement.status === 'EXPIRED') { + const error = createBilingualError(400, ErrorMessages.ANNOUNCEMENT_EXPIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.announcementNurse.update({ + where: { + announcement_id_nurse_id: { + announcement_id: announcementId, + nurse_id: nurseId, + } + }, + data: { + status: 'REJECTED' + } + }); + } + + public async deleteAnnouncement(doctorId: string, announcementId: string): Promise { + const announcement = await prisma.announcement.findUnique({ + where: { + id: announcementId + }, + select: { + doctor_id: true, + status: true + } + }); + + if (!announcement) { + const error = createBilingualError(404, ErrorMessages.ANNOUNCEMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (announcement.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (announcement.status === 'EXPIRED') { + const error = createBilingualError(400, ErrorMessages.ANNOUNCEMENT_EXPIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.announcement.update({ + where: { + id: announcementId + }, + data: { + status: 'EXPIRED', + deleted_at: new Date() + } + }); + } + + public async editAnnouncement(doctorId: string, announcementId: string, data: EditAnnouncementDto): Promise { + const updateData: any = { ...data }; + const announcement = await prisma.announcement.findUnique({ + where: { + id: announcementId + }, + select: { + doctor_id: true, + status: true + } + }); + + if (!announcement) { + const error = createBilingualError(404, ErrorMessages.ANNOUNCEMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (announcement.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (announcement.status === 'EXPIRED') { + const error = createBilingualError(400, ErrorMessages.ANNOUNCEMENT_EXPIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (data.working_days !== undefined) { + updateData.working_days = { + deleteMany: {}, + create: data.working_days.map(day => ({ + day_of_week: day.day_of_week, + start_time: day.start_time, + end_time: day.end_time + })) + }; + } + + await prisma.announcement.update({ + where: { + id: announcementId + }, + data: updateData + }); + } + + public async getAnnouncementApplicants(doctorId: string, announcementId: string): Promise { + const announcement = await prisma.announcement.findUnique({ + where: { + id: announcementId + }, + select: { + doctor_id: true + } + }); + + if (!announcement) { + const error = createBilingualError(404, ErrorMessages.ANNOUNCEMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (announcement.doctor_id !== doctorId) { + const error = createBilingualError(403, ErrorMessages.UNAUTHORIZED_ACCESS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const applicants = await prisma.announcementNurse.findMany({ + where: { + announcement_id: announcementId, + status: 'PENDING' + }, + select: { + nurse: { + select: { + years_of_experience: true, + nationalCardUrl: true, + bonusFileUrl: true, + brief: true, + user: { + select: { + id: true, + name: true, + email: true, + gender: true, + phone: true, + date_of_birth: true, + photo_url: true, + } + } + } + } + } + }); + + return Promise.all(applicants.map(async ({ nurse }) => ({ + id: nurse.user.id, + name: nurse.user.name, + email: nurse.user.email, + gender: nurse.user.gender, + phone: nurse.user.phone, + age: await this.userService.calculateUserAge(nurse.user.date_of_birth), + profilePic: nurse.user.photo_url, + years_of_experience: nurse.years_of_experience, + nationalCardUrl: nurse.nationalCardUrl, + bonusFileUrl: nurse.bonusFileUrl, + brief: nurse.brief, + }))); + } + + public async getWorkingNurses(doctorId: string): Promise { + const workingNurses = await prisma.nurseSchedule.findMany({ + where: { + doctor_id: doctorId, + deleted_at: null + }, + orderBy: { + day_of_week: 'asc', + }, + select: { + nurse: { + select: { + id: true, + years_of_experience: true, + nationalCardUrl: true, + bonusFileUrl: true, + brief: true, + user: { + select: { + id: true, + name: true, + email: true, + gender: true, + phone: true, + date_of_birth: true, + photo_url: true, + } + } + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + }, + day_of_week: true, + start_time: true, + end_time: true, + } + }); + const result: NurseFullDetails[] = []; + + for (const row of workingNurses) { + const workingDay = { + day_of_week: row.day_of_week, + start_time: row.start_time, + end_time: row.end_time + }; + + let nurse = result.find(n => n.id === row.nurse.id); + + if (!nurse) { + nurse = { + id: row.nurse.id, + name: row.nurse.user.name, + email: row.nurse.user.email, + gender: row.nurse.user.gender, + phone: row.nurse.user.phone, + age: await this.userService.calculateUserAge(row.nurse.user.date_of_birth), + profilePic: row.nurse.user.photo_url, + years_of_experience: row.nurse.years_of_experience, + nationalCardUrl: row.nurse.nationalCardUrl, + bonusFileUrl: row.nurse.bonusFileUrl, + brief: row.nurse.brief, + clinics: [], + }; + result.push(nurse); + } + + let clinic = nurse.clinics.find(c => c.id === row.clinic?.id); + + if (!clinic) { + clinic = { ...row.clinic, working_days: [] }; + nurse.clinics.push(clinic); + } + + clinic.working_days.push(workingDay); + } + return result; + } + + public async postAnnouncement(doctorId: string, data: PostAnnouncementDto): Promise { + const doctor = await prisma.doctor.findUnique({ + where: { + id: doctorId + }, + select: { + account_status: true + } + }); + + if (!doctor) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (doctor.account_status !== DoctorAccountStatus.APPROVED) { + const error = createBilingualError(403, ErrorMessages.DOCTOR_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const clinicDoctor = await prisma.clinicDoctor.findUnique({ + where: { + clinic_id_doctor_id: { + clinic_id: data.clinic_id, + doctor_id: doctorId + } + } + }); + + if (!clinicDoctor) { + const error = createBilingualError(404, ErrorMessages.DOCTOR_NOT_ASSOCIATED_WITH_CLINIC); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.announcement.create({ + data: { + doctor_id: doctorId, + clinic_id: data.clinic_id, + gender: data.gender, + max_age: data.max_age, + years_of_experience: data.years_of_experience, + notes: data.notes, + working_days: { + create: data.working_days.map(day => ({ + day_of_week: day.day_of_week, + start_time: day.start_time, + end_time: day.end_time + })) + } + } + }); + } + +} + + + diff --git a/src/services/encryption.service.ts b/src/services/encryption.service.ts new file mode 100644 index 0000000..c79a79d --- /dev/null +++ b/src/services/encryption.service.ts @@ -0,0 +1,83 @@ +import { Service } from 'typedi'; +import * as crypto from 'crypto'; +import { HttpException } from '@/exceptions/HttpException'; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; + + +@Service() +export class EncryptionService { + private readonly algorithm = 'aes-256-gcm'; + private readonly ivLength = 12; + private readonly tagLength = 16; + private readonly keyLength = 32; + + + public encryptFile(fileBuffer: Buffer, dek: Buffer): Buffer { + const iv = crypto.randomBytes(this.ivLength); + const cipher = crypto.createCipheriv(this.algorithm, dek, iv); + + const encryptedData = Buffer.concat([cipher.update(fileBuffer), cipher.final()]); + const tag = cipher.getAuthTag(); + + return Buffer.concat([iv, tag, encryptedData]) + } + + public decryptFile(encryptedFile: Buffer, dek: Buffer): Buffer { + const iv = encryptedFile.subarray(0, this.ivLength); + const tag = encryptedFile.subarray(this.ivLength, this.ivLength + this.tagLength); + const encryptedData = encryptedFile.subarray(this.ivLength + this.tagLength); + + const decipher = crypto.createDecipheriv(this.algorithm, dek, iv); + decipher.setAuthTag(tag); + + return Buffer.concat([decipher.update(encryptedData), decipher.final()]); + + } + + public encryptDEK(dek: Buffer): string { + const masterKey = this.getMasterKey(); + const iv = crypto.randomBytes(this.ivLength); + + const cipher = crypto.createCipheriv(this.algorithm, masterKey, iv); + + const encryptedData = Buffer.concat([cipher.update(dek), cipher.final()]); + const tag = cipher.getAuthTag(); + return Buffer.concat([iv, tag, encryptedData]).toString('hex'); + + } + + public decryptDEK(encryptedDEK: string): Buffer { + const masterKey = this.getMasterKey(); + const data = Buffer.from(encryptedDEK, 'hex'); + + const iv = data.subarray(0, this.ivLength); + const tag = data.subarray(this.ivLength, this.ivLength + this.tagLength); + const encryptedData = data.subarray(this.ivLength + this.tagLength); + + const decipher = crypto.createDecipheriv(this.algorithm, masterKey, iv); + decipher.setAuthTag(tag); + + return Buffer.concat([decipher.update(encryptedData), decipher.final()]); + + } + + + public generateDEK(): Buffer { + return crypto.randomBytes(this.keyLength); + } + + private getMasterKey(): Buffer { + const masterKey = process.env.MASTER_ENCRYPTION_KEY; + console.log('Master Key:', masterKey); + if (!masterKey) { + const error = createBilingualError(500, ErrorMessages.MASTER_KEY_NOT_SET); + throw new HttpException(error.status, error.message, error.messageAr); + } + const keyBuffer = Buffer.from(masterKey, 'hex'); + if (keyBuffer.length !== this.keyLength) { + const error = createBilingualError(500, ErrorMessages.INVALID_MASTER_KEY_LENGTH); + throw new HttpException(error.status, error.message, error.messageAr); + } + return Buffer.from(masterKey, 'hex'); + } +} \ No newline at end of file diff --git a/src/services/fabric.service.ts b/src/services/fabric.service.ts new file mode 100644 index 0000000..3f59aeb --- /dev/null +++ b/src/services/fabric.service.ts @@ -0,0 +1,340 @@ +import * as grpc from '@grpc/grpc-js'; +import { connect, Contract, Gateway, Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import * as crypto from 'crypto'; +import { TextDecoder } from 'util'; +import { HttpException } from '@/exceptions/HttpException'; +import { MedicalRecord } from '@/interfaces/medical-records.interface'; +import { FabricIdentity } from '@/interfaces/fabric-identity.interface'; +import identityStorage from '@/services/identity-storage.service'; +import { backupService } from '@/services/backup.service'; + +interface GatewayConnection { + gateway: Gateway; + client: grpc.Client; + contract: Contract; + identity: FabricIdentity; + lastUsed: Date; +} + +class FabricService { + private readonly utf8Decoder = new TextDecoder(); + + // Connection cache with TTL + private connections: Map = new Map(); + private readonly CONNECTION_TTL_MS = 30 * 60 * 1000; // 30 minutes + private cleanupInterval: NodeJS.Timeout | null = null; + + constructor() { + this.startCleanupInterval(); + } + + public async getGatewayConnection(clinicId: string, forceNew = false): Promise { + if (forceNew) { + const stale = this.connections.get(clinicId); + if (stale) { + try { stale.gateway.close(); } catch (_) {} + try { stale.client.close(); } catch (_) {} + this.connections.delete(clinicId); + console.log(`🔄 Evicted stale connection for clinic: ${clinicId}`); + } + } else { + const cached = this.connections.get(clinicId); + if (cached) { + cached.lastUsed = new Date(); + return cached; + } + } + + const identity = await identityStorage.getIdentity(clinicId); + const connection = await this.createConnection(identity); + this.connections.set(clinicId, connection); + + console.log(`Created new gateway connection for clinic: ${clinicId}`); + return connection; + } + + private async createConnection(identity: FabricIdentity): Promise { + try { + const client = await this.newGrpcConnection(identity); + + const gateway = connect({ + client, + identity: this.createIdentity(identity), + signer: this.createSigner(identity), + }); + + const network = gateway.getNetwork(identity.channelName); + const contract = network.getContract(identity.chaincodeName); + + return { + gateway, + client, + contract, + identity, + lastUsed: new Date(), + }; + } catch (error: any) { + console.error(`Failed to create connection for clinic ${identity.clinicId}:`, error.message); + throw new HttpException(503, `Failed to connect to Fabric network: ${error.message}`); + } + } + + private async newGrpcConnection(identity: FabricIdentity): Promise { + const tlsPem = identity.tlsCertificate.endsWith('\n') ? identity.tlsCertificate : identity.tlsCertificate + '\n'; + const tlsRootCert = Buffer.from(tlsPem); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + + return new grpc.Client(identity.peerEndpoint, tlsCredentials, { + 'grpc.ssl_target_name_override': identity.peerHostAlias, + 'grpc.keepalive_time_ms': 120000, + 'grpc.http2.min_time_between_pings_ms': 120000, + 'grpc.keepalive_timeout_ms': 20000, + 'grpc.http2.max_pings_without_data': 0, + 'grpc.keepalive_permit_without_calls': 1, + }); + } + + private createIdentity(identity: FabricIdentity): Identity { + return { + mspId: identity.mspId, + credentials: Buffer.from(identity.certificate), + }; + } + + private createSigner(identity: FabricIdentity): Signer { + const privateKey = crypto.createPrivateKey(identity.privateKey); + return signers.newPrivateKeySigner(privateKey); + } + + public async initLedger(clinicId: string, backupData: MedicalRecord[] = []): Promise { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Submit Transaction: InitLedger (clinic: ${clinicId}, records: ${backupData.length})`); + await contract.submitTransaction('InitLedger', JSON.stringify(backupData)); + console.log('*** InitLedger transaction committed successfully'); + } + + public async storeRecordKey(clinicId: string, patientId: string, recordId: string, encryptedDEK: string): Promise { + backupService.storeRecordKey(patientId, recordId, encryptedDEK); + + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Submit Transaction: StoreRecordKey (clinic: ${clinicId}, patient: ${patientId}, record: ${recordId})`); + await contract.submit('StoreRecordKey', { + arguments: [patientId, recordId], + transientData: { encryptedDEK: Buffer.from(encryptedDEK) }, + }); + } catch (error) { + console.error(`StoreRecordKey transaction failed for clinic ${clinicId}:`, error); + } + } + + public async getRecordKey(clinicId: string, patientId: string, recordId: string): Promise { + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Evaluate Transaction: GetRecordKey (clinic: ${clinicId}, patient: ${patientId}, record: ${recordId})`); + const resultBytes = await contract.evaluateTransaction('GetRecordKey', patientId, recordId); + return this.utf8Decoder.decode(resultBytes); + } catch (error) { + console.error(`GetRecordKey error for clinic ${clinicId}, falling back to BackupService:`, error); + return backupService.getRecordKey(patientId, recordId); + } + } + + public async recordKeyExists(clinicId: string, patientId: string, recordId: string): Promise { + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Evaluate Transaction: RecordKeyExists (clinic: ${clinicId}, patient: ${patientId}, record: ${recordId})`); + const resultBytes = await contract.evaluateTransaction('RecordKeyExists', patientId, recordId); + return this.utf8Decoder.decode(resultBytes) === 'true'; + } catch (error) { + console.error(`RecordKeyExists error for clinic ${clinicId}, falling back to BackupService:`, error); + return backupService.recordKeyExists(patientId, recordId); + } + } + + public async getAllRecords(clinicId: string): Promise { + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Evaluate Transaction: GetAllRecords (clinic: ${clinicId})`); + const resultBytes = await contract.evaluateTransaction('GetAllRecords'); + const resultJson = this.utf8Decoder.decode(resultBytes); + return JSON.parse(resultJson) as MedicalRecord[]; + } catch (error) { + console.error(`GetAllRecords failed for clinic ${clinicId}, falling back to BackupService:`, error); + return backupService.getAllRecords(); + } + } + + public async addRecord(clinicId: string, payload: MedicalRecord): Promise { + try { + const identity = await identityStorage.getIdentity(clinicId); + backupService.addRecord(payload, identity.mspId); + } catch (e) { + console.error(`BackupService addRecord error:`, e); + } + + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Submit Transaction: AddRecord (clinic: ${clinicId})`); + + await contract.submit('AddRecord', { + arguments: [payload.patientId, payload.recordId, payload.doctorId, payload.type], + transientData: { + ipfsCid: Buffer.from(payload.ipfsCidKey), + }, + }); + } catch (error) { + console.error(`AddRecord transaction failed for clinic ${clinicId}:`, error); + } + } + + public async getRecordsByPatient(clinicId: string, patientId: string, retry = true): Promise { + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Evaluate Transaction: GetRecordsByPatient (clinic: ${clinicId}, patient: ${patientId})`); + + const resultBytes = await contract.evaluateTransaction('GetRecordsByPatient', patientId); + + const resultJson = this.utf8Decoder.decode(resultBytes); + return JSON.parse(resultJson) as MedicalRecord[]; + } catch (err: unknown) { + const msg = (err instanceof Error ? err.message : String(err)) || ''; + console.error(`GetRecordsByPatient error for clinic ${clinicId}:`, err); + // Fallback to BackupService if not an explicit access denial + if (msg.toLowerCase().includes('not authorized')) { + throw new HttpException(403, `Access denied for clinic ${clinicId} to records of patient ${patientId}`, msg); + } + // ABORTED (gRPC code 10) usually means the channel is stale — evict and retry once + if (retry && (msg.includes('ABORTED') || msg.includes('10 ABORTED'))) { + console.warn(`ABORTED on GetRecordsByPatient for clinic ${clinicId}, retrying with fresh connection...`); + try { + await this.getGatewayConnection(clinicId, true); + return await this.getRecordsByPatient(clinicId, patientId, false); + } catch (retryErr) { + console.error(`Retry failed, falling back to BackupService for patient ${patientId}`); + try { + const identity = await identityStorage.getIdentity(clinicId); + return backupService.getRecordsByPatient(patientId, identity.mspId); + } catch (e) { + return []; + } + } + } + + console.warn(`Falling back to BackupService for GetRecordsByPatient (patient: ${patientId})`); + try { + const identity = await identityStorage.getIdentity(clinicId); + return backupService.getRecordsByPatient(patientId, identity.mspId); + } catch (e) { + return []; + } + } + } + + public async grantAccess(clinicId: string, patientId: string, targetClinic: string): Promise { + const targetMsp = (await identityStorage.getIdentity(targetClinic)).mspId; + + try { + const clientIdentity = await identityStorage.getIdentity(clinicId); + backupService.grantAccess(patientId, clientIdentity.mspId, targetMsp); + } catch (e) { + console.error(`BackupService grantAccess error:`, e); + } + + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Submit Transaction: GrantAccess (clinic: ${clinicId}, patient: ${patientId})`); + await contract.submitTransaction('GrantAccess', patientId, targetMsp); + } catch (error) { + console.error(`GrantAccess transaction failed for clinic ${clinicId}:`, error); + } + } + + public async updateRecord(clinicId: string, patientId: string, payload: Omit): Promise { + backupService.updateRecord(patientId, payload); + + try { + const { contract, identity } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Submit Transaction: UpdateRecord (clinic: ${clinicId})`); + + const transientData: Record = {}; + if (payload.ipfsCidKey) { + transientData.ipfsCid = Buffer.from(payload.ipfsCidKey); + } + + await contract.submit('UpdateRecord', { + arguments: [patientId, payload.recordId, payload.doctorId, payload.type], + ...(Object.keys(transientData).length > 0 ? { transientData } : {}), + }); + } catch (error) { + console.error(`UpdateRecord transaction failed for clinic ${clinicId}:`, error); + } + } + + public async deleteRecord(clinicId: string, patientId: string, recordId: string): Promise { + backupService.deleteRecord(patientId, recordId); + + try { + const { contract } = await this.getGatewayConnection(clinicId); + console.log(`\n--> Submit Transaction: DeleteRecord (clinic: ${clinicId}, patient: ${patientId}, record: ${recordId})`); + await contract.submitTransaction('DeleteRecord', patientId, recordId); + } catch (error) { + console.error(`DeleteRecord transaction failed for clinic ${clinicId}:`, error); + } + } + + public async closeConnection(clinicId: string): Promise { + const connection = this.connections.get(clinicId); + if (connection) { + connection.gateway.close(); + connection.client.close(); + this.connections.delete(clinicId); + console.log(`Closed connection for clinic: ${clinicId}`); + } + } + + public closeAllConnections(): void { + for (const [label, connection] of this.connections) { + connection.gateway.close(); + connection.client.close(); + console.log(`Closed connection for: ${label}`); + } + this.connections.clear(); + + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + } + + private startCleanupInterval(): void { + this.cleanupInterval = setInterval( + () => { + const now = new Date().getTime(); + + for (const [label, connection] of this.connections) { + const age = now - connection.lastUsed.getTime(); + if (age > this.CONNECTION_TTL_MS) { + connection.gateway.close(); + connection.client.close(); + this.connections.delete(label); + console.log(`Cleaned up stale connection for: ${label}`); + } + } + }, + 5 * 60 * 1000, + ); // Check every 5 minutes + } + + public getConnectionStats(): { total: number; connections: Array<{ clinicId: string; lastUsed: string }> } { + return { + total: this.connections.size, + connections: Array.from(this.connections.entries()).map(([clinicId, conn]) => ({ + clinicId, + lastUsed: conn.lastUsed.toISOString(), + })), + }; + } +} + +export default FabricService; diff --git a/src/services/googleAuth.service.ts b/src/services/googleAuth.service.ts new file mode 100644 index 0000000..7552ebf --- /dev/null +++ b/src/services/googleAuth.service.ts @@ -0,0 +1,65 @@ +import { CreateGoogleUsersDto } from "@/dtos/googleUsers.dto"; +import { User, UserLoginData } from "@/interfaces"; +import { PrismaClient } from "@prisma/client"; +import { Service } from "typedi"; +import { HttpException } from "@/exceptions/HttpException"; +import { ErrorMessages, createBilingualError } from "@/utils/errorMessages"; +import prisma from "@/config/prisma"; + + +@Service() +export class GoogleAuthService { + + public async createInitialProfileGoogle(newUserData: CreateGoogleUsersDto): Promise { + const username = newUserData.email.split('@')[0]; + const createdUser: User = await prisma.user.create({ + data: { + email: newUserData.email, + name: newUserData.name, + isVerified: newUserData.isEmailVerified, + username, + phone: '', + gender: "MALE", + date_of_birth: new Date('2000-01-01'), + password_hash: '', + }, + }); + await prisma.patient.create({ + data: { + id: createdUser.id, + bc_address: '', + consent: false, + } + }); + return createdUser; + } + + public async updatePhoneNumber(userId: string, phone: string): Promise { + await prisma.user.update({ + where: { id: userId }, + data: { phone }, + }); + } + + public async getGoogleUserData(userId: string): Promise { + const user: UserLoginData | null = await prisma.user.findUnique({ + where: { id: userId }, + select: { + email: true, + name: true, + username: true, + role: true, + phone: true, + gender: true, + date_of_birth: true, + isVerified: true, + hasCompletedProfile: true, + } + }); + if (!user) { + const err = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(err.status, err.message, err.messageAr); + } + return user; + } +} \ No newline at end of file diff --git a/src/services/identity-storage.service.ts b/src/services/identity-storage.service.ts new file mode 100644 index 0000000..a880525 --- /dev/null +++ b/src/services/identity-storage.service.ts @@ -0,0 +1,237 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import { FabricIdentity, FabricIdentityInput } from '@/interfaces/fabric-identity.interface'; +import { HttpException } from '@/exceptions/HttpException'; + +export class IdentityStorageService { + private readonly storagePath: string; + private readonly encryptionKey: Buffer; + private readonly defaultClinicId = 'default-clinic'; + + constructor() { + this.storagePath = process.env.FABRIC_IDENTITY_STORAGE_PATH || + path.resolve(__dirname, '../../data/fabric-identities.json'); + + // for private key encryption + const keyEnv = process.env.FABRIC_IDENTITY_ENCRYPTION_KEY; + if (keyEnv) { + this.encryptionKey = Buffer.from(keyEnv, 'hex'); + } else { + this.encryptionKey = crypto.scryptSync('development-only-key', 'salt', 32); + } + } + + private async readAll(): Promise { + try { + const dir = path.dirname(this.storagePath); + await fs.mkdir(dir, { recursive: true }); + const data = await fs.readFile(this.storagePath, 'utf-8'); + const stored = JSON.parse(data) as FabricIdentity[]; + return stored.map(identity => ({ + ...identity, + privateKey: this.decrypt(identity.privateKey), + })); + } catch (error: any) { + if (error.code === 'ENOENT') { + return []; + } + throw error; + } + } + + + public async storeIdentity(input: FabricIdentityInput): Promise { + const identities = await this.readAll(); + const now = new Date().toISOString(); + const existing = identities.find(id => id.clinicId === input.clinicId); + + const identity: FabricIdentity = { + clinicId: input.clinicId, + mspId: input.mspId, + certificate: input.certificate, + privateKey: input.privateKey, + peerEndpoint: input.peerEndpoint, + peerHostAlias: input.peerHostAlias, + tlsCertificate: input.tlsCertificate, + channelName: input.channelName || 'mychannel', + chaincodeName: input.chaincodeName || 'test', + createdAt: existing?.createdAt || now, + updatedAt: now, + }; + + this.validateIdentity(identity); + + const updated = identities.filter(id => id.clinicId !== identity.clinicId); + updated.push(identity); + await this.persistToStorage(updated); + + console.log(`✅ Stored identity for clinic: ${identity.clinicId} (MSP: ${identity.mspId})`); + + return this.sanitizeIdentity(identity); + } + + + public async getIdentity(clinicId: string): Promise { + const identities = await this.readAll(); + const identity = identities.find(id => id.clinicId === clinicId); + + if (!identity) { + throw new HttpException(404, `Identity not found for clinic: ${clinicId}`); + } + + return identity; + } + + public async listIdentities(): Promise>> { + const identities = await this.readAll(); + + return identities.map(identity => ({ + clinicId: identity.clinicId, + mspId: identity.mspId, + peerEndpoint: identity.peerEndpoint, + peerHostAlias: identity.peerHostAlias, + channelName: identity.channelName, + chaincodeName: identity.chaincodeName, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + })); + } + public async defaultIdentity(): Promise { + const identities = await this.readAll(); + const defaultIdentity = identities.find(id => id.clinicId === this.defaultClinicId); + if (!defaultIdentity) { + throw new HttpException(404, 'Default identity not found'); + } + return defaultIdentity; + } + + public async deleteIdentity(clinicId: string): Promise { + const identities = await this.readAll(); + const index = identities.findIndex(id => id.clinicId === clinicId); + + if (index === -1) { + throw new HttpException(404, `Identity not found for clinic: ${clinicId}`); + } + + identities.splice(index, 1); + await this.persistToStorage(identities); + + console.log(`🗑️ Deleted identity for clinic: ${clinicId}`); + } + + public async hasIdentity(clinicId: string): Promise { + const identities = await this.readAll(); + return identities.some(id => id.clinicId === clinicId); + } + + + private validateIdentity(identity: FabricIdentity): void { + if (!identity.clinicId || identity.clinicId.trim() === '') { + throw new HttpException(400, 'Clinic ID is required'); + } + + if (!identity.mspId || identity.mspId.trim() === '') { + throw new HttpException(400, 'MSP ID is required'); + } + + if (!identity.certificate || !identity.certificate.includes('BEGIN CERTIFICATE')) { + throw new HttpException(400, 'Invalid certificate PEM format'); + } + + if (!identity.privateKey || !identity.privateKey.includes('BEGIN')) { + throw new HttpException(400, 'Invalid private key PEM format'); + } + + if (!identity.peerEndpoint || !identity.peerEndpoint.includes(':')) { + throw new HttpException(400, 'Invalid peer endpoint format (expected host:port)'); + } + + if (!identity.tlsCertificate || !identity.tlsCertificate.includes('BEGIN CERTIFICATE')) { + throw new HttpException(400, 'Invalid TLS certificate PEM format'); + } + } + + + private async persistToStorage(identities: FabricIdentity[]): Promise { + const toStore = identities.map(identity => ({ + ...identity, + privateKey: this.encrypt(identity.privateKey), + })); + + await fs.writeFile( + this.storagePath, + JSON.stringify(toStore, null, 2), + { mode: 0o600 } // Read/write only for owner + ); + } + + private encrypt(plaintext: string): string { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv); + + let encrypted = cipher.update(plaintext, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + + return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; + } + + + private decrypt(ciphertext: string): string { + if (!this.isEncryptedPayload(ciphertext)) { + // Value is plain text or a non-AES placeholder (e.g. dummy keys) + return ciphertext; + } + + const [ivHex, authTagHex, encrypted] = ciphertext.split(':'); + + try { + const iv = Buffer.from(ivHex, 'hex'); + const authTag = Buffer.from(authTagHex, 'hex'); + + const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch { + // Keep backward compatibility with legacy/plaintext values that happen to contain ':' + return ciphertext; + } + } + + private isEncryptedPayload(value: string): boolean { + const parts = value.split(':'); + if (parts.length !== 3) { + return false; + } + + const [ivHex, authTagHex, encryptedHex] = parts; + const isHex = (input: string) => /^[0-9a-fA-F]+$/.test(input); + + // AES-256-GCM format: 16-byte IV + 16-byte auth tag + hex ciphertext + if (ivHex.length !== 32 || authTagHex.length !== 32) { + return false; + } + + if (encryptedHex.length === 0 || encryptedHex.length % 2 !== 0) { + return false; + } + + return isHex(ivHex) && isHex(authTagHex) && isHex(encryptedHex); + } + + private sanitizeIdentity(identity: FabricIdentity): FabricIdentity { + return { + ...identity, + privateKey: '[REDACTED]', + }; + } +} + +export default new IdentityStorageService(); \ No newline at end of file diff --git a/src/services/ipfs.service.ts b/src/services/ipfs.service.ts new file mode 100644 index 0000000..052f373 --- /dev/null +++ b/src/services/ipfs.service.ts @@ -0,0 +1,60 @@ +import { PinataSDK } from 'pinata'; +import { HttpException } from '@/exceptions/HttpException'; +import { Service } from 'typedi'; +import { Blob, File } from 'buffer'; + +@Service() +export class IpfsService { + private pinata: PinataSDK; + + constructor() { + this.pinata = new PinataSDK({ + pinataJwt: process.env.PINATA_JWT, + pinataGateway: process.env.PINATA_GATEWAY, + }); + } + + public async uploadFile(fileData: Buffer, fileName: string, mimeType: string): Promise { + try { + const file = new File([fileData], fileName, { type: mimeType }); + const upload = await this.pinata.upload.file(file); + return upload.cid; + } + catch (e) { + throw new HttpException(500, `IPFS upload failed: ${e.message}`); + } + } + + public async getFile(cid: string): Promise { + try { + const response = await this.pinata.gateways.get(cid); + + if (response.data instanceof Blob) { + const arrayBuffer = await response.data.arrayBuffer(); + return Buffer.from(arrayBuffer); + } + return Buffer.from(response.data as string, 'binary'); + } + catch (e) { + throw new HttpException(500, `IPFS fetch failed: ${e.message}`); + } + } + + public async checkHealth(): Promise<{ status: string; message: string }> { + try { + await this.pinata.testAuthentication(); + return { status: 'ok', message: 'IPFS connection is healthy' }; + } catch (e) { + throw new HttpException(503, `IPFS connection failed: ${e.message}`); + } + } + + public async deleteFile(cid: string): Promise { + try { + await this.pinata.files.delete([cid]); + } + catch (e) { + throw new HttpException(500, `IPFS delete failed: ${e.message}`); + } + } +} \ No newline at end of file diff --git a/src/services/key-management.service.ts b/src/services/key-management.service.ts new file mode 100644 index 0000000..af48c05 --- /dev/null +++ b/src/services/key-management.service.ts @@ -0,0 +1,45 @@ +import { Service } from 'typedi'; +import { EncryptionService } from './encryption.service'; +import { HttpException } from '@/exceptions/HttpException'; +import FabricService from '@/services/fabric.service'; + +@Service() +export class KeyManagementService { + + private encryptionService = new EncryptionService(); + private fabricService = new FabricService(); + + /** + * Generates a fresh DEK, wraps it with the master key, and stores the + * encrypted form in the record's implicit private data collection on the + * blockchain. Throws if a key already exists for this record. + */ + public async createRecordKey(clinicId: string, patientId: string, recordId: string): Promise { + const exists = await this.fabricService.recordKeyExists(clinicId, patientId, recordId); + if (exists) { + throw new HttpException(400, `Encryption key already exists for record: ${recordId}`); + } + + const recordDEK = this.encryptionService.generateDEK(); + const encryptedDEK = this.encryptionService.encryptDEK(recordDEK); + recordDEK.fill(0); + + await this.fabricService.storeRecordKey(clinicId, patientId, recordId, encryptedDEK); + } + + /** + * Fetches the encrypted DEK for a specific record from the blockchain and + * decrypts it with the master key. Creates a new key automatically if one + * does not yet exist. + */ + public async getRecordDEK(clinicId: string, patientId: string, recordId: string): Promise { + const exists = await this.fabricService.recordKeyExists(clinicId, patientId, recordId); + if (!exists) { + await this.createRecordKey(clinicId, patientId, recordId); + } + + const encryptedDEK = await this.fabricService.getRecordKey(clinicId, patientId, recordId); + return this.encryptionService.decryptDEK(encryptedDEK); + } +} + diff --git a/src/services/medical-records.service.ts b/src/services/medical-records.service.ts new file mode 100644 index 0000000..2b9c863 --- /dev/null +++ b/src/services/medical-records.service.ts @@ -0,0 +1,546 @@ +import { HttpException } from '@/exceptions/HttpException'; +import { CreateDoctorRecordJsonDto, CreateMedicalRecordDto } from '@/dtos/medical-records.dto'; +import { MedicalRecord, MedicalRecordFile } from '@/interfaces/medicalRecords.interface'; +import prisma from '@/config/prisma'; +import { Prisma } from '@prisma/client'; +import { Service } from 'typedi'; +import { IpfsService } from '@/services/ipfs.service'; +import { EncryptionService } from '@/services/encryption.service'; +import { KeyManagementService } from '@/services/key-management.service'; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { randomUUID } from 'crypto'; +import FabricService from '@/services/fabric.service'; +import { RecordType } from '@/interfaces/enums.interface'; +import { IdentityStorageService } from '@/services/identity-storage.service'; +import { backupService } from '@/services/backup.service'; + +@Service() +export class MedicalRecordService { + private ipfsService = new IpfsService(); + private encryptionService = new EncryptionService(); + private keyManagementService = new KeyManagementService(); + private fabricService = new FabricService(); + private identityStorageService = new IdentityStorageService(); + + public async createMedicalRecord( + clinicId: string, + patientId: string, + doctorId: string, + fileData: CreateMedicalRecordDto, + fileBuffer: Buffer, + fileName: string, + mimeType: string, + ): Promise { + const recordId = randomUUID(); + + const recordDEK = await this.keyManagementService.getRecordDEK(clinicId, patientId, recordId); + const encryptedFile = this.encryptionService.encryptFile(fileBuffer, recordDEK); + recordDEK.fill(0); + + const cid = await this.ipfsService.uploadFile(encryptedFile, fileName, mimeType); + + await prisma.medicalRecord.create({ + data: { + id: recordId, + patient_id: patientId, + doctor_id: doctorId, + clinic_id: clinicId, + appointment_id: (fileData as any).appointmentId, + name: fileData.name, + cid: cid, + type: fileData.type, + mime_type: mimeType, + } as Prisma.MedicalRecordUncheckedCreateInput, + }); + + await this.fabricService.addRecord(clinicId, { + patientId, + recordId, + doctorId, + type: fileData.type, + ipfsCidKey: cid, + }); + } + + public async getRecordFile(callerClinicId: string, recordId: string): Promise { + const record = await prisma.medicalRecord.findFirst({ + where: { + id: recordId, + deleted_at: null, + }, + select: { + id: true, + patient_id: true, + clinic_id: true, + doctor_id: true, + appointment_id: true, + name: true, + type: true, + mime_type: true, + cid: true, + }, + }); + + if (!record) { + const error = createBilingualError(404, ErrorMessages.RECORD_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const authorizedRecords = await this.fabricService.getRecordsByPatient(callerClinicId, record.patient_id); + const isAuthorized = authorizedRecords.some(r => r.recordId === recordId); + if (!isAuthorized) { + throw new HttpException(403, 'Access denied: your clinic is not authorized to access this record'); + } + + const encryptedFile = await this.ipfsService.getFile(record.cid); + + const recordDEK = await this.keyManagementService.getRecordDEK(record.clinic_id, record.patient_id, record.id); + const decryptedFile = this.encryptionService.decryptFile(encryptedFile, recordDEK); + recordDEK.fill(0); + + return { + id: record.id, + patient_id: record.patient_id, + clinic_id: record.clinic_id, + doctor_id: record.doctor_id ?? undefined, + appointment_id: record.appointment_id ?? undefined, + name: record.name, + type: record.type, + mime_type: record.mime_type, + cid: record.cid, + buffer: decryptedFile, + }; + } + + public async checkIpfsHealth(): Promise<{ status: string; message: string }> { + return this.ipfsService.checkHealth(); + } + + public async getPatientFiles(patientId: string): Promise { + const records = await prisma.medicalRecord.findMany({ + where: { + patient_id: patientId, + deleted_at: null, + }, + select: { + id: true, + patient_id: true, + clinic_id: true, + doctor_id: true, + appointment_id: true, + name: true, + type: true, + mime_type: true, + cid: true, + }, + orderBy: { + created_at: 'desc', + }, + }); + + return records.map(record => ({ + id: record.id, + patient_id: record.patient_id, + clinic_id: record.clinic_id, + doctor_id: record.doctor_id ?? undefined, + appointment_id: record.appointment_id ?? undefined, + name: record.name, + type: record.type, + cid: record.cid, + mime_type: record.mime_type, + })); + } + + public async deleteRecord(callerClinicId: string, recordId: string): Promise { + const record = await prisma.medicalRecord.findFirst({ + where: { + id: recordId, + deleted_at: null, + }, + }); + + if (!record) { + const error = createBilingualError(404, ErrorMessages.RECORD_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (record.deleted_at) { + const error = createBilingualError(404, ErrorMessages.RECORD_ALREADY_DELETED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await this.fabricService.deleteRecord(callerClinicId, record.patient_id, recordId); + + await prisma.medicalRecord.update({ + where: { id: recordId }, + data: { deleted_at: new Date() }, + }); + } + + public async grantAccess(patientId: string, targetClinicId: string): Promise { + const records = await prisma.medicalRecord.findMany({ + where: { patient_id: patientId, deleted_at: null }, + select: { clinic_id: true }, + }); + + const ownerClinicIds = [...new Set(records.map(r => r.clinic_id))]; + + for (const ownerClinicId of ownerClinicIds) { + await this.fabricService.grantAccess(ownerClinicId, patientId, targetClinicId); + } + } + + public async addDoctorRecord(clinicId: string, patientId: string, doctorId: string, dto: CreateDoctorRecordJsonDto): Promise { + const clinicDoctor = await prisma.clinicDoctor.findUnique({ + where: { clinic_id_doctor_id: { clinic_id: clinicId, doctor_id: doctorId } }, + }); + if (!clinicDoctor) { + throw new HttpException(403, 'Doctor is not associated with this clinic'); + } + + const recordId = randomUUID(); + + const contentBuffer = Buffer.from(JSON.stringify(dto.content), 'utf-8'); + const recordDEK = await this.keyManagementService.getRecordDEK(clinicId, patientId, recordId); + const encryptedFile = this.encryptionService.encryptFile(contentBuffer, recordDEK); + recordDEK.fill(0); + + const cid = await this.ipfsService.uploadFile(encryptedFile, `${recordId}.enc`, 'application/octet-stream'); + + await prisma.medicalRecord.create({ + data: { + id: recordId, + patient_id: patientId, + doctor_id: doctorId, + clinic_id: clinicId, + name: dto.name, + cid: cid, + type: dto.type, + mime_type: 'application/json', + } as Prisma.MedicalRecordUncheckedCreateInput, + }); + + await this.fabricService.addRecord(clinicId, { + patientId, + recordId, + doctorId, + type: dto.type, + ipfsCidKey: cid, + }); + + return recordId; + } + + public async getPatientRecordsForDoctor(callerClinicId: string, patientId: string): Promise { + const authorizedOnChain = await this.fabricService.getRecordsByPatient(callerClinicId, patientId); + const authorizedIds = new Set(authorizedOnChain.map(r => r.recordId)); + + const records = await prisma.medicalRecord.findMany({ + where: { patient_id: patientId, deleted_at: null }, + select: { + id: true, + patient_id: true, + clinic_id: true, + doctor_id: true, + appointment_id: true, + name: true, + type: true, + mime_type: true, + cid: true, + }, + orderBy: { created_at: 'desc' }, + }); + + return records + .filter(r => authorizedIds.has(r.id)) + .map(record => ({ + id: record.id, + patient_id: record.patient_id, + clinic_id: record.clinic_id, + doctor_id: record.doctor_id ?? undefined, + appointment_id: record.appointment_id ?? undefined, + name: record.name, + type: record.type, + cid: record.cid, + mime_type: record.mime_type, + })); + } + + public async getSOAPNotes(callerClinicId: string, patientId: string): Promise> { + const records = await prisma.medicalRecord.findMany({ + where: { + patient_id: patientId, + mime_type: 'application/json', + deleted_at: null, + }, + select: { + id: true, + patient_id: true, + clinic_id: true, + cid: true, + }, + orderBy: { created_at: 'desc' }, + }); + + const authorizedRecords = await this.fabricService.getRecordsByPatient(callerClinicId, patientId); + const authorizedIds = new Set(authorizedRecords.map(r => r.recordId)); + + const results: Array<{ recordId: string; content: any }> = []; + + for (const record of records) { + if (!authorizedIds.has(record.id)) continue; + + try { + const encryptedFile = await this.ipfsService.getFile(record.cid); + const recordDEK = await this.keyManagementService.getRecordDEK(record.clinic_id, record.patient_id, record.id); + const decryptedFile = this.encryptionService.decryptFile(encryptedFile, recordDEK); + recordDEK.fill(0); + + const content = JSON.parse(decryptedFile.toString('utf-8')); + results.push({ recordId: record.id, content }); + } catch (e) { + console.warn(`Skipping record ${record.id}: not a JSON record (${e.message})`); + } + } + + return results; + } + + public async getSOAPNotesForPatient(patientId: string, type?: RecordType): Promise> { + const records = await prisma.medicalRecord.findMany({ + where: { + patient_id: patientId, + mime_type: 'application/json', + ...(type ? { type } : {}), + deleted_at: null, + }, + select: { + id: true, + patient_id: true, + clinic_id: true, + cid: true, + }, + orderBy: { created_at: 'desc' }, + }); + + const distinctClinicIds = [...new Set(records.map(r => r.clinic_id))]; + + const authorizedIds = new Set(); + for (const clinicId of distinctClinicIds) { + const authorizedRecords = await this.fabricService.getRecordsByPatient(clinicId, patientId); + authorizedRecords.forEach(r => authorizedIds.add(r.recordId)); + } + + const results: Array<{ recordId: string; content: any }> = []; + + for (const record of records) { + if (!authorizedIds.has(record.id)) continue; + + try { + const encryptedFile = await this.ipfsService.getFile(record.cid); + const recordDEK = await this.keyManagementService.getRecordDEK(record.clinic_id, record.patient_id, record.id); + const decryptedFile = this.encryptionService.decryptFile(encryptedFile, recordDEK); + recordDEK.fill(0); + + const content = JSON.parse(decryptedFile.toString('utf-8')); + results.push({ recordId: record.id, content }); + } catch (e) { + console.warn(`Skipping record ${record.id}: not a JSON record (${e.message})`); + } + } + + return results; + } + + public async getMedicalHistory(patientId: string): Promise> { + return this.getSOAPNotesForPatient(patientId, RecordType.MEDICAL_HISTORY); + } + + public async addPatientMedicalHistory(patientId: string, dto: { name: string; content: Record }): Promise { + const recordId = randomUUID(); + + const contentBuffer = Buffer.from(JSON.stringify(dto.content), 'utf-8'); + const defaultClinicId = (await this.identityStorageService.defaultIdentity()).clinicId; + const recordDEK = await this.keyManagementService.getRecordDEK(defaultClinicId, patientId, recordId); + const encryptedFile = this.encryptionService.encryptFile(contentBuffer, recordDEK); + recordDEK.fill(0); + + const cid = await this.ipfsService.uploadFile(encryptedFile, `${recordId}.enc`, 'application/octet-stream'); + + await prisma.medicalRecord.create({ + data: { + id: recordId, + patient_id: patientId, + clinic_id: defaultClinicId, + name: dto.name, + cid, + type: RecordType.MEDICAL_HISTORY, + mime_type: 'application/json', + } as Prisma.MedicalRecordUncheckedCreateInput, + }); + + await this.fabricService.addRecord(defaultClinicId, { + patientId, + recordId, + doctorId: patientId, + type: RecordType.MEDICAL_HISTORY, + ipfsCidKey: cid, + }); + + return recordId; + } + + public async updatePatientMedicalHistory( + patientId: string, + recordId: string, + dto: { name?: string; content?: Record }, + ): Promise { + const record = await prisma.medicalRecord.findFirst({ + where: { + id: recordId, + patient_id: patientId, + type: RecordType.MEDICAL_HISTORY, + deleted_at: null, + }, + }); + + if (!record) { + const error = createBilingualError(404, ErrorMessages.RECORD_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const oldCid = record.cid; + + if (dto.content !== undefined) { + const contentBuffer = Buffer.from(JSON.stringify(dto.content), 'utf-8'); + const recordDEK = await this.keyManagementService.getRecordDEK(record.clinic_id, patientId, recordId); + const encryptedFile = this.encryptionService.encryptFile(contentBuffer, recordDEK); + recordDEK.fill(0); + + const newCid = await this.ipfsService.uploadFile(encryptedFile, `${recordId}.enc`, 'application/octet-stream'); + + await prisma.medicalRecord.update({ + where: { id: recordId }, + data: { + cid: newCid, + ...(dto.name !== undefined ? { name: dto.name } : {}), + }, + }); + + await this.fabricService.updateRecord(record.clinic_id, patientId, { + recordId, + doctorId: patientId, + type: RecordType.MEDICAL_HISTORY, + ipfsCidKey: newCid, + }); + + try { + await this.ipfsService.deleteFile(oldCid); + } catch (e) { + console.warn(`Old IPFS file cleanup skipped for ${recordId}: ${e.message}`); + } + } else if (dto.name !== undefined) { + await prisma.medicalRecord.update({ + where: { id: recordId }, + data: { name: dto.name }, + }); + } + } + + public async deletePatientMedicalHistory(patientId: string, recordId: string): Promise { + const record = await prisma.medicalRecord.findFirst({ + where: { + id: recordId, + patient_id: patientId, + type: RecordType.MEDICAL_HISTORY, + deleted_at: null, + }, + }); + + if (!record) { + const error = createBilingualError(404, ErrorMessages.RECORD_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const defaultClinicId = (await this.identityStorageService.defaultIdentity()).clinicId; + await this.fabricService.deleteRecord(defaultClinicId, patientId, recordId); + + await prisma.medicalRecord.update({ + where: { id: recordId }, + data: { deleted_at: new Date() }, + }); + } + + public async getVisitSummariesForDoctor(doctorId: string, patientId: string): Promise> { + return this.getJsonRecordsForDoctor(doctorId, patientId, RecordType.VISIT_SUMMARY); + } + + public async getMedicalHistoryForDoctor(doctorId: string, patientId: string): Promise> { + return this.getJsonRecordsForDoctor(doctorId, patientId, RecordType.MEDICAL_HISTORY); + } + + private async getJsonRecordsForDoctor(doctorId: string, patientId: string, type: RecordType): Promise> { + const records = await prisma.medicalRecord.findMany({ + where: { patient_id: patientId, mime_type: 'application/json', type: type, deleted_at: null }, + select: { id: true, patient_id: true, clinic_id: true, cid: true }, + orderBy: { created_at: 'desc' }, + }); + + const doctorClinics = await prisma.clinicDoctor.findMany({ + where: { doctor_id: doctorId }, + select: { clinic_id: true }, + }); + const doctorClinicIds = doctorClinics.map(c => c.clinic_id); + + const authorizedIds = new Set(); + for (const clinicId of doctorClinicIds) { + try { + const authorized = await this.fabricService.getRecordsByPatient(clinicId, patientId); + authorized.forEach(r => authorizedIds.add(r.recordId)); + } catch (e) { + console.warn(`Chain check skipped for clinic ${clinicId}: ${e.message}`); + } + } + + const results: Array<{ recordId: string; content: any }> = []; + for (const record of records) { + if (!authorizedIds.has(record.id)) continue; + try { + const encryptedFile = await this.ipfsService.getFile(record.cid); + const recordDEK = await this.keyManagementService.getRecordDEK(record.clinic_id, record.patient_id, record.id); + const decryptedFile = this.encryptionService.decryptFile(encryptedFile, recordDEK); + recordDEK.fill(0); + results.push({ recordId: record.id, content: JSON.parse(decryptedFile.toString('utf-8')) }); + } catch (e) { + console.warn(`Skipping record ${record.id}: ${e.message}`); + } + } + return results; + } + + public async deleteAllRecords(): Promise<{ deleted: number }> { + const records = await prisma.medicalRecord.findMany({ + select: { id: true, patient_id: true, clinic_id: true, cid: true }, + }); + + backupService.deleteAllRecords(); + + for (const record of records) { + try { + await this.fabricService.deleteRecord(record.clinic_id, record.patient_id, record.id); + } catch (e) { + console.warn(`Chain delete skipped for ${record.id}: ${e.message}`); + } + + try { + await this.ipfsService.deleteFile(record.cid); + } catch (e) { + console.warn(`IPFS delete skipped for ${record.id}: ${e.message}`); + } + + await prisma.medicalRecord.delete({ where: { id: record.id } }); + } + + return { deleted: records.length }; + } +} diff --git a/src/services/nurse.service.ts b/src/services/nurse.service.ts new file mode 100644 index 0000000..858a21c --- /dev/null +++ b/src/services/nurse.service.ts @@ -0,0 +1,566 @@ +import { Service } from "typedi"; +import { HttpException } from "@/exceptions/HttpException"; +import { ErrorMessages, createBilingualError } from "@/utils/errorMessages"; +import { hash, compare } from "bcrypt"; +import { AuthService } from "./auth.service"; +import { NurseSignupRequestDto, NurseLoginRequestDto } from "@/dtos/nurses.dto"; +import { NurseLoginData, NurseApplications, NurseSchedule } from "@/interfaces/nurse.interface"; +import { AppointmentData } from "@/interfaces"; +import { NURSE_FILES } from "@/interfaces"; +import prisma from '@/config/prisma'; +import { Role, NurseAccountStatus } from "@prisma/client"; +import cloudinary from "@/utils/cloudinary"; +import fs from "fs"; +import { DoctorAnnouncements } from "@/interfaces/doctors.interface"; + +@Service() +export class NurseService { + + private authService = new AuthService(); + + public async nurseSignup(nurseData: NurseSignupRequestDto, nurseFiles: {}) { + const existingUser = await prisma.user.findUnique({ + where: { email: nurseData.email } + }); + + if (existingUser) { + const error = createBilingualError(409, ErrorMessages.EMAIL_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const username = nurseData.email.split('@')[0]; + + const existingUsername = await prisma.user.findUnique({ + where: { username } + }); + + if (existingUsername) { + const error = createBilingualError(409, ErrorMessages.USERNAME_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const files = nurseFiles as { [key: string]: Express.Multer.File[] | undefined }; + + if (!files?.nationalCard?.length) { + const error = createBilingualError(400, ErrorMessages.NATIONAL_CARD_REQUIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const hashedPassword = await hash(nurseData.password, 10); + + const createdUserId = await prisma.$transaction(async (tx) => { + const createdUser = await tx.user.create({ + data: { + email: nurseData.email, + name: nurseData.name, + username, + phone: nurseData.phone, + gender: nurseData.gender, + date_of_birth: new Date(nurseData.date_of_birth), + password_hash: hashedPassword, + role: Role.NURSE, + isVerified: true, + hasCompletedProfile: true, + }, + }); + + await tx.nurse.create({ + data: { + id: createdUser.id, + account_status: NurseAccountStatus.PENDING, + years_of_experience: nurseData.years_of_experience, + brief: nurseData.brief, + }, + }); + return createdUser.id; + }); + + if (nurseFiles && Object.keys(nurseFiles).length > 0) { + const nurseFilesArray = Object.values(nurseFiles).flat() as Express.Multer.File[]; + + await this._uploadFiles(nurseFilesArray, createdUserId); + } + } + + public async nurseLogin(nurseLoginData: NurseLoginRequestDto): Promise<{ cookies: string[]; NurseAccountData: NurseLoginData } | boolean> { + + const nurseUserData = await prisma.user.findFirst({ + where: { + OR: [ + { email: nurseLoginData.emailOrUsername }, + { username: nurseLoginData.emailOrUsername } + ] + }, + select: { + id: true, + email: true, + username: true, + name: true, + phone: true, + gender: true, + hasCompletedProfile: true, + password_hash: true, + nurse: { + select: { + account_status: true + } + } + } + }); + + if (!nurseUserData) { + const error = createBilingualError(401, ErrorMessages.USER_NOT_FOUND_CREDENTIALS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const isPasswordMatching = await compare(nurseLoginData.password, nurseUserData.password_hash); + + if (!isPasswordMatching) { + const error = createBilingualError(401, ErrorMessages.USER_NOT_FOUND_CREDENTIALS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (nurseUserData.nurse?.account_status !== NurseAccountStatus.APPROVED) { + const error = createBilingualError(403, ErrorMessages.NURSE_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + if (!nurseUserData.hasCompletedProfile) { + return false; + } + + const NurseAccountData: NurseLoginData = + { + id: nurseUserData.id, + name: nurseUserData.name, + email: nurseUserData.email, + username: nurseUserData.username, + phone: nurseUserData.phone, + gender: nurseUserData.gender, + nurse: { + account_status: nurseUserData.nurse?.account_status + } + } + + const token = await this.authService.createTokens(nurseUserData, nurseLoginData.rememberMe); + const cookies = this.authService.createCookies(token); + + return { cookies, NurseAccountData }; + } + + public async nurseSetPassword(nurseId: string, password: string): Promise { + const hashedPassword = await hash(password, 10); + const nurseUserData = await prisma.user.findUnique({ + where: { id: nurseId }, + select: { hasCompletedProfile: true } + }); + if (!nurseUserData) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + if (nurseUserData.hasCompletedProfile) { + const error = createBilingualError(400, ErrorMessages.NURSE_PASSWORD_ALREADY_SET); + throw new HttpException(error.status, error.message, error.messageAr); + } + await prisma.user.update({ + where: { id: nurseId }, + data: { + password_hash: hashedPassword, + hasCompletedProfile: true + } + }); + } + + public async applyToAnnouncement(nurseId: string, announcementId: string): Promise { + const nurseData = await prisma.nurse.findUnique({ + where: { id: nurseId }, + select: { account_status: true } + }); + + if (nurseData.account_status !== NurseAccountStatus.APPROVED) { + const error = createBilingualError(404, ErrorMessages.NURSE_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const announcement = await prisma.announcement.findUnique({ + where: { + id: announcementId, + }, + select: { + status: true, + } + }); + + if (announcement.status == 'EXPIRED') { + const error = createBilingualError(404, ErrorMessages.ANNOUNCEMENT_EXPIRED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const existingApplication = await prisma.announcementNurse.findUnique({ + where: { + announcement_id_nurse_id: { + nurse_id: nurseId, + announcement_id: announcementId + } + } + }); + + if (existingApplication) { + const error = createBilingualError(409, ErrorMessages.APPLICATION_ALREADY_EXISTS); + throw new HttpException(error.status, error.message, error.messageAr); + } + + await prisma.announcementNurse.create({ + data: { + nurse_id: nurseId, + announcement_id: announcementId, + status: 'PENDING' + } + }); + } + + public async getNurseSchedule(nurseId: string): Promise { + const nurseData = await prisma.nurse.findUnique({ + where: { + id: nurseId + }, + select: { + account_status: true + } + }); + + if (nurseData.account_status !== NurseAccountStatus.APPROVED) { + const error = createBilingualError(404, ErrorMessages.NURSE_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const schedules = await prisma.nurseSchedule.findMany({ + where: { + nurse_id: nurseId, + is_active: true, + deleted_at: null, + }, + orderBy: { + day_of_week: 'asc', + }, + select: { + id: true, + doctor: { + select: { + user: { + select: { + id: true, + name: true, + gender: true, + photo_url: true, + } + } + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + }, + day_of_week: true, + start_time: true, + end_time: true, + } + }); + + if (!schedules.length) { + return []; + } + + const groupedMap = new Map(); + + for (const schedule of schedules) { + const key = `${schedule.doctor.user.id}_${schedule.clinic?.id}`; + + if (groupedMap.has(key)) { + groupedMap.get(key).working_days.push({ + day_of_week: schedule.day_of_week, + start_time: schedule.start_time, + end_time: schedule.end_time, + }); + } + else { + groupedMap.set(key, { + id: schedule.id, + doctor: { + id: schedule.doctor.user.id, + name: schedule.doctor.user.name, + gender: schedule.doctor.user.gender, + profilePic: schedule.doctor.user.photo_url, + }, + clinic: { + id: schedule.clinic?.id || null, + name: schedule.clinic?.name || null, + address: schedule.clinic?.address || null, + address_maps_link: schedule.clinic?.address_maps_link || null, + }, + working_days: [ + { + day_of_week: schedule.day_of_week, + start_time: schedule.start_time, + end_time: schedule.end_time, + } + ], + }); + } + } + + return Array.from(groupedMap.values()); + } + + public async getNurseApplications(nurseId: string): Promise { + const nurseData = await prisma.nurse.findUnique({ + where: { + id: nurseId + }, + select: { + account_status: true + } + }); + + if (!nurseData || nurseData.account_status !== NurseAccountStatus.APPROVED) { + const error = createBilingualError(404, ErrorMessages.NURSE_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const applications = await prisma.announcementNurse.findMany({ + where: { + nurse_id: nurseId + }, + select: { + id: true, + status: true, + announcement: { + select: { + id: true, + doctor: { + select: { + user: { + select: { + id: true, + name: true, + gender: true, + photo_url: true, + } + } + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + }, + working_days: { + select: { + day_of_week: true, + start_time: true, + end_time: true, + }, + orderBy: { + day_of_week: 'asc', + } + }, + status: true, + gender: true, + max_age: true, + years_of_experience: true, + notes: true, + } + }, + } + }); + + if (!applications.length) { + return []; + } + + return applications.map(application => ({ + id: application.announcement.id, + application_status: application.status, + doctor: { + id: application.announcement.doctor.user.id, + name: application.announcement.doctor.user.name, + gender: application.announcement.doctor.user.gender, + profilePic: application.announcement.doctor.user.photo_url, + }, + clinic: { + id: application.announcement.clinic.id, + name: application.announcement.clinic.name, + address: application.announcement.clinic.address, + address_maps_link: application.announcement.clinic.address_maps_link, + }, + working_days: application.announcement.working_days.map(workDay => ({ + day_of_week: workDay.day_of_week, + start_time: workDay.start_time, + end_time: workDay.end_time, + })), + status: application.announcement.status, + gender: application.announcement.gender || undefined, + max_age: application.announcement.max_age || undefined, + years_of_experience: application.announcement.years_of_experience || undefined, + notes: application.announcement.notes || undefined, + })); + } + + public async getAllAnnouncements(nurseId: string): Promise { + const nurseData = await prisma.nurse.findUnique({ + where: { id: nurseId }, + select: { account_status: true } + }); + + if (nurseData.account_status !== NurseAccountStatus.APPROVED) { + const error = createBilingualError(404, ErrorMessages.NURSE_ACCOUNT_NOT_APPROVED); + throw new HttpException(error.status, error.message, error.messageAr); + } + const announcements = await prisma.announcement.findMany({ + where: { + deleted_at: null, + status: 'PENDING', + }, + select: { + id: true, + doctor: { + select: { + user: { + select: { + id: true, + name: true, + gender: true, + photo_url: true, + } + } + } + }, + clinic: { + select: { + id: true, + name: true, + address: true, + address_maps_link: true, + } + }, + working_days: { + select: { + day_of_week: true, + start_time: true, + end_time: true, + }, + orderBy: { + day_of_week: 'asc', + } + }, + status: true, + gender: true, + max_age: true, + years_of_experience: true, + notes: true, + + } + }); + + if (!announcements) { + return []; + } + + return announcements.map(announcement => ({ + id: announcement.id, + doctor: { + id: announcement.doctor.user.id, + name: announcement.doctor.user.name, + gender: announcement.doctor.user.gender, + profilePic: announcement.doctor.user.photo_url, + }, + clinic: { + id: announcement.clinic.id, + name: announcement.clinic.name, + address: announcement.clinic.address, + address_maps_link: announcement.clinic.address_maps_link, + }, + working_days: announcement.working_days.map(wd => ({ + day_of_week: wd.day_of_week, + start_time: wd.start_time, + end_time: wd.end_time, + })), + status: announcement.status, + gender: announcement.gender || undefined, + max_age: announcement.max_age || undefined, + years_of_experience: announcement.years_of_experience || undefined, + notes: announcement.notes || undefined, + })); + + } + + + private async _uploadFiles(files: Express.Multer.File[], nurseId: string): Promise { + const uploadedFiles: { public_id: string }[] = []; + + try { + for (const file of files) { + if (!Object.values(NURSE_FILES).includes(file.fieldname as any)) { + const error = createBilingualError(400, ErrorMessages.UNKNOWN_FILE_FIELDNAME); + throw new HttpException(error.status, error.message, error.messageAr); + } + } + + const uploadResults = await Promise.all( + files.map(file => + cloudinary.uploader.upload(file.path, { + folder: `NURSES/documents/${nurseId}`, + overwrite: false, + public_id: `NURSE_${nurseId}_${file.fieldname}_${Date.now()}` + }) + ) + ); + + uploadedFiles.push(...uploadResults.map(r => ({ public_id: r.public_id }))); + + const updateData: any = {}; + files.forEach((file, index) => { + const uploadResult = uploadResults[index]; + + switch (file.fieldname) { + case NURSE_FILES.NATIONAL_CARD: + updateData.nationalCardUrl = uploadResult.secure_url; + updateData.nationalCardPublicId = uploadResult.public_id; + break; + case NURSE_FILES.BONUS_FILE: + updateData.bonusFileUrl = uploadResult.secure_url; + updateData.bonusFilePublicId = uploadResult.public_id; + break; + } + fs.unlinkSync(file.path); + }); + + await prisma.nurse.update({ + where: { id: nurseId }, + data: updateData + }); + + } catch (error) { + if (uploadedFiles.length > 0) { + await Promise.all( + uploadedFiles.map(f => cloudinary.uploader.destroy(f.public_id).catch(() => { })) + ); + + } + + files.forEach(file => { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + } + }); + throw error; + } + } +} diff --git a/src/services/queue.service.ts b/src/services/queue.service.ts new file mode 100644 index 0000000..0764b10 --- /dev/null +++ b/src/services/queue.service.ts @@ -0,0 +1,147 @@ +import prisma from '@/config/prisma'; +import { HttpException } from "@/exceptions/HttpException"; +import { createBilingualError, ErrorMessages } from '@/utils/errorMessages'; +import { QueuePosition } from '@/interfaces/queue.interface'; +import { DayOfWeek } from '@prisma/client'; +import { Service } from 'typedi'; + +@Service() +export class QueueService { + + public async getQueuePosition(appointmentId: string): Promise { + await this.calculateQueuePosition(appointmentId) + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + }, + select: { + position: true, + estimated_time: true, + patients_ahead: true, + } + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + return { + position: appointment.position, + estimatedWaitMinutes: appointment.estimated_time, + patientsAhead: appointment.patients_ahead, + }; + } + + public async calculateQueuePosition(appointmentId: string): Promise { + const appointment = await prisma.appointment.findUnique({ + where: { + id: appointmentId, + }, + select: { + doctor_id: true, + clinic_id: true, + scheduled_time: true, + slot_duration: true, + } + }); + + if (!appointment) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const dayOfWeek = this.getDayOfWeek(appointment.scheduled_time.getUTCDay()); + + const schedule = await prisma.doctorSchedule.findFirst({ + where: { + doctor_id: appointment.doctor_id, + clinic_id: appointment?.clinic_id || null, + day_of_week: dayOfWeek, + is_active: true, + deleted_at: null, + }, + select: { + buffer_time: true, + + } + }); + + const bufferTime = schedule?.buffer_time || 0; + + const startOfDay = new Date(appointment.scheduled_time); + startOfDay.setUTCHours(0, 0, 0, 0); + + const endOfDay = new Date(appointment.scheduled_time); + endOfDay.setUTCHours(23, 59, 59, 999); + + const todayAppointments = await prisma.appointment.findMany({ + where: { + doctor_id: appointment.doctor_id, + scheduled_time: { + gte: startOfDay, + lte: endOfDay, + }, + deleted_at: null, + status: {in : ['CONFIRMED', 'COMPLETED']} + }, + orderBy: { + scheduled_time: 'asc', + }, + select: { + id: true, + scheduled_time: true, + slot_duration: true, + status: true, + } + }); + + const currentIdx = todayAppointments.findIndex(app => app.id === appointmentId); + + if (currentIdx === -1) { + const error = createBilingualError(404, ErrorMessages.APPOINTMENT_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + + const patientsAhead = todayAppointments.slice(0, currentIdx).filter(app => app.status === 'CONFIRMED').length; + const position = currentIdx + 1; + // NOOTEEE --> now time - scheduled time but in mins + + const nowUTC = new Date(); + const egyptOffset = 2 * 60 * 60 * 1000; + const now = new Date(nowUTC.getTime() + egyptOffset); + let estimatedWaitMinutes = 0 + + // const estimatedWaitMinutes = appointmentsAhead.reduce((total, app) => total + app.slot_duration + bufferTime, 0); + if (appointment.scheduled_time.getTime() >= now.getTime()){ + estimatedWaitMinutes = Math.max(0, Math.round((appointment.scheduled_time.getTime() - now.getTime()) / (1000 * 60))); + } + this.updateQueueParameters(appointmentId, position, patientsAhead, estimatedWaitMinutes); + } + + private async updateQueueParameters(appointmentId: string, position: number, patientsAhead: number, estimatedWaitMinutes: number): Promise { + await prisma.appointment.update({ + where: { + id: appointmentId, + }, + data: { + position, + patients_ahead: patientsAhead, + estimated_time: estimatedWaitMinutes, + }, + }); + } + + public getDayOfWeek(jsDay: number): DayOfWeek { + const days: DayOfWeek[] = [ + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY, + ]; + return days[jsDay]; + } +} diff --git a/src/services/socket.service.ts b/src/services/socket.service.ts new file mode 100644 index 0000000..1bcdb72 --- /dev/null +++ b/src/services/socket.service.ts @@ -0,0 +1,182 @@ +import { Server as HttpServer } from 'http'; +import { Server, Socket } from 'socket.io'; +import { verify } from 'jsonwebtoken'; +import { SocketStoredInToken } from '@/interfaces'; +import { SECRET_KEY } from '@/config'; +import { AppointmentService } from './appointment.service'; +import { AppointmentStatusChangedPayload } from '@/interfaces'; +import { QueueService } from './queue.service'; +import { Container, Service } from 'typedi'; + +interface AuthenticatedSocket extends Socket { + userId?: string; + userRole?: string; +} + +function parseCookies(cookieHeader: string = ''): Record { + return Object.fromEntries( + cookieHeader.split(';').map(c => c.trim().split('=').map(decodeURIComponent)) + ); +} + + +@Service() +export class SocketService { + private io: Server; + // userId --> set of socketIds (each tab/device = different socketId) + private userSocketMap: Map> = new Map(); + private appointmentService = Container.get(AppointmentService); + private queueService = Container.get(QueueService); + + public initialize(httpServer: HttpServer): void { + this.io = new Server(httpServer, { + cors: { + origin: process.env.ORIGIN, + credentials: true, + + }, + // polling is just a fallback if websocket fails + transports: ['websocket', 'polling'], + }); + this.io.use(this.authMiddleware.bind(this)); + this.io.on('connection', this.handleConnection.bind(this)); + } + public isUserConnected(userId: string): boolean { + return this.userSocketMap.has(userId) && this.userSocketMap.get(userId).size > 0; + } + + public getTotalConnectedUsers(): number { + return this.userSocketMap.size; + } + + public getIO(): Server { + return this.io; + } + + private async authMiddleware(socket: AuthenticatedSocket, next: (err?: Error) => void): Promise { + try { + const token = parseCookies(socket.handshake.headers.cookie)['Authorization']?.replace(/^Bearer\s+/i, ''); + if (!token) { + return next(new Error('Authentication error: Token not provided')); + } + + const decoded = verify(token, SECRET_KEY) as SocketStoredInToken; + socket.userId = decoded.id; + socket.userRole = decoded.role; + next(); + } + catch (error) { + next(new Error('Authentication error: Invalid token')); + } + } + + private handleConnection(socket: AuthenticatedSocket): void { + const userId = socket.userId; + const userRole = socket.userRole; + + if (!userId) { + socket.disconnect(); + return; + } + + if (!this.userSocketMap.has(userId)) { + this.userSocketMap.set(userId, new Set()); + } + this.userSocketMap.get(userId)?.add(socket.id); + + // personal room (all tabs/devices get the event) + socket.join(`user_${userId}`); + + socket.on('disconnect', () => { + this.handleDisconnection(socket); + }); + + socket.emit('connected', { + message: 'Successfully connected to socket server', + userId: userId + }); + socket.on('request_initial_data', () => { + if (userRole === 'PATIENT') this.sendInitialPatientData(userId); + else if (userRole === 'DOCTOR') this.sendInitialDoctorData(userId); + else if (userRole === 'NURSE') this.sendInitialNurseData(userId); + }); + + if (userRole === 'PATIENT') { + this.sendInitialPatientData(userId); + } + else if (userRole === 'DOCTOR') { + this.sendInitialDoctorData(userId); + } + else if (userRole === 'NURSE') { + this.sendInitialNurseData(userId); + } + + } + + private handleDisconnection(socket: AuthenticatedSocket): void { + const userId = socket.userId; + if (!userId) return; + + if (userId && this.userSocketMap.has(userId)) { + this.userSocketMap.get(userId).delete(socket.id); + + if (this.userSocketMap.get(userId).size === 0) { + this.userSocketMap.delete(userId); + } + } + } + + public emitToUser(userId: string, event: string, data: any): void { + if (this.isUserConnected(userId)) { + this.io.to(`user_${userId}`).emit(event, data); + } + } + + public async emitQueueUpdatesToPatients(doctorId: string, date?: Date): Promise { + const appointments = await this.appointmentService.getAppointmentsForDay(doctorId, date); + for (const app of appointments) { + const queuePosition = await this.queueService.getQueuePosition(app.id); + this.emitToUser(app.patient_id, 'queue_updated', queuePosition); + } + } + + public async emitAppointmentStatusChanged(payload: AppointmentStatusChangedPayload) { + this.emitToUser(payload.doctorId, 'appointment_status_changed', payload); + this.emitToUser(payload.patientId, 'appointment_status_changed', payload); + } + + + private async sendInitialPatientData(patientId: string): Promise { + try { + const appointments = await this.appointmentService.getTodayAppointment(patientId) ?? []; + const appointmentsWithQueue = await Promise.all(appointments.map(async (app) => { + await this.queueService.calculateQueuePosition(app.id); + const queuePosition = await this.queueService.getQueuePosition(app.id); + return { ...app, queuePosition }; + })); + this.emitToUser(patientId, 'initial_data', { appointments: appointmentsWithQueue }); + } + catch (error) { + console.error('Error sending initial patient data:', error); + } + } + + private async sendInitialDoctorData(doctorId: string): Promise { + try { + const schedule = await this.appointmentService.getCurrentDoctorSchedule(doctorId); + this.emitToUser(doctorId, 'initial_data', { schedule }); + } catch (error) { + console.error('error sending initial doctor data:', error); + } + } + + private async sendInitialNurseData(nurseId: string): Promise { + try { + const appointments = await this.appointmentService.getNurseAppointmentsToday(nurseId); + this.emitToUser(nurseId, 'initial_data', { appointments }); + } + catch (error) { + console.error('Error sending initial nurse data:', error); + } + } +} diff --git a/src/services/superAdmin.service.ts b/src/services/superAdmin.service.ts new file mode 100644 index 0000000..1949628 --- /dev/null +++ b/src/services/superAdmin.service.ts @@ -0,0 +1,105 @@ +import { AddAdminFromSuperAdminDto, AdminFromSuperAdminResponseDto } from "@/dtos/superAdmins.dto"; +import { HttpException } from "@/exceptions/HttpException"; +import { User } from "@/interfaces"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; +import { PrismaClient, Role } from "@prisma/client"; +import { hash } from "bcrypt"; +import { Service } from "typedi"; + +const prisma = new PrismaClient(); + +@Service() +export class SuperAdminService { + + public async addAdmin(adminData: AddAdminFromSuperAdminDto): Promise { + // Logic to add a new admin + // Check if email already exists + const existingUser = await prisma.user.findUnique({ + where: { email: adminData.email } + }); + if (existingUser) { + const err = createBilingualError(409, ErrorMessages.EMAIL_EXISTS); + throw new HttpException(err.status, err.message, err.messageAr); + } + const username = adminData.email.split('@')[0]; + + // Check if username exists + const existingUsername = await prisma.user.findUnique({ + where: { username } + }); + if (existingUsername) { + const err = createBilingualError(409, ErrorMessages.USERNAME_EXISTS); + throw new HttpException(err.status, err.message, err.messageAr); + } + + const hashedPassword = await hash(adminData.password, 10); + + const newAdmin = await prisma.user.create({ + data: { + email: adminData.email, + name: adminData.name, + username: username, + password_hash: hashedPassword, + role: Role.ADMIN, + phone: adminData.phone, + gender: adminData.gender, + isVerified: true, + hasCompletedProfile: true, + date_of_birth: new Date(adminData.date_of_birth), + }, + select: { + email: true, + name: true, + username: true, + phone: true, + role: true, + gender: true, + isVerified: true, + hasCompletedProfile: true, + date_of_birth: true, + photo_url: true, + } + }); + return newAdmin; + } + + public async getAllAdmins(): Promise { + const admins = await prisma.user.findMany({ + where: { role: Role.ADMIN }, + select: { + id: true, + email: true, + name: true, + username: true, + phone: true, + role: true, + gender: true, + isVerified: true, + hasCompletedProfile: true, + date_of_birth: true, + photo_url: true, + } + }); + return admins; + } + + public async getAdminById(adminId: string): Promise { + const admin = await prisma.user.findUnique({ + where: { id: adminId, role: Role.ADMIN }, + select: { + email: true, + name: true, + username: true, + phone: true, + role: true, + gender: true, + isVerified: true, + hasCompletedProfile: true, + date_of_birth: true, + photo_url: true, + } + }); + return admin; + } + +} \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..a20566e --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,123 @@ +import cloudinary from "@/utils/cloudinary"; +import { AvailabilityType, Gender, PrismaClient, Role } from "@prisma/client"; +import { Service } from "typedi"; +import fs from "fs"; +import { createBilingualError, ErrorMessages } from "@/utils/errorMessages"; +import { HttpException } from "@/exceptions/HttpException"; +import { UpdateUserProfileDto } from "@/dtos/users.dto"; +import { compare } from "bcrypt"; + +const prisma = new PrismaClient(); + +@Service() +export class UserService { + + public async updateProfilePicture(userId: string, localFilePath: string, userRole: string): Promise { + + const oldProfilePictureId = await prisma.user.findUnique({ + where: { id: userId }, + select: { photo_public_id: true } + }); + + if (oldProfilePictureId?.photo_public_id) { + // Delete old profile picture from Cloudinary + await cloudinary.uploader.destroy(oldProfilePictureId.photo_public_id); + } + + // Upload new profile picture to Cloudinary + const uploadResult = await cloudinary.uploader.upload(localFilePath, { + folder: `${userRole}S/profile_pictures`, + overwrite: false, + public_id: `${userRole}_${userId}_profile_picture_${Date.now()}` + }); + + fs.unlinkSync(localFilePath); // Remove local file after upload + + // Update user record with new profile picture info + await prisma.user.update({ + where: { id: userId }, + data: { + photo_url: uploadResult.secure_url, + photo_public_id: uploadResult.public_id + } + }); + } + + public async getProfilePicture(userId: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { photo_url: true } + }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + return user?.photo_url || null; + } + + public async deleteProfilePicture(userId: string): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { photo_public_id: true } + }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + if (user.photo_public_id) { + // Delete profile picture from Cloudinary + await cloudinary.uploader.destroy(user.photo_public_id); + // Update user record to remove photo info + await prisma.user.update({ + where: { id: userId }, + data: { + photo_url: null, + photo_public_id: null + } + }); + } + } + + public async calculateUserAge(dateOfBirth: Date): Promise { + const today = new Date(); + const birthDate = new Date(dateOfBirth); + let age = today.getFullYear() - birthDate.getFullYear(); + const monthDiff = today.getMonth() - birthDate.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + return age; + } + public async updateUserProfile(userId: string, name?: string, phone?: string, gender?: Gender, dateOfBirth?: string, availability_type?: AvailabilityType): Promise { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + const error = createBilingualError(404, ErrorMessages.USER_NOT_FOUND); + throw new HttpException(error.status, error.message, error.messageAr); + } + const updateData: UpdateUserProfileDto = {}; + if (name) updateData.name = name; + if (phone) updateData.phone = phone; + if (gender) updateData.gender = gender; + if (dateOfBirth) updateData.date_of_birth = new Date(dateOfBirth); + if(availability_type && user.role === Role.DOCTOR) { + await prisma.$transaction([ + prisma.user.update({ + where: { id: userId }, + data: updateData + }), + prisma.doctor.update({ + where: { id: userId }, + data: { availability_type } + }) + ]); + } + else { + await prisma.user.update({ + where: { id: userId }, + data: updateData + }); + } + } +} \ No newline at end of file diff --git a/src/swagger-output.json b/src/swagger-output.json new file mode 100644 index 0000000..7b9f5cf --- /dev/null +++ b/src/swagger-output.json @@ -0,0 +1,9448 @@ +{ + "swagger": "2.0", + "info": { + "title": "My API", + "description": "Description", + "version": "1.0.0" + }, + "host": "localhost:3000", + "basePath": "/", + "tags": [ + { + "name": "Auth", + "description": "Authentication and account endpoints" + }, + { + "name": "Admin", + "description": "Admin endpoints" + }, + { + "name": "Super Admin", + "description": "Super Admin endpoints" + }, + { + "name": "MedicalRecords", + "description": "Hyperledger Fabric medical record endpoints" + }, + { + "name": "Doctors", + "description": "Doctor account endpoints" + }, + { + "name": "Clinics", + "description": "Clinic endpoints" + }, + { + "name": "Appointments", + "description": "Appointment endpoints" + }, + { + "name": "Queue", + "description": "Queue endpoints" + }, + { + "name": "Users", + "description": "User account endpoints" + }, + { + "name": "Nurses", + "description": "Nurse account endpoints" + }, + { + "name": "AI Appointments", + "description": "AI-generated SOAP notes for appointments" + } + ], + "schemes": [ + "http" + ], + "securityDefinitions": { + "bearerAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "Enter your Bearer token: Bearer " + } + }, + "paths": { + "/auth/signup": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "User signup data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "user@example.com" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "password": { + "type": "string", + "example": "password123" + }, + "rememberMe": { + "type": "boolean", + "example": false + } + }, + "required": [ + "email", + "name", + "phone", + "password", + "rememberMe" + ] + } + } + ], + "responses": { + "201": { + "description": "User successfully created", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "email": { + "type": "string", + "example": "user@example.com" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "isEmailVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "gender": {}, + "date_of_birth": {}, + "role": { + "type": "string", + "example": "PATIENT" + }, + "photoUrl": {} + } + }, + "messageEn": { + "type": "string", + "example": "Signed Up Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم انشاء الحساب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "User login data", + "required": true, + "schema": { + "type": "object", + "properties": { + "emailOrUsername": { + "type": "string", + "example": "user@example.com" + }, + "password": { + "type": "string", + "example": "password123" + }, + "rememberMe": { + "type": "boolean", + "example": false + } + }, + "required": [ + "emailOrUsername", + "password" + ] + } + } + ], + "responses": { + "200": { + "description": "Login successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "email": { + "type": "string", + "example": "user@example.com" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "role": { + "type": "string", + "example": "PATIENT" + }, + "photo_url": { + "type": "string", + "example": "https://example.com/photo.jpg" + }, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "string", + "example": "Cardiology" + }, + "account_status": { + "type": "string", + "example": "APPROVED" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Logged In Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تسجيل الدخول بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/logout": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Logout successful", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Logged Out Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تسجيل الخروج بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/refresh": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "RefreshToken", + "in": "header", + "description": "Refresh token (sent via RefreshToken cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Token refreshed successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": {} + }, + "accessToken": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number", + "example": 3600 + }, + "expiresAt": { + "type": "string", + "example": "2025-12-12T12:00:00.000Z" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Token Refreshed Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث الرمز بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/complete-profile-info": { + "patch": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Complete user profile", + "required": true, + "schema": { + "type": "object", + "properties": { + "gender": { + "type": "string", + "example": "Male" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + } + }, + "required": [ + "gender", + "date_of_birth" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Profile completed successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Profile Completed Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إكمال الملف الشخصي بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/verify-otp": { + "patch": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Verify OTP", + "required": true, + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "example": "123456" + } + }, + "required": [ + "otp" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie or)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OTP verified successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "boolean", + "example": true + }, + "messageEn": { + "type": "string", + "example": "OTP Verified Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم التحقق من رمز التحقق بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/forget-password": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Request password reset", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "user@example.com" + } + }, + "required": [ + "email" + ] + } + } + ], + "responses": { + "200": { + "description": "Password reset email sent", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Password Reset Email Sent Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إرسال بريد إعادة تعيين كلمة المرور بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/reset-password": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Reset password", + "required": true, + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "reset-token" + }, + "newPassword": { + "type": "string", + "example": "newPassword123" + } + }, + "required": [ + "token", + "newPassword" + ] + } + } + ], + "responses": { + "200": { + "description": "Password reset successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Password Reset Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إعادة تعيين كلمة المرور بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/resend-otp": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cooki)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OTP resent successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "OTP Resent Successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إعادة إرسال رمز التحقق بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/google": { + "get": { + "tags": [ + "Auth" + ], + "description": "", + "responses": { + "302": { + "description": "Redirects to Google OAuth consent page" + } + } + } + }, + "/auth/google/callback": { + "get": { + "tags": [ + "Auth" + ], + "description": "", + "responses": { + "302": { + "description": "Redirects after Google authentication" + } + } + } + }, + "/auth/google/update-phone": { + "patch": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Update Google user phone", + "required": true, + "schema": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "example": "1234567890" + } + }, + "required": [ + "phone" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Phone number updated successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "example": "1234567890" + } + } + }, + "messageEn": { + "type": "string", + "example": "Phone number updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث رقم الهاتف بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/google/userData": { + "get": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "User data retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "user@example.com" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "username": { + "type": "string", + "example": "johndoe" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + } + } + }, + "messageEn": { + "type": "string", + "example": "User data retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات المستخدم بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/check-password": { + "post": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "User current password", + "required": true, + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "current_password123" + } + } + } + } + ], + "responses": { + "200": { + "description": "Password check successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "isMatch": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Password is correct" + }, + "messageAr": { + "type": "string", + "example": "كلمة المرور صحيحة" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/auth/change-password": { + "patch": { + "tags": [ + "Auth" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "User password change data", + "required": true, + "schema": { + "type": "object", + "properties": { + "newPassword": { + "type": "string", + "example": "new_password123" + } + } + } + } + ], + "responses": { + "200": { + "description": "Password changed successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Password changed successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تغيير كلمة المرور بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/fabric/onboard": { + "post": { + "tags": [ + "FabricIdentity" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Identity onboarding data", + "required": true, + "schema": { + "type": "object", + "properties": { + "clinicId": { + "type": "string", + "example": "clinic-uuid-here" + }, + "mspId": { + "type": "string", + "example": "Org1MSP" + }, + "certificate": { + "type": "string", + "example": "PEM certificate" + }, + "privateKey": { + "type": "string", + "example": "PEM private key" + }, + "peerEndpoint": { + "type": "string", + "example": "localhost:7051" + }, + "peerHostAlias": { + "type": "string", + "example": "peer0.org1.example.com" + }, + "tlsCertificate": { + "type": "string", + "example": "PEM TLS certificate" + }, + "channelName": { + "type": "string", + "example": "mychannel" + }, + "chaincodeName": { + "type": "string", + "example": "test" + } + }, + "required": [ + "clinicId", + "mspId", + "certificate", + "privateKey", + "peerEndpoint", + "peerHostAlias", + "tlsCertificate" + ] + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/fabric/identities": { + "get": { + "tags": [ + "FabricIdentity" + ], + "description": "", + "responses": { + "default": { + "description": "" + } + } + } + }, + "/fabric/identities/{clinicId}": { + "delete": { + "tags": [ + "FabricIdentity" + ], + "description": "", + "parameters": [ + { + "name": "clinicId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/fabric/connections": { + "get": { + "tags": [ + "FabricIdentity" + ], + "description": "", + "responses": { + "default": { + "description": "" + } + } + } + }, + "/fabric/init-ledger": { + "post": { + "tags": [ + "FabricIdentity" + ], + "description": "Initialize the ledger, optionally seeding it with backup records", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Optional backup data to seed the ledger", + "required": false, + "schema": { + "type": "object", + "properties": { + "backupData": { + "type": "array", + "items": { + "type": "object", + "properties": { + "patientId": { + "type": "string", + "example": "patient-uuid" + }, + "recordId": { + "type": "string", + "example": "record-uuid" + }, + "doctorId": { + "type": "string", + "example": "doctor-uuid" + }, + "type": { + "type": "string", + "example": "LAB_RESULT" + }, + "ownerMsp": { + "type": "string", + "example": "Org1MSP" + }, + "authorizedMsps": { + "type": "array", + "example": [], + "items": {} + } + } + } + } + } + } + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/records/health": { + "get": { + "tags": [ + "MedicalRecords" + ], + "description": "", + "responses": { + "default": { + "description": "" + } + } + } + }, + "/admin/doctors": { + "post": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Doctor data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE or FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + } + }, + "required": [ + "email", + "name", + "phone", + "gender", + "date_of_birth" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "Doctor added successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "role": { + "type": "string", + "example": "DOCTOR" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {}, + "account_status": { + "type": "string", + "example": "PENDING" + } + } + }, + "photoUrl": {} + } + }, + "messageEn": { + "type": "string", + "example": "Doctor account created successfully." + }, + "messageAr": { + "type": "string", + "example": ".تم إنشاء حساب الطبيب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctors retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "role": { + "type": "string", + "example": "DOCTOR" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photoUrl": {}, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {}, + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "mastersCertificateUrl": { + "type": "string", + "example": "" + }, + "graduationCertificateUrl": { + "type": "string", + "example": "" + }, + "fellowshipCertificateUrl": { + "type": "string", + "example": "" + }, + "professionalPracticeCardUrl": { + "type": "string", + "example": "" + }, + "membershipCardUrl": { + "type": "string", + "example": "" + }, + "unionSpecializationCertificateUrl": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctors retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الأطباء بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/nurses": { + "post": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Nurse data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE or FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "years_of_experience": { + "type": "number", + "example": 3 + } + }, + "required": [ + "email", + "name", + "phone", + "gender", + "date_of_birth" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "Nurse added successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "role": { + "type": "string", + "example": "NURSE" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": {}, + "nationalCardUrl": {}, + "bonusFileUrl": {} + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurse account created successfully." + }, + "messageAr": { + "type": "string", + "example": ".تم إنشاء حساب الممرض بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Nurses retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "role": { + "type": "string", + "example": "NURSE" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": { + "type": "string", + "example": "Experienced nurse in ICU" + }, + "nationalCardUrl": { + "type": "string", + "example": "" + }, + "bonusFileUrl": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurses retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الممرضين بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/doctors/unverified": { + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Unverified doctors retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + }, + "photoUrl": {}, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {}, + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "mastersCertificateUrl": { + "type": "string", + "example": "" + }, + "graduationCertificateUrl": { + "type": "string", + "example": "" + }, + "fellowshipCertificateUrl": { + "type": "string", + "example": "" + }, + "professionalPracticeCardUrl": { + "type": "string", + "example": "" + }, + "membershipCardUrl": { + "type": "string", + "example": "" + }, + "unionSpecializationCertificateUrl": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Unverified doctors retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الأطباء غير المعتمدين بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/nurses/unverified": { + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Unverified nurses retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "PENDING" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": { + "type": "string", + "example": "Experienced nurse in ICU" + }, + "nationalCardUrl": { + "type": "string", + "example": "" + }, + "bonusFileUrl": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Unverified nurses retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الممرضين غير المعتمدين بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/doctors/verify/{id}": { + "patch": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Doctor ID" + }, + { + "name": "body", + "in": "body", + "description": "Verification status", + "required": true, + "schema": { + "type": "object", + "properties": { + "isApproved": { + "type": "boolean", + "example": true + } + }, + "required": [ + "isApproved" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie or Authorization header)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctor verification status updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Doctor verification status updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث حالة اعتماد الطبيب بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/nurses/verify/{id}": { + "patch": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Nurse ID" + }, + { + "name": "body", + "in": "body", + "description": "Verification status", + "required": true, + "schema": { + "type": "object", + "properties": { + "isVerified": { + "type": "boolean", + "example": true + } + }, + "required": [ + "isVerified" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie or Authorization header)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Nurse verification status updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Nurse verification status updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث حالة اعتماد الممرض بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/doctors/{id}": { + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Doctor ID" + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctor retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "role": { + "type": "string", + "example": "DOCTOR" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photoUrl": {}, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {}, + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "mastersCertificateUrl": { + "type": "string", + "example": "" + }, + "graduationCertificateUrl": { + "type": "string", + "example": "" + }, + "fellowshipCertificateUrl": { + "type": "string", + "example": "" + }, + "professionalPracticeCardUrl": { + "type": "string", + "example": "" + }, + "membershipCardUrl": { + "type": "string", + "example": "" + }, + "unionSpecializationCertificateUrl": { + "type": "string", + "example": "" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctor retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الطبيب بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/nurses/{id}": { + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Nurse ID" + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Nurse retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "role": { + "type": "string", + "example": "NURSE" + }, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": { + "type": "string", + "example": "Experienced nurse in ICU" + }, + "nationalCardUrl": { + "type": "string", + "example": "" + }, + "bonusFileUrl": { + "type": "string", + "example": "" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurse retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الممرض بنجاح." + } + }, + "xml": { + "name": "main" + } + } + }, + "404": { + "description": "Nurse not found" + } + } + } + }, + "/admin/clinics": { + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Get clinics successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "Clinic Name" + }, + "is_active": { + "type": "boolean", + "example": false + }, + "opening_at": { + "type": "string", + "example": "08:00" + }, + "closing_at": { + "type": "string", + "example": "16:00" + }, + "address": { + "type": "string", + "example": "123 Main St, City, Country" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+St,+City,+Country" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinics retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات العيادات بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/clinics/{id}": { + "get": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the clinic to retrieve" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Clinic retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "Clinic Name" + }, + "is_active": { + "type": "boolean", + "example": false + }, + "opening_at": { + "type": "string", + "example": "08:00" + }, + "closing_at": { + "type": "string", + "example": "16:00" + }, + "address": { + "type": "string", + "example": "123 Main St, City, Country" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+St,+City,+Country" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinic retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات العيادة بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/admin/clinics/{id}/set-active-status": { + "patch": { + "tags": [ + "Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the clinic to set active status" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "set active status", + "required": true, + "schema": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean", + "example": true + } + } + } + } + ], + "responses": { + "200": { + "description": "Clinic active status toggled successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "Clinic Name" + }, + "is_active": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinic active status toggled successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تبديل حالة العيادة بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/admins": { + "post": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Admin data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "admin@example.com" + }, + "name": { + "type": "string", + "example": "Jane Smith" + }, + "password": { + "type": "string", + "example": "SecurePass123!" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE or FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + } + }, + "required": [ + "email", + "name", + "password", + "phone", + "gender", + "date_of_birth" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "Admin added successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "admin@example.com" + }, + "name": { + "type": "string", + "example": "Jane Smith" + }, + "role": { + "type": "string", + "example": "ADMIN" + }, + "username": { + "type": "string", + "example": "janesmith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01T00:00:00.000Z" + }, + "photo_url": {}, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Admin added successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إضافة المسؤول بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Admins retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "admin@example.com" + }, + "name": { + "type": "string", + "example": "Jane Smith" + }, + "role": { + "type": "string", + "example": "ADMIN" + }, + "username": { + "type": "string", + "example": "janesmith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01T00:00:00.000Z" + }, + "photo_url": {}, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Admins retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع المسؤولين بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/admins/{id}": { + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Admin ID" + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Admin retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "admin@example.com" + }, + "name": { + "type": "string", + "example": "Jane Smith" + }, + "role": { + "type": "string", + "example": "ADMIN" + }, + "username": { + "type": "string", + "example": "janesmith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01T00:00:00.000Z" + }, + "photo_url": {}, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Admin retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع المسؤول بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/doctors": { + "post": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Doctor data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. John Doe" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE or FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1990-01-01" + } + }, + "required": [ + "email", + "name", + "phone", + "gender", + "date_of_birth" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "Doctor added successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "role": { + "type": "string", + "example": "DOCTOR" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {} + } + }, + "photoUrl": {} + } + }, + "messageEn": { + "type": "string", + "example": "Doctor added successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إضافة الطبيب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctors retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "role": { + "type": "string", + "example": "DOCTOR" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photoUrl": {}, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {}, + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "mastersCertificateUrl": { + "type": "string", + "example": "" + }, + "graduationCertificateUrl": { + "type": "string", + "example": "" + }, + "fellowshipCertificateUrl": { + "type": "string", + "example": "" + }, + "professionalPracticeCardUrl": { + "type": "string", + "example": "" + }, + "membershipCardUrl": { + "type": "string", + "example": "" + }, + "unionSpecializationCertificateUrl": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctors retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الأطباء بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/nurses": { + "post": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Nurse data", + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE or FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "years_of_experience": { + "type": "number", + "example": 3 + } + }, + "required": [ + "email", + "name", + "phone", + "gender", + "date_of_birth" + ] + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "Nurse added successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "role": { + "type": "string", + "example": "NURSE" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": {}, + "nationalCardUrl": {}, + "bonusFileUrl": {} + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurse account created successfully." + }, + "messageAr": { + "type": "string", + "example": ".تم إنشاء حساب الممرض بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Nurses retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "role": { + "type": "string", + "example": "NURSE" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": { + "type": "string", + "example": "Experienced nurse in ICU" + }, + "nationalCardUrl": { + "type": "string", + "example": "" + }, + "bonusFileUrl": { + "type": "string", + "example": "" + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurses retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الممرضين بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/doctors/{id}": { + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Doctor ID" + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + }, + { + "name": "accept-language", + "in": "header", + "description": "Language preference (en or ar)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctor retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "doctor@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "role": { + "type": "string", + "example": "DOCTOR" + }, + "username": { + "type": "string", + "example": "smith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "isVerified": { + "type": "boolean", + "example": false + }, + "hasCompletedProfile": { + "type": "boolean", + "example": false + }, + "photoUrl": {}, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "CARDIOLOGY" + }, + "value": { + "type": "string", + "example": "Cardiology" + } + } + }, + "avg_time": {}, + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "mastersCertificateUrl": { + "type": "string", + "example": "" + }, + "graduationCertificateUrl": { + "type": "string", + "example": "" + }, + "fellowshipCertificateUrl": { + "type": "string", + "example": "" + }, + "professionalPracticeCardUrl": { + "type": "string", + "example": "" + }, + "membershipCardUrl": { + "type": "string", + "example": "" + }, + "unionSpecializationCertificateUrl": { + "type": "string", + "example": "" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctor retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الطبيب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/nurses/{id}": { + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Nurse ID" + }, + { + "name": "Authorization", + "in": "header", + "description": "Bearer access token (sent via Authorization cookie)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Nurse retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "1" + }, + "email": { + "type": "string", + "example": "nurse@example.com" + }, + "name": { + "type": "string", + "example": "Nurse Jane" + }, + "username": { + "type": "string", + "example": "jane" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "date_of_birth": { + "type": "string", + "example": "1995-06-15" + }, + "role": { + "type": "string", + "example": "NURSE" + }, + "isVerified": { + "type": "boolean", + "example": true + }, + "hasCompletedProfile": { + "type": "boolean", + "example": true + }, + "photo_url": {}, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "brief": { + "type": "string", + "example": "Experienced nurse in ICU" + }, + "nationalCardUrl": { + "type": "string", + "example": "" + }, + "bonusFileUrl": { + "type": "string", + "example": "" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurse retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الممرض بنجاح." + } + }, + "xml": { + "name": "main" + } + } + }, + "404": { + "description": "Nurse not found" + } + } + } + }, + "/super-admin/clinics": { + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Get clinics successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "Clinic Name" + }, + "is_active": { + "type": "boolean", + "example": false + }, + "opening_at": { + "type": "string", + "example": "08:00" + }, + "closing_at": { + "type": "string", + "example": "16:00" + }, + "address": { + "type": "string", + "example": "123 Main St, City, Country" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+St,+City,+Country" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinics retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات العيادات بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/super-admin/clinics/{id}": { + "get": { + "tags": [ + "Super Admin" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Clinic ID" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Clinic retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "Clinic Name" + }, + "is_active": { + "type": "boolean", + "example": false + }, + "opening_at": { + "type": "string", + "example": "08:00" + }, + "closing_at": { + "type": "string", + "example": "16:00" + }, + "address": { + "type": "string", + "example": "123 Main St, City, Country" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+St,+City,+Country" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinic retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات العيادة بنجاح." + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/doctors/signup": { + "post": { + "tags": [ + "Doctors" + ], + "description": "", + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "name": "email", + "in": "formData", + "description": "Doctor email address", + "required": true, + "type": "string" + }, + { + "name": "name", + "in": "formData", + "description": "Doctor full name", + "required": true, + "type": "string" + }, + { + "name": "phone", + "in": "formData", + "description": "Doctor phone number", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "formData", + "description": "Doctor password", + "required": true, + "type": "string" + }, + { + "name": "gender", + "in": "formData", + "description": "Doctor gender (MALE or FEMALE)", + "required": true, + "type": "string", + "enum": [ + "MALE", + "FEMALE" + ] + }, + { + "name": "availability_type", + "in": "formData", + "description": "availability type of the doctor", + "required": false, + "type": "string", + "enum": [ + "UNSET", + "ONLINE", + "OFFLINE", + "BOTH" + ] + }, + { + "name": "date_of_birth", + "in": "formData", + "description": "Doctor date of birth (YYYY-MM-DD)", + "required": true, + "type": "string" + }, + { + "name": "graduationCertificate", + "in": "formData", + "description": "Graduation certificate PDF", + "required": true, + "type": "file" + }, + { + "name": "membershipCard", + "in": "formData", + "description": "Membership card PDF", + "required": true, + "type": "file" + }, + { + "name": "professionalPracticeCard", + "in": "formData", + "description": "Professional practice card PDF", + "required": true, + "type": "file" + }, + { + "name": "mastersCertificate", + "in": "formData", + "description": "Masters certificate PDF", + "required": true, + "type": "file" + }, + { + "name": "fellowshipCertificate", + "in": "formData", + "description": "Fellowship certificate PDF", + "required": true, + "type": "file" + }, + { + "name": "unionSpecializationCertificate", + "in": "formData", + "description": "Union specialization certificate PDF", + "required": true, + "type": "file" + } + ], + "responses": { + "201": { + "description": "Doctor signup successful", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Doctor registered successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تسجيل الطبيب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/doctors/login": { + "post": { + "tags": [ + "Doctors" + ], + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Doctor login data", + "required": true, + "schema": { + "type": "object", + "properties": { + "emailOrUsername": { + "type": "string", + "example": "doctor@example.com" + }, + "password": { + "type": "string", + "example": "SecurePassword123" + }, + "rememberMe": { + "type": "string", + "example": "true" + } + }, + "required": [ + "emailOrUsername", + "password", + "rememberMe" + ] + } + } + ], + "responses": { + "200": { + "description": "Doctor login successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "email": { + "type": "string", + "example": "test@example.com" + }, + "name": { + "type": "string", + "example": "Dr. Smith" + }, + "username": { + "type": "string", + "example": "drsmith" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "doctor": { + "type": "object", + "properties": { + "specialization": { + "type": "string", + "example": "CARDIOLOGY" + }, + "account_status": { + "type": "string", + "example": "APPROVED" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctor logged in successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تسجيل دخول الطبيب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/doctors/set-password": { + "patch": { + "tags": [ + "Doctors" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "New password data", + "required": true, + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "NewSecurePassword123" + } + }, + "required": [ + "password" + ] + } + } + ], + "responses": { + "200": { + "description": "Password set successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Password updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث كلمة المرور بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/doctors/announcement": { + "post": { + "tags": [ + "Doctors" + ], + "description": "Allows doctor to post a nurse hiring announcement", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Announcement data", + "required": true, + "schema": { + "type": "object", + "properties": { + "clinic_id": { + "type": "string", + "example": "uuid-of-the-clinic" + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "TUESDAY" + }, + "start_time": { + "type": "string", + "example": "10:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + }, + "required": [ + "day_of_week", + "start_time", + "end_time" + ] + } + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "max_age": { + "type": "number", + "example": 40 + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "notes": { + "type": "string", + "example": "Looking for an experienced nurse" + } + }, + "required": [ + "clinic_id", + "working_days" + ] + } + } + ], + "responses": { + "201": { + "description": "Announcement posted successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Announcement created successfully" + }, + "messageAr": { + "type": "string", + "example": "تم نشر الإعلان بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "403": { + "description": "Doctor account not approved (PENDING or REJECTED)" + }, + "404": { + "description": "Doctor not found or does not belong to the specified clinic" + } + } + } + }, + "/doctors/announcements": { + "get": { + "tags": [ + "Doctors" + ], + "description": "Retrieves all nurse hiring announcements posted by the doctor", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Announcements retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "doctor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Dr. Ahmed Ali" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/example/image.jpg" + } + } + }, + "clinic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Al Salam Clinic" + }, + "address": { + "type": "string", + "example": "123 Main St, Cairo" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=..." + } + } + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "MONDAY" + }, + "start_time": { + "type": "string", + "example": "09:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + } + } + }, + "status": { + "type": "string", + "example": "PENDING" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "max_age": { + "type": "number", + "example": 40 + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "notes": { + "type": "string", + "example": "Looking for an experienced nurse" + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Announcements retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الإعلانات بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "404": { + "description": "Doctor not found" + } + } + } + }, + "/doctors/announcements/{announcementId}/applicants": { + "get": { + "tags": [ + "Doctors" + ], + "description": "Retrieves all PENDING nurse applicants for a specific announcement", + "parameters": [ + { + "name": "announcementId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the announcement to retrieve applicants for" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Applicants retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Max Mustermann" + }, + "email": { + "type": "string", + "example": "max.mustermann@example.com" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "phone": { + "type": "string", + "example": "+201234567890" + }, + "age": { + "type": "number", + "example": 28 + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/example/photo.jpg" + }, + "years_of_experience": { + "type": "number", + "example": 5 + }, + "nationalCardUrl": { + "type": "string", + "example": "https://res.cloudinary.com/example/national_card.pdf" + }, + "brief": { + "type": "string", + "example": "Experienced ICU nurse with 5 years in critical care" + }, + "bonusFileUrl": { + "type": "string", + "example": "https://res.cloudinary.com/example/bonus.pdf" + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Applicants retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع المتقدمين بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "403": { + "description": "Forbidden – announcement does not belong to this doctor" + }, + "404": { + "description": "Announcement not found" + } + } + } + }, + "/doctors/nurses": { + "get": { + "tags": [ + "Doctors" + ], + "description": "Retrieves all nurses working with the doctor", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Nurses retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Max Mustermann" + }, + "email": { + "type": "string", + "example": "max.mustermann@example.com" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "phone": { + "type": "string", + "example": "+201234567890" + }, + "age": { + "type": "number", + "example": 25 + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/example/photo.jpg" + }, + "years_of_experience": { + "type": "number", + "example": 2 + }, + "nationalCardUrl": { + "type": "string", + "example": "https://res.cloudinary.com/example/national_card.pdf" + }, + "bonusFileUrl": { + "type": "string", + "example": "https://res.cloudinary.com/example/bonus.pdf" + }, + "brief": { + "type": "string", + "example": "Experienced ICU nurse with 5 years in critical care" + }, + "clinics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Medical Park Clinic" + }, + "address": { + "type": "string", + "example": "123 Main St, New Cairo" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=..." + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "TUESDAY" + }, + "start_time": { + "type": "string", + "example": "10:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + } + } + } + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurses retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الممرضين بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + } + } + } + }, + "/doctors/announcements/{applicantId}/approve": { + "patch": { + "tags": [ + "Doctors" + ], + "description": "Approves a nurse applicant for a specific announcement", + "parameters": [ + { + "name": "applicantId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the nurse applicant to approve" + }, + { + "name": "announcementId", + "in": "query", + "description": "ID of the announcement the applicant applied to", + "required": true, + "type": "string" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Applicant approved successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Applicant approved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم قبول المتقدم بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "403": { + "description": "Forbidden – announcement does not belong to this doctor" + }, + "404": { + "description": "Applicant or announcement not found" + } + } + } + }, + "/doctors/announcements/{applicantId}/reject": { + "patch": { + "tags": [ + "Doctors" + ], + "description": "Rejects a nurse applicant for a specific announcement", + "parameters": [ + { + "name": "applicantId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the nurse applicant to reject" + }, + { + "name": "announcementId", + "in": "query", + "description": "ID of the announcement the applicant applied to", + "required": true, + "type": "string" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Applicant rejected successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Applicant rejected successfully" + }, + "messageAr": { + "type": "string", + "example": "تم رفض المتقدم بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "403": { + "description": "Forbidden – announcement does not belong to this doctor" + }, + "404": { + "description": "Applicant or announcement not found" + } + } + } + }, + "/doctors/announcements/{announcementId}": { + "delete": { + "tags": [ + "Doctors" + ], + "description": "Deletes a specific nurse hiring announcement", + "parameters": [ + { + "name": "announcementId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the announcement to delete" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Announcement deleted successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Announcement deleted successfully" + }, + "messageAr": { + "type": "string", + "example": "تم حذف الإعلان بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "403": { + "description": "Forbidden – announcement does not belong to this doctor" + }, + "404": { + "description": "Announcement not found" + } + } + }, + "patch": { + "tags": [ + "Doctors" + ], + "description": "Edits a specific nurse hiring announcement (only if it is still PENDING)", + "parameters": [ + { + "name": "announcementId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the announcement to edit" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Updated announcement data (only include fields to be updated)", + "required": true, + "schema": { + "type": "object", + "properties": { + "clinic_id": { + "type": "string", + "example": "uuid-of-the-clinic" + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "TUESDAY" + }, + "start_time": { + "type": "string", + "example": "10:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + }, + "required": [ + "day_of_week", + "start_time", + "end_time" + ] + } + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "max_age": { + "type": "number", + "example": 40 + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "notes": { + "type": "string", + "example": "Looking for an experienced nurse" + } + }, + "required": [ + "clinic_id", + "working_days" + ] + } + } + ], + "responses": { + "200": { + "description": "Announcement updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Announcement updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث الإعلان بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "403": { + "description": "Forbidden – announcement does not belong to this doctor or is not PENDING" + }, + "404": { + "description": "Announcement not found" + } + } + } + }, + "/clinics": { + "post": { + "tags": [ + "Clinics" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Clinic creation data", + "required": true, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Downtown Medical Clinic" + }, + "opening_at": { + "type": "string", + "example": "09:00" + }, + "closing_at": { + "type": "string", + "example": "17:00" + }, + "address": { + "type": "string", + "example": "123 Main Street, City Center" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+Street" + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + }, + "fees": { + "type": "number", + "example": 100 + } + }, + "required": [ + "name", + "opening_at", + "closing_at", + "address", + "phone", + "fees" + ] + } + } + ], + "responses": { + "201": { + "description": "Clinic created successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Clinic created successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إنشاء العيادة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Clinics" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Get doctor clinics successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "Clinic Name" + }, + "address": { + "type": "string", + "example": "123 Main St, City, Country" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+St,+City,+Country" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "opening_at": { + "type": "string", + "example": "09:00" + }, + "closing_at": { + "type": "string", + "example": "17:00" + }, + "canPayOnline": { + "type": "boolean", + "example": true + }, + "is_active": { + "type": "boolean", + "example": true + }, + "created_at": { + "type": "string", + "example": "2024-01-01T00:00:00.000Z" + }, + "fees": { + "type": "number", + "example": 100 + }, + "created_by": { + "type": "string", + "example": "doctor-uuid" + }, + "isOwner": { + "type": "boolean", + "example": true + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctor's clinics retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع عيادات الطبيب بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/clinics/{id}": { + "get": { + "tags": [ + "Clinics" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the clinic" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Clinic details retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid-123" + }, + "name": { + "type": "string", + "example": "Downtown Medical Clinic" + }, + "is_active": { + "type": "boolean", + "example": true + }, + "opening_at": { + "type": "string", + "example": "09:00" + }, + "closing_at": { + "type": "string", + "example": "17:00" + }, + "address": { + "type": "string", + "example": "123 Main Street, City Center" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+Street" + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + }, + "created_at": { + "type": "string", + "example": "2024-01-01T00:00:00.000Z" + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinic retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات العيادة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "patch": { + "tags": [ + "Clinics" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the clinic to update" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Clinic update data (all fields are optional)", + "required": true, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Downtown Medical Clinic - Updated" + }, + "opening_at": { + "type": "string", + "example": "08:00" + }, + "closing_at": { + "type": "string", + "example": "18:00" + }, + "address": { + "type": "string", + "example": "456 New Street, City Center" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=456+New+Street" + }, + "phone": { + "type": "string", + "example": "+1234567891" + }, + "canPayOnline": { + "type": "boolean", + "example": false + }, + "fees": { + "type": "number", + "example": 150 + } + } + } + } + ], + "responses": { + "200": { + "description": "Clinic updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Clinic updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث العيادة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "delete": { + "tags": [ + "Clinics" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the clinic to delete" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Clinic deleted successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Clinic deleted successfully" + }, + "messageAr": { + "type": "string", + "example": "تم حذف العيادة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/clinics/{id}/fees": { + "patch": { + "tags": [ + "Clinics" + ], + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "The unique identifier of the clinic to update fees for" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Clinic fees update data", + "required": true, + "schema": { + "type": "object", + "properties": { + "fees": { + "type": "number", + "example": 200 + } + } + } + } + ], + "responses": { + "200": { + "description": "Clinic fees updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Clinic fees updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث رسوم العيادة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/appointments/doctors": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all doctors available for booking appointments", + "parameters": [ + { + "name": "lang", + "in": "query", + "description": "Required language for specialization", + "required": true, + "type": "string" + }, + { + "name": "gender", + "in": "query", + "description": "Filter doctors by gender (MALE or FEMALE)", + "required": false, + "type": "string" + }, + { + "name": "minFees", + "in": "query", + "description": "Minimum fees filter", + "required": false, + "type": "number" + }, + { + "name": "maxFees", + "in": "query", + "description": "Maximum fees filter", + "required": false, + "type": "number" + }, + { + "name": "isOnline", + "in": "query", + "description": "Filter for online availability (true for online, false for offline)", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "Doctors retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "doctor-uuid" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "age": { + "type": "number", + "example": 45 + }, + "specialization": { + "type": "string", + "example": "IMMUNOLOGY" + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "fees": { + "type": "number", + "example": 200 + }, + "is_online": { + "type": "boolean", + "example": true + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg" + }, + "clinics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "New Cairo Medical Clinic" + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + }, + "opening_at": { + "type": "string", + "example": "09:00" + }, + "closing_at": { + "type": "string", + "example": "17:00" + }, + "address": { + "type": "string", + "example": "123 Main Street, Medical Park" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+Street" + } + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Doctors retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الأطباء بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request" + } + } + } + }, + "/appointments/clinics": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all active clinics available for booking appointments", + "parameters": [ + { + "name": "lang", + "in": "query", + "description": "Required language for specialization", + "required": true, + "type": "string" + }, + { + "name": "canPayOnline", + "in": "query", + "description": "Filter clinics that support online payment (true) or not (false)", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "Clinics retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clinic-uuid" + }, + "name": { + "type": "string", + "example": "New Cairo Medical Clinic" + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "canPayOnline": { + "type": "boolean", + "example": true + }, + "opening_at": { + "type": "string", + "example": "09:00" + }, + "closing_at": { + "type": "string", + "example": "17:00" + }, + "address": { + "type": "string", + "example": "123 Main Street, Medical Park" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=123+Main+Street" + }, + "doctors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "doctor-uuid2" + }, + "name": { + "type": "string", + "example": "House" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "age": { + "type": "number", + "example": 45 + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "fees": { + "type": "number", + "example": 200 + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg" + } + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinics retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع العيادات بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request" + } + } + } + }, + "/appointments/clinic/{clinicId}/doctors": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all doctors in a specific clinic", + "parameters": [ + { + "name": "clinicId", + "in": "path", + "required": true, + "type": "string", + "description": "Clinic ID" + }, + { + "name": "gender", + "in": "query", + "description": "Filter doctors by gender (MALE or FEMALE)", + "required": false, + "type": "string" + }, + { + "name": "minFees", + "in": "query", + "description": "Minimum fees filter", + "required": false, + "type": "number" + }, + { + "name": "maxFees", + "in": "query", + "description": "Maximum fees filter", + "required": false, + "type": "number" + } + ], + "responses": { + "200": { + "description": "Clinic doctors retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "doctor-uuid" + }, + "name": { + "type": "string", + "example": "John Doe" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "age": { + "type": "number", + "example": 45 + }, + "specialization": { + "type": "string", + "example": "IMMUNOLOGY" + }, + "phone": { + "type": "string", + "example": "+1234567890" + }, + "fees": { + "type": "number", + "example": 200 + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg" + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Clinic doctors retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع أطباء العيادة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request" + } + } + } + }, + "/appointments/doctor/{doctorId}/available-days": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get available days for booking with a specific doctor (up to 30 days ahead)", + "parameters": [ + { + "name": "doctorId", + "in": "path", + "required": true, + "type": "string", + "description": "Doctor ID" + }, + { + "name": "clinicId", + "in": "query", + "description": "Clinic ID (required for offline appointments)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Available days retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2026-02-03" + }, + "dayOfWeek": { + "type": "string", + "example": "MONDAY" + }, + "displayDate": { + "type": "string", + "example": "Monday, February 3, 2026" + } + } + } + }, + "message": { + "type": "string", + "example": "Available days retrieved successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing doctor ID or invalid parameters" + } + } + } + }, + "/appointments/doctor/{doctorId}/available-slots": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get available time slots for a specific doctor on a given date", + "parameters": [ + { + "name": "doctorId", + "in": "path", + "required": true, + "type": "string", + "description": "Doctor ID" + }, + { + "name": "date", + "in": "query", + "description": "Date in YYYY-MM-DD format", + "required": true, + "type": "string" + }, + { + "name": "clinicId", + "in": "query", + "description": "Clinic ID (required for offline appointments)", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Available slots retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "start": { + "type": "string", + "example": "09:30" + }, + "end": { + "type": "string", + "example": "09:50" + }, + "available": { + "type": "boolean", + "example": false + }, + "online": { + "type": "boolean", + "example": true + } + } + } + }, + "message": { + "type": "string", + "example": "Available slots retrieved successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing date, invalid format, or past date" + } + } + } + }, + "/appointments/book": { + "post": { + "tags": [ + "Appointments" + ], + "description": "Book a new appointment with a doctor", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Appointment booking details", + "required": true, + "schema": { + "type": "object", + "properties": { + "doctorId": { + "type": "string", + "example": "doctor-uuid" + }, + "clinicId": { + "type": "string", + "example": "clinic-uuid" + }, + "scheduledTime": { + "type": "string", + "example": "2026-02-03T09:00:00.000Z" + } + } + } + } + ], + "responses": { + "201": { + "description": "Appointment booked successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Appointment booked successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - invalid data or slot not available", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Error message describing the issue" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized - user not authenticated" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/appointments/patient/appointments": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all appointments for the patient", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a patient)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Patient appointments retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "appointment-uuid" + }, + "doctorId": { + "type": "string", + "example": "doctor-uuid" + }, + "clinicId": {}, + "status": { + "type": "string", + "example": "CONFIRMED" + }, + "slot_duration": { + "type": "number", + "example": 20 + }, + "doctor_name": { + "type": "string", + "example": "Dr. House" + }, + "doctor_profile_pic": { + "type": "string", + "example": "https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg" + }, + "appointment_date": { + "type": "string", + "example": "2026-03-03" + }, + "start_time": { + "type": "string", + "example": "09:00" + }, + "end_time": { + "type": "string", + "example": "09:20" + }, + "clinic_name": { + "type": "string", + "example": "New Cairo Medical Clinic" + }, + "clinic_address": { + "type": "string", + "example": "123 Main Street, Medical Park" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.app.goo.gl/iKB7dwMcneuUaULYA" + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - patient ID missing" + }, + "401": { + "description": "Unauthorized - patient not authenticated" + } + } + } + }, + "/appointments/patient/today-appointment": { + "get": { + "tags": [ + "Appointments" + ], + "summary": "Get all appointments for the patient today", + "description": "Returns all appointments scheduled for today for the patient", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (patient role required)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Today's appointments retrieved successfully", + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "appointment-uuid-2" + }, + "doctorId": { + "type": "string", + "example": "doctor-uuid" + }, + "clinicId": { + "type": "string", + "example": "clinic-uuid" + }, + "status": { + "type": "string", + "example": "CONFIRMED" + }, + "is_online": { + "type": "boolean", + "example": false + }, + "slot_duration": { + "type": "number", + "example": 20 + }, + "doctor_name": { + "type": "string", + "example": "Dr. Wilson" + }, + "doctor_profile_pic": { + "type": "string", + "example": "https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg" + }, + "appointment_date": { + "type": "string", + "example": "2026-02-05" + }, + "start_time": { + "type": "string", + "example": "14:30" + }, + "end_time": { + "type": "string", + "example": "14:50" + }, + "clinic_name": { + "type": "string", + "example": "Downtown Clinic" + }, + "clinic_address": { + "type": "string", + "example": "456 Nile Corniche" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.app.goo.gl/iKB7dwMcneuUaULYA" + }, + "position": {}, + "estimatedWaitMinutes": {}, + "patientsAhead": {} + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request (invalid authentication or missing required fields)" + }, + "401": { + "description": "Unauthorized - missing or invalid authentication token" + }, + "403": { + "description": "Forbidden - user is not authorized as a patient" + } + } + } + }, + "/appointments/patient/{appointmentId}": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get details of a specific appointment for the patient", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "Appointment ID" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a patient)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Appointment details retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "appointment-uuid" + }, + "doctorId": { + "type": "string", + "example": "doctor-uuid" + }, + "clinicId": { + "type": "string", + "example": "clinic-uuid" + }, + "status": { + "type": "string", + "example": "CONFIRMED" + }, + "slot_duration": { + "type": "number", + "example": 30 + }, + "doctor_name": { + "type": "string", + "example": "Dr. House" + }, + "doctor_profile_pic": { + "type": "string", + "example": "https://res.cloudinary.com/deh1n7kqj/image/upload/v1770577124/DOCTORS/profile_pictures/DOCTOR_102ef1ca-3084-41f3-a225-1058e7059ee8_profile_picture_1770577124527.jpg" + }, + "appointment_date": { + "type": "string", + "example": "2026-02-03" + }, + "start_time": { + "type": "string", + "example": "09:00" + }, + "end_time": { + "type": "string", + "example": "09:30" + }, + "clinic_name": { + "type": "string", + "example": "New Cairo Medical Clinic" + }, + "clinic_address": { + "type": "string", + "example": "123 Main Street, Medical Park" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.app.goo.gl/iKB7dwMcneuUaULYA" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - patient ID missing" + }, + "401": { + "description": "Unauthorized - patient not authenticated" + }, + "404": { + "description": "Appointment not found or does not belong to the patient" + } + } + } + }, + "/appointments/patient/{appointmentId}/reschedule": { + "patch": { + "tags": [ + "Appointments" + ], + "description": "Reschedule an appointment to a new time by the patient", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "Appointment ID to reschedule" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a patient)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "New scheduled time for the appointment", + "required": true, + "schema": { + "type": "object", + "properties": { + "newScheduledTime": { + "type": "string", + "example": "2026-02-10T10:30:00.000Z" + } + } + } + } + ], + "responses": { + "200": { + "description": "Appointment rescheduled successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Appointment rescheduled successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing or invalid parameters (patient ID, new scheduled time, etc.)" + }, + "401": { + "description": "Unauthorized - patient not authenticated" + }, + "403": { + "description": "Forbidden - appointment does not belong to the authenticated patient" + }, + "404": { + "description": "Appointment not found or time slot not available" + } + } + } + }, + "/appointments/{appointmentId}/cancel": { + "delete": { + "tags": [ + "Appointments" + ], + "description": "Cancel an appointment (soft delete). Can be cancelled by either patient or doctor.", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "Appointment ID to cancel" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Appointment cancelled successfully", + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Appointment cancelled successfully" + }, + "data": { + "type": "object", + "properties": { + "appointmentId": { + "type": "string", + "example": "appointment-uuid" + }, + "cancelledAt": { + "type": "string", + "example": "2026-01-29T12:00:00.000Z" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - appointment already cancelled, completed, or too late to cancel", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Error message describing the issue" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized - user not authenticated" + }, + "403": { + "description": "Forbidden - user is not the patient or doctor of this appointment" + }, + "404": { + "description": "Appointment not found" + } + } + } + }, + "/appointments/doctor/{appointmentId}/reschedule": { + "patch": { + "tags": [ + "Appointments" + ], + "description": "Reschedule an appointment by adding minutes (delay) as a doctor", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "Appointment ID to reschedule" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Minutes to add (max 60)", + "required": true, + "schema": { + "type": "object", + "properties": { + "minutes": { + "type": "number", + "example": 30 + } + } + } + } + ], + "responses": { + "200": { + "description": "Appointment rescheduled successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Appointment rescheduled successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing minutes, exceeds limit, or invalid parameters" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + }, + "403": { + "description": "Forbidden - appointment does not belong to the doctor" + }, + "404": { + "description": "Appointment not found" + } + } + } + }, + "/appointments/doctor/upcomming-schedule": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get the doctor", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctor schedule retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2026-02-05" + }, + "displayDate": { + "type": "string", + "example": "Wednesday, February 5, 2026" + }, + "appointments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "appointment-uuid-3" + }, + "status": { + "type": "string", + "example": "CONFIRMED" + }, + "slot_duration": { + "type": "number", + "example": 45 + }, + "patient_name": { + "type": "string", + "example": "Bob Johnson" + }, + "appointment_date": { + "type": "string", + "example": "2026-02-05" + }, + "start_time": { + "type": "string", + "example": "14:00" + }, + "end_time": { + "type": "string", + "example": "14:45" + }, + "clinic_name": { + "type": "string", + "example": "Downtown Health Center" + }, + "clinic_address": { + "type": "string", + "example": "456 Oak Avenue" + } + } + } + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - doctor ID missing or invalid" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + } + } + } + }, + "/appointments/doctor/schedule": { + "post": { + "tags": [ + "Appointments" + ], + "description": "Enter or update a doctor", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Schedule details", + "required": true, + "schema": { + "type": "object", + "properties": { + "clinicId": { + "type": "string", + "example": "clinic-uuid (optional)" + }, + "workingDay": { + "type": "number", + "example": 1 + }, + "startTime": { + "type": "string", + "example": "09:00" + }, + "endTime": { + "type": "string", + "example": "17:00" + }, + "slotDuration": { + "type": "number", + "example": 30 + }, + "bufferTime": { + "type": "number", + "example": 5 + }, + "isOnline": { + "type": "boolean", + "example": true + } + } + } + } + ], + "responses": { + "201": { + "description": "Schedule created successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Schedule created successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - invalid parameters or doctor ID missing" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + } + } + }, + "get": { + "tags": [ + "Appointments" + ], + "description": "Get the doctor\\'s own schedule", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctor schedule retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "schedule-uuid" + }, + "clinicId": { + "type": "string", + "example": "clinic-uuid (optional)" + }, + "dayOfWeek": { + "type": "string", + "example": "MONDAY" + }, + "startTime": { + "type": "string", + "example": "09:00" + }, + "endTime": { + "type": "string", + "example": "17:00" + }, + "slotDuration": { + "type": "number", + "example": 30 + }, + "bufferTime": { + "type": "number", + "example": 5 + }, + "isOnline": { + "type": "boolean", + "example": true + }, + "isActive": { + "type": "boolean", + "example": true + }, + "breakStart": {}, + "breakEnd": {} + } + } + }, + "message": { + "type": "string", + "example": "Doctor schedule retrieved successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - doctor ID missing" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + } + } + }, + "patch": { + "tags": [ + "Appointments" + ], + "description": "Edit a specific entry in the doctor\\'s schedule", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Schedule edit details (all fields optional except scheduleId)", + "required": true, + "schema": { + "type": "object", + "properties": { + "scheduleId": { + "type": "string", + "example": "schedule-uuid" + }, + "clinicId": { + "type": "string", + "example": "clinic-uuid (optional)" + }, + "workingDay": { + "type": "number", + "example": 1 + }, + "startTime": { + "type": "string", + "example": "09:00" + }, + "endTime": { + "type": "string", + "example": "17:00" + }, + "slotDuration": { + "type": "number", + "example": 30 + }, + "bufferTime": { + "type": "number", + "example": 5 + }, + "isOnline": { + "type": "boolean", + "example": true + }, + "isActive": { + "type": "boolean", + "example": false + }, + "breakStart": { + "type": "string", + "example": "2026-02-01" + }, + "breakEnd": { + "type": "string", + "example": "2026-02-22" + } + } + } + } + ], + "responses": { + "200": { + "description": "Schedule updated successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Schedule updated successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - invalid parameters or conflict" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + }, + "403": { + "description": "Forbidden - schedule does not belong to the doctor" + }, + "404": { + "description": "Schedule not found" + } + } + } + }, + "/appointments/doctor/current-schedule": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all confirmed appointments for doctor today", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Today's appointments retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "appointment-uuid-2" + }, + "status": { + "type": "string", + "example": "CONFIRMED" + }, + "slot_duration": { + "type": "number", + "example": 20 + }, + "patient_name": { + "type": "string", + "example": "Jane Smith" + }, + "appointment_date": { + "type": "string", + "example": "2026-02-03" + }, + "start_time": { + "type": "string", + "example": "10:15" + }, + "end_time": { + "type": "string", + "example": "10:35" + }, + "clinic_name": {}, + "clinic_address": {} + } + } + }, + "message": { + "type": "object", + "properties": { + "en": { + "type": "string", + "example": "Doctor's schedule retrieved successfully" + }, + "ar": { + "type": "string", + "example": "تم استرجاع جدول الطبيب بنجاح" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request" + }, + "401": { + "description": "Unauthorized - invalid or missing token" + } + } + } + }, + "${this.path}/doctor/{appointmentId}/context": { + "get": { + "description": "", + "parameters": [ + { + "name": "this.path", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "default": { + "description": "" + } + } + } + }, + "/appointments/doctor/daily-schedule": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all doctor appointments for a specific date", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "date", + "in": "query", + "description": "Date in YYYY-MM-DD format", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Daily schedule retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "appointment-uuid-2" + }, + "status": { + "type": "string", + "example": "COMPLETED" + }, + "slot_duration": { + "type": "number", + "example": 20 + }, + "patient_name": { + "type": "string", + "example": "Jane Hopper" + }, + "appointment_date": { + "type": "string", + "example": "2026-02-05" + }, + "start_time": { + "type": "string", + "example": "10:15" + }, + "end_time": { + "type": "string", + "example": "10:35" + }, + "clinic_name": {}, + "clinic_address": {} + } + } + }, + "message": { + "type": "object", + "properties": { + "en": { + "type": "string", + "example": "Doctor's schedule retrieved successfully" + }, + "ar": { + "type": "string", + "example": "تم استرجاع جدول الطبيب بنجاح" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing or invalid date parameter, or invalid date format" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + } + } + } + }, + "/appointments/doctor/schedule/check-appointments": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Check for existing confirmed appointments in a doctor schedule", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "scheduleId", + "in": "query", + "description": "Schedule ID to check", + "required": true, + "type": "string" + }, + { + "name": "startDate", + "in": "query", + "description": "Optional for vacation. format: YYYY-MM-DD", + "required": false, + "type": "string" + }, + { + "name": "endDate", + "in": "query", + "description": "Optional for vacation. format: YYYY-MM-DD", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Check completed successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "existing": { + "type": "boolean", + "example": true + }, + "numOfAppointments": { + "type": "number", + "example": 3 + } + } + }, + "message": { + "type": "string", + "example": "Check completed successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing/invalid parameters" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + }, + "403": { + "description": "Forbidden - schedule does not belong to the doctor" + }, + "404": { + "description": "Schedule not found" + } + } + } + }, + "/appointments/doctor/vacation": { + "patch": { + "tags": [ + "Appointments" + ], + "description": "Set vacation period for a specific doctor schedule. This will automatically cancel any existing confirmed appointments in the period (use vacation-check first to warn the doctor)", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Vacation details", + "required": true, + "schema": { + "type": "object", + "properties": { + "scheduleId": { + "type": "string", + "example": "schedule-uuid" + }, + "startDate": { + "type": "string", + "example": "2026-03-01" + }, + "endDate": { + "type": "string", + "example": "2026-03-15" + } + } + } + } + ], + "responses": { + "200": { + "description": "Vacation set successfully (any conflicting appointments cancelled)" + }, + "400": { + "description": "Bad request - invalid dates or date range" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + }, + "403": { + "description": "Forbidden - schedule does not belong to the doctor" + }, + "404": { + "description": "Schedule not found" + } + } + }, + "get": { + "tags": [ + "Appointments" + ], + "description": "Get all vacation periods for the doctor, grouped by schedule with details including affected appointments", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Doctor vacations retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "breakStart": { + "type": "string", + "example": "2026-03-01" + }, + "breakEnd": { + "type": "string", + "example": "2026-03-15" + }, + "vacations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "vacationId": { + "type": "string", + "example": "vacation-uuid-2" + }, + "scheduleId": { + "type": "string", + "example": "schedule-uuid-2" + }, + "clinicId": {}, + "clinicName": {}, + "clinicAddress": {}, + "dayOfWeek": { + "type": "string", + "example": "WEDNESDAY" + }, + "isOnline": { + "type": "boolean", + "example": true + }, + "status": { + "type": "string", + "example": "ACTIVE" + }, + "cancelledAppointments": { + "type": "number", + "example": 2 + } + } + } + } + } + } + }, + "message": { + "type": "object", + "properties": { + "en": { + "type": "string", + "example": "Doctor's vacations retrieved successfully" + }, + "ar": { + "type": "string", + "example": "تم استرجاع إجازات الطبيب بنجاح" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - doctor ID missing or invalid" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + } + } + } + }, + "/appointments/doctor/schedule/delete": { + "delete": { + "tags": [ + "Appointments" + ], + "description": "Delete a doctor\\'s schedule. If there are any appointments linked to this schedule, they will be automatically cancelled", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Schedule deletion payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "scheduleId": { + "type": "string", + "example": "schedule-uuid" + } + } + } + } + ], + "responses": { + "200": { + "description": "Schedule successfully deleted (any associated confirmed appointments were cancelled)", + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "object", + "properties": { + "en": { + "type": "string", + "example": "Schedule deleted successfully" + }, + "ar": { + "type": "string", + "example": "تم حذف الجدول بنجاح" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing scheduleId in body or invalid request" + }, + "401": { + "description": "Unauthorized - missing or invalid token" + }, + "403": { + "description": "Forbidden - schedule does not belong to the authenticated doctor" + }, + "404": { + "description": "Schedule not found" + } + } + } + }, + "/appointments/doctor/vacation/cancel": { + "patch": { + "tags": [ + "Appointments" + ], + "description": "Cancel a specific vacation period for a doctor schedule", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a doctor)", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Vacation cancellation details", + "required": true, + "schema": { + "type": "object", + "properties": { + "vacationId": { + "type": "string", + "example": "vacation-uuid" + }, + "scheduleId": { + "type": "string", + "example": "schedule-uuid" + } + } + } + } + ], + "responses": { + "200": { + "description": "Vacation removed successfully" + }, + "400": { + "description": "Bad request - missing vacationId or scheduleId, or invalid parameters" + }, + "401": { + "description": "Unauthorized - doctor not authenticated" + }, + "403": { + "description": "Forbidden - vacation or schedule does not belong to the doctor" + }, + "404": { + "description": "Vacation or schedule not found" + } + } + } + }, + "/appointments/{appointmentId}/agora-token": { + "get": { + "tags": [ + "Appointments" + ], + "description": "Get Agora token and channel name for a specific appointment", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the appointment" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (patient or doctor of the appointment)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Agora token and channel name retrieved successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Agora token retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع توكن أجورا بنجاح" + }, + "data": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "string" + }, + "appId": { + "type": "string", + "example": "string" + } + } + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/queue/position/{appointmentId}": { + "get": { + "tags": [ + "Queue" + ], + "description": "Get queue position, number of patients ahead, and estimated waiting time for a specific appointment", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "Appointment ID to get its queue position" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Queue position retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "position": { + "type": "number", + "example": 3 + }, + "patientsAhead": { + "type": "number", + "example": 2 + }, + "estimatedWaitMinutes": { + "type": "number", + "example": 60 + } + } + }, + "message": { + "type": "string", + "example": "Queue position retrieved successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Appointment ID is required" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Appointment not found or doctor not working on this day" + } + } + } + }, + "/users/profile-picture": { + "patch": { + "tags": [ + "Users" + ], + "description": "", + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "profilePicture", + "in": "formData", + "type": "file", + "required": true, + "description": "Profile picture file" + } + ], + "responses": { + "200": { + "description": "Profile picture updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Profile picture updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث صورة الملف الشخصي بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "get": { + "tags": [ + "Users" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Get profile picture successful", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "url": { + "type": "string", + "example": "https://res.cloudinary.com/your-cloud-name/image/upload/v1696543210/doctors/profile_pictures/doctor_1_profile_picture_1696543210.jpg" + } + } + }, + "messageEn": { + "type": "string", + "example": "Profile picture retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع صورة الملف الشخصي بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + }, + "delete": { + "tags": [ + "Users" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Profile picture deleted successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Profile picture deleted successfully" + }, + "messageAr": { + "type": "string", + "example": "تم حذف صورة الملف الشخصي بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/users/update-profile": { + "patch": { + "tags": [ + "Users" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "User profile update data", + "required": false, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "John Doe" + }, + "phone": { + "type": "string", + "example": "1234567890" + }, + "gender": { + "type": "string", + "example": "MALE or FEMALE" + }, + "dateOfBirth": { + "type": "string", + "example": "1990-01-01" + }, + "availability_type": { + "type": "string", + "example": "ONLINE, OFFLINE, BOTH or UNSET" + } + } + } + } + ], + "responses": { + "200": { + "description": "Profile updated successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Profile updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث الملف الشخصي بنجاح" + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/nurses/signup": { + "post": { + "tags": [ + "Nurses" + ], + "description": "Creates a new nurse account. Requires national ID card upload and optional bonus file", + "consumes": [ + "multipart/form-data" + ], + "parameters": [ + { + "name": "name", + "in": "formData", + "description": "name of the nurse", + "required": true, + "type": "string" + }, + { + "name": "email", + "in": "formData", + "description": "Email address", + "required": true, + "type": "string" + }, + { + "name": "phone", + "in": "formData", + "description": "Phone number", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "formData", + "description": "Initial password for the account", + "required": true, + "type": "string" + }, + { + "name": "years_of_experience", + "in": "formData", + "description": "Number of years of professional nursing experience", + "required": true, + "type": "integer" + }, + { + "name": "gender", + "in": "formData", + "description": "Gender (must match Prisma enum: MALE or FEMALE)", + "required": true, + "type": "string" + }, + { + "name": "date_of_birth", + "in": "formData", + "description": "Date of birth (format YYYY-MM-DD)", + "required": true, + "type": "string" + }, + { + "name": "brief", + "in": "formData", + "description": "Short professional summary / bio (optional)", + "required": false, + "type": "string" + }, + { + "name": "nationalCard", + "in": "formData", + "description": "National ID card or passport scan (PDF only)", + "required": true, + "type": "file" + }, + { + "name": "bonusFile", + "in": "formData", + "description": "Additional document: nursing license, experience certificate, etc.", + "required": false, + "type": "file" + } + ], + "responses": { + "201": { + "description": "Account created successfully – awaiting admin approval", + "schema": { + "type": "object", + "properties": { + "message_en": { + "type": "string", + "example": "Nurse account created successfully. Please wait for verification." + }, + "message_ar": { + "type": "string", + "example": "تم إنشاء حساب الممرضة بنجاح. يرجى الانتظار للموافقة عليه." + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Validation failed (missing fields, wrong file type, invalid date format, etc.)" + }, + "500": { + "description": "Server error during file upload or database transaction" + } + } + } + }, + "/nurses/login": { + "post": { + "tags": [ + "Nurses" + ], + "description": "Authenticates nurse credentials", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "Nurse login data", + "required": true, + "schema": { + "type": "object", + "properties": { + "emailOrUsername": { + "type": "string", + "example": "nurse@example.com" + }, + "password": { + "type": "string", + "example": "SecurePassword123" + }, + "rememberMe": { + "type": "string", + "example": "true" + } + }, + "required": [ + "emailOrUsername", + "password", + "rememberMe" + ] + } + } + ], + "responses": { + "200": { + "description": "Login successful – approved nurse with completed profile", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Maxine Lee" + }, + "email": { + "type": "string", + "example": "maxine.lee@example.com" + }, + "username": { + "type": "string", + "example": "maxine.lee" + }, + "phone": { + "type": "string", + "example": "+201234567890" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "nurse": { + "type": "object", + "properties": { + "account_status": { + "type": "string", + "example": "APPROVED" + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurse retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع بيانات الممرض بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Invalid credentials (wrong email/username or password)" + }, + "403": { + "description": "Account not approved (PENDING or REJECTED)" + } + } + } + }, + "/nurses/set-password": { + "patch": { + "tags": [ + "Nurses" + ], + "description": "", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "New password data", + "required": true, + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "NewSecurePassword123" + } + }, + "required": [ + "password" + ] + } + } + ], + "responses": { + "200": { + "description": "Password set successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Password updated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم تحديث كلمة المرور بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Password already set / validation error" + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "403": { + "description": "Forbidden – user is not a nurse role" + }, + "404": { + "description": "Nurse user not found" + } + } + } + }, + "/nurses/announcements": { + "get": { + "tags": [ + "Nurses" + ], + "description": "Retrieves all active announcements for the nurse", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Announcements retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "doctor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Dr. House" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/example/image.jpg" + } + } + }, + "clinic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Al Salam Clinic" + }, + "address": { + "type": "string", + "example": "123 Main St, Cairo" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=..." + } + } + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "MONDAY" + }, + "start_time": { + "type": "string", + "example": "09:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + } + } + }, + "status": { + "type": "string", + "example": "PENDING" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "max_age": { + "type": "number", + "example": 40 + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "notes": { + "type": "string", + "example": "Looking for an experienced nurse" + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Announcements retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الإعلانات بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "404": { + "description": "Nurse account not approved (PENDING or REJECTED)" + } + } + } + }, + "/nurses/applications": { + "get": { + "tags": [ + "Nurses" + ], + "description": "get all announcements the nurse has applied to", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Applications retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "application_status": { + "type": "string", + "example": "PENDING" + }, + "doctor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Dr. House" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/example/image.jpg" + } + } + }, + "clinic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Al Salam Clinic" + }, + "address": { + "type": "string", + "example": "123 Main St, Cairo" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=..." + } + } + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "MONDAY" + }, + "start_time": { + "type": "string", + "example": "09:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + } + } + }, + "status": { + "type": "string", + "example": "POSTED" + }, + "gender": { + "type": "string", + "example": "FEMALE" + }, + "max_age": { + "type": "number", + "example": 40 + }, + "years_of_experience": { + "type": "number", + "example": 3 + }, + "notes": { + "type": "string", + "example": "Looking for an experienced nurse" + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Applications retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع الطلبات بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Nurse ID not found in token" + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "404": { + "description": "Nurse account not approved (PENDING or REJECTED)" + } + } + } + }, + "/nurses/announcements/{announcementId}/apply": { + "post": { + "tags": [ + "Nurses" + ], + "description": "Apply to a specific announcement", + "parameters": [ + { + "name": "announcementId", + "in": "path", + "required": true, + "type": "string", + "description": "ID of the announcement to apply for" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Application submitted successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Applied to announcement successfully" + }, + "messageAr": { + "type": "string", + "example": "تم التقديم على الإعلان بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Invalid announcement ID / already applied / validation error" + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "404": { + "description": "Announcement not found / Nurse account not approved (PENDING or REJECTED)" + } + } + } + }, + "/nurses/schedule": { + "get": { + "tags": [ + "Nurses" + ], + "description": "get the schedule for the nurse", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Schedule retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "doctor": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Dr. House" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "profilePic": { + "type": "string", + "example": "https://res.cloudinary.com/example/image.jpg" + } + } + }, + "clinic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Al Salam Clinic" + }, + "address": { + "type": "string", + "example": "123 Main St, Cairo" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=..." + } + } + }, + "working_days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "day_of_week": { + "type": "string", + "example": "MONDAY" + }, + "start_time": { + "type": "string", + "example": "09:00" + }, + "end_time": { + "type": "string", + "example": "17:00" + } + } + } + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Nurse schedule retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع جدول الممرضة بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Nurse ID not found in token" + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "404": { + "description": "Nurse account not approved or no schedule assigned" + } + } + } + }, + "/nurses/appointments": { + "get": { + "tags": [ + "Nurses" + ], + "description": "Get all appointments for a specific doctor on a given date", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "doctorId", + "in": "query", + "description": "The ID of the doctor whose appointments are being retrieved", + "required": true, + "type": "string" + }, + { + "name": "clinicId", + "in": "query", + "description": "The ID of the clinic to filter appointments by", + "required": false, + "type": "string" + }, + { + "name": "date", + "in": "query", + "description": "The date to retrieve appointments for, in YYYY-MM-DD format", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Appointments retrieved successfully", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "clinic": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Al Salam Clinic" + }, + "address": { + "type": "string", + "example": "123 Main St, Cairo" + }, + "address_maps_link": { + "type": "string", + "example": "https://maps.google.com/?q=..." + } + } + }, + "patient": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "uuid-string" + }, + "name": { + "type": "string", + "example": "Ahmed Hassan" + }, + "gender": { + "type": "string", + "example": "MALE" + }, + "phone": { + "type": "string", + "example": "+201012345678" + } + } + }, + "status": { + "type": "string", + "example": "CONFIRMED" + }, + "slot_duration": { + "type": "number", + "example": 30 + }, + "appointment_date": { + "type": "string", + "example": "2025-03-15" + }, + "start_time": { + "type": "string", + "example": "09:00 AM" + }, + "end_time": { + "type": "string", + "example": "09:30 AM" + } + } + } + }, + "messageEn": { + "type": "string", + "example": "Appointments retrieved successfully" + }, + "messageAr": { + "type": "string", + "example": "تم استرجاع المواعيد بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request – missing or invalid parameters (nurseId, doctorId, date)" + }, + "401": { + "description": "Unauthorized – missing or invalid token" + }, + "500": { + "description": "Internal server error" + } + } + } + }, + "/nurses/appointments/{appointmentId}/complete": { + "patch": { + "tags": [ + "Nurses" + ], + "description": "Mark an appointment as completed. Only accessible by authenticated nurses", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the appointment to complete" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication (must be a nurse)", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Appointment marked as completed successfully", + "schema": { + "type": "object", + "properties": { + "messageEn": { + "type": "string", + "example": "Appointment completed successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إكمال الموعد بنجاح" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing nurse ID or invalid appointment" + }, + "401": { + "description": "Unauthorized - missing or invalid token" + }, + "403": { + "description": "Forbidden - appointment does not belong to the authenticated user" + }, + "404": { + "description": "Appointment not found" + } + } + } + }, + "/medical-records/health/ipfs": { + "get": { + "tags": [ + "Medical Records - Public" + ], + "description": "Checks connectivity to the IPFS (Pinata) service", + "responses": { + "200": { + "description": "IPFS connection is healthy", + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "message": { + "type": "string", + "example": "IPFS connection is healthy" + } + }, + "xml": { + "name": "main" + } + } + }, + "503": { + "description": "IPFS service is unreachable" + } + } + } + }, + "/medical-records/clinics/{clinicId}/patients/{patientId}/visit-summaries": { + "post": { + "tags": [ + "Medical Records - Doctor" + ], + "description": "Doctor creates a JSON-based medical record for a patient. Validates the doctor works at the clinic. Content is encrypted and stored on IPFS.", + "parameters": [ + { + "name": "clinicId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the clinic" + }, + { + "name": "patientId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the patient" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Medical record payload", + "required": true, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "SOAP Note 2026-03-08" + }, + "type": { + "type": "string", + "example": "SOAP_NOTE" + }, + "content": { + "type": "object", + "properties": { + "subjective": { + "type": "string", + "example": "Patient reports headache" + }, + "objective": { + "type": "string", + "example": "BP 120/80" + }, + "assessment": { + "type": "string", + "example": "Tension headache" + }, + "plan": { + "type": "string", + "example": "Ibuprofen 400mg" + } + } + } + } + } + } + ], + "responses": { + "201": { + "description": "Medical record created successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Medical record created successfully" + }, + "data": { + "type": "object", + "properties": { + "recordId": { + "type": "string", + "example": "uuid-string" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Validation failed" + }, + "403": { + "description": "Doctor is not associated with this clinic" + } + } + } + }, + "/medical-records/patient/visit-summaries": { + "get": { + "tags": [ + "Medical Records - Patient" + ], + "description": "Patient retrieves all their VISIT_SUMMARY records, decrypted and authorized across all clinics on-chain.", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Visit summaries retrieved successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Visit summaries retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "recordId": { + "type": "string", + "example": "uuid-string" + }, + "content": { + "type": "object", + "properties": {} + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + } + } + } + }, + "/medical-records/patient/medical-history": { + "post": { + "tags": [ + "Medical Records - Patient" + ], + "description": "Patient adds a new MEDICAL_HISTORY entry. Type is fixed — only name and content are required.", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Previous Surgeries" + }, + "content": { + "type": "object", + "properties": { + "conditions": { + "type": "array", + "example": [ + "hypertension" + ], + "items": { + "type": "string" + } + }, + "surgeries": { + "type": "array", + "example": [ + "appendectomy" + ], + "items": { + "type": "string" + } + } + } + } + } + } + } + ], + "responses": { + "201": { + "description": "Medical history entry created successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Medical history entry created successfully" + }, + "data": { + "type": "object", + "properties": { + "recordId": { + "type": "string", + "example": "uuid-string" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Validation failed" + }, + "401": { + "description": "Unauthorized" + } + } + }, + "get": { + "tags": [ + "Medical Records - Patient" + ], + "description": "Patient retrieves all their MEDICAL_HISTORY records, decrypted and authorized across all clinics on-chain.", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Medical history retrieved successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Medical history retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "recordId": { + "type": "string", + "example": "uuid-string" + }, + "content": { + "type": "object", + "properties": {} + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + } + } + } + }, + "/medical-records/patient/medical-history/{recordId}": { + "patch": { + "tags": [ + "Medical Records - Patient" + ], + "description": "Patient updates an existing MEDICAL_HISTORY record they own. At least one of name or content must be provided. If content changes, the file is re-encrypted and re-uploaded to IPFS.", + "parameters": [ + { + "name": "recordId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the record to update" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Updated History Title" + }, + "content": { + "type": "object", + "properties": { + "conditions": { + "type": "array", + "example": [ + "hypertension" + ], + "items": { + "type": "string" + } + }, + "surgeries": { + "type": "array", + "example": [ + "appendectomy" + ], + "items": { + "type": "string" + } + } + } + } + } + } + } + ], + "responses": { + "200": { + "description": "Medical history entry updated successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Medical history entry updated successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Validation failed" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Record not found or not owned by patient" + } + } + }, + "delete": { + "tags": [ + "Medical Records - Patient" + ], + "description": "Patient soft-deletes one of their own MEDICAL_HISTORY records. Also removes it from blockchain and IPFS.", + "parameters": [ + { + "name": "recordId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the record to delete" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Medical history entry deleted successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Medical history entry deleted successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Record not found or not owned by patient" + } + } + } + }, + "/medical-records/{patientId}/visit-summaries": { + "get": { + "tags": [ + "Medical Records - Doctor" + ], + "description": "Doctor retrieves VISIT_SUMMARY records for a patient. Only returns records the doctor\\'s clinic(s) are authorized to access on-chain.", + "parameters": [ + { + "name": "patientId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the patient" + } + ], + "responses": { + "200": { + "description": "Visit summaries retrieved successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Visit summaries retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "recordId": { + "type": "string", + "example": "uuid-string" + }, + "content": { + "type": "object", + "properties": {} + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/medical-records/{patientId}/medical-history": { + "get": { + "tags": [ + "Medical Records - Doctor" + ], + "description": "Doctor retrieves MEDICAL_HISTORY records for a patient. Only returns records the doctor\\'s clinic(s) are authorized to access on-chain.", + "parameters": [ + { + "name": "patientId", + "in": "path", + "required": true, + "type": "string", + "description": "UUID of the patient" + } + ], + "responses": { + "200": { + "description": "Medical history retrieved successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Medical history retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "recordId": { + "type": "string", + "example": "uuid-string" + }, + "content": { + "type": "object", + "properties": {} + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/medical-records/grant-access": { + "post": { + "tags": [ + "Medical Records - Patient" + ], + "description": "Patient grants a target clinic access to ALL their medical records across all owner clinics.", + "parameters": [ + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "targetClinicId": { + "type": "string", + "example": "uuid-string" + } + } + } + } + ], + "responses": { + "200": { + "description": "Access granted successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Access granted successfully" + } + }, + "xml": { + "name": "main" + } + } + }, + "401": { + "description": "Unauthorized – missing or invalid token" + } + } + } + }, + "/record/dev/all": { + "delete": { + "tags": [ + "Medical Records" + ], + "description": "DEV ONLY — hard-deletes every medical record from DB, IPFS, and blockchain. No authentication required.", + "responses": { + "200": { + "description": "All records deleted", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Deleted 5 records from DB, IPFS, and blockchain" + }, + "data": { + "type": "object", + "properties": { + "deleted": { + "type": "number", + "example": 5 + } + } + } + }, + "xml": { + "name": "main" + } + } + } + } + } + }, + "/{appointmentId}/upload-url": { + "get": { + "tags": [ + "AI Appointments" + ], + "description": "Get a pre-signed upload URL for uploading audio files to S3", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the appointment" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "userType", + "in": "query", + "description": "Type of user recording the audio (DOCTOR, PATIENT, or MIXED)", + "required": true, + "type": "string", + "enum": [ + "DOCTOR", + "PATIENT", + "MIXED" + ] + } + ], + "responses": { + "200": { + "description": "Upload URL generated successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Upload URL generated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إنشاء رابط التحميل بنجاح" + }, + "data": { + "type": "object", + "properties": { + "uploadUrl": { + "type": "string", + "example": "string" + }, + "objectKey": { + "type": "string", + "example": "string" + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - invalid userType parameter" + }, + "401": { + "description": "Unauthorized - missing or invalid token" + }, + "404": { + "description": "Appointment not found" + } + } + } + }, + "/{appointmentId}/process-audio-ai": { + "post": { + "tags": [ + "AI Appointments" + ], + "description": "Process audio recordings using AI to generate SOAP notes. Accepts either separate doctor/patient audio keys or a single mixed audio key", + "parameters": [ + { + "name": "appointmentId", + "in": "path", + "required": true, + "type": "string", + "description": "The ID of the appointment" + }, + { + "name": "Authorization", + "in": "cookie", + "description": "Bearer token for authentication", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "description": "Audio file keys for processing. Provide either (doctorKey AND patientKey) OR mixedKey", + "required": true, + "schema": { + "type": "object", + "properties": { + "doctorKey": { + "type": "string", + "example": "appointments/appointmentId/DOCTOR.webm" + }, + "patientKey": { + "type": "string", + "example": "appointments/appointmentId/PATIENT.webm" + }, + "mixedKey": { + "type": "string", + "example": "appointments/appointmentId/MIXED.webm" + }, + "prompt": { + "type": "string", + "example": "string" + } + }, + "required": [ + "doctorKey", + "patientKey", + "mixedKey", + "prompt" + ] + } + } + ], + "responses": { + "202": { + "description": "SOAP notes generated successfully", + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "SOAP generated successfully" + }, + "messageAr": { + "type": "string", + "example": "تم إنشاء ملاحظات SOAP بنجاح" + }, + "data": { + "type": "object", + "properties": { + "SOAP": { + "type": "object", + "properties": { + "subjective": { + "type": "string", + "example": "string" + }, + "objective": { + "type": "string", + "example": "string" + }, + "assessment": { + "type": "string", + "example": "string" + }, + "plan": { + "type": "string", + "example": "string" + } + } + } + } + } + }, + "xml": { + "name": "main" + } + } + }, + "400": { + "description": "Bad request - missing required audio keys" + }, + "401": { + "description": "Unauthorized - missing or invalid token" + }, + "404": { + "description": "Appointment not found" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ] +} \ No newline at end of file diff --git a/src/swagger.mjs b/src/swagger.mjs new file mode 100644 index 0000000..2a2158b --- /dev/null +++ b/src/swagger.mjs @@ -0,0 +1,50 @@ +import swaggerAutogen from 'swagger-autogen'; + +const doc = { + info: { + title: 'My API', + description: 'Description', + }, + host: 'localhost:3000', + schemes: ['http'], + securityDefinitions: { + bearerAuth: { + type: 'apiKey', + in: 'header', + name: 'Authorization', + description: 'Enter your Bearer token: Bearer ', + }, + }, + security: [{ bearerAuth: [] }], + tags: [ + { name: 'Auth', description: 'Authentication and account endpoints' }, + { name: 'Admin', description: 'Admin endpoints' }, + { name: 'Super Admin', description: 'Super Admin endpoints' }, + { name: 'MedicalRecords', description: 'Hyperledger Fabric medical record endpoints' }, + { name: 'Doctors', description: 'Doctor account endpoints' }, + { name: 'Clinics', description: 'Clinic endpoints' }, + { name: 'Appointments', description: 'Appointment endpoints' }, + { name: 'Queue', description: 'Queue endpoints' }, + { name: 'Users', description: 'User account endpoints' }, + { name: 'Nurses', description: 'Nurse account endpoints' }, + { name: 'AI Appointments', description: 'AI-generated SOAP notes for appointments' } + ], +}; + +const outputFile = './swagger-output.json'; +const endpointsFiles = [ + './routes/auth.route.ts', + './routes/fabric.route.ts', + './routes/admin.route.ts', + './routes/superAdmin.route.ts', + './routes/doctors.route.ts', + './routes/clinic.route.ts', + './routes/appointment.route.ts', + './routes/queue.route.ts', + './routes/user.route.ts', + './routes/nurse.route.ts', + './routes/medical-record.route.ts', + './routes/ai_appointments.route.ts' +]; + +swaggerAutogen()(outputFile, endpointsFiles, doc); diff --git a/src/test/BADspeechSynthesizer.js b/src/test/BADspeechSynthesizer.js new file mode 100644 index 0000000..1cbdb05 --- /dev/null +++ b/src/test/BADspeechSynthesizer.js @@ -0,0 +1,105 @@ +import textToSpeech from '@google-cloud/text-to-speech'; +import fs from 'fs'; +import util from 'util'; + +async function generateClinicalAudio() { + // Initialize the Google Cloud TTS client + const client = new textToSpeech.TextToSpeechClient(); + + // The text to synthesize + const fullTranscript = ` + Doctor: أهلاً بك يا أستاذة منى، اتفضلي استريحي. قوليلي، إيه اللي بيشتكي منه النهاردة؟ +Patient: أهلاً بيك يا دكتور. والله أنا بقالي فترة تعبانة جداً، مفاصلي كلها بتوجعني ومش قادرة أمارس حياتي الطبيعية خالص. +Doctor: ألف سلامة عليكي. طيب خلينا ناخد الموضوع واحدة واحدة. الـ joint pain ده أو وجع المفاصل بدأ معاكي من إمتى بالظبط؟ +Patient: يعني تقريباً من حوالي تلات أو أربع شهور كده. في الأول كان وجع خفيف وبيروح، بس بقاله شهرين زايد أوي وما بيروحش. +Doctor: تمام. طيب الوجع ده متركز في مفاصل معينة ولا في جسمك كله؟ يعني إيه أكتر مفاصل حاسة فيها بالـ pain؟ +Patient: أكتر حاجة إيديا، صوابعي بتوجعني جداً، ورسغ إيدي، وكمان ركبي الاتنين. +Doctor: طيب بالنسبة لإيديكي، الوجع ده symmetrical؟ يعني موجود في الإيد اليمين والشمال زي بعض بالظبط؟ +Patient: أيوة بالظبط يا دكتور، الإيدين زي بعض. +Doctor: ولما بتصحي من النوم الصبح، هل بتحسي إن مفاصلك متخشبة؟ يعني فيه morning stiffness؟ +Patient: أيوة جداً! دي أكتر حاجة مضيقاني. بصحى من النوم حاسة إني متكتفة ومش قادرة أتني صوابعي خالص، ولا حتى أقدر أمسك كوباية الشاي أو أسرح شعري. +Doctor: الـ morning stiffness ده بيستمر معاكي وقت قد إيه تقريباً لحد ما تبدأي تحسي إن المفاصل فكت شوية وتقدري تستخدميها؟ بياخد أكتر من ساعة؟ +Patient: أيوة، ساعات بياخد ساعتين أو تلاتة الصبح على ما أقدر أحرك إيدي طبيعي. +Doctor: طيب، هل بتلاحظي أي تورم أو احمرار في المفاصل؟ يعني فيه swelling أو redness؟ +Patient: التورم موجود، بحس إن عقل صوابعي وارمة وتخينة كده عن الطبيعي، وساعات بحس إنها دافية شوية لما بحط إيدي عليها. +Doctor: أمم، تمام. طيب الوجع ده بيزيد مع الحركة والمجهود ولا بيزيد وإنتي مرتاحة؟ +Patient: هو بيزيد الصبح زي ما قلتلك وإنا لسة قايمة من السرير، ولما ببدأ أتحرك وأعمل شغل البيت بحس إنه بيخف شوية، بس بيرجع يتعبني تاني لو عملت مجهود زيادة. +Doctor: عظيم جداً، ده بيسموه inflammatory pain pattern. طيب هل فيه أي أعراض تانية بره المفاصل؟ يعني مثلاً حاسة بـ fatigue، إرهاق عام، أو سخونية low-grade fever؟ +Patient: الإرهاق ده فظيع، أنا دايماً حاسة إني مهدودة ومافيش طاقة. وساعات فعلاً بحس إن جسمي مكسر ودافي شوية بالليل. وكمان خسيت حوالي ٤ كيلو في الشهرين اللي فاتوا من غير ما أعمل دايت. +Doctor: طيب هل بيجيلك أي skin rash، طفح جلدي في وشك أو جسمك؟ أو قرح في البق oral ulcers متكررة؟ +Patient: لا مفيش طفح جلدي، بس بيجيلي قرح في البق كل فترة كده وبتوجعني. +Doctor: هل بتحسي بـ dry eyes أو dry mouth؟ يعني عينيكي أو ريقك بينشفوا بصورة ملحوظة؟ +Patient: عيني بتنشف شوية وبحس فيها بزي رمل كده ساعات، وبضطر أشرب مية كتير عشان ريقي بينشف. +Doctor: هل شعرك بيقع بشكل غير طبيعي؟ Hair loss؟ +Patient: بيقع شوية بس عادي يعني، مش لدرجة إني أصلع. +Doctor: في أي وجع في الصدر أو كرشة نفس لما بتاخدي نفس عميق؟ +Patient: لا الحمد لله، مفيش الكلام ده. +Doctor: هل في حد في العيلة عنده أي autoimmune diseases؟ يعني أمراض مناعية زي الروماتويد، الذئبة الحمراء SLE، أو الصدفية؟ +Patient: أيوة، خالتي بتتعالج من الروماتويد بقالها سنين. +Doctor: طيب، في أي أمراض مزمنة تانية بتتعالجي منها؟ ضغط، سكر، أو أي مشاكل في القلب؟ +Patient: لا الحمد لله، ما باخدش أي أدوية غير المسكنات اليومين دول عشان الوجع. +Doctor: بتاخدي مسكنات إيه، والجرعة بتاعتها قد إيه تقريباً؟ +Patient: باخد بروفين ٤٠٠، تلات مرات في اليوم، بس مبقاش يجيب نتيجة زي الأول. + +(Doctor pauses for physical examination) + +Doctor: طيب تعالي نتفضل على السرير عشان أعملك physical examination ونفحص المفاصل دي. +Patient: حاضر يا دكتور. +Doctor: هضغط على المفاصل دي شوية، لو حسيتي بوجع قوليلي. ده بيوجع؟ +Patient: آآه، ده بيوجع أوي. +Doctor: تمام، الوجع ده في الـ MCP joints أو مفاصل الصوابع. في هنا واضح synovitis، يعني التهاب في الغشاء المبطن للمفصل. المفاصل دي وارمة وـ tender جداً. طيب نتني الرسغ كده... ده بيوجع؟ +Patient: أيوة وجع شديد هنا في الإيد اليمين أكتر. +Doctor: مظبوط، فيه swelling في الـ wrist joint. طيب نفرد الركب ونثنيها... في شوية crepitus أو طرقعة هنا، بس برضه فيه mild effusion، ارتشاح بسيط في الركبة اليمين. تقدري تقومي تقعدي على الكرسي تاني. + +(Patient sits back down) + +Doctor: بصي يا أستاذة منى، من الـ history اللي أخدته منك ومن الـ clinical examination، الأعراض اللي عندك دي زي الـ symmetrical polyarthritis والـ prolonged morning stiffness بتمشي أكتر مع مرض الروماتويد المفصلي، أو الـ Rheumatoid Arthritis. +Patient: روماتويد؟ زي خالتي؟ طب ده معناه إني مش هقدر أحرك إيدي بعد كده وهتتعوج؟ +Doctor: لا خالص، ماتقلقيش. الطب اتقدم جداً، ولو اكتشفنا الموضوع بدري وبدأنا العلاج، بنقدر نتحكم في المرض تماماً وتعيشي حياتك بشكل طبيعي جداً من غير أي deformities. بس طبعاً عشان نأكد الـ diagnosis ده، لازم نطلب شوية تحاليل وأشعة. +Patient: تحاليل إيه يا دكتور؟ +Doctor: هطلب منك مجموعة labs كاملة. أولاً هنعمل CBC عشان نطمن على صورة الدم ونشوف لو فيه anemia of chronic disease. وهنعمل دلالات التهاب زي الـ ESR والـ CRP، ودول أكيد هيطلعوا عاليين شوية. +Patient: تمام. +Doctor: والأهم بقى، هنعمل تحاليل المناعة الخاصة بالروماتويد، وهي الـ Rheumatoid Factor أو الـ RF، وتحليل تاني أدق اسمه Anti-CCP. دول بيساعدونا نأكد التشخيص بنسبة كبيرة. وكمان هطلب وظايف كبد وكلى، ALT, AST, Creatinine عشان نطمن قبل ما نبدأ أي أدوية قوية. +Patient: والأشعة دي على إيدي؟ +Doctor: بالظبط، هنعمل X-ray أو أشعة عادية على الإيدين والرسغ، وكمان على الركب. دي هتبين لنا لو فيه أي bone erosions أو تآكل في العضم، أو joint space narrowing. دي بتكون زي baseline أو خط بداية نتابع بيه حالة المفاصل بعد كده. +Patient: هعملهم وأجيبهم لحضرتك على طول. بس أنا حالياً مش قادرة أستحمل الوجع لحد ما التحاليل تطلع، مفيش حاجة تريّحني؟ +Doctor: أكيد طبعاً. إحنا هنمشي على خطتين. خطة سريعة للوجع، وخطة طويلة الأمد للمرض نفسه. دلوقتي هكتبلك NSAIDs، دي مضادات التهاب غير ستيرويدية هتاخديها بعد الأكل عشان الوجع. وهديكي كمان low-dose corticosteroids، يعني نسبة كورتيزون بسيطة جداً، زي الـ Prednisone 5 mg كل يوم الصبح. دي بنسميها bridging therapy، بتهدي الالتهاب بسرعة جداً لحد ما الأدوية الأساسية تبدأ تشتغل. +Patient: أنا بخاف من الكورتيزون أوي يا دكتور، مش ده بينفخ الجسم وبيعمل هشاشة؟ +Doctor: الجرعة دي صغيرة جداً لفترة مؤقتة، شهر أو اتنين بالكتير، ومش هتلحق تعمل الـ side effects دي. ماتقلقيش خالص. ولما التحاليل تطلع ونتأكد من التشخيص، هنبدأ في مجموعة أدوية اسمها DMARDs، أو الأدوية المعدلة لطبيعة المرض، وأشهرهم دواء اسمه Methotrexate. ده اللي بيوقف نشاط المرض وبيحمي المفاصل من التآكل. +Patient: تمام يا دكتور، أنا هعمل التحاليل دي النهاردة. +Doctor: ممتاز. أنا هسجل كل الـ clinical notes بتاعتك على السيستم عندنا، وهضيفلك الأدوية المبدئية. من خلال الـ medication tracker بتاع المستشفى، هيجيلك تنبيهات بمواعيد الأدوية عشان ما تنسيش، خصوصاً إن الأدوية المناعية بعد كده محتاجة التزام دقيق جداً. وأول ما التحاليل والأشعة يخلصوا، هتقدري ترفقيهم على السيستم والـ appointment management هيحددلك أقرب ميعاد للمتابعة معايا. +Patient: دي حاجة ممتازة والله بتسهل علينا كتير. شكراً جداً يا دكتور، طمنتني. +Doctor: العفو يا أستاذة منى، ألف سلامة عليكي، ومستني أشوفك بالتحاليل الأسبوع الجاي إن شاء الله. + `; + + // Construct the request + const request = { + input: { text: fullTranscript }, + // Using an Egyptian Arabic voice model + voice: { + languageCode: 'ar-EG', + name: 'ar-EG-Wavenet-B' // 'A' is female, 'B' is male, 'C' is male, 'D' is female + }, + // Set output format to MP3 + audioConfig: { + audioEncoding: 'MP3', + speakingRate: 1.0, // Adjust speed if needed (0.25 to 4.0) + }, + }; + + try { + console.log('Generating audio, please wait...'); + // Perform the text-to-speech request + const [response] = await client.synthesizeSpeech(request); + + // Write the binary audio content to a local file + const writeFile = util.promisify(fs.writeFile); + await writeFile('rheumatology_consultation.mp3', response.audioContent, 'binary'); + + console.log('Success! Audio saved to rheumatology_consultation.mp3'); + } catch (error) { + console.error('Error generating audio:', error); + } +} + +generateClinicalAudio(); \ No newline at end of file diff --git a/src/test/audioAI.test.js b/src/test/audioAI.test.js new file mode 100644 index 0000000..b25049f --- /dev/null +++ b/src/test/audioAI.test.js @@ -0,0 +1,217 @@ +const fs = require('fs'); +const path = require('path'); + +// Configuration +const appointmentId = "dbd33650-7a88-4988-94ec-0c38e4a9ab07"; // Use a real appointment ID from your database +const backendBaseUrl = "http://localhost:3000/appointments/ai"; + +/** + * Test flow for MIXED audio scenario (single file with both doctor and patient) + */ +async function testMixedAudioFlow() { + const fileName = "mixed.webm"; + const fileType = "MIXED"; + const filePath = path.join(__dirname, fileName); + + console.log("=== STARTING MIXED AUDIO TEST ===\n"); + + // 1. Verify the file exists locally + if (!fs.existsSync(filePath)) { + console.error(`❌ Error: Cannot find ${fileName}`); + console.error("Please record a test conversation and save it as mixed.webm in the test directory."); + return; + } + + try { + // Step 1: Request presigned URL + console.log(`🚀 Step 1: Requesting presigned URL for ${fileType} audio...`); + const urlResponse = await fetch(`${backendBaseUrl}/${appointmentId}/upload-url?userType=${fileType}`); + + if (!urlResponse.ok) { + const errorText = await urlResponse.text(); + throw new Error(`Failed to get upload URL: ${errorText}`); + } + + const urlData = await urlResponse.json(); + const { uploadUrl, objectKey } = urlData.data; + console.log(` ✅ Presigned URL received!`); + console.log(` 📁 Object Key: ${objectKey}`); + + // Step 2: Upload file to B2 + const fileBuffer = fs.readFileSync(filePath); + console.log(`\n☁️ Step 2: Uploading ${fileBuffer.byteLength} bytes to Backblaze B2...`); + + const b2Response = await fetch(uploadUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'audio/webm' + }, + body: fileBuffer + }); + + if (!b2Response.ok) { + throw new Error(`B2 upload failed with status: ${b2Response.status}`); + } + console.log(` ✅ File uploaded successfully!`); + + // Step 3: Trigger AI processing + console.log(`\n🧠 Step 3: Triggering AI processing...`); + + const aiPayload = { + mixedKey: objectKey + }; + + const aiResponse = await fetch(`${backendBaseUrl}/${appointmentId}/process-audio-ai`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(aiPayload) + }); + + const aiResult = await aiResponse.json(); + + if (aiResponse.status === 202) { + console.log(` 🎉 SUCCESS! AI processing started.`); + console.log(`\n👀 Check your backend terminal for: + - Whisper transcription + - Llama 3.1 speaker diarization + - [DOCTOR] and [PATIENT] tagged transcript`); + } else { + console.error(`❌ AI processing failed with status: ${aiResponse.status}`); + console.error(JSON.stringify(aiResult, null, 2)); + } + + } catch (error) { + console.error("\n💥 Test failed:", error.message); + } +} + +/** + * Test flow for SEPARATE audio scenario (doctor.webm and patient.webm) + */ +async function testSeparateAudioFlow() { + const doctorFile = "doctor.webm"; + const patientFile = "patient.webm"; + const doctorPath = path.join(__dirname, doctorFile); + const patientPath = path.join(__dirname, patientFile); + + console.log("\n=== STARTING SEPARATE AUDIO TEST ===\n"); + + // 1. Verify files exist + if (!fs.existsSync(doctorPath) || !fs.existsSync(patientPath)) { + console.error(`❌ Error: Missing audio files`); + console.error(`Required files: ${doctorFile} and ${patientFile}`); + return; + } + + try { + // Step 1: Get presigned URLs for both files + console.log(`🚀 Step 1: Requesting presigned URLs...`); + + const [doctorUrlRes, patientUrlRes] = await Promise.all([ + fetch(`${backendBaseUrl}/${appointmentId}/upload-url?userType=DOCTOR`), + fetch(`${backendBaseUrl}/${appointmentId}/upload-url?userType=PATIENT`) + ]); + + if (!doctorUrlRes.ok || !patientUrlRes.ok) { + throw new Error(`Failed to get upload URLs`); + } + + const doctorUrlData = await doctorUrlRes.json(); + const patientUrlData = await patientUrlRes.json(); + + const { uploadUrl: doctorUploadUrl, objectKey: doctorKey } = doctorUrlData.data; + const { uploadUrl: patientUploadUrl, objectKey: patientKey } = patientUrlData.data; + + console.log(` ✅ URLs received!`); + console.log(` 📁 Doctor Key: ${doctorKey}`); + console.log(` 📁 Patient Key: ${patientKey}`); + + // Step 2: Upload both files to B2 + console.log(`\n☁️ Step 2: Uploading files to Backblaze B2...`); + + const doctorBuffer = fs.readFileSync(doctorPath); + const patientBuffer = fs.readFileSync(patientPath); + + const [doctorB2Res, patientB2Res] = await Promise.all([ + fetch(doctorUploadUrl, { + method: 'PUT', + headers: { 'Content-Type': 'audio/webm' }, + body: doctorBuffer + }), + fetch(patientUploadUrl, { + method: 'PUT', + headers: { 'Content-Type': 'audio/webm' }, + body: patientBuffer + }) + ]); + + if (!doctorB2Res.ok || !patientB2Res.ok) { + throw new Error(`B2 upload failed`); + } + console.log(` ✅ Both files uploaded successfully!`); + + // Step 3: Trigger AI processing + console.log(`\n🧠 Step 3: Triggering AI processing...`); + + const aiPayload = { + doctorKey: doctorKey, + patientKey: patientKey + }; + + const aiResponse = await fetch(`${backendBaseUrl}/${appointmentId}/process-audio-ai`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(aiPayload) + }); + + const aiResult = await aiResponse.json(); + + if (aiResponse.status === 202) { + console.log(` 🎉 SUCCESS! AI processing started.`); + console.log(`\n👀 Check your backend terminal for: + - Whisper transcription of both audio files + - Merged and time-aligned transcript`); + } else { + console.error(`❌ AI processing failed with status: ${aiResponse.status}`); + console.error(JSON.stringify(aiResult, null, 2)); + } + + } catch (error) { + console.error("\n💥 Test failed:", error.message); + } +} + +// Main execution +async function runTests() { + const args = process.argv.slice(2); + const testType = args[0] || 'mixed'; + + if (testType === 'mixed') { + await testMixedAudioFlow(); + } else if (testType === 'separate') { + await testSeparateAudioFlow(); + } else if (testType === 'both') { + await testMixedAudioFlow(); + await testSeparateAudioFlow(); + } else { + console.log(` +Usage: node mixedAudioAI.test.js [test-type] + +Test types: + mixed - Test with single mixed audio file (default) + separate - Test with separate doctor and patient audio files + both - Run both tests + +Examples: + node mixedAudioAI.test.js mixed + node mixedAudioAI.test.js separate + node mixedAudioAI.test.js both + `); + } +} + +runTests(); \ No newline at end of file diff --git a/src/test/auth.test.ts b/src/test/auth.test.ts new file mode 100644 index 0000000..f2d07b4 --- /dev/null +++ b/src/test/auth.test.ts @@ -0,0 +1,81 @@ +import { User } from '@prisma/client'; +import bcrypt from 'bcrypt'; +import request from 'supertest'; +import App from '@/app'; +import { CreateUserDto } from '@dtos/users.dto'; +import AuthRoute from '@routes/auth.route'; + +afterAll(async () => { + await new Promise(resolve => setTimeout(() => resolve(), 500)); +}); + +describe('Testing Auth', () => { + describe('[POST] /signup', () => { + it('response should have the Create userData', async () => { + const userData: CreateUserDto = { + email: 'test@email.com', + password: 'q1w2e3r4', + }; + + const authRoute = new AuthRoute(); + const users = authRoute.authController.authService.users; + + users.findUnique = jest.fn().mockReturnValue(null); + users.create = jest.fn().mockReturnValue({ + id: 1, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + + const app = new App([authRoute]); + return request(app.getServer()).post(`${authRoute.path}signup`).send(userData).expect(201); + }); + }); + + describe('[POST] /login', () => { + it('response should have the Set-Cookie header with the Authorization token', async () => { + const userData: CreateUserDto = { + email: 'test@email.com', + password: 'q1w2e3r4', + }; + + const authRoute = new AuthRoute(); + const users = authRoute.authController.authService.users; + + users.findUnique = jest.fn().mockReturnValue({ + id: 1, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + + const app = new App([authRoute]); + return request(app.getServer()) + .post(`${authRoute.path}login`) + .send(userData) + .expect('Set-Cookie', /^Authorization=.+/); + }); + }); + + // describe('[POST] /logout', () => { + // it('logout Set-Cookie Authorization=; Max-age=0', async () => { + // const user: User = { + // id: 1, + // email: 'test@email.com', + // password: 'q1w2e3r4', + // }; + + // const authRoute = new AuthRoute(); + // const users = authRoute.authController.authService.users; + + // users.findFirst = jest.fn().mockReturnValue({ + // ...user, + // password: await bcrypt.hash(user.password, 10), + // }); + + // const app = new App([authRoute]); + // return request(app.getServer()) + // .post(`${authRoute.path}logout`) + // .expect('Set-Cookie', /^Authorization=\;/); + // }); + // }); +}); diff --git a/src/test/doctor.webm b/src/test/doctor.webm new file mode 100644 index 0000000..343c3b6 Binary files /dev/null and b/src/test/doctor.webm differ diff --git a/src/test/index.test.ts b/src/test/index.test.ts new file mode 100644 index 0000000..8b3825c --- /dev/null +++ b/src/test/index.test.ts @@ -0,0 +1,18 @@ +import request from 'supertest'; +import App from '@/app'; +import IndexRoute from '@routes/index.route'; + +afterAll(async () => { + await new Promise(resolve => setTimeout(() => resolve(), 500)); +}); + +describe('Testing Index', () => { + describe('[GET] /', () => { + it('response statusCode 200', () => { + const indexRoute = new IndexRoute(); + const app = new App([indexRoute]); + + return request(app.getServer()).get(`${indexRoute.path}`).expect(200); + }); + }); +}); diff --git a/src/test/mixed.webm b/src/test/mixed.webm new file mode 100644 index 0000000..1af0f80 Binary files /dev/null and b/src/test/mixed.webm differ diff --git a/src/test/patient.webm b/src/test/patient.webm new file mode 100644 index 0000000..687ee05 Binary files /dev/null and b/src/test/patient.webm differ diff --git a/src/test/separateAudioAI.test.js b/src/test/separateAudioAI.test.js new file mode 100644 index 0000000..7f02265 --- /dev/null +++ b/src/test/separateAudioAI.test.js @@ -0,0 +1,96 @@ +const fs = require('fs'); +const path = require('path'); + +async function testRealUploadFlow() { + // Configuration + const appointmentId = "dbd33650-7a88-4988-94ec-0c38e4a9ab07"; + const backendBaseUrl = "http://localhost:3000/appointments/ai"; + + /** + * Helper function to execute the full upload lifecycle for a single file + */ + async function uploadAudioTrack(fileType, fileName) { + const filePath = path.join(__dirname, fileName); + + // 1. Verify the file exists locally + if (!fs.existsSync(filePath)) { + console.error(`❌ Error: Cannot find ${fileName}. Make sure it is in the same directory as this script.`); + return null; + } + + console.log(`\n🚀 Processing [${fileType.toUpperCase()}] audio track...`); + + // 2. Ask backend for the Presigned URL + console.log(` ➡️ Requesting Presigned URL from Backend...`); + const urlResponse = await fetch(`${backendBaseUrl}/${appointmentId}/upload-url?userType=${fileType}`); + + if (!urlResponse.ok) { + throw new Error(`Backend failed to generate URL: ${await urlResponse.text()}`); + } + + const data = await urlResponse.json(); + const { uploadUrl, objectKey } = data.data; + + // 3. Read the physical file into a Buffer + const fileBuffer = fs.readFileSync(filePath); + + // 4. Upload the Buffer directly to Backblaze B2 + console.log(` ➡️ Uploading ${fileBuffer.byteLength} bytes directly to Backblaze B2...`); + const b2Response = await fetch(uploadUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'audio/webm' // Must strictly match the backend's PutObjectCommand + }, + body: fileBuffer + }); + + if (!b2Response.ok) { + throw new Error(`B2 Upload failed with status ${b2Response.status}: ${await b2Response.text()}`); + } + + console.log(` ✅ Success! File uploaded to B2 at key: ${objectKey}`); + return objectKey; + } + + try { + console.log("=== STARTING CLOUD UPLOAD TEST ==="); + + // Upload both tracks sequentially + const doctorKey = await uploadAudioTrack('DOCTOR', 'doctor.webm'); + const patientKey = await uploadAudioTrack('PATIENT', 'patient.webm'); + + if (!doctorKey || !patientKey) { + console.log("\n⚠️ Aborting test: One or both audio files are missing."); + return; + } + + // --- STEP 5: WAKE UP THE AI --- + console.log(`\n🧠 All files uploaded. Triggering the AI Pipeline...`); + const aiPayload = { + doctorKey: doctorKey, + patientKey: patientKey + }; + + const aiResponse = await fetch(`${backendBaseUrl}/${appointmentId}/process-audio-ai`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(aiPayload) + }); + + if (aiResponse.status === 202) { + console.log(`🎉 SUCCESS! Backend returned 202 Accepted.`); + console.log(`👀 Check your Express server terminal now! You should see the files downloading from B2, transcribing via Whisper, and merging.`); + } else { + console.error(`❌ AI Trigger failed. Status: ${aiResponse.status}`); + console.error(await aiResponse.text()); + } + + } catch (error) { + console.error("\n💥 Test script crashed:", error); + } +} + +// Execute the test +testRealUploadFlow(); \ No newline at end of file diff --git a/src/test/users.test.ts b/src/test/users.test.ts new file mode 100644 index 0000000..8bf00fd --- /dev/null +++ b/src/test/users.test.ts @@ -0,0 +1,134 @@ +import { PrismaClient, User } from '@prisma/client'; +import bcrypt from 'bcrypt'; +import request from 'supertest'; +import App from '@/app'; +import { CreateUserDto } from '@dtos/users.dto'; +import UserRoute from '@routes/users.route'; + +afterAll(async () => { + await new Promise(resolve => setTimeout(() => resolve(), 500)); +}); + +describe('Testing Users', () => { + describe('[GET] /users', () => { + it('response findAll users', async () => { + const usersRoute = new UserRoute(); + const users = usersRoute.usersController.userService.users; + + users.findMany = jest.fn().mockReturnValue([ + { + id: 1, + email: 'a@email.com', + password: await bcrypt.hash('q1w2e3r4!', 10), + }, + { + id: 2, + email: 'b@email.com', + password: await bcrypt.hash('a1s2d3f4!', 10), + }, + { + id: 3, + email: 'c@email.com', + password: await bcrypt.hash('z1x2c3v4!', 10), + }, + ]); + + const app = new App([usersRoute]); + return request(app.getServer()).get(`${usersRoute.path}`).expect(200); + }); + }); + + describe('[GET] /users/:id', () => { + it('response findOne user', async () => { + const userId = 1; + + const usersRoute = new UserRoute(); + const users = usersRoute.usersController.userService.users; + + users.findUnique = jest.fn().mockReturnValue({ + id: 1, + email: 'a@email.com', + password: await bcrypt.hash('q1w2e3r4!', 10), + }); + + const app = new App([usersRoute]); + return request(app.getServer()).get(`${usersRoute.path}/${userId}`).expect(200); + }); + }); + + describe('[POST] /users', () => { + it('response Create user', async () => { + const userData: CreateUserDto = { + email: 'test@email.com', + password: 'q1w2e3r4', + }; + + const usersRoute = new UserRoute(); + const users = usersRoute.usersController.userService.users; + + users.findUnique = jest.fn().mockReturnValue(null); + users.create = jest.fn().mockReturnValue({ + id: 1, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + + const app = new App([usersRoute]); + return request(app.getServer()).post(`${usersRoute.path}`).send(userData).expect(201); + }); + }); + + describe('[PUT] /users/:id', () => { + it('response Update user', async () => { + const userId = 1; + const userData: CreateUserDto = { + email: 'test@email.com', + password: 'q1w2e3r4', + }; + + const usersRoute = new UserRoute(); + const users = usersRoute.usersController.userService.users; + + users.findUnique = jest.fn().mockReturnValue({ + id: userId, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + users.update = jest.fn().mockReturnValue({ + id: userId, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + + const app = new App([usersRoute]); + return request(app.getServer()).put(`${usersRoute.path}/${userId}`).send(userData).expect(200); + }); + }); + + describe('[DELETE] /users/:id', () => { + it('response Delete user', async () => { + const userId = 1; + const userData: CreateUserDto = { + email: 'test@email.com', + password: 'q1w2e3r4', + }; + + const usersRoute = new UserRoute(); + const users = usersRoute.usersController.userService.users; + + users.findUnique = jest.fn().mockReturnValue({ + id: userId, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + users.delete = jest.fn().mockReturnValue({ + id: userId, + email: userData.email, + password: await bcrypt.hash(userData.password, 10), + }); + + const app = new App([usersRoute]); + return request(app.getServer()).delete(`${usersRoute.path}/${userId}`).expect(200); + }); + }); +}); diff --git a/src/utils/catchAsync.ts b/src/utils/catchAsync.ts new file mode 100644 index 0000000..4dbd3c3 --- /dev/null +++ b/src/utils/catchAsync.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from 'express'; + +type AsyncFunction = ( + req: Request, + res: Response, + next: NextFunction +) => Promise; + + +export const catchAsync = (fn: AsyncFunction) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; + diff --git a/src/utils/cloudinary.ts b/src/utils/cloudinary.ts new file mode 100644 index 0000000..ab7f8c4 --- /dev/null +++ b/src/utils/cloudinary.ts @@ -0,0 +1,11 @@ +import { CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, CLOUDINARY_CLOUD_NAME } from '@/config'; +import { v2 as cloudinary } from 'cloudinary'; + +// Configure with credentials from your Cloudinary Dashboard +cloudinary.config({ + cloud_name: CLOUDINARY_CLOUD_NAME, + api_key: CLOUDINARY_API_KEY, + api_secret: CLOUDINARY_API_SECRET +}); + +export default cloudinary; \ No newline at end of file diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts new file mode 100644 index 0000000..91fe825 --- /dev/null +++ b/src/utils/errorMessages.ts @@ -0,0 +1,403 @@ +export const ErrorMessages = { + // Authentication errors + USER_NOT_FOUND: { + en: 'User not found', + ar: 'المستخدم غير موجود', + }, + + EMAIL_EXISTS: { + en: `This email already exists`, + ar: `البريد الإلكتروني موجود بالفعل`, + }, + USERNAME_EXISTS: { + en: 'This username already exists', + ar: 'اسم المستخدم موجود بالفعل' + }, + USER_NOT_FOUND_CREDENTIALS: { + en: 'User with the provided credentials was not found', + ar: 'لم يتم العثور على المستخدم ببيانات الاعتماد المقدمة', + }, + PASSWORD_NOT_MATCHING: { + en: 'Password is not matching', + ar: 'كلمة المرور غير صحيحة', + }, + USER_NOT_EXIST: { + en: "User doesn't exist", + ar: 'المستخدم غير موجود', + }, + REFRESH_TOKEN_NOT_PROVIDED: { + en: 'Refresh token not provided', + ar: 'لم يتم تقديم رمز التحديث', + }, + INVALID_REFRESH_TOKEN: { + en: 'Invalid or expired refresh token', + ar: 'رمز التحديث غير صالح أو منتهي الصلاحية', + }, + USER_EMAIL_NOT_FOUND: { + en: 'User email not found', + ar: 'البريد الإلكتروني للمستخدم غير موجود', + }, + + OTP_REQUIRED: { + en: 'OTP is required', + ar: 'رمز التحقق مطلوب', + }, + INVALID_OTP: { + en: 'Invalid OTP', + ar: 'رمز التحقق غير صالح', + }, + OTP_EXPIRED: { + en: 'OTP has expired', + ar: 'انتهت صلاحية رمز التحقق', + }, + EMAIL_SENT_IF_EXISTS: { + en: 'Email will be sent if account exists', + ar: 'سيتم إرسال البريد الإلكتروني إذا كان الحساب موجودًا', + }, + INVALID_PASSWORD_RESET_TOKEN: { + en: 'Invalid or expired password reset token', + ar: 'رمز إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية', + }, + + EMAIL_REQUIRED: { + en: 'Email is required', + ar: 'البريد الإلكتروني مطلوب', + }, + + // Authentication middleware errors + WRONG_AUTHENTICATION_TOKEN: { + en: 'Wrong authentication token', + ar: 'رمز المصادقة غير صحيح', + }, + AUTHENTICATION_REQUIRED: { + en: 'Authentication required', + ar: 'المصادقة مطلوبة', + }, + + // Validation errors + VALIDATION_ERROR: { + en: 'Validation error', + ar: 'خطأ في التحقق من البيانات', + }, + NO_PROFILE_DATA_PROVIDED: { + en: 'No profile data provided for update', + ar: 'لم يتم تقديم بيانات الملف الشخصي للتحديث', + }, + // Google Auth errors + NO_EMAIL_IN_GOOGLE_PROFILE: { + en: 'No email found in Google profile', + ar: 'لم يتم العثور على البريد الإلكتروني في ملف Google الشخصي', + }, + GOOGLE_AUTH_ERROR: { + en: 'Error in Google authentication', + ar: 'خطأ في المصادقة عبر Google', + }, + + // patient + PATIENT_KEY_NOT_FOUND: { + en: 'Patient key not found', + ar: 'مفتاح المريض غير موجود', + }, + PATIENT_KEY_ALREADY_EXISTS: { + en: 'Patient key already exists', + ar: 'مفتاح المريض موجود بالفعل', + }, + + //Doctor specific errors + DOCTOR_ACCOUNT_NOT_APPROVED: { + en: 'Doctor account is not approved yet', + ar: 'حساب الطبيب غير مفعل بعد', + }, + DOCTOR_PASSWORD_ALREADY_SET: { + en: 'Password has already been set', + ar: 'تم تعيين كلمة المرور بالفعل', + }, + MAX_CLINICS_REACHED: { + en: 'Maximum number of created clinics reached', + ar: 'تم الوصول إلى الحد الأقصى لعدد العيادات', + }, + ANNOUNCEMENT_NOT_FOUND: { + en: 'Announcement not found', + ar: 'الإعلان غير موجود', + }, + UNAUTHORIZED_ACCESS: { + en: 'You are not authorized to access this', + ar: 'ليس لديك صلاحية للوصول إلى هذا ', + }, + DOCTOR_ID_NOT_FOUND: { + en: 'Doctor ID not found in request', + ar: 'معرف الطبيب غير موجود في الطلب', + }, + + // nurse + NURSE_PASSWORD_ALREADY_SET: { + en: 'Password has already been set', + ar: 'تم تعيين كلمة المرور بالفعل', + }, + NURSE_ACCOUNT_NOT_APPROVED: { + en: 'Nurse account is not approved yet', + ar: 'حساب الممرضة غير مفعل بعد', + }, + NATIONAL_CARD_REQUIRED: { + en: 'National card image is required', + ar: 'صورة البطاقة الوطنية مطلوبة', + }, + NURSE_ID_NOT_FOUND: { + en: 'Nurse ID not found in request', + ar: 'معرف الممرضة غير موجود في الطلب', + }, + ANNOUNCEMENT_EXPIRED: { + en: 'Announcement has expired', + ar: 'انتهت صلاحية الإعلان', + }, + APPLICATION_ALREADY_EXISTS: { + en: 'You have already applied to this announcement', + ar: 'لقد تقدمت بالفعل لهذا الإعلان', + }, + APPLICATION_NOT_FOUND: { + en: 'Application not found', + ar: 'الطلب غير موجود', + }, + APPLICATION_ALREADY_PROCESSED: { + en: 'This application has already been processed', + ar: 'تمت معالجة هذا الطلب بالفعل', + }, + NURSE_DATA_NOT_FOUND: { + en: 'Nurse data not found', + ar: 'بيانات الممرضة غير موجودة', + }, + + // File upload errors + NO_FILE_UPLOADED: { + en: 'No file uploaded', + ar: 'لم يتم تحميل أي ملف', + }, + UNSUPPORTED_IMAGE_FILE_FORMAT: { + en: 'Unsupported file format. Only JPEG and PNG allowed.', + ar: 'تنسيق ملف غير مدعوم. يُسمح فقط بملفات JPEG و PNG.', + }, + UNSUPPORTED_FILE_FORMAT_PDF: { + en: 'Unsupported file format. Only PDF allowed.', + ar: 'تنسيق ملف غير مدعوم. يُسمح فقط بملفات PDF.', + }, + NO_PROFILE_PICTURE: { + en: 'No profile picture found', + ar: 'لم يتم العثور على صورة الملف الشخصي', + }, + UNKNOWN_FILE_FIELDNAME: { + en: 'Unknown file fieldname', + ar: 'اسم حقل الملف غير معروف', + }, + DOCTOR_NOT_WORKING_ON_DAY: { + en: "Doctor is not available on this day", + ar: "الطبيب غير متاح في هذا اليوم" + }, + TIME_OUTSIDE_SCHEDULE: { + en: "Requested time is outside doctor's working hours", + ar: "الوقت المطلوب خارج ساعات عمل الطبيب" + }, + DAY_OUTSIDE_SCHEDULE: { + en: "Requested date is outside doctor's working days", + ar: "الموعد المطلوب خارج أيام عمل الطبيب" + }, + DOCTOR_NOT_ASSOCIATED_WITH_CLINIC: { + en: 'Doctor is not associated with this clinic', + ar: 'الطبيب غير مرتبط بهذه العيادة' + }, + END_TIME_BEFORE_START_TIME: { + en: 'End time must be after start time', + ar: 'وقت الانتهاء يجب أن يكون بعد وقت البداية' + }, + SCHEDULE_ALREADY_EXISTS: { + en: 'Schedule already exists for this day and clinic', + ar: 'الجدول موجود بالفعل لهذا اليوم والعيادة' + }, + SPECIALIZATION_LANG: { + en: "Language must be 'en' or 'ar'", + ar: "يجب أن تكون اللغة 'en' أو 'ar'" + }, + RECORD_NOT_FOUND: { + en: 'Record not found', + ar: 'السجل غير موجود', + }, + RECORD_ALREADY_DELETED: { + en: 'Record has already been deleted', + ar: 'تم حذف السجل بالفعل', + }, + + // Clinic errors + CLINIC_NOT_FOUND: { + en: 'Clinic not found', + ar: 'العيادة غير موجودة', + }, + CLINIC_REQUIRED_FOR_OFFLINE: { + en: 'Clinic ID is required for offline appointments', + ar: 'معرف العيادة مطلوب للمواعيد غير المتصلة بالإنترنت', + }, + UNAUTHORIZED_CLINIC_DELETION: { + en: 'You are not authorized to delete this clinic', + ar: 'ليس لديك صلاحية لحذف هذه العيادة', + }, + UNAUTHORIZED_CLINIC_UPDATE: { + en: 'You are not authorized to update this clinic', + ar: 'ليس لديك صلاحية لتحديث هذه العيادة', + }, + SCHEDULE_ALREADY_DELETED: { + en: 'This schedule has already been deleted', + ar: 'تم حذف هذا الجدول مسبقًا', + }, + SCHEDULE_ID_REQUIRED: { + en: 'Schedule ID is required', + ar: 'معرف الجدول مطلوب', + }, + VACATION_DATES_REQUIRED: { + en: 'Vacation dates are required', + ar: 'تواريخ الإجازة مطلوبة', + }, + INVALID_DATE_RANGE: { + en: 'Invalid date range', + ar: 'نطاق التاريخ غير صالح', + }, + // appointments + DOCTOR_ID_REQUIRED: { + en: 'Doctor ID is required', + ar: 'معرف الطبيب مطلوب', + }, + INVALID_FEES_RANGE: { + en: 'Invalid fees range.', + ar: 'نطاق الرسوم غير صالح.' + }, + APPOINTMENT_ALREADY_EXISTS: { + en: 'Appointment already exists.', + ar: 'الموعد موجود بالفعل.' + }, + PATIENT_ID_REQUIRED: { + en: 'Patient ID is required', + ar: 'معرف المريض مطلوب', + }, + SCHEDULED_TIME_REQUIRED: { + en: 'Scheduled time is required', + ar: 'وقت الموعد مطلوب', + }, + INVALID_SCHEDULED_TIME: { + en: 'Invalid scheduled time format', + ar: 'تنسيق وقت الموعد غير صالح', + }, + DATE_REQUIRED: { + en: 'Date is required', + ar: 'التاريخ مطلوب', + }, + INVALID_DATE_FORMAT: { + en: 'Invalid date format. Please use YYYY-MM-DD', + ar: 'تنسيق التاريخ غير صالح. يرجى استخدام YYYY-MM-DD', + }, + NO_AVAILABLE_DAYS: { + en: 'No available days found for this doctor', + ar: 'لم يتم العثور على أيام متاحة لهذا الطبيب', + }, + SLOT_NOT_AVAILABLE: { + en: 'This time slot is not available', + ar: 'هذا الوقت غير متاح', + }, + APPOINTMENT_IN_PAST: { + en: 'Cannot book appointment in the past', + ar: 'لا يمكن حجز موعد في الماضي', + }, + TIME_SLOT_NOT_AVAILABLE: { + en: "This time slot is not available", + ar: "هذا الوقت غير متاح" + }, + MINUTES_EXCEEDED_LIMIT: { + en: "The maximum allowed delay must not exceed 60 minutes.", + ar: "يجب ألا يتجاوز الحد الأقصى للتأجيل المسموح به 60 دقيقة." + }, + SCHEDULE_NOT_FOUND: { + en: "Schedule not found", + ar: "لم يتم العثور على الجدول" + }, + UNAUTHORIZED_SCHEDULE_ACCESS: { + en: "You are not authorized to access this schedule", + ar: "غير مصرح لك بالوصول إلى هذا الجدول" + }, + SCHEDULE_CONFLICT_DIFFERENT_CLINIC: { + en: "There is a scheduling conflict on another clinic", + ar: "يوجد تعارض في المواعيد في عيادة اخرى" + }, + ONLINE_OFFLINE_CONFLICT: { + en: "There is a conflict between online and offline schedules", + ar: "يوجد تعارض بين المواعيد الإلكترونية والحضورية" + }, + EITHER_ONLINE_OR_OFFLINE: { + en: "Please choose either online or offline", + ar: "يرجى اختيار إما الإلكتروني أو الحضوري" + }, + APPOINTMENT_ALREADY_COMPLETED: { + en: 'Appointment is already completed', + ar: 'الموعد مكتمل بالفعل', + }, + CANNOT_BE_COMPLETED_BEFORE_SCHEDULED_TIME: { + en: "Appointment cannot be completed before its scheduled time", + ar: "لا يمكن إكمال الموعد قبل وقته المحدد" + }, + // Generic errors + SOMETHING_WENT_WRONG: { + en: 'Something went wrong', + ar: 'حدث خطأ ما', + }, + MASTER_KEY_NOT_SET: { + en: 'Master encryption key is not set in environment variables', + ar: 'مفتاح التشفير الرئيسي غير مضبوط', + }, + INVALID_MASTER_KEY_LENGTH: { + en: 'Invalid master key length. Expected 32 bytes', + ar: 'طول مفتاح رئيسي غير صالح. يجب أن يكون 32 بايت', + }, + + APPOINTMENT_NOT_FOUND: { + en: "Appointment not found", + ar: "الموعد غير موجود" + }, + APPOINTMENT_ID_REQUIRED: { + en: "Appointment ID is required", + ar: "معرف الموعد مطلوب" + }, + UNAUTHORIZED_APPOINTMENT_ACCESS: { + en: "You are not authorized to access this appointment", + ar: "غير مصرح لك بالوصول إلى هذا الموعد" + }, + APPOINTMENT_ALREADY_DELETED: { + en: "Appointment has already been deleted", + ar: "تم حذف الموعد بالفعل" + }, + INVALID_RESCHEDULE_PARAMETERS: { + en: "Provide either new scheduled time or shift minutes", + ar: "يرجى تقديم وقت موعد جديد أو عدد دقائق التغيير" + }, + + // Agora Errors + AGORA_CREDENTIALS_NOT_CONFIGURED: { + en: "Agora credentials are not configured", + ar: "بيانات اعتماد Agora غير مكونة" + }, + + // AI Errors + INVALID_USER_TYPE: { + en: "Invalid user type. Must be 'doctor' or 'patient'", + ar: "نوع المستخدم غير صالح. يجب أن يكون 'doctor' أو 'patient'" + }, + MISSING_AUDIO_KEYS: { + en: "Missing audio keys. Provide either doctorKey and patientKey, or mixedKey", + ar: "مفاتيح الصوت مفقودة. يرجى تقديم إما doctorKey و patientKey، أو mixedKey" + } +}; + +// Helper function to create bilingual error +export const createBilingualError = ( + status: number, + messageObj: { en: string; ar: string }, +) => { + const message = messageObj.en; + const messageAr = messageObj.ar; + + return { status, message, messageAr }; +}; diff --git a/src/utils/errorWrapper.ts b/src/utils/errorWrapper.ts new file mode 100644 index 0000000..b81a1c8 --- /dev/null +++ b/src/utils/errorWrapper.ts @@ -0,0 +1,12 @@ +import { NextFunction, Request, Response } from 'express'; + +/** + * Wraps async route handlers to catch errors and pass them to the error middleware + * @param fn - The async function to wrap + * @returns A wrapped function that catches errors + */ +export const errorWrapper = (fn: Function) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..12829d3 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,67 @@ +import { existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import winston from 'winston'; +import winstonDaily from 'winston-daily-rotate-file'; +import { LOG_DIR } from '@config'; + + + +// logs dir +const logDir: string = join(__dirname, LOG_DIR); + +if (!existsSync(logDir)) { + mkdirSync(logDir); +} + +// Define log format +const logFormat = winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`); + +/* + * Log Level + * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6 + */ +const logger = winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss', + }), + logFormat, + ), + transports: [ + // debug log setting + new winstonDaily({ + level: 'debug', + datePattern: 'YYYY-MM-DD', + dirname: logDir + '/debug', // log file /logs/debug/*.log in save + filename: `%DATE%.log`, + maxFiles: 30, // 30 Days saved + json: false, + zippedArchive: true, + }), + // error log setting + new winstonDaily({ + level: 'error', + datePattern: 'YYYY-MM-DD', + dirname: logDir + '/error', // log file /logs/error/*.log in save + filename: `%DATE%.log`, + maxFiles: 30, // 30 Days saved + handleExceptions: true, + json: false, + zippedArchive: true, + }), + ], +}); + +logger.add( + new winston.transports.Console({ + format: winston.format.combine(winston.format.splat(), winston.format.colorize()), + }), +); + +const stream = { + write: (message: string) => { + logger.info(message.substring(0, message.lastIndexOf('\n'))); + }, +}; + +export { logger, stream }; diff --git a/src/utils/nodeMailerService.ts b/src/utils/nodeMailerService.ts new file mode 100644 index 0000000..4532f68 --- /dev/null +++ b/src/utils/nodeMailerService.ts @@ -0,0 +1,10 @@ +import nodemailer from 'nodemailer'; +import { GMAIL_USER, GMAIL_APP_PASSWORD } from '@config'; + +export const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: GMAIL_USER, + pass: GMAIL_APP_PASSWORD, + }, +}); diff --git a/src/utils/passsportGoogle.ts b/src/utils/passsportGoogle.ts new file mode 100644 index 0000000..55bcbc1 --- /dev/null +++ b/src/utils/passsportGoogle.ts @@ -0,0 +1,64 @@ +import passport from 'passport'; +import { Strategy as GoogleStrategy, Profile } from 'passport-google-oauth20'; +import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL } from '@/config'; +import { PrismaClient } from '@prisma/client'; +import { CreateGoogleUsersDto } from '@/dtos/googleUsers.dto'; +import { GoogleAuthService } from '@/services/googleAuth.service'; +import { User } from '@/interfaces'; +import Container from 'typedi'; +import { ErrorMessages } from '@/utils/errorMessages'; + +// Define a custom error type with Arabic message support +interface BilingualError extends Error { + messageAr?: string; +} + +const prisma = new PrismaClient(); +const googleAuthService = Container.get(GoogleAuthService); + +passport.use(new GoogleStrategy({ + clientID: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + callbackURL: GOOGLE_CALLBACK_URL +}, + // This "verify" function is called when Google successfully authenticates the user. + // 'profile' contains the user's Google profile information. + // 'done' is a callback you must call to tell Passport the authentication is complete. + async (accessToken, refreshToken, profile: Profile, done) => { + try { + // Extract email from Google profile + const email = profile.emails?.[0]?.value; + const name = profile.displayName; + const isEmailVerified = profile.emails?.[0]?.verified; + + if (!email) { + const error: BilingualError = new Error(ErrorMessages.NO_EMAIL_IN_GOOGLE_PROFILE.en); + error.messageAr = ErrorMessages.NO_EMAIL_IN_GOOGLE_PROFILE.ar; + return done(error, undefined); + } + + // Find user in database by email + const user = await prisma.user.findUnique({ + where: { email } + }); + + if (!user) { + const newGoogleUserData: CreateGoogleUsersDto = { + email, + name, + isEmailVerified: isEmailVerified || false, + }; + const createdUser:User = await googleAuthService.createInitialProfileGoogle(newGoogleUserData); + // Pass isNewUser flag in the info object + return done(null, createdUser, { isNewUser: true }); + } + // Existing user - not new + return done(null, user, { isNewUser: false }); + } catch (error) { + console.error('Error in Google authentication:', error); + const err: BilingualError = new Error(ErrorMessages.GOOGLE_AUTH_ERROR.en); + err.messageAr = ErrorMessages.GOOGLE_AUTH_ERROR.ar; + return done(err, undefined); + } + } +)); diff --git a/src/utils/responseMessages.ts b/src/utils/responseMessages.ts new file mode 100644 index 0000000..39a0344 --- /dev/null +++ b/src/utils/responseMessages.ts @@ -0,0 +1,356 @@ +export const SuccessResponseMessages = { + // Success messages for Auth + SIGNED_UP_SUCCESSFULLY: { + message_en: "Signed up successfully.", + message_ar: "تم انشاء حساب جديد بنجاح.", + }, + LOGGED_IN_SUCCESSFULLY: { + message_en: "Logged in successfully.", + message_ar: "تم تسجيل الدخول بنجاح.", + }, + LOGGED_OUT_SUCCESSFULLY: { + message_en: "Logged out successfully.", + message_ar: "تم تسجيل الخروج بنجاح.", + }, + TOKEN_REFRESHED_SUCCESSFULLY: { + message_en: "Token refreshed successfully.", + message_ar: "تم تحديث رمز الدخول بنجاح.", + }, + PROFILE_COMPLETED_SUCCESSFULLY: { + message_en: "Profile completed successfully.", + message_ar: "تم إكمال الملف الشخصي بنجاح.", + }, + OTP_VERIFIED_SUCCESSFULLY: { + message_en: "OTP verified successfully.", + message_ar: "تم التحقق من رمز التحقق بنجاح.", + }, + PASSWORD_RESET_EMAIL_SENT_SUCCESSFULLY: { + message_en: "Password reset email sent successfully.", + message_ar: "تم إرسال بريد إعادة تعيين كلمة المرور بنجاح.", + }, + PASSWORD_RESET_SUCCESSFULLY: { + message_en: "Password reset successfully.", + message_ar: "تم إعادة تعيين كلمة المرور بنجاح.", + }, + OTP_RESENT_SUCCESSFULLY: { + message_en: "OTP resent successfully.", + message_ar: "تم إعادة إرسال رمز التحقق بنجاح.", + }, + PASSWORD_CHECK_SUCCESSFUL: { + message_en: "Password check successful.", + message_ar: "تم التحقق من كلمة المرور بنجاح.", + }, + PASSWORD_CHANGED_SUCCESSFULLY: { + message_en: "Password changed successfully.", + message_ar: "تم تغيير كلمة المرور بنجاح.", + }, + + // Success messages for Doctors by Admin + DOCTOR_CREATED: { + message_en: "Doctor created successfully.", + message_ar: "تم إنشاء حساب الطبيب بنجاح.", + }, + DOCTOR_RETRIEVED: { + message_en: "Doctor retrieved successfully.", + message_ar: "تم استرجاع بيانات الطبيب بنجاح.", + }, + DOCTORS_RETRIEVED: { + message_en: "Doctors retrieved successfully.", + message_ar: "تم استرجاع بيانات الأطباء بنجاح.", + }, + UNVERIFIED_DOCTORS_RETRIEVED: { + message_en: "Unverified doctors retrieved successfully.", + message_ar: "تم استرجاع بيانات الأطباء غير المعتمدين بنجاح.", + }, + DOCTOR_VERIFICATION_STATUS_UPDATED: { + message_en: "Doctor verification status updated successfully.", + message_ar: "تم تحديث حالة اعتماد الطبيب بنجاح.", + }, + + // get messages for Nurses by Admin + UNVERIFIED_NURSES_RETRIEVED: { + message_en: "Unverified nurses retrieved successfully.", + message_ar: "تم استرجاع بيانات الممرضات غير المعتمدين بنجاح.", + }, + NURSE_CREATED: { + message_en: "Nurse created successfully.", + message_ar: "تم إنشاء حساب الممرضة بنجاح.", + }, + NURSES_RETRIEVED: { + message_en: "Nurse retrieved successfully.", + message_ar: "تم استرجاع بيانات الممرضة بنجاح.", + }, + + + // Success messages for Clinics + CLINIC_CREATED_SUCCESSFULLY: { + message_en: "Clinic created successfully.", + message_ar: "تم إنشاء العيادة بنجاح.", + }, + CLINIC_RETRIEVED: { + message_en: "Clinic data retrieved successfully.", + message_ar: "تم استرجاع بيانات العيادة بنجاح.", + }, + CLINIC_UPDATED_SUCCESSFULLY: { + message_en: "Clinic data updated successfully.", + message_ar: "تم تحديث بيانات العيادة بنجاح.", + }, + CLINIC_DELETED_SUCCESSFULLY: { + message_en: "Clinic deleted successfully.", + message_ar: "تم حذف العيادة بنجاح.", + }, + CLINIC_DOCTORS_RETRIEVED: { + message_en: "Clinic's doctors retrieved successfully.", + message_ar: "تم استرجاع أطباء العيادة بنجاح.", + }, + CLINICS_RETRIEVED_SUCCESSFULLY: { + message_en: "Clinics retrieved successfully.", + message_ar: "تم استرجاع بيانات العيادات بنجاح.", + }, + CLINIC_STATUS_UPDATED: { + message_en: "Clinic active status updated successfully.", + message_ar: "تم تحديث حالة العيادة بنجاح.", + }, + CLINIC_FEES_UPDATED_SUCCESSFULLY: { + message_en: "Clinic fees updated successfully.", + message_ar: "تم تحديث رسوم العيادة بنجاح.", + }, + + // Success messages for Doctors + DOCTOR_CREATED_WAITING_VERIFICATION: { + message_en: "Doctor account created successfully. Please wait for verification.", + message_ar: "تم إنشاء حساب الطبيب بنجاح. يرجى الانتظار للموافقة عليه.", + }, + PASSWORD_SET_SUCCESSFULLY_BY_DOCTOR: { + message_en: "Password set successfully.", + message_ar: "تم تعيين كلمة المرور بنجاح.", + }, + ANNOUNCEMENT_CREATED_SUCCESSFULLY: { + message_en: "Announcement created successfully.", + message_ar: "تم إنشاء الإعلان بنجاح.", + }, + ANNOUNCEMENTS_RETRIEVED_SUCCESSFULLY: { + message_en: "Announcements retrieved successfully.", + message_ar: "تم استرجاع الإعلانات بنجاح.", + }, + APPLICANTS_RETRIEVED_SUCCESSFULLY: { + message_en: "Announcement applicants retrieved successfully.", + message_ar: "تم استرجاع المتقدمين للإعلان بنجاح.", + }, + NURSES_RETRIEVED_SUCCESSFULLY: { + message_en: "Nurses retrieved successfully.", + message_ar: "تم استرجاع الممرضين بنجاح.", + }, + ANNOUNCEMENTS_RETRIEVED: { + message_en: "Announcements retrieved successfully.", + message_ar: "تم استرجاع الإعلانات بنجاح.", + }, + + // Success messages for nurses + NURSE_CREATED_WAITING_VERIFICATION: { + message_en: "Nurse account created successfully. Please wait for verification.", + message_ar: "تم إنشاء حساب الممرضة بنجاح. يرجى الانتظار للموافقة عليه.", + }, + NURSE_RETRIEVED: { + message_en: "Nurse retrieved successfully.", + message_ar: "تم استرجاع بيانات الممرضة بنجاح.", + }, + PASSWORD_SET_SUCCESSFULLY_BY_NURSE: { + message_en: 'Password set successfully by nurse', + message_ar: 'تم تعيين كلمة المرور بنجاح من قبل الممرضة', + }, + NURSE_VERIFICATION_STATUS_UPDATED: { + message_en: "Nurse verification status updated successfully.", + message_ar: "تم تحديث حالة اعتماد الممرضة بنجاح.", + }, + APPLIED_TO_ANNOUNCEMENT_SUCCESSFULLY: { + message_en: "Applied to announcement successfully.", + message_ar: "تم التقديم للإعلان بنجاح.", + }, + APPLICANT_APPROVED_SUCCESSFULLY: { + message_en: "Applicant approved successfully.", + message_ar: "تم الموافقة على المتقدم بنجاح.", + }, + APPLICANT_REJECTED_SUCCESSFULLY: { + message_en: "Applicant rejected successfully.", + message_ar: "تم رفض المتقدم بنجاح.", + }, + ANNOUNCEMENT_DELETED_SUCCESSFULLY: { + message_en: "Announcement deleted successfully.", + message_ar: "تم حذف الإعلان بنجاح.", + }, + ANNOUNCEMENT_EDITED_SUCCESSFULLY: { + message_en: "Announcement edited successfully.", + message_ar: "تم تعديل الإعلان بنجاح.", + }, + APPLICATIONS_RETRIEVED: { + message_en: "Applications retrieved successfully.", + message_ar: "تم استرجاع الطلبات بنجاح.", + }, + NURSE_SCHEDULE_RETRIEVED: { + message_en: "Nurse schedule retrieved successfully.", + message_ar: "تم استرجاع جدول الممرضة بنجاح.", + }, + + // Success messages for Google Auth + PHONE_NUMBER_UPDATED_SUCCESSFULLY: { + message_en: "Phone number updated successfully.", + message_ar: "تم تحديث رقم الهاتف بنجاح.", + }, + GOOGLE_USER_DATA_RETRIEVED: { + message_en: "Google user data retrieved successfully.", + message_ar: "تم استرجاع بيانات مستخدم جوجل بنجاح.", + }, + + // Success messages for Super Admin + ADMIN_ADDED_SUCCESSFULLY: { + message_en: "Admin added successfully.", + message_ar: "تم إضافة المسؤول بنجاح.", + }, + ADMINS_RETRIEVED_SUCCESSFULLY: { + message_en: "Admins retrieved successfully.", + message_ar: "تم استرجاع بيانات المسؤولين بنجاح.", + }, + ADMIN_RETRIEVED_SUCCESSFULLY: { + message_en: "Admin retrieved successfully.", + message_ar: "تم استرجاع بيانات المسؤول بنجاح.", + }, + + // Success messages for User + PROFILE_PICTURE_UPDATED_SUCCESSFULLY: { + message_en: "Profile picture updated successfully.", + message_ar: "تم تحديث صورة الملف الشخصي بنجاح.", + }, + PROFILE_PICTURE_RETRIEVED_SUCCESSFULLY: { + message_en: "Profile picture retrieved successfully.", + message_ar: "تم استرجاع صورة الملف الشخصي بنجاح.", + }, + PROFILE_PICTURE_DELETED_SUCCESSFULLY: { + message_en: "Profile picture deleted successfully.", + message_ar: "تم حذف صورة الملف الشخصي بنجاح.", + }, + USER_PROFILE_UPDATED_SUCCESSFULLY: { + message_en: "User profile updated successfully.", + message_ar: "تم تحديث الملف الشخصي للمستخدم بنجاح.", + }, + // success messages for appointments + APPOINTMENT_BOOKED_SUCCESSFULLY: { + message_en: "Appointment booked successfully.", + message_ar: "تم حجز الموعد بنجاح.", + }, + APPOINTMENT_RESCHEDULED_SUCCESSFULLY: { + message_en: "Appointment rescheduled successfully.", + message_ar: "تم إعادة جدولة الموعد بنجاح.", + }, + APPOINTMENTS_RESCHEDULED_SUCCESSFULLY: { + message_en: "Appointments rescheduled successfully.", + message_ar: "تم إعادة جدولة المواعيد بنجاح.", + }, + APPOINTMENT_CANCELLED_SUCCESSFULLY: { + message_en: "Appointment cancelled successfully.", + message_ar: "تم إلغاء الموعد بنجاح.", + }, + AVAILABLE_DAYS_RETRIEVED: { + message_en: "Available days retrieved successfully.", + message_ar: "تم استرجاع الأيام المتاحة بنجاح.", + }, + AVAILABLE_SLOTS_RETRIEVED: { + message_en: "Available slots retrieved successfully.", + message_ar: "تم استرجاع الأوقات المتاحة بنجاح.", + }, + PATIENT_APPOINTMENTS_RETRIEVED: { + message_en: "Patient appointments retrieved successfully.", + message_ar: "تم استرجاع مواعيد المريض بنجاح.", + }, + APPOINTMENT_DETAILS_RETRIEVED: { + message_en: "Appointment details retrieved successfully.", + message_ar: "تم استرجاع تفاصيل الموعد بنجاح.", + }, + PATIENT_TODAY_APPOINTMENT_RETRIEVED: { + message_en: "Patient's today appointment retrieved successfully.", + message_ar: "تم استرجاع موعد المريض لليوم بنجاح.", + }, + QUEUE_POSITION_RETRIEVED: { + message_en: "Queue position retrieved successfully.", + message_ar: "تم استرجاع موقعك في قائمة الانتظار بنجاح.", + }, + SCHEDULE_CREATED_SUCCESSFULLY: { + message_en: 'Schedule created successfully', + message_ar: 'تم إنشاء الجدول بنجاح' + }, + DOCTOR_SCHEDULE_RETRIEVED: { + message_en: 'Doctor schedule retrieved successfully', + message_ar: 'تم استرجاع جدول الطبيب بنجاح' + }, + SCHEDULE_UPDATED_SUCCESSFULLY: { + message_en: 'Schedule updated successfully', + message_ar: 'تم تحديث الجدول بنجاح' + }, + VACATION_SET_SUCCESSFULLY: { + message_en: 'Vacation set successfully', + message_ar: 'تم تعيين الإجازة بنجاح', + }, + SCHEDULE_DELETED_SUCCESSFULLY: { + message_en: 'Schedule deleted successfully', + message_ar: 'تم حذف الجدول بنجاح', + }, + APPOINTMENTS_CHECK_COMPLETED: { + message_en: 'Appointments check completed', + message_ar: 'تم فحص المواعيد' + }, + VACATION_REMOVED_SUCCESSFULLY: { + message_en: 'Vacation removed successfully', + message_ar: 'تم حذف الإجازة بنجاح', + }, + DOCTOR_VACATIONS_RETRIEVED: { + message_en: 'Doctor vacations retrieved successfully', + message_ar: 'تم استرجاع إجازات الطبيب بنجاح', + }, + DOCTORS_RETRIEVED_SUCCESSFULLY: { + message_en: 'Online doctors retrieved successfully', + message_ar: 'تم استرجاع الأطباء المتاحين عبر الإنترنت بنجاح', + }, + APPOINTMENTS_BY_NURSE_RETRIEVED: { + message_en: 'Appointments retrieved to the nurse successfully', + message_ar: 'تم استرجاع المواعيد للممرضة بنجاح', + }, + APPOINTMENT_COMPLETED_SUCCESSFULLY: { + message_en: 'Appointment completed successfully', + message_ar: 'تم إكمال الموعد بنجاح', + }, + + // Agora success messages + AGORA_TOKEN_GENERATED_SUCCESSFULLY: { + message_en: "Agora token generated successfully.", + message_ar: "تم إنشاء رمز Agora بنجاح.", + }, + + // BackBlaze B2 success messages + UPLOAD_URL_GENERATED: { + message_en: "BackBlaze B2 upload URL generated successfully.", + message_ar: "تم إنشاء رابط التحميل لـ BackBlaze B2 بنجاح.", + }, + + // AI Appointments success messages + AI_PROCESSING_STARTED: { + message_en: "AI processing started successfully.", + message_ar: "تم بدء المعالجة بالذكاء الاصطناعي بنجاح.", + }, + SOAP_GENERATED: { + message_en: "SOAP notes generated successfully.", + message_ar: "تم إنشاء ملاحظات SOAP بنجاح.", + }, + +} + +interface MultiLangMessageObj { + message_en: string; + message_ar: string; +} + +export const createMultiLangMessage = (multiLangMessageObj: MultiLangMessageObj) => { + return { + messageEn: multiLangMessageObj.message_en, + messageAr: multiLangMessageObj.message_ar, + }; +} \ No newline at end of file diff --git a/src/utils/specializationTransform.ts b/src/utils/specializationTransform.ts new file mode 100644 index 0000000..ca2db81 --- /dev/null +++ b/src/utils/specializationTransform.ts @@ -0,0 +1,52 @@ +import { Transform } from 'class-transformer'; +import { getSpecializationKey, SpecializationKey } from '@/constants/specializations'; + +/** + * Transform decorator that converts English or Arabic specialization value to key + * Example: "Cardiology" or "أمراض القلب" -> "CARDIOLOGY" + */ +export function TransformSpecialization() { + return Transform(({ value }) => { + if (!value || typeof value !== 'string') { + return value; + } + + // If it's already a key (uppercase with underscores), return as is + if (value === value.toUpperCase() && /^[A-Z_]+$/.test(value)) { + return value; + } + + // Try to get the key from the display value (EN or AR) + const key = getSpecializationKey(value); + return key || value; // Return key if found, otherwise return original value for validation to catch + }); +} + +/** + * Format specialization response based on language preference + * @param key - The specialization key stored in DB + * @param lang - Language preference ('en' or 'ar') + * @returns Formatted specialization object + */ +export function formatSpecializationResponse(key: SpecializationKey, lang?: 'en' | 'ar') { + const { getSpecialization } = require('@/constants/specializations'); + const spec = getSpecialization(key); + + if (!spec) { + return { key, value: key }; + } + + // If language is specified, return only that language + if (lang === 'en') { + return { key, value: spec.en }; + } else if (lang === 'ar') { + return { key, value: spec.ar }; + } + + // Default: return both languages + return { + key, + en: spec.en, + ar: spec.ar, + }; +} diff --git a/src/utils/validateEnv.ts b/src/utils/validateEnv.ts new file mode 100644 index 0000000..a6f1904 --- /dev/null +++ b/src/utils/validateEnv.ts @@ -0,0 +1,8 @@ +import { cleanEnv, port, str } from 'envalid'; + +export const ValidateEnv = () => { + cleanEnv(process.env, { + NODE_ENV: str(), + PORT: port(), + }); +}; diff --git a/src/validators/specialization.validator.ts b/src/validators/specialization.validator.ts new file mode 100644 index 0000000..00d9272 --- /dev/null +++ b/src/validators/specialization.validator.ts @@ -0,0 +1,40 @@ +import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'; +import { isValidSpecialization, VALID_SPECIALIZATIONS_EN, VALID_SPECIALIZATIONS_AR, SpecializationEnglishValue, SpecializationArabicValue } from '@/constants/specializations'; + +export function IsValidSpecialization(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isValidSpecialization', + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + validator: { + validate(value: any, args: ValidationArguments) { + if (typeof value !== 'string') { + return false; + } + + // Check if it's a valid key + if (isValidSpecialization(value)) { + return true; + } + + // Check if it's a valid English value + if (VALID_SPECIALIZATIONS_EN.includes(value as SpecializationEnglishValue)) { + return true; + } + + // Check if it's a valid Arabic value + if (VALID_SPECIALIZATIONS_AR.includes(value as SpecializationArabicValue)) { + return true; + } + + return false; + }, + defaultMessage(args: ValidationArguments) { + return `${args.property} must be a valid specialization (you can use English name, Arabic name, or key)`; + }, + }, + }); + }; +} diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 0000000..27c8387 --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,808 @@ +openapi: 3.0.0 +info: + title: GP Backend Authentication API + description: Comprehensive API documentation for authentication routes including email/password auth and Google OAuth + version: 1.0.0 + contact: + name: API Support + email: support@gpbackend.com + +servers: + - url: http://localhost:3000 + description: Development server + - url: https://api.gpbackend.com + description: Production server + +tags: + - name: Authentication + description: Email/Password authentication endpoints + - name: Google OAuth + description: Google OAuth authentication endpoints + - name: Fabric + description: Hyperledger Fabric asset management endpoints + +paths: + /assets: + get: + tags: + - Fabric + summary: Get all assets + description: Retrieve a list of all assets from the blockchain ledger. + responses: + '200': + description: A list of assets. + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Asset' + message: + type: string + example: findAll + '500': + description: Internal Server Error + /assets/{id}: + get: + tags: + - Fabric + summary: Get asset by ID + description: Retrieve a specific asset from the blockchain ledger by its ID. + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The ID of the asset to retrieve. + responses: + '200': + description: The requested asset. + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Asset' + message: + type: string + example: findOne + '404': + description: Asset not found + '500': + description: Internal Server Error + /auth/signup: + post: + tags: + - Authentication + summary: User registration + description: Register a new user with email, name, and password. Sends OTP email for verification. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserDto' + example: + email: user@example.com + name: John Doe + password: SecurePass123 + phone: '+1234567890' + responses: + '201': + description: User successfully registered + headers: + Set-Cookie: + schema: + type: string + example: Authorization=eyJhbGc...; Path=/; HttpOnly; RefreshToken=eyJhbGc...; Path=/; HttpOnly + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + message: + type: string + example: Signed Up Successfully + '400': + $ref: '#/components/responses/BadRequest' + '409': + description: User already exists + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: This email already exists + messageAr: البريد الإلكتروني موجود بالفعل + + /auth/login: + post: + tags: + - Authentication + summary: User login + description: Authenticate user with email and password + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginUserDto' + example: + emailOrUsername: user@example.com + password: SecurePass123 + rememberMe: true + responses: + '200': + description: Login successful + headers: + Set-Cookie: + schema: + type: string + example: Authorization=eyJhbGc...; Path=/; HttpOnly; RefreshToken=eyJhbGc...; Path=/; HttpOnly + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/PatientLoginData' + message: + type: string + example: Logged In Successfully + '400': + $ref: '#/components/responses/BadRequest' + '401': + description: Invalid credentials + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Password is not matching + messageAr: كلمة المرور غير صحيحة + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: User with the provided credentials was not found + messageAr: لم يتم العثور على المستخدم ببيانات الاعتماد المقدمة + + /auth/logout: + post: + tags: + - Authentication + summary: User logout + description: Logout authenticated user and clear cookies + security: + - bearerAuth: [] + - cookieAuth: [] + responses: + '200': + description: Logout successful + headers: + Set-Cookie: + schema: + type: string + example: Authorization=; Max-age=0; RefreshToken=; Max-age=0 + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Logged Out Successfully + '401': + $ref: '#/components/responses/Unauthorized' + + /auth/refresh: + post: + tags: + - Authentication + summary: Refresh access token + description: Generate new access token using refresh token from cookies + security: + - cookieAuth: [] + responses: + '200': + description: Token refreshed successfully + headers: + Set-Cookie: + schema: + type: string + example: Authorization=eyJhbGc...; Path=/; HttpOnly + content: + application/json: + schema: + type: object + properties: + data: + type: object + properties: + user: + $ref: '#/components/schemas/User' + accessToken: + type: object + properties: + expiresIn: + type: number + example: 3600 + expiresAt: + type: string + format: date-time + example: '2025-11-04T15:30:00.000Z' + message: + type: string + example: Token Refreshed Successfully + '401': + $ref: '#/components/responses/Unauthorized' + '403': + description: Invalid or expired refresh token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Invalid or expired refresh token + messageAr: رمز التحديث غير صالح أو منتهي الصلاحية + + /auth/complete-profile-info: + patch: + tags: + - Authentication + summary: Complete user profile + description: Update user profile with phone number, gender, and date of birth + security: + - bearerAuth: [] + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CompleteUserProfileDto' + example: + gender: MALE + date_of_birth: '1990-01-15' + responses: + '200': + description: Profile updated successfully + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/User' + message: + type: string + example: Profile Completed Successfully + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /auth/verify-otp: + patch: + tags: + - Authentication + summary: Verify email OTP + description: Verify the OTP sent to user's email during registration + security: + - bearerAuth: [] + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - otp + properties: + otp: + type: string + description: 6-digit OTP code + example: '123456' + responses: + '200': + description: OTP verified successfully + content: + application/json: + schema: + type: object + properties: + data: + type: boolean + example: true + message: + type: string + example: OTP Verified Successfully + '400': + description: Invalid or expired OTP + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Invalid OTP + messageAr: رمز التحقق غير صالح + '401': + $ref: '#/components/responses/Unauthorized' + + /auth/resend-otp: + post: + tags: + - Authentication + summary: Resend OTP + description: Resend OTP verification code to authenticated user's email + security: + - bearerAuth: [] + - cookieAuth: [] + responses: + '200': + description: OTP resent successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: OTP Resent Successfully + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: User not found + messageAr: المستخدم غير موجود + + /auth/forget-password: + post: + tags: + - Authentication + summary: Request password reset + description: Send password reset email with reset token + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + properties: + email: + type: string + format: email + description: User's email address + example: user@example.com + responses: + '200': + description: Password reset email sent + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Password Reset Email Sent Successfully + '400': + description: Email is required + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Validation error + messageAr: خطأ في التحقق من البيانات + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: User email not found + messageAr: البريد الإلكتروني للمستخدم غير موجود + + /auth/reset-password: + post: + tags: + - Authentication + summary: Reset password + description: Reset user password using the token from email + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ResetPasswordDto' + example: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + newPassword: NewSecurePass123 + responses: + '200': + description: Password reset successful + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Password Reset Successfully + '400': + $ref: '#/components/responses/BadRequest' + '401': + description: Invalid or expired reset token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Invalid or expired password reset token + messageAr: رمز إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية + + /auth/google: + get: + tags: + - Google OAuth + summary: Initiate Google OAuth + description: Redirect user to Google OAuth consent screen for authentication + responses: + '302': + description: Redirect to Google OAuth + headers: + Location: + schema: + type: string + example: https://accounts.google.com/o/oauth2/v2/auth?... + + # /auth/google/callback: + # get: + # tags: + # - Google OAuth + # summary: Google OAuth callback + # description: Handle Google OAuth callback and create user session + # parameters: + # - in: query + # name: code + # schema: + # type: string + # description: Authorization code from Google + # - in: query + # name: state + # schema: + # type: string + # description: State parameter for CSRF protection + # responses: + # '302': + # description: Redirect to dashboard on success or login on failure + # headers: + # Location: + # schema: + # type: string + # example: /dashboard + # Set-Cookie: + # schema: + # type: string + # example: Authorization=eyJhbGc...; Path=/; HttpOnly; RefreshToken=eyJhbGc...; Path=/; HttpOnly + # '401': + # description: Authentication failed + # headers: + # Location: + # schema: + # type: string + # example: /login + + /auth/google/update-phone: + patch: + tags: + - Google OAuth + summary: Update phone number for Google OAuth user + description: Add or update phone number for users who signed in with Google + security: + - bearerAuth: [] + - cookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateGoogleUserPhoneDto' + example: + phone: '+1234567890' + responses: + '200': + description: Phone number updated successfully + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Phone Number Updated Successfully + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + /auth/google/userData: + get: + tags: + - Google OAuth + summary: Get Google OAuth user data + description: Retrieve authenticated Google user's profile information + security: + - bearerAuth: [] + - cookieAuth: [] + responses: + '200': + description: User data retrieved successfully + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/PatientLoginData' + message: + type: string + example: Google User Data Retrieved Successfully + '401': + $ref: '#/components/responses/Unauthorized' + '404': + description: User not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: User not found + messageAr: المستخدم غير موجود + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT access token in Authorization header + cookieAuth: + type: apiKey + in: cookie + name: Authorization + description: JWT access token in cookie + + schemas: + CreateUserDto: + type: object + required: + - email + - name + - password + - phone + properties: + email: + type: string + format: email + description: User's email address + name: + type: string + description: User's full name + minLength: 1 + phone: + type: string + description: User's phone number with country code + example: '+1234567890' + password: + type: string + format: password + description: User's password + minLength: 8 + maxLength: 32 + + LoginUserDto: + type: object + required: + - emailOrUsername + - password + - rememberMe + properties: + emailOrUsername: + type: string + description: User's email address or username + password: + type: string + format: password + description: User's password + rememberMe: + type: boolean + description: Keep user logged in for extended period + default: false + + CompleteUserProfileDto: + type: object + required: + - gender + - date_of_birth + properties: + gender: + type: string + enum: [MALE, FEMALE] + description: User's gender + date_of_birth: + type: string + format: date + description: User's date of birth in ISO 8601 format + example: '1990-01-15' + + ResetPasswordDto: + type: object + required: + - token + - newPassword + properties: + token: + type: string + description: Password reset token from email + newPassword: + type: string + format: password + description: New password for the account + minLength: 8 + maxLength: 32 + + UpdateGoogleUserPhoneDto: + type: object + required: + - phone + properties: + phone: + type: string + description: User's phone number + maxLength: 15 + example: '+1234567890' + + Asset: + type: object + properties: + ID: + type: string + description: The unique identifier of the asset. + Color: + type: string + description: The color of the asset. + Size: + type: string + description: The size of the asset. + Owner: + type: string + description: The current owner of the asset. + AppraisedValue: + type: string + description: The appraised value of the asset. + + User: + type: object + properties: + id: + type: string + format: uuid + description: Unique user identifier + email: + type: string + format: email + description: User's email address + name: + type: string + description: User's full name + phone: + type: string + nullable: true + description: User's phone number + gender: + type: string + enum: [MALE, FEMALE] + nullable: true + description: User's gender + date_of_birth: + type: string + format: date + nullable: true + description: User's date of birth + isVerified: + type: boolean + description: Whether email is verified + hasCompletedProfile: + type: boolean + description: Whether user has completed profile + default: false + created_at: + type: string + format: date-time + description: Account creation timestamp + updated_at: + type: string + format: date-time + description: Last update timestamp + + PatientLoginData: + type: object + properties: + name: + type: string + description: User's full name + email: + type: string + format: email + description: User's email address + username: + type: string + description: User's username + phone: + type: string + description: User's phone number + gender: + type: string + enum: [MALE, FEMALE] + date_of_birth: + type: string + format: date + description: User's date of birth + isVerified: + type: boolean + description: Whether email is verified + hasCompletedProfile: + type: boolean + description: Whether user has completed profile + default: false + + Error: + type: object + properties: + messageEn: + type: string + description: Error message in English + messageAr: + type: string + description: Error message in Arabic + + responses: + BadRequest: + description: Bad request - validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Validation error + messageAr: خطأ في التحقق من البيانات + + Unauthorized: + description: Unauthorized - authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + messageEn: Authentication required + messageAr: المصادقة مطلوبة diff --git a/test-ci.sh b/test-ci.sh new file mode 100755 index 0000000..d916943 --- /dev/null +++ b/test-ci.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +echo "Testing CI pipeline locally" +echo "" + +docker run -it --rm \ + -v $(pwd):/app \ + -w /app \ + node:22 \ + bash -c " + echo 'Installing dependencies' + npm ci + + echo 'Generating Prisma client' + npx prisma generate + + " + +echo "" +echo "✨ All CI checks passed! Safe to push yayyyy" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2361a19 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "target": "es2017", + "lib": ["es2017", "esnext.asynciterable"], + "typeRoots": ["node_modules/@types"], + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "module": "commonjs", + "pretty": true, + "sourceMap": true, + "declaration": true, + "outDir": "dist", + "allowJs": true, + "noEmit": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "importHelpers": true, + "baseUrl": "src", + "paths": { + "@/*": ["*"], + "@config": ["config"], + "@controllers/*": ["controllers/*"], + "@dtos/*": ["dtos/*"], + "@exceptions/*": ["exceptions/*"], + "@interfaces/*": ["interfaces/*"], + "@middlewares/*": ["middlewares/*"], + "@routes/*": ["routes/*"], + "@services/*": ["services/*"], + "@utils/*": ["utils/*"], + "@constants/*": ["constants/*"], + "@validators/*": ["validators/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.json", ".env", "src/test/separateAudioAI.test.js"], + "exclude": ["node_modules", "src/http", "src/logs"] +}