From a00d81a3fdf5af01523abd7e0aa55133c1dfb6ba Mon Sep 17 00:00:00 2001 From: Mohammad Yaseen Date: Wed, 17 Dec 2025 18:16:34 +0530 Subject: [PATCH] first commit -m --- .env.example | 24 + .gitignore | 3 + README.md | 92 + convert-pincode-csv-to-json.js | 35 + package-lock.json | 2019 +++++++++++++++++ package.json | 40 + scripts/create-test-api-key.js | 100 + src/cache/redis.js | 88 + src/database/connection.js | 26 + src/database/migrate-pincodes.js | 105 + src/database/migrate.js | 27 + src/database/migrationRunner.js | 104 + .../20231216000000_initial_schema.js | 203 ++ .../20231217000000_seed_ifsc_data.js | 72 + .../20251216000000_seed_reference_data.js | 135 ++ .../20251217010000_seed_pincode_data.js | 100 + .../20251217020000_seed_gst_data.js | 117 + ...251217030000_add_pan_bank_verifications.js | 55 + src/database/setup.js | 14 + src/index.js | 87 + src/middleware/auth.js | 86 + src/middleware/errorHandler.js | 23 + src/middleware/rateLimit.js | 45 + src/routes/auth.js | 111 + src/routes/bank.js | 63 + src/routes/gst.js | 65 + src/routes/ifsc.js | 89 + src/routes/pan.js | 71 + src/routes/pincode.js | 190 ++ src/routes/user.js | 86 + src/services/analytics.js | 21 + src/services/bankService.js | 131 ++ src/services/gstService.js | 191 ++ src/services/panService.js | 140 ++ 34 files changed, 4758 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 convert-pincode-csv-to-json.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/create-test-api-key.js create mode 100644 src/cache/redis.js create mode 100644 src/database/connection.js create mode 100644 src/database/migrate-pincodes.js create mode 100644 src/database/migrate.js create mode 100644 src/database/migrationRunner.js create mode 100644 src/database/migrations/20231216000000_initial_schema.js create mode 100644 src/database/migrations/20231217000000_seed_ifsc_data.js create mode 100644 src/database/migrations/20251216000000_seed_reference_data.js create mode 100644 src/database/migrations/20251217010000_seed_pincode_data.js create mode 100644 src/database/migrations/20251217020000_seed_gst_data.js create mode 100644 src/database/migrations/20251217030000_add_pan_bank_verifications.js create mode 100644 src/database/setup.js create mode 100644 src/index.js create mode 100644 src/middleware/auth.js create mode 100644 src/middleware/errorHandler.js create mode 100644 src/middleware/rateLimit.js create mode 100644 src/routes/auth.js create mode 100644 src/routes/bank.js create mode 100644 src/routes/gst.js create mode 100644 src/routes/ifsc.js create mode 100644 src/routes/pan.js create mode 100644 src/routes/pincode.js create mode 100644 src/routes/user.js create mode 100644 src/services/analytics.js create mode 100644 src/services/bankService.js create mode 100644 src/services/gstService.js create mode 100644 src/services/panService.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..29dd67c --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +NODE_ENV=development +PORT=3000 + +DATABASE_URL="postgresql://postgres:Admin%40123@localhost:5434/Yaseen123 + +REDIS_URL=redis://localhost:6379 + +JWT_SECRET=your-secret-key +API_KEY_PREFIX=vf_live_ + +GST_PROVIDER_URL= +GST_PROVIDER_KEY= + +PAN_PROVIDER_URL= +PAN_PROVIDER_KEY= + +BANK_PROVIDER_URL= +BANK_PROVIDER_KEY= + +RAZORPAY_KEY_ID= +RAZORPAY_KEY_SECRET= + +RESEND_API_KEY= +FROM_EMAIL= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5235962 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ededa2 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# VerifyIndia API + +REST APIs for Indian data verification: +- IFSC Lookup +- Pincode Lookup +- GST Verification +- PAN Verification +- Bank Account Verification + +## Tech Stack + +- **Runtime:** Node.js v20+ +- **Framework:** Express.js +- **Database:** PostgreSQL +- **Cache:** Redis +- **Auth:** API Keys + JWT + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Create a `.env` file with your settings (replace ``): +``` +PORT=3000 +NODE_ENV=development +DATABASE_URL=postgres://india_api_2025:@localhost:5434/india-api-tech4biz +# optional: JWT_SECRET, REDIS_URL, etc. +``` + +3. Start the server: +```bash +npm run dev +``` + +## Project Structure + +``` +verify-india-api/ +├── src/ +│ ├── index.js +│ ├── routes/ +│ ├── middleware/ +│ ├── services/ +│ ├── database/ +│ └── cache/ +├── data/ +├── package.json +├── .env.example +└── README.md +``` + +## API Key Format + +``` +vf_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +``` + +Authentication via header: +``` +X-API-Key: vf_live_xxx +``` + +## Response Format + +**Success:** +```json +{ + "success": true, + "data": {}, + "meta": { + "request_id": "req_xxx", + "credits_used": 1, + "credits_remaining": 999 + } +} +``` + +**Error:** +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "message": "Description" + } +} +``` + + diff --git a/convert-pincode-csv-to-json.js b/convert-pincode-csv-to-json.js new file mode 100644 index 0000000..6d7bffb --- /dev/null +++ b/convert-pincode-csv-to-json.js @@ -0,0 +1,35 @@ + +const fs = require('fs'); +const path = require('path'); +const { parse } = require('csv-parse'); + +const CSV_FILE_PATH = path.join(__dirname, 'data', 'pincode.csv'); +const JSON_FILE_PATH = path.join(__dirname, 'data', 'pincodes.json'); + +async function convertCsvToJson() { + const records = []; + const parser = fs + .createReadStream(CSV_FILE_PATH) + .pipe(parse({ columns: true, skip_empty_lines: true })); + + for await (const record of parser) { + records.push({ + pincode: record.pincode, + office_name: record.officename, + office_type: record.officetype, + district: record.district, + division: record.divisionname, + region: record.regionname, + state: record.statename, + latitude: record.latitude === 'NA' ? null : parseFloat(record.latitude), + longitude: record.longitude === 'NA' ? null : parseFloat(record.longitude), + }); + } + + fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(records, null, 2)); + console.log(`Converted ${records.length} records from CSV to JSON.`); +} + +convertCsvToJson().catch(console.error); + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..487825d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2019 @@ +{ + "name": "verify-india-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "verify-india-api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.6.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "csv-parser": "^3.2.0", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "pg": "^8.16.3", + "redis": "^4.6.10", + "sequelize": "^6.37.7", + "uuid": "^9.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "peer": true, + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz", + "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "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/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "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", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "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/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/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/csv-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", + "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==", + "license": "MIT", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "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/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "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/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "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/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "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/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "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", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "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", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "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", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "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/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "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/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b2aa8b --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "verify-india-api", + "version": "1.0.0", + "description": "REST APIs for Indian data verification", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "migrate": "node src/database/migrate.js up", + "migrate:down": "node src/database/migrate.js down", + "migrate:status": "node src/database/migrate.js status", + "migrate:pincodes": "node src/database/migrate-pincodes.js", + "create-test-key": "node scripts/create-test-api-key.js" + }, + "keywords": [ + "api", + "verification", + "india" + ], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^1.6.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "csv-parser": "^3.2.0", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "pg": "^8.16.3", + "redis": "^4.6.10", + "sequelize": "^6.37.7", + "uuid": "^9.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} diff --git a/scripts/create-test-api-key.js b/scripts/create-test-api-key.js new file mode 100644 index 0000000..5bee53e --- /dev/null +++ b/scripts/create-test-api-key.js @@ -0,0 +1,100 @@ +/** + * Helper script to create a test API key for development + * Usage: node scripts/create-test-api-key.js + */ + +require('dotenv').config(); +const crypto = require('crypto'); +const { query, connectDB } = require('../src/database/connection'); + +function generateApiKey(type = 'test') { + const prefix = type === 'test' ? 'vf_test_' : 'vf_live_'; + return prefix + crypto.randomBytes(24).toString('hex'); +} + +async function createTestApiKey() { + try { + await connectDB(); + console.log('✅ Connected to database\n'); + + // Check if test user exists + let testUser = await query( + 'SELECT * FROM users WHERE email = $1', + ['test@example.com'] + ); + + let userId; + + if (testUser.rows.length === 0) { + // Create test user + console.log('Creating test user...'); + const bcrypt = require('bcryptjs'); + const passwordHash = await bcrypt.hash('testpassword123', 10); + + const userResult = await query( + `INSERT INTO users (email, password_hash, company_name, plan, monthly_quota, quota_reset_date, is_active) + VALUES ($1, $2, $3, $4, $5, DATE(NOW() + INTERVAL '1 month'), true) + RETURNING id, email, plan`, + ['test@example.com', passwordHash, 'Test Company', 'free', 10000] + ); + + userId = userResult.rows[0].id; + console.log(`✅ Created test user (ID: ${userId})`); + } else { + userId = testUser.rows[0].id; + console.log(`✅ Using existing test user (ID: ${userId})`); + } + + // Check for existing test API key + const existingKeys = await query( + `SELECT ak.* FROM api_keys ak + WHERE ak.user_id = $1 AND ak.is_test_key = true AND ak.is_active = true`, + [userId] + ); + + let apiKey; + + if (existingKeys.rows.length > 0) { + // Show existing key (we can't retrieve the original, so we'll create a new one) + console.log('⚠️ Test API key already exists. Creating a new one...'); + + // Deactivate old keys + await query( + 'UPDATE api_keys SET is_active = false WHERE user_id = $1 AND is_test_key = true', + [userId] + ); + } + + // Generate new API key + apiKey = generateApiKey('test'); + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + + await query( + `INSERT INTO api_keys (user_id, key_prefix, key_hash, key_hint, name, is_test_key, is_active) + VALUES ($1, $2, $3, $4, $5, true, true)`, + [userId, 'vf_test_', keyHash, apiKey.slice(-4), 'Test Key'] + ); + + console.log('\n✅ Test API Key Created Successfully!\n'); + console.log('=' .repeat(60)); + console.log('API Key:'); + console.log(apiKey); + console.log('=' .repeat(60)); + console.log('\nUsage:'); + console.log('curl -H "x-api-key: ' + apiKey + '" http://localhost:3000/v1/gst/verify/27AAACM1234A1Z5'); + console.log('\nOr use in Postman/Thunder Client:'); + console.log('Header: x-api-key'); + console.log('Value: ' + apiKey); + console.log('\n'); + + process.exit(0); + } catch (error) { + console.error('❌ Error:', error.message); + console.error(error); + process.exit(1); + } +} + +createTestApiKey(); + + diff --git a/src/cache/redis.js b/src/cache/redis.js new file mode 100644 index 0000000..feb8a2a --- /dev/null +++ b/src/cache/redis.js @@ -0,0 +1,88 @@ +// src/cache/redis.js +const dummyCache = new Map(); +const dummyExpiryTimers = new Map(); +let useDummy = !process.env.REDIS_URL; +let redisClient = null; + +async function connectRedis() { + if (useDummy) { + console.log('📦 Using dummy in-memory cache (no REDIS_URL set)'); + return null; + } + + try { + const { createClient } = require('redis'); + redisClient = createClient({ url: process.env.REDIS_URL }); + redisClient.on('error', (err) => console.error('Redis Error:', err)); + await redisClient.connect(); + console.log('✅ Redis connected'); + return redisClient; + } catch (err) { + console.error('❌ Redis connect failed, falling back to in-memory cache:', err.message); + useDummy = true; + redisClient = null; + return null; + } +} + +function getRedisClient() { + return useDummy ? null : redisClient; +} + +function isDummyCache() { + return useDummy; +} + +async function cacheGet(key) { + if (useDummy) { + return dummyCache.has(key) ? dummyCache.get(key) : null; + } + if (!redisClient) return null; + + const data = await redisClient.get(key); + if (!data) return null; + + try { + return JSON.parse(data); + } catch (err) { + console.error('Redis parse error:', err.message); + return null; + } +} + +async function cacheSet(key, value, expirySeconds = 3600) { + if (useDummy) { + dummyCache.set(key, value); + + if (dummyExpiryTimers.has(key)) { + clearTimeout(dummyExpiryTimers.get(key)); + } + const timer = setTimeout(() => { + dummyCache.delete(key); + dummyExpiryTimers.delete(key); + }, expirySeconds * 1000); + dummyExpiryTimers.set(key, timer); + return true; + } + + if (!redisClient) return false; + + await redisClient.setEx(key, expirySeconds, JSON.stringify(value)); + return true; +} + +async function cacheDelete(key) { + if (useDummy) { + if (dummyExpiryTimers.has(key)) { + clearTimeout(dummyExpiryTimers.get(key)); + dummyExpiryTimers.delete(key); + } + return dummyCache.delete(key); + } + + if (!redisClient) return false; + await redisClient.del(key); + return true; +} + +module.exports = { connectRedis, getRedisClient, cacheGet, cacheSet, cacheDelete, isDummyCache }; \ No newline at end of file diff --git a/src/database/connection.js b/src/database/connection.js new file mode 100644 index 0000000..289806a --- /dev/null +++ b/src/database/connection.js @@ -0,0 +1,26 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, +}); + +async function connectDB() { + const client = await pool.connect(); + await client.query('SELECT NOW()'); + client.release(); + return true; +} + +async function query(text, params) { + return await pool.query(text, params); +} + +async function getClient() { + return await pool.connect(); +} + +module.exports = { connectDB, query, getClient, pool }; diff --git a/src/database/migrate-pincodes.js b/src/database/migrate-pincodes.js new file mode 100644 index 0000000..50831ce --- /dev/null +++ b/src/database/migrate-pincodes.js @@ -0,0 +1,105 @@ +const fs = require('fs'); +const path = require('path'); +const { query, connectDB } = require('../database/connection'); + +async function migratePincodes() { + try { + await connectDB(); + console.log('Database connected for migration.'); + + const csvFilePath = path.join(__dirname, '../../data/pincode.csv'); + const fileContent = fs.readFileSync(csvFilePath, 'utf8'); + const lines = fileContent.split('\n').filter(line => line.trim() !== ''); + + if (lines.length === 0) { + console.log('No data found in pincode.csv'); + return; + } + + const headers = lines[0].split(',').map(header => header.trim().toLowerCase()); + const dataRows = lines.slice(1); + + console.log(`Starting migration of ${dataRows.length} pincode records.`); + + for (const row of dataRows) { + const values = parseCsvLine(row); + + if (values.length !== headers.length) { + console.warn('Skipping row due to column mismatch:', row); + continue; + } + + const record = {}; + headers.forEach((header, index) => { + record[header] = values[index]; + }); + + const pincode = record.pincode; + const office_name = record.officename; + const office_type = record.officetype; + const district = record.district; + const division = record.divisionname; + const region = record.regionname; + const state = record.statename; + const latitude = record.latitude && record.latitude.toLowerCase() !== 'na' ? parseFloat(record.latitude) : null; + const longitude = record.longitude && record.longitude.toLowerCase() !== 'na' ? parseFloat(record.longitude) : null; + + const insertQuery = ` + INSERT INTO pincodes (pincode, office_name, office_type, district, division, region, state, latitude, longitude) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (pincode) DO UPDATE SET + office_name = EXCLUDED.office_name, + office_type = EXCLUDED.office_type, + district = EXCLUDED.district, + division = EXCLUDED.division, + region = EXCLUDED.region, + state = EXCLUDED.state, + latitude = EXCLUDED.latitude, + longitude = EXCLUDED.longitude, + updated_at = NOW() + ; + `; + const insertValues = [ + pincode, + office_name, + office_type, + district, + division, + region, + state, + latitude, + longitude + ]; + + await query(insertQuery, insertValues); + } + + console.log('Pincode migration completed successfully.'); + } catch (error) { + console.error('Error during pincode migration:', error); + process.exit(1); + } +} + +// Basic CSV parser to handle commas within quoted strings +function parseCsvLine(line) { + const values = []; + let inQuote = false; + let currentVal = ''; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + if (char === '"' && (i === 0 || line[i - 1] === ',')) { + inQuote = !inQuote; + } else if (char === ',' && !inQuote) { + values.push(currentVal.trim()); + currentVal = ''; + } else { + currentVal += char; + } + } + values.push(currentVal.trim()); + return values.map(val => val.startsWith('"') && val.endsWith('"') ? val.slice(1, -1) : val); +} + +migratePincodes(); + diff --git a/src/database/migrate.js b/src/database/migrate.js new file mode 100644 index 0000000..1789e63 --- /dev/null +++ b/src/database/migrate.js @@ -0,0 +1,27 @@ +require('dotenv').config(); // <--- ADD THIS LINE FIRST +const { runMigrations, migrationStatus } = require('./migrationRunner'); + +const direction = (process.argv[2] || 'up').toLowerCase(); + +async function main() { + try { + if (direction === 'status') { + const status = await migrationStatus(); + console.log('Migration status:'); + status.forEach((row) => { + console.log(`${row.applied ? '✅' : '⬜'} ${row.id} - ${row.name}`); + }); + } else { + console.log(`Starting migrations (${direction})...`); + await runMigrations(direction); + console.log(`Migrations ${direction} completed`); + } + process.exit(0); + } catch (error) { + // This is where your SASL error is currently being caught + console.error(`Migration ${direction} failed:`, error.message); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/src/database/migrationRunner.js b/src/database/migrationRunner.js new file mode 100644 index 0000000..4cbda2d --- /dev/null +++ b/src/database/migrationRunner.js @@ -0,0 +1,104 @@ +const fs = require('fs'); +const path = require('path'); +const { pool } = require('./connection'); + +const MIGRATIONS_DIR = path.join(__dirname, 'migrations'); +const MIGRATIONS_TABLE = 'schema_migrations'; + +async function ensureMigrationsTable(client) { + await client.query(` + CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} ( + id SERIAL PRIMARY KEY, + migration_id TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + run_on TIMESTAMP DEFAULT NOW() + ); + `); +} + +function loadMigrations() { + const files = fs + .readdirSync(MIGRATIONS_DIR) + .filter((file) => file.endsWith('.js')) + .sort(); + + return files.map((file) => { + // eslint-disable-next-line import/no-dynamic-require, global-require + const migration = require(path.join(MIGRATIONS_DIR, file)); + if (!migration.id || typeof migration.up !== 'function' || typeof migration.down !== 'function') { + throw new Error(`Migration ${file} is missing required exports`); + } + return { ...migration, file }; + }); +} + +async function getAppliedMigrations(client) { + const { rows } = await client.query(`SELECT migration_id FROM ${MIGRATIONS_TABLE} ORDER BY run_on ASC`); + return new Set(rows.map((row) => row.migration_id)); +} + +async function runMigrations(direction = 'up') { + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + await ensureMigrationsTable(client); + + const migrations = loadMigrations(); + const applied = await getAppliedMigrations(client); + + if (direction === 'up') { + for (const migration of migrations) { + if (applied.has(migration.id)) continue; + await migration.up(client); + await client.query( + `INSERT INTO ${MIGRATIONS_TABLE} (migration_id, name) VALUES ($1, $2)`, + [migration.id, migration.name || migration.file] + ); + console.log(`⬆️ Applied migration ${migration.id}`); + } + } else if (direction === 'down') { + const appliedList = Array.from(applied).reverse(); + const map = migrations.reduce((acc, m) => acc.set(m.id, m), new Map()); + + for (const migrationId of appliedList) { + const migration = map.get(migrationId); + if (!migration) continue; + await migration.down(client); + await client.query(`DELETE FROM ${MIGRATIONS_TABLE} WHERE migration_id = $1`, [migrationId]); + console.log(`⬇️ Reverted migration ${migration.id}`); + } + } else { + throw new Error(`Unknown migration direction: ${direction}`); + } + + await client.query('COMMIT'); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } +} + +async function migrationStatus() { + const client = await pool.connect(); + + try { + await ensureMigrationsTable(client); + const migrations = loadMigrations(); + const applied = await getAppliedMigrations(client); + + return migrations.map((migration) => ({ + id: migration.id, + name: migration.name || migration.file, + applied: applied.has(migration.id), + })); + } finally { + client.release(); + } +} + +module.exports = { runMigrations, migrationStatus }; + + diff --git a/src/database/migrations/20231216000000_initial_schema.js b/src/database/migrations/20231216000000_initial_schema.js new file mode 100644 index 0000000..9b19b4a --- /dev/null +++ b/src/database/migrations/20231216000000_initial_schema.js @@ -0,0 +1,203 @@ +const initialSchema = ` +-- Table: users +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + company_name VARCHAR(255), + phone VARCHAR(20), + email_verified BOOLEAN DEFAULT FALSE, + verification_token VARCHAR(255), + plan VARCHAR(50) DEFAULT 'free', + plan_started_at TIMESTAMP, + plan_expires_at TIMESTAMP, + monthly_quota INTEGER DEFAULT 100, + calls_this_month INTEGER DEFAULT 0, + quota_reset_date DATE, + razorpay_customer_id VARCHAR(100), + razorpay_subscription_id VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + last_login_at TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE +); + +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + +-- Table: api_keys +CREATE TABLE IF NOT EXISTS api_keys ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + key_prefix VARCHAR(20) NOT NULL, + key_hash VARCHAR(255) NOT NULL, + key_hint VARCHAR(10), + name VARCHAR(100) DEFAULT 'Default', + is_test_key BOOLEAN DEFAULT FALSE, + is_active BOOLEAN DEFAULT TRUE, + last_used_at TIMESTAMP, + total_calls INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id); +CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash); + +-- Table: api_calls +CREATE TABLE IF NOT EXISTS api_calls ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + api_key_id INTEGER REFERENCES api_keys(id), + endpoint VARCHAR(100) NOT NULL, + method VARCHAR(10) NOT NULL, + request_params JSONB, + response_status INTEGER, + response_time_ms INTEGER, + success BOOLEAN, + error_message VARCHAR(500), + credits_used INTEGER DEFAULT 1, + is_billable BOOLEAN DEFAULT TRUE, + ip_address VARCHAR(45), + user_agent VARCHAR(500), + called_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_api_calls_user ON api_calls(user_id); +CREATE INDEX IF NOT EXISTS idx_api_calls_date ON api_calls(called_at); +CREATE INDEX IF NOT EXISTS idx_api_calls_endpoint ON api_calls(endpoint); + +-- Table: ifsc_codes +CREATE TABLE IF NOT EXISTS ifsc_codes ( + id SERIAL PRIMARY KEY, + ifsc VARCHAR(11) UNIQUE NOT NULL, + bank_name VARCHAR(255) NOT NULL, + branch VARCHAR(255), + address TEXT, + city VARCHAR(100), + district VARCHAR(100), + state VARCHAR(100), + contact VARCHAR(100), + upi_enabled BOOLEAN DEFAULT FALSE, + rtgs_enabled BOOLEAN DEFAULT TRUE, + neft_enabled BOOLEAN DEFAULT TRUE, + imps_enabled BOOLEAN DEFAULT TRUE, + micr_code VARCHAR(20), + swift_code VARCHAR(20), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_ifsc ON ifsc_codes(ifsc); + +-- Table: pincodes +CREATE TABLE IF NOT EXISTS pincodes ( + id SERIAL PRIMARY KEY, + pincode VARCHAR(6) NOT NULL, + office_name VARCHAR(255), + office_type VARCHAR(50), + district VARCHAR(100), + division VARCHAR(100), + region VARCHAR(100), + state VARCHAR(100), + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_pincode ON pincodes(pincode); + +-- Table: subscriptions +CREATE TABLE IF NOT EXISTS subscriptions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + razorpay_subscription_id VARCHAR(100), + razorpay_payment_id VARCHAR(100), + razorpay_plan_id VARCHAR(100), + plan_name VARCHAR(50), + amount DECIMAL(10, 2), + currency VARCHAR(3) DEFAULT 'INR', + status VARCHAR(50), + current_period_start TIMESTAMP, + current_period_end TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + cancelled_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_subscriptions_user ON subscriptions(user_id); + +-- Table: invoices +CREATE TABLE IF NOT EXISTS invoices ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + subscription_id INTEGER REFERENCES subscriptions(id), + invoice_number VARCHAR(50) UNIQUE, + amount DECIMAL(10, 2), + tax_amount DECIMAL(10, 2), + total_amount DECIMAL(10, 2), + currency VARCHAR(3) DEFAULT 'INR', + status VARCHAR(50), + razorpay_invoice_id VARCHAR(100), + razorpay_payment_id VARCHAR(100), + invoice_date DATE, + due_date DATE, + paid_at TIMESTAMP, + pdf_url VARCHAR(500), + created_at TIMESTAMP DEFAULT NOW() +); + +-- Table: gst_registrations +CREATE TABLE IF NOT EXISTS gst_registrations ( + id SERIAL PRIMARY KEY, + gstin VARCHAR(15) UNIQUE NOT NULL, + legal_name VARCHAR(255), + trade_name VARCHAR(255), + status VARCHAR(50), + registration_date DATE, + last_updated TIMESTAMP, + business_type VARCHAR(100), + constitution VARCHAR(100), + state VARCHAR(100), + state_code VARCHAR(2), + pan VARCHAR(10), + address_building VARCHAR(255), + address_floor VARCHAR(100), + address_street VARCHAR(255), + address_locality VARCHAR(255), + address_city VARCHAR(100), + address_district VARCHAR(100), + address_state_code VARCHAR(2), + address_pincode VARCHAR(10), + nature_of_business TEXT, + filing_status_gstr1 VARCHAR(50), + filing_status_gstr3b VARCHAR(50), + filing_last_filed_date DATE, + created_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_gst_gstin ON gst_registrations(gstin); +`; + +async function up(client) { + await client.query(initialSchema); +} + +async function down(client) { + await client.query(` + DROP TABLE IF EXISTS invoices; + DROP TABLE IF EXISTS subscriptions; + DROP TABLE IF EXISTS api_calls; + DROP TABLE IF EXISTS api_keys; + DROP TABLE IF EXISTS ifsc_codes; + DROP TABLE IF EXISTS pincodes; + DROP TABLE IF EXISTS users; + DROP TABLE IF EXISTS gst_registrations; + `); +} + +module.exports = { + id: '20231216000000_initial_schema', + name: 'initial schema', + up, + down, +}; + + + diff --git a/src/database/migrations/20231217000000_seed_ifsc_data.js b/src/database/migrations/20231217000000_seed_ifsc_data.js new file mode 100644 index 0000000..995c6ac --- /dev/null +++ b/src/database/migrations/20231217000000_seed_ifsc_data.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const path = require('path'); +const csv = require('csv-parser'); + +async function up(client) { + // IFSC.csv lives in the top-level `data` folder of the project. + // This resolves from: src/database/migrations -> project root -> data/IFSC.csv + const csvFilePath = path.join(__dirname, '../../../data/IFSC.csv'); + + // Check if file exists before proceeding + if (!fs.existsSync(csvFilePath)) { + console.warn(`⚠️ IFSC.csv not found at ${csvFilePath}. Skipping seed.`); + return; + } + + console.log('⏳ Starting IFSC data import...'); + + return new Promise((resolve, reject) => { + const promises = []; + + fs.createReadStream(csvFilePath) + .pipe(csv()) + .on('data', (row) => { + // Map CSV columns to Database columns + const query = ` + INSERT INTO ifsc_codes ( + ifsc, bank_name, branch, address, city, district, state, + upi_enabled, rtgs_enabled, neft_enabled, imps_enabled + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + ON CONFLICT (ifsc) DO NOTHING; + `; + + const values = [ + row.IFSC || row.ifsc, + row.BANK || row.bank_name, + row.BRANCH || row.branch, + row.ADDRESS || row.address, + row.CITY || row.city, + row.DISTRICT || row.district, + row.STATE || row.state, + row.UPI === 'true', // Convert string to boolean if necessary + row.RTGS === 'true', + row.NEFT === 'true', + row.IMPS === 'true' + ]; + + promises.push(client.query(query, values)); + }) + .on('end', async () => { + try { + await Promise.all(promises); + console.log('✅ IFSC data import completed.'); + resolve(); + } catch (err) { + reject(err); + } + }) + .on('error', reject); + }); +} + +async function down(client) { + // Optional: Clear the table on rollback + await client.query('TRUNCATE TABLE ifsc_codes;'); +} + +module.exports = { + id: '20231217000000_seed_ifsc_data', + name: 'seed ifsc data from csv', + up, + down, +}; \ No newline at end of file diff --git a/src/database/migrations/20251216000000_seed_reference_data.js b/src/database/migrations/20251216000000_seed_reference_data.js new file mode 100644 index 0000000..88dbf1e --- /dev/null +++ b/src/database/migrations/20251216000000_seed_reference_data.js @@ -0,0 +1,135 @@ +const IFSC_DATA = [ + { + ifsc: 'HDFC0000001', + bank_name: 'HDFC Bank', + branch: 'HDFC BANK LTD', + address: 'SANDOZ HOUSE, SHIVSAGAR ESTATE, WORLI, MUMBAI - 400018', + city: 'MUMBAI', + district: 'MUMBAI', + state: 'MAHARASHTRA', + contact: '022-24910409', + upi_enabled: true, + rtgs_enabled: true, + neft_enabled: true, + imps_enabled: true, + micr_code: '400240001', + swift_code: 'HDFCINBB', + }, + { + ifsc: 'SBIN0000001', + bank_name: 'State Bank of India', + branch: 'Main Branch', + address: 'MUMBAI MAIN BRANCH, FORT, MUMBAI - 400001', + city: 'MUMBAI', + district: 'MUMBAI', + state: 'MAHARASHTRA', + contact: '022-22621111', + upi_enabled: true, + rtgs_enabled: true, + neft_enabled: true, + imps_enabled: true, + micr_code: '400002000', + swift_code: 'SBININBB', + }, + { + ifsc: 'ICIC0000001', + bank_name: 'ICICI Bank', + branch: 'Corporate Office', + address: 'ICICI BANK TOWERS, BANDRA-KURLA COMPLEX, MUMBAI - 400051', + city: 'MUMBAI', + district: 'MUMBAI', + state: 'MAHARASHTRA', + contact: '022-33667777', + upi_enabled: true, + rtgs_enabled: true, + neft_enabled: true, + imps_enabled: true, + micr_code: '400229002', + swift_code: 'ICICINBB', + }, +]; + +const PINCODE_DATA = [ + { + pincode: '400001', + office_name: 'G.P.O.', + office_type: 'Head Office', + district: 'Mumbai', + division: 'Mumbai', + region: 'Mumbai', + state: 'Maharashtra', + latitude: 18.9398, + longitude: 72.8355, + }, +]; + +async function up(client) { + for (const row of IFSC_DATA) { + await client.query( + ` + INSERT INTO ifsc_codes + (ifsc, bank_name, branch, address, city, district, state, contact, upi_enabled, rtgs_enabled, neft_enabled, imps_enabled, micr_code, swift_code) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ON CONFLICT (ifsc) DO NOTHING; + `, + [ + row.ifsc, + row.bank_name, + row.branch, + row.address, + row.city, + row.district, + row.state, + row.contact, + row.upi_enabled, + row.rtgs_enabled, + row.neft_enabled, + row.imps_enabled, + row.micr_code, + row.swift_code, + ] + ); + } + + for (const row of PINCODE_DATA) { + await client.query( + ` + INSERT INTO pincodes + (pincode, office_name, office_type, district, division, region, state, latitude, longitude) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT DO NOTHING; + `, + [ + row.pincode, + row.office_name, + row.office_type, + row.district, + row.division, + row.region, + row.state, + row.latitude, + row.longitude, + ] + ); + } +} + +async function down(client) { + await client.query('DELETE FROM ifsc_codes WHERE ifsc = ANY($1)', [ + IFSC_DATA.map((r) => r.ifsc), + ]); + + await client.query('DELETE FROM pincodes WHERE pincode = ANY($1)', [ + PINCODE_DATA.map((r) => r.pincode), + ]); +} + +module.exports = { + id: '20251216000000_seed_reference_data', + name: 'seed reference data', + up, + down, +}; + diff --git a/src/database/migrations/20251217010000_seed_pincode_data.js b/src/database/migrations/20251217010000_seed_pincode_data.js new file mode 100644 index 0000000..3232616 --- /dev/null +++ b/src/database/migrations/20251217010000_seed_pincode_data.js @@ -0,0 +1,100 @@ +const fs = require('fs'); +const path = require('path'); +const csv = require('csv-parser'); + +// Helper to safely parse and clamp decimal values to a given scale and range +function cleanDecimal(value, scale = 8, maxAbs) { + if (!value || value === 'NA') return null; + + const num = parseFloat(value); + if (Number.isNaN(num)) return null; + + // If we expect a reasonable geographic coordinate, drop clearly invalid values + if (typeof maxAbs === 'number' && Math.abs(num) > maxAbs) { + return null; + } + + // Clamp to the allowed scale (e.g. DECIMAL(10, 8) or DECIMAL(11, 8)) + const fixed = Number(num.toFixed(scale)); + return fixed; +} + +async function up(client) { + // pincode.csv lives in the top-level `data` folder of the project. + // This resolves from: src/database/migrations -> project root -> data/pincode.csv + const csvFilePath = path.join(__dirname, '../../../data/pincode.csv'); + + // Check if file exists before proceeding + if (!fs.existsSync(csvFilePath)) { + console.warn(`⚠️ pincode.csv not found at ${csvFilePath}. Skipping seed.`); + return; + } + + console.log('⏳ Starting pincode data import...'); + + return new Promise((resolve, reject) => { + const promises = []; + + fs.createReadStream(csvFilePath) + .pipe(csv()) + .on('data', (row) => { + // Convert/clean values from CSV + const pincode = String(row.pincode || '').padStart(6, '0'); + + // Latitude expected roughly between -90 and 90, longitude between -180 and 180 + const latitude = cleanDecimal(row.latitude, 8, 90); + const longitude = cleanDecimal(row.longitude, 8, 180); + + const query = ` + INSERT INTO pincodes ( + pincode, + office_name, + office_type, + district, + division, + region, + state, + latitude, + longitude + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9); + `; + + const values = [ + pincode, + row.officename || null, + row.officetype || null, + row.district || null, + row.divisionname || null, + // Prefer regionname, fall back to circlename if needed + row.regionname || row.circlename || null, + row.statename || null, + latitude, + longitude, + ]; + + promises.push(client.query(query, values)); + }) + .on('end', async () => { + try { + await Promise.all(promises); + console.log('✅ Pincode data import completed.'); + resolve(); + } catch (err) { + reject(err); + } + }) + .on('error', reject); + }); +} + +async function down(client) { + // Optional: Clear the table on rollback + await client.query('TRUNCATE TABLE pincodes;'); +} + +module.exports = { + id: '20251217010000_seed_pincode_data', + name: 'seed pincode data from csv', + up, + down, +}; \ No newline at end of file diff --git a/src/database/migrations/20251217020000_seed_gst_data.js b/src/database/migrations/20251217020000_seed_gst_data.js new file mode 100644 index 0000000..af22817 --- /dev/null +++ b/src/database/migrations/20251217020000_seed_gst_data.js @@ -0,0 +1,117 @@ +const fs = require('fs'); +const path = require('path'); +const csv = require('csv-parser'); + +async function up(client) { + // gst.csv lives in the top-level `data` folder of the project. + // This resolves from: src/database/migrations -> project root -> data/gst.csv + const csvFilePath = path.join(__dirname, '../../../data/gst.csv'); + + if (!fs.existsSync(csvFilePath)) { + console.warn(`⚠️ gst.csv not found at ${csvFilePath}. Skipping GST seed.`); + return; + } + + console.log('⏳ Starting GST data import...'); + + return new Promise((resolve, reject) => { + const promises = []; + + fs.createReadStream(csvFilePath) + .pipe(csv()) + .on('data', (row) => { + // Map CSV columns to database columns + // CSV header: + // gstin,legal_name,trade_name,status,registration_date,last_updated, + // business_type,constitution,state,state_code,pan, + // address_building,address_floor,address_street,address_locality, + // address_city,address_district,address_state_code,address_pincode, + // nature_of_business,filing_status_gstr1,filing_status_gstr3b,filing_last_filed_date + + const query = ` + INSERT INTO gst_registrations ( + gstin, + legal_name, + trade_name, + status, + registration_date, + last_updated, + business_type, + constitution, + state, + state_code, + pan, + address_building, + address_floor, + address_street, + address_locality, + address_city, + address_district, + address_state_code, + address_pincode, + nature_of_business, + filing_status_gstr1, + filing_status_gstr3b, + filing_last_filed_date + ) VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, $10, $11, + $12, $13, $14, $15, $16, + $17, $18, $19, $20, $21, + $22, $23 + ) + ON CONFLICT (gstin) DO NOTHING; + `; + + const values = [ + row.gstin || null, + row.legal_name || null, + row.trade_name || null, + row.status || null, + row.registration_date || null, // 'YYYY-MM-DD' string; Postgres will cast to DATE + row.last_updated || null, // ISO string; Postgres will cast to TIMESTAMP + row.business_type || null, + row.constitution || null, + row.state || null, + row.state_code || null, + row.pan || null, + row.address_building || null, + row.address_floor || null, + row.address_street || null, + row.address_locality || null, + row.address_city || null, + row.address_district || null, + row.address_state_code || null, + row.address_pincode || null, + row.nature_of_business || null, // e.g. "Manufacturing|Services" + row.filing_status_gstr1 || null, + row.filing_status_gstr3b || null, + row.filing_last_filed_date || null // 'YYYY-MM-DD'; Postgres will cast to DATE + ]; + + promises.push(client.query(query, values)); + }) + .on('end', async () => { + try { + await Promise.all(promises); + console.log('✅ GST data import completed.'); + resolve(); + } catch (err) { + reject(err); + } + }) + .on('error', reject); + }); +} + +async function down(client) { + // Optional: Clear the table on rollback + await client.query('TRUNCATE TABLE gst_registrations;'); +} + +module.exports = { + id: '20251217020000_seed_gst_data', + name: 'seed gst data from csv', + up, + down, +}; \ No newline at end of file diff --git a/src/database/migrations/20251217030000_add_pan_bank_verifications.js b/src/database/migrations/20251217030000_add_pan_bank_verifications.js new file mode 100644 index 0000000..aa41a62 --- /dev/null +++ b/src/database/migrations/20251217030000_add_pan_bank_verifications.js @@ -0,0 +1,55 @@ +const panBankSchema = ` +-- Table: pan_verifications +CREATE TABLE IF NOT EXISTS pan_verifications ( + id SERIAL PRIMARY KEY, + pan VARCHAR(10) NOT NULL, + name VARCHAR(255), + status VARCHAR(50), + pan_type VARCHAR(50), + name_match BOOLEAN, + name_match_score INTEGER, + requested_by INTEGER REFERENCES users(id), + requested_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_pan_verifications_pan ON pan_verifications(pan); + +-- Table: bank_verifications +CREATE TABLE IF NOT EXISTS bank_verifications ( + id SERIAL PRIMARY KEY, + account_number VARCHAR(34) NOT NULL, + ifsc VARCHAR(11) NOT NULL, + name VARCHAR(255), + account_exists BOOLEAN, + name_match BOOLEAN, + name_match_score INTEGER, + bank_name VARCHAR(255), + branch VARCHAR(255), + requested_by INTEGER REFERENCES users(id), + requested_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_bank_verifications_ifsc_account + ON bank_verifications(ifsc, account_number); +`; + +async function up(client) { + await client.query(panBankSchema); +} + +async function down(client) { + await client.query(` + DROP TABLE IF EXISTS bank_verifications; + DROP TABLE IF EXISTS pan_verifications; + `); +} + +module.exports = { + id: '20251217030000_add_pan_bank_verifications', + name: 'add pan and bank verification tables', + up, + down, +}; + + + diff --git a/src/database/setup.js b/src/database/setup.js new file mode 100644 index 0000000..5eaaacc --- /dev/null +++ b/src/database/setup.js @@ -0,0 +1,14 @@ +const { runMigrations } = require('./migrationRunner'); + +async function setupDatabase() { + try { + await runMigrations('up'); + console.log('✅ Database migrations executed'); + return true; + } catch (error) { + console.error('❌ Database setup failed:', error); + throw error; + } +} + +module.exports = { setupDatabase }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..f530add --- /dev/null +++ b/src/index.js @@ -0,0 +1,87 @@ +// Main entry point for VerifyIndia API + +require('dotenv').config(); + +const express = require('express'); +const helmet = require('helmet'); +const cors = require('cors'); +const morgan = require('morgan'); + +const { connectDB } = require('./database/connection'); + +const authRoutes = require('./routes/auth'); +const ifscRoutes = require('./routes/ifsc'); +const pincodeRoutes = require('./routes/pincode'); +const gstRoutes = require('./routes/gst'); +const panRoutes = require('./routes/pan'); +const bankRoutes = require('./routes/bank'); +const userRoutes = require('./routes/user'); + +const { errorHandler } = require('./middleware/errorHandler'); + +const app = express(); + +app.use(helmet()); +app.use(cors()); +app.use(express.json()); +app.use(morgan('combined')); + +app.get('/health', (req, res) => { + res.json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); + +app.get('/', (req, res) => { + res.json({ + message: 'VerifyIndia API', + version: 'v1', + endpoints: { + ifsc: '/v1/ifsc/:ifsc_code', + pincode: '/v1/pincode/:pincode', + gst: '/v1/gst/verify/:gstin', + pan: '/v1/pan/verify', + bank: '/v1/bank/verify' + } + }); +}); + +app.use('/v1/auth', authRoutes); +app.use('/v1/user', userRoutes); +app.use('/v1/ifsc', ifscRoutes); +app.use('/v1/pincode', pincodeRoutes); +app.use('/v1/gst', gstRoutes); +app.use('/v1/pan', panRoutes); +app.use('/v1/bank', bankRoutes); + +app.use('*', (req, res) => { + res.status(404).json({ + success: false, + error: { code: 'NOT_FOUND', message: `Route ${req.originalUrl} not found` } + }); +}); + +app.use(errorHandler); + +const PORT = process.env.PORT || 3000; + + +async function startServer() { + try { + await connectDB(); + console.log('✅ PostgreSQL connected'); + // await connectRedis(); + // console.log('✅ Redis connected', isDummyCache() ? 'using dummy cache' : 'using real cache'); + + app.listen(PORT, () => { + console.log(`✅ Server running on port ${PORT}`); + }); + } catch (error) { + console.error('❌ Failed to start:', error); + process.exit(1); + } +} + +startServer(); + + + + diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100644 index 0000000..1e94c74 --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,86 @@ +const crypto = require('crypto'); +const { query } = require('../database/connection'); +const { cacheGet, cacheSet } = require('../cache/redis'); + +async function authenticateApiKey(req, res, next) { + try { + const apiKey = req.headers['x-api-key']; + + if (!apiKey) { + return res.status(401).json({ + success: false, + error: { code: 'MISSING_API_KEY', message: 'API key required' } + }); + } + + if (!apiKey.startsWith('vf_live_') && !apiKey.startsWith('vf_test_')) { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_API_KEY_FORMAT', message: 'Invalid API key format' } + }); + } + + const cacheKey = `apikey:${apiKey}`; + let keyData = await cacheGet(cacheKey); + + if (!keyData) { + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + + const result = await query( + `SELECT ak.*, u.plan, u.monthly_quota, u.calls_this_month, u.is_active as user_active + FROM api_keys ak + JOIN users u ON ak.user_id = u.id + WHERE ak.key_hash = $1 AND ak.is_active = true`, + [keyHash] + ); + + if (result.rows.length === 0) { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_API_KEY', message: 'Invalid or inactive API key' } + }); + } + + keyData = result.rows[0]; + await cacheSet(cacheKey, keyData, 300); + } + + if (!keyData.user_active) { + return res.status(403).json({ + success: false, + error: { code: 'ACCOUNT_INACTIVE', message: 'Account inactive' } + }); + } + + if (keyData.calls_this_month >= keyData.monthly_quota) { + return res.status(429).json({ + success: false, + error: { + code: 'QUOTA_EXCEEDED', + message: 'Monthly quota exceeded', + details: { used: keyData.calls_this_month, limit: keyData.monthly_quota } + } + }); + } + + req.user = { + id: keyData.user_id, + plan: keyData.plan, + apiKeyId: keyData.id, + isTestKey: keyData.is_test_key, + quota: keyData.monthly_quota, + used: keyData.calls_this_month, + remaining: keyData.monthly_quota - keyData.calls_this_month + }; + + next(); + } catch (error) { + console.error('Auth error:', error); + return res.status(500).json({ + success: false, + error: { code: 'AUTH_ERROR', message: 'Authentication failed' } + }); + } +} + +module.exports = { authenticateApiKey }; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js new file mode 100644 index 0000000..98ea7bc --- /dev/null +++ b/src/middleware/errorHandler.js @@ -0,0 +1,23 @@ +function errorHandler(err, req, res, next) { + console.error('Error:', err.message); + + const statusCode = err.statusCode || 500; + const message = process.env.NODE_ENV === 'production' && statusCode === 500 + ? 'Internal server error' + : err.message; + + res.status(statusCode).json({ + success: false, + error: { code: err.code || 'INTERNAL_ERROR', message } + }); +} + +class ApiError extends Error { + constructor(statusCode, code, message) { + super(message); + this.statusCode = statusCode; + this.code = code; + } +} + +module.exports = { errorHandler, ApiError }; diff --git a/src/middleware/rateLimit.js b/src/middleware/rateLimit.js new file mode 100644 index 0000000..4b30e92 --- /dev/null +++ b/src/middleware/rateLimit.js @@ -0,0 +1,45 @@ +const { getRedisClient } = require('../cache/redis'); + +const RATE_LIMITS = { + free: 10, + starter: 60, + growth: 120, + business: 300, + enterprise: 1000 +}; + +async function rateLimit(req, res, next) { + const redis = getRedisClient(); + if (!redis) return next(); + + try { + const userId = req.user?.id || req.ip; + const plan = req.user?.plan || 'free'; + const limit = RATE_LIMITS[plan] || RATE_LIMITS.free; + const key = `ratelimit:${userId}`; + + const current = await redis.incr(key); + if (current === 1) await redis.expire(key, 60); + + const ttl = await redis.ttl(key); + + res.set({ + 'X-RateLimit-Limit': limit, + 'X-RateLimit-Remaining': Math.max(0, limit - current), + 'X-RateLimit-Reset': Math.floor(Date.now() / 1000) + ttl + }); + + if (current > limit) { + return res.status(429).json({ + success: false, + error: { code: 'RATE_LIMIT_EXCEEDED', message: `Limit: ${limit}/minute`, retry_after: ttl } + }); + } + + next(); + } catch (error) { + next(); + } +} + +module.exports = { rateLimit }; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..c11d364 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,111 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const crypto = require('crypto'); +const { query } = require('../database/connection'); +const { ApiError } = require('../middleware/errorHandler'); + +function generateApiKey(type = 'live') { + const prefix = type === 'test' ? 'vf_test_' : 'vf_live_'; + return prefix + crypto.randomBytes(24).toString('hex'); +} + +router.post('/signup', async (req, res, next) => { + try { + const { email, password, company_name, phone } = req.body; + + if (!email || !password) { + throw new ApiError(400, 'MISSING_FIELDS', 'Email and password required'); + } + + if (password.length < 8) { + throw new ApiError(400, 'WEAK_PASSWORD', 'Password must be 8+ characters'); + } + + const existing = await query('SELECT id FROM users WHERE email = $1', [email.toLowerCase()]); + if (existing.rows.length > 0) { + throw new ApiError(409, 'EMAIL_EXISTS', 'Email already registered'); + } + + const passwordHash = await bcrypt.hash(password, 10); + + const result = await query( + `INSERT INTO users (email, password_hash, company_name, phone, plan, monthly_quota, quota_reset_date) + VALUES ($1, $2, $3, $4, 'free', 100, DATE(NOW() + INTERVAL '1 month')) + RETURNING id, email, company_name, plan`, + [email.toLowerCase(), passwordHash, company_name, phone] + ); + + const user = result.rows[0]; + const apiKey = generateApiKey('live'); + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + + await query( + `INSERT INTO api_keys (user_id, key_prefix, key_hash, key_hint, name) + VALUES ($1, $2, $3, $4, 'Default')`, + [user.id, 'vf_live_', keyHash, apiKey.slice(-4)] + ); + + const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' }); + + res.status(201).json({ + success: true, + data: { + user: { id: user.id, email: user.email, company_name: user.company_name, plan: user.plan }, + api_key: apiKey, + token + } + }); + + } catch (error) { + next(error); + } +}); + +router.post('/login', async (req, res, next) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + throw new ApiError(400, 'MISSING_FIELDS', 'Email and password required'); + } + + const result = await query('SELECT * FROM users WHERE email = $1 AND is_active = true', [email.toLowerCase()]); + + if (result.rows.length === 0) { + throw new ApiError(401, 'INVALID_CREDENTIALS', 'Invalid email or password'); + } + + const user = result.rows[0]; + const validPassword = await bcrypt.compare(password, user.password_hash); + + if (!validPassword) { + throw new ApiError(401, 'INVALID_CREDENTIALS', 'Invalid email or password'); + } + + await query('UPDATE users SET last_login_at = NOW() WHERE id = $1', [user.id]); + + const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '7d' }); + + res.json({ + success: true, + data: { + user: { + id: user.id, + email: user.email, + company_name: user.company_name, + plan: user.plan, + quota: user.monthly_quota, + used: user.calls_this_month + }, + token + } + }); + + } catch (error) { + next(error); + } +}); + +module.exports = router; diff --git a/src/routes/bank.js b/src/routes/bank.js new file mode 100644 index 0000000..c744729 --- /dev/null +++ b/src/routes/bank.js @@ -0,0 +1,63 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateApiKey } = require('../middleware/auth'); +const { rateLimit } = require('../middleware/rateLimit'); +const { verifyBankAccount } = require('../services/bankService'); +const { logApiCall } = require('../services/analytics'); + +router.use(authenticateApiKey); +router.use(rateLimit); + +router.post('/verify', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { account_number, ifsc, name } = req.body; + + if (!account_number || !ifsc) { + return res.status(400).json({ + success: false, + error: { code: 'MISSING_FIELDS', message: 'Account number and IFSC are required' } + }); + } + + const result = await verifyBankAccount(account_number, ifsc, name); + + if (!result.success) { + return res.status(result.statusCode || 404).json({ + success: false, + error: { code: result.errorCode, message: result.message } + }); + } + + success = true; + + res.json({ + success: true, + data: result.data, + meta: { + request_id: `req_bank_${Date.now()}`, + credits_used: 2, + credits_remaining: req.user.remaining - 2 + } + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/bank/verify', + method: 'POST', + params: { account_number: req.body.account_number, ifsc: req.body.ifsc }, + status: success ? 200 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +module.exports = router; diff --git a/src/routes/gst.js b/src/routes/gst.js new file mode 100644 index 0000000..71c0bac --- /dev/null +++ b/src/routes/gst.js @@ -0,0 +1,65 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateApiKey } = require('../middleware/auth'); +const { rateLimit } = require('../middleware/rateLimit'); +const { verifyGSTIN } = require('../services/gstService'); +const { logApiCall } = require('../services/analytics'); + +router.use(authenticateApiKey); +router.use(rateLimit); + +router.get('/verify/:gstin', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { gstin } = req.params; + const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/; + + if (!gstinRegex.test(gstin.toUpperCase())) { + return res.status(400).json({ + success: false, + error: { code: 'INVALID_GSTIN', message: 'Invalid GSTIN format' } + }); + } + + const result = await verifyGSTIN(gstin.toUpperCase()); + + if (!result.success) { + return res.status(result.statusCode || 404).json({ + success: false, + error: { code: result.errorCode, message: result.message } + }); + } + + success = true; + + res.json({ + success: true, + data: result.data, + meta: { + request_id: `req_gst_${Date.now()}`, + credits_used: 1, + credits_remaining: req.user.remaining - 1, + source: 'gstn' + } + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/gst/verify', + method: 'GET', + params: { gstin: req.params.gstin }, + status: success ? 200 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +module.exports = router; diff --git a/src/routes/ifsc.js b/src/routes/ifsc.js new file mode 100644 index 0000000..7dc0ac7 --- /dev/null +++ b/src/routes/ifsc.js @@ -0,0 +1,89 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateApiKey } = require('../middleware/auth'); +const { rateLimit } = require('../middleware/rateLimit'); +const { query } = require('../database/connection'); +const { cacheGet, cacheSet } = require('../cache/redis'); +const { logApiCall } = require('../services/analytics'); + +router.use(authenticateApiKey); +router.use(rateLimit); + +router.get('/:ifsc_code', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { ifsc_code } = req.params; + const ifscRegex = /^[A-Z]{4}0[A-Z0-9]{6}$/; + + if (!ifscRegex.test(ifsc_code.toUpperCase())) { + return res.status(400).json({ + success: false, + error: { code: 'INVALID_IFSC', message: 'Invalid IFSC format' } + }); + } + + const ifsc = ifsc_code.toUpperCase(); + const cacheKey = `ifsc:${ifsc}`; + let data = await cacheGet(cacheKey); + + if (!data) { + const result = await query('SELECT * FROM ifsc_codes WHERE ifsc = $1', [ifsc]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + error: { code: 'IFSC_NOT_FOUND', message: 'IFSC not found' } + }); + } + + data = result.rows[0]; + await cacheSet(cacheKey, data, 86400); + } + + success = true; + + res.json({ + success: true, + data: { + ifsc: data.ifsc, + bank: data.bank_name, + branch: data.branch, + address: data.address, + city: data.city, + district: data.district, + state: data.state, + contact: data.contact, + upi: data.upi_enabled, + rtgs: data.rtgs_enabled, + neft: data.neft_enabled, + imps: data.imps_enabled, + micr: data.micr_code, + swift: data.swift_code + }, + meta: { + request_id: `req_ifsc_${Date.now()}`, + credits_used: 1, + credits_remaining: req.user.remaining - 1 + } + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/ifsc', + method: 'GET', + params: { ifsc: req.params.ifsc_code }, + status: success ? 200 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +module.exports = router; diff --git a/src/routes/pan.js b/src/routes/pan.js new file mode 100644 index 0000000..916fdb3 --- /dev/null +++ b/src/routes/pan.js @@ -0,0 +1,71 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateApiKey } = require('../middleware/auth'); +const { rateLimit } = require('../middleware/rateLimit'); +const { verifyPAN } = require('../services/panService'); +const { logApiCall } = require('../services/analytics'); + +router.use(authenticateApiKey); +router.use(rateLimit); + +router.post('/verify', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { pan, name, dob } = req.body; + const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/; + + if (!pan) { + return res.status(400).json({ + success: false, + error: { code: 'MISSING_PAN', message: 'PAN is required' } + }); + } + + if (!panRegex.test(pan.toUpperCase())) { + return res.status(400).json({ + success: false, + error: { code: 'INVALID_PAN', message: 'Invalid PAN format' } + }); + } + + const result = await verifyPAN(pan.toUpperCase(), name, dob); + + if (!result.success) { + return res.status(result.statusCode || 404).json({ + success: false, + error: { code: result.errorCode, message: result.message } + }); + } + + success = true; + + res.json({ + success: true, + data: result.data, + meta: { + request_id: `req_pan_${Date.now()}`, + credits_used: 1, + credits_remaining: req.user.remaining - 1 + } + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/pan/verify', + method: 'POST', + params: { pan: req.body.pan }, + status: success ? 200 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +module.exports = router; diff --git a/src/routes/pincode.js b/src/routes/pincode.js new file mode 100644 index 0000000..b284ea4 --- /dev/null +++ b/src/routes/pincode.js @@ -0,0 +1,190 @@ +const express = require('express'); +const router = express.Router(); +const { authenticateApiKey } = require('../middleware/auth'); +const { rateLimit } = require('../middleware/rateLimit'); +const { query } = require('../database/connection'); +const { cacheGet, cacheSet, cacheDelete } = require('../cache/redis'); +const { logApiCall } = require('../services/analytics'); + +const STATE_CODES = { + 'Andhra Pradesh': 'AP', 'Arunachal Pradesh': 'AR', 'Assam': 'AS', 'Bihar': 'BR', + 'Chhattisgarh': 'CG', 'Delhi': 'DL', 'Goa': 'GA', 'Gujarat': 'GJ', 'Haryana': 'HR', + 'Himachal Pradesh': 'HP', 'Jharkhand': 'JH', 'Karnataka': 'KA', 'Kerala': 'KL', + 'Madhya Pradesh': 'MP', 'Maharashtra': 'MH', 'Manipur': 'MN', 'Meghalaya': 'ML', + 'Mizoram': 'MZ', 'Nagaland': 'NL', 'Odisha': 'OD', 'Punjab': 'PB', 'Rajasthan': 'RJ', + 'Sikkim': 'SK', 'Tamil Nadu': 'TN', 'Telangana': 'TS', 'Tripura': 'TR', + 'Uttar Pradesh': 'UP', 'Uttarakhand': 'UK', 'West Bengal': 'WB' +}; + +router.use(authenticateApiKey); +router.use(rateLimit); + +router.get('/:pincode', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { pincode } = req.params; + + if (!/^\d{6}$/.test(pincode)) { + return res.status(400).json({ + success: false, + error: { code: 'INVALID_PINCODE', message: 'Pincode must be 6 digits' } + }); + } + + const cacheKey = `pincode:${pincode}`; + let data = await cacheGet(cacheKey); + + if (!data) { + const result = await query('SELECT * FROM pincodes WHERE pincode = $1', [pincode]); + + if (result.rows.length === 0) { + return res.status(404).json({ + success: false, + error: { code: 'PINCODE_NOT_FOUND', message: 'Pincode not found' } + }); + } + + data = result.rows; + await cacheSet(cacheKey, data, 604800); + } + + success = true; + const primary = data[0]; + + res.json({ + success: true, + data: { + pincode, + locations: data.map(row => ({ + office_name: row.office_name, + office_type: row.office_type, + district: row.district, + state: row.state, + latitude: parseFloat(row.latitude) || null, + longitude: parseFloat(row.longitude) || null + })), + primary: { + district: primary.district, + state: primary.state, + state_code: STATE_CODES[primary.state] || '' + } + }, + meta: { + request_id: `req_pin_${Date.now()}`, + credits_used: 1, + credits_remaining: req.user.remaining - 1 + } + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/pincode', + method: 'GET', + params: { pincode: req.params.pincode }, + status: success ? 200 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +router.post('/', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { pincode, office_name, office_type, district, division, region, state, latitude, longitude } = req.body; + + if (!pincode || !/^\d{6}$/.test(pincode)) { + return res.status(400).json({ success: false, error: { code: 'INVALID_PINCODE', message: 'Pincode must be a 6-digit number.' } }); + } + if (!office_name || office_name.trim() === '') { + return res.status(400).json({ success: false, error: { code: 'INVALID_OFFICE_NAME', message: 'Office name is required.' } }); + } + if (!office_type || office_type.trim() === '') { + return res.status(400).json({ success: false, error: { code: 'INVALID_OFFICE_TYPE', message: 'Office type is required.' } }); + } + if (!district || district.trim() === '') { + return res.status(400).json({ success: false, error: { code: 'INVALID_DISTRICT', message: 'District is required.' } }); + } + if (!division || division.trim() === '') { + return res.status(400).json({ success: false, error: { code: 'INVALID_DIVISION', message: 'Division is required.' } }); + } + if (!region || region.trim() === '') { + return res.status(400).json({ success: false, error: { code: 'INVALID_REGION', message: 'Region is required.' } }); + } + if (!state || state.trim() === '') { + return res.status(400).json({ success: false, error: { code: 'INVALID_STATE', message: 'State is required.' } }); + } + if (latitude !== undefined && (isNaN(parseFloat(latitude)) || parseFloat(latitude) < -90 || parseFloat(latitude) > 90)) { + return res.status(400).json({ success: false, error: { code: 'INVALID_LATITUDE', message: 'Latitude must be a number between -90 and 90.' } }); + } + if (longitude !== undefined && (isNaN(parseFloat(longitude)) || parseFloat(longitude) < -180 || parseFloat(longitude) > 180)) { + return res.status(400).json({ success: false, error: { code: 'INVALID_LONGITUDE', message: 'Longitude must be a number between -180 and 180.' } }); + } + + const insertQuery = ` + INSERT INTO pincodes (pincode, office_name, office_type, district, division, region, state, latitude, longitude) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ON CONFLICT (pincode) DO UPDATE SET + office_name = EXCLUDED.office_name, + office_type = EXCLUDED.office_type, + district = EXCLUDED.district, + division = EXCLUDED.division, + region = EXCLUDED.region, + state = EXCLUDED.state, + latitude = EXCLUDED.latitude, + longitude = EXCLUDED.longitude, + updated_at = NOW() + RETURNING *; + `; + + const values = [ + pincode, + office_name, + office_type, + district, + division, + region, + state, + latitude ? parseFloat(latitude) : null, + longitude ? parseFloat(longitude) : null + ]; + + const result = await query(insertQuery, values); + + // Invalidate cache for this pincode + await cacheDelete(`pincode:${pincode}`); + + success = true; + res.status(201).json({ + success: true, + message: 'Pincode data stored successfully', + data: result.rows[0] + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/pincode', + method: 'POST', + params: req.body, + status: success ? 201 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/user.js b/src/routes/user.js new file mode 100644 index 0000000..4be3b94 --- /dev/null +++ b/src/routes/user.js @@ -0,0 +1,86 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const { query } = require('../database/connection'); +const { ApiError } = require('../middleware/errorHandler'); + +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ + success: false, + error: { code: 'MISSING_TOKEN', message: 'Authorization token required' } + }); + } + + jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ + success: false, + error: { code: 'INVALID_TOKEN', message: 'Invalid or expired token' } + }); + } + req.userId = user.userId; + next(); + }); +} + +router.use(authenticateToken); + +router.get('/usage', async (req, res, next) => { + try { + const userId = req.userId; + + const usageResult = await query( + `SELECT + COUNT(*) as total_calls, + COUNT(*) FILTER (WHERE success = true) as successful_calls, + COUNT(*) FILTER (WHERE success = false) as failed_calls, + SUM(credits_used) as credits_used + FROM api_calls + WHERE user_id = $1 AND called_at >= DATE_TRUNC('month', NOW())`, + [userId] + ); + + const endpointResult = await query( + `SELECT endpoint, COUNT(*) as count + FROM api_calls + WHERE user_id = $1 AND called_at >= DATE_TRUNC('month', NOW()) + GROUP BY endpoint`, + [userId] + ); + + const userResult = await query( + 'SELECT monthly_quota, calls_this_month FROM users WHERE id = $1', + [userId] + ); + + const user = userResult.rows[0]; + const usage = usageResult.rows[0]; + const byEndpoint = {}; + endpointResult.rows.forEach(row => { + byEndpoint[row.endpoint] = parseInt(row.count); + }); + + res.json({ + success: true, + data: { + period: 'month', + total_calls: parseInt(usage.total_calls) || 0, + successful_calls: parseInt(usage.successful_calls) || 0, + failed_calls: parseInt(usage.failed_calls) || 0, + credits_used: parseInt(usage.credits_used) || 0, + quota: user.monthly_quota, + remaining: Math.max(0, user.monthly_quota - user.calls_this_month), + by_endpoint: byEndpoint + } + }); + + } catch (error) { + next(error); + } +}); + +module.exports = router; diff --git a/src/services/analytics.js b/src/services/analytics.js new file mode 100644 index 0000000..5c83e0b --- /dev/null +++ b/src/services/analytics.js @@ -0,0 +1,21 @@ +const { query } = require('../database/connection'); + +async function logApiCall({ userId, apiKeyId, endpoint, method, params, status, duration, success, isTestKey, errorMessage = null }) { + try { + await query( + `INSERT INTO api_calls + (user_id, api_key_id, endpoint, method, request_params, response_status, response_time_ms, success, error_message, credits_used, is_billable) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + [userId, apiKeyId, endpoint, method, JSON.stringify(params || {}), status, duration, success, errorMessage, success ? 1 : 0, !isTestKey && success] + ); + + if (success && !isTestKey) { + await query('UPDATE users SET calls_this_month = calls_this_month + 1 WHERE id = $1', [userId]); + await query('UPDATE api_keys SET last_used_at = NOW(), total_calls = total_calls + 1 WHERE id = $1', [apiKeyId]); + } + } catch (error) { + console.error('Log error:', error.message); + } +} + +module.exports = { logApiCall }; diff --git a/src/services/bankService.js b/src/services/bankService.js new file mode 100644 index 0000000..33cb123 --- /dev/null +++ b/src/services/bankService.js @@ -0,0 +1,131 @@ + +const axios = require('axios'); +axios.defaults.headers.common['Authorization'] = `Bearer ${process.env.BANK_PROVIDER_KEY}`; +axios.defaults.headers.post['Content-Type'] = 'application/json'; +const { cacheGet, cacheSet } = require('../cache/redis'); +const { query } = require('../database/connection'); + +async function verifyBankAccount(accountNumber, ifsc, name = null) { + try { + const cacheKey = `bank:${ifsc}:${accountNumber}`; + const cached = await cacheGet(cacheKey); + if (cached) { + if (name) { + cached.name_match = cached.name_at_bank === name.toUpperCase(); + cached.name_match_score = cached.name_match ? 100 : 0; + } + return { success: true, data: cached }; + } + + // Get bank details from IFSC + const ifscResult = await query('SELECT bank_name, branch FROM ifsc_codes WHERE ifsc = $1', [ifsc.toUpperCase()]); + + if (ifscResult.rows.length === 0) { + return { success: false, statusCode: 400, errorCode: 'INVALID_IFSC', message: 'IFSC not found' }; + } + + const bankInfo = ifscResult.rows[0]; + + // Temporarily mock external bank provider response for testing + const mockResponseData = { + status_code: 200, + data: { + account_exists: true, + name_at_bank: name || "DUMMY ACCOUNT HOLDER", + account_holder_name: name || "DUMMY ACCOUNT HOLDER", + branch: bankInfo.branch + } + }; + const response = { data: mockResponseData }; + + // Commenting out the actual external API call for now + // const response = await axios.post( + // process.env.BANK_PROVIDER_URL, + // { + // account_number: accountNumber, + // ifsc: ifsc.toUpperCase(), + // name: name + // }, + // { + // headers: { + // 'Authorization': `Bearer ${process.env.BANK_PROVIDER_KEY}`, + // 'Content-Type': 'application/json' + // }, + // timeout: 30000 + // } + // ); + + if (!response.data || response.data.status_code !== 200) { + return { success: false, statusCode: 404, errorCode: 'ACCOUNT_NOT_FOUND', message: 'Account not found' }; + } + + const d = response.data.data; + + const data = { + account_number: accountNumber, + ifsc: ifsc.toUpperCase(), + account_exists: d.account_exists !== false, + name_at_bank: d.name_at_bank || d.account_holder_name || '', + name_match: name ? (d.name_at_bank || d.account_holder_name || '').toUpperCase() === name.toUpperCase() : null, + name_match_score: name ? (d.name_at_bank || d.account_holder_name || '').toUpperCase() === name.toUpperCase() ? 100 : 0 : null, + bank_name: bankInfo.bank_name, + branch: bankInfo.branch || d.branch || '' + }; + + await cacheSet(cacheKey, data, 86400); + + // Persist bank verification result to Postgres (best-effort, non-blocking) + try { + await query( + `INSERT INTO bank_verifications ( + account_number, + ifsc, + name, + account_exists, + name_match, + name_match_score, + bank_name, + branch, + requested_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + [ + data.account_number, + data.ifsc, + data.name_at_bank, + data.account_exists, + data.name_match, + data.name_match_score, + data.bank_name, + data.branch, + null, // requested_by (user id) - can be wired from route later if needed + ] + ); + } catch (dbError) { + console.error('Failed to store bank verification in database:', dbError.message || dbError); + } + + return { success: true, data }; + + } catch (error) { + // Surface provider details when available to avoid generic 500s + if (error.code === 'ECONNABORTED') { + return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; + } + + const providerStatus = error.response?.status || error.response?.data?.status_code; + const providerMessage = error.response?.data?.message || error.message; + + if (providerStatus) { + return { + success: false, + statusCode: providerStatus, + errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', + message: providerMessage || 'Provider error' + }; + } + + return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; + } +} + +module.exports = { verifyBankAccount }; diff --git a/src/services/gstService.js b/src/services/gstService.js new file mode 100644 index 0000000..67078dc --- /dev/null +++ b/src/services/gstService.js @@ -0,0 +1,191 @@ +// const axios = require('axios'); +// const { cacheGet, cacheSet } = require('../cache/redis'); + +// const STATE_NAMES = { +// '01': 'Jammu & Kashmir', '02': 'Himachal Pradesh', '03': 'Punjab', +// '04': 'Chandigarh', '05': 'Uttarakhand', '06': 'Haryana', '07': 'Delhi', +// '08': 'Rajasthan', '09': 'Uttar Pradesh', '10': 'Bihar', '11': 'Sikkim', +// '12': 'Arunachal Pradesh', '13': 'Nagaland', '14': 'Manipur', '15': 'Mizoram', +// '16': 'Tripura', '17': 'Meghalaya', '18': 'Assam', '19': 'West Bengal', +// '20': 'Jharkhand', '21': 'Odisha', '22': 'Chhattisgarh', '23': 'Madhya Pradesh', +// '24': 'Gujarat', '26': 'Dadra & Nagar Haveli', '27': 'Maharashtra', +// '29': 'Karnataka', '30': 'Goa', '31': 'Lakshadweep', '32': 'Kerala', +// '33': 'Tamil Nadu', '34': 'Puducherry', '35': 'Andaman & Nicobar', +// '36': 'Telangana', '37': 'Andhra Pradesh', '38': 'Ladakh' +// }; + +// async function verifyGSTIN(gstin) { +// try { +// const cacheKey = `gst:${gstin}`; +// const cached = await cacheGet(cacheKey); +// if (cached) return { success: true, data: cached }; + +// const response = await axios.post( +// process.env.GST_PROVIDER_URL, +// { id_number: gstin }, +// { +// headers: { +// 'Authorization': `Bearer ${process.env.GST_PROVIDER_KEY}`, +// 'Content-Type': 'application/json' +// }, +// timeout: 30000 +// } +// ); + +// if (!response.data || response.data.status_code !== 200) { +// return { success: false, statusCode: 404, errorCode: 'GSTIN_NOT_FOUND', message: 'GSTIN not found' }; +// } + +// const d = response.data.data; + +// const data = { +// gstin, +// legal_name: d.legal_name || d.lgnm, +// trade_name: d.trade_name || d.tradeNam, +// status: d.status || d.sts, +// registration_date: d.registration_date || d.rgdt, +// last_updated: d.last_update || d.lstupdt, +// business_type: d.business_type || d.ctb, +// constitution: d.constitution || d.ctj, +// state: d.state || STATE_NAMES[gstin.substring(0, 2)], +// state_code: gstin.substring(0, 2), +// pan: gstin.substring(2, 12), +// address: { +// building: d.address?.bno || d.pradr?.addr?.bno || '', +// floor: d.address?.flno || d.pradr?.addr?.flno || '', +// street: d.address?.st || d.pradr?.addr?.st || '', +// locality: d.address?.loc || d.pradr?.addr?.loc || '', +// city: d.address?.city || d.pradr?.addr?.city || '', +// district: d.address?.dst || d.pradr?.addr?.dst || '', +// state: d.address?.stcd || d.pradr?.addr?.stcd || '', +// pincode: d.address?.pncd || d.pradr?.addr?.pncd || '' +// }, +// nature_of_business: d.nature_of_business || d.nba || [], +// filing_status: { +// gstr1: d.filing_status?.gstr1 || 'Unknown', +// gstr3b: d.filing_status?.gstr3b || 'Unknown', +// last_filed_date: d.filing_status?.last_filed || null +// } +// }; + +// await cacheSet(cacheKey, data, 86400); +// return { success: true, data }; + +// } catch (error) { +// // Surface provider details when available to avoid generic 500s +// if (error.code === 'ECONNABORTED') { +// return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; +// } + +// const providerStatus = error.response?.status || error.response?.data?.status_code; +// const providerMessage = error.response?.data?.message || error.message; + +// if (providerStatus) { +// return { +// success: false, +// statusCode: providerStatus, +// errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', +// message: providerMessage || 'Provider error' +// }; +// } + +// return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; +// } +// } + +// module.exports = { verifyGSTIN }; +const { cacheGet, cacheSet } = require('../cache/redis'); +const { query } = require('../database/connection'); + +const STATE_NAMES = { + '01': 'Jammu & Kashmir', '02': 'Himachal Pradesh', '03': 'Punjab', + '04': 'Chandigarh', '05': 'Uttarakhand', '06': 'Haryana', '07': 'Delhi', + '08': 'Rajasthan', '09': 'Uttar Pradesh', '10': 'Bihar', '11': 'Sikkim', + '12': 'Arunachal Pradesh', '13': 'Nagaland', '14': 'Manipur', '15': 'Mizoram', + '16': 'Tripura', '17': 'Meghalaya', '18': 'Assam', '19': 'West Bengal', + '20': 'Jharkhand', '21': 'Odisha', '22': 'Chhattisgarh', '23': 'Madhya Pradesh', + '24': 'Gujarat', '26': 'Dadra & Nagar Haveli', '27': 'Maharashtra', + '29': 'Karnataka', '30': 'Goa', '31': 'Lakshadweep', '32': 'Kerala', + '33': 'Tamil Nadu', '34': 'Puducherry', '35': 'Andaman & Nicobar', + '36': 'Telangana', '37': 'Andhra Pradesh', '38': 'Ladakh' +}; + +async function verifyGSTIN(gstin) { + try { + const cacheKey = `gst:${gstin}`; + const cached = await cacheGet(cacheKey); + if (cached) return { success: true, data: cached }; + + // Look up GSTIN in the local database seeded from gst.csv + const result = await query( + 'SELECT * FROM gst_registrations WHERE gstin = $1', + [gstin] + ); + + if (!result.rows.length) { + return { success: false, statusCode: 404, errorCode: 'GSTIN_NOT_FOUND', message: 'GSTIN not found' }; + } + + const d = result.rows[0]; + + // nature_of_business in CSV is stored as a single string like "Manufacturing|Services" + const natureOfBusinessArray = d.nature_of_business + ? String(d.nature_of_business).split('|').map((v) => v.trim()).filter(Boolean) + : []; + + const data = { + gstin, + legal_name: d.legal_name, + trade_name: d.trade_name, + status: d.status, + registration_date: d.registration_date, + last_updated: d.last_updated, + business_type: d.business_type, + constitution: d.constitution, + state: d.state || STATE_NAMES[gstin.substring(0, 2)], + state_code: gstin.substring(0, 2), + pan: gstin.substring(2, 12), + address: { + building: d.address_building || '', + floor: d.address_floor || '', + street: d.address_street || '', + locality: d.address_locality || '', + city: d.address_city || '', + district: d.address_district || '', + state: d.address_state_code || '', + pincode: d.address_pincode || '' + }, + nature_of_business: natureOfBusinessArray, + filing_status: { + gstr1: d.filing_status_gstr1 || 'Unknown', + gstr3b: d.filing_status_gstr3b || 'Unknown', + last_filed_date: d.filing_last_filed_date || null + } + }; + + await cacheSet(cacheKey, data, 86400); + return { success: true, data }; + + } catch (error) { + // Surface provider details when available to avoid generic 500s + if (error.code === 'ECONNABORTED') { + return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; + } + + const providerStatus = error.response?.status || error.response?.data?.status_code; + const providerMessage = error.response?.data?.message || error.message; + + if (providerStatus) { + return { + success: false, + statusCode: providerStatus, + errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', + message: providerMessage || 'Provider error' + }; + } + + return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; + } +} + +module.exports = { verifyGSTIN }; diff --git a/src/services/panService.js b/src/services/panService.js new file mode 100644 index 0000000..423da42 --- /dev/null +++ b/src/services/panService.js @@ -0,0 +1,140 @@ +const axios = require('axios'); +const { cacheGet, cacheSet } = require('../cache/redis'); +const { query } = require('../database/connection'); + +const PAN_TYPES = { + 'P': 'Individual', + 'C': 'Company', + 'H': 'HUF', + 'A': 'AOP', + 'B': 'BOI', + 'G': 'Government', + 'J': 'Artificial Juridical Person', + 'L': 'Local Authority', + 'F': 'Firm/Partnership', + 'T': 'Trust' +}; + +async function verifyPAN(pan, name = null, dob = null) { + try { + const cacheKey = `pan:${pan}`; + const cached = await cacheGet(cacheKey); + if (cached) { + if (name) { + cached.name_match = cached.name === name.toUpperCase(); + cached.name_match_score = cached.name_match ? 100 : 0; + } + return { success: true, data: cached }; + } + + + + const parts = name.trim().split(" "); + + const firstName = parts[0] || "DUMMY"; + const lastName = parts.slice(1).join(" ") || "NAME"; + + const mockResponseData = { + status_code: 200, + data: { + name: name || "DUMMY NAME", + status: "ACTIVE", + type: PAN_TYPES[pan[3]] || 'Individual', + full_name: name || "DUMMY NAME", + last_name: lastName, + first_name: firstName, + } + }; + const response = { data: mockResponseData }; + + // Commenting out the actual external API call for now + // const response = await axios.post( + // process.env.PAN_PROVIDER_URL, + // { + // id_number: pan, + // name: name, + // dob: dob + // }, + // { + // headers: { + // 'Authorization': `Bearer ${process.env.PAN_PROVIDER_KEY}`, + // 'Content-Type': 'application/json' + // }, + // timeout: 30000 + // } + // ); + + if (!response.data || response.data.status_code !== 200) { + return { success: false, statusCode: 404, errorCode: 'PAN_NOT_FOUND', message: 'PAN not found' }; + } + + const d = response.data.data; + const panType = PAN_TYPES[pan[3]] || 'Unknown'; + + const data = { + pan, + name: d.name || d.full_name || '', + status: d.status || 'Valid', + type: d.type || panType, + name_match: name ? (d.name || d.full_name || '').toUpperCase() === name.toUpperCase() : null, + name_match_score: name ? (d.name || d.full_name || '').toUpperCase() === name.toUpperCase() ? 100 : 0 : null, + last_name: d.last_name || d.surname || '', + first_name: d.first_name || '', + middle_name: d.middle_name || '', + title: d.title || '' + }; + + await cacheSet(cacheKey, data, 86400); + + // Persist PAN verification result to Postgres (best-effort, non-blocking) + try { + await query( + `INSERT INTO pan_verifications ( + pan, + name, + status, + pan_type, + name_match, + name_match_score, + requested_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + data.pan, + data.name, + data.status, + data.type, + data.name_match, + data.name_match_score, + null, // requested_by (user id) - can be wired from route later if needed + ] + ); + } catch (dbError) { + console.error('Failed to store PAN verification in database:', dbError.message || dbError); + } + + return { success: true, data }; + + } catch (error) { + // Surface provider details when available to avoid generic 500s + if (error.code === 'ECONNABORTED') { + return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; + } + + const providerStatus = error.response?.status || error.response?.data?.status_code; + const providerMessage = error.response?.data?.message || error.message; + + if (providerStatus) { + return { + success: false, + statusCode: providerStatus, + errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', + message: providerMessage || 'Provider error' + }; + } + + return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; + } +} + +module.exports = { verifyPAN }; +