diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95c3690..6a617b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, dev] pull_request: jobs: @@ -27,6 +27,9 @@ jobs: - name: Verify pnpm run: pnpm -v + - name: Force npm registry + run: pnpm config set registry https://registry.npmjs.org/ + - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/apps/core-api/package-lock.json b/apps/core-api/package-lock.json new file mode 100644 index 0000000..455649d --- /dev/null +++ b/apps/core-api/package-lock.json @@ -0,0 +1,2662 @@ +{ + "name": "core-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "core-api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "amqplib": "^0.10.9", + "bcrypt": "^6.0.0", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "ioredis": "^5.9.2", + "jsonwebtoken": "^9.0.3", + "meilisearch": "^0.55.0", + "mongoose": "^9.1.5", + "morgan": "^1.10.1", + "winston": "^3.19.0" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "@types/bcrypt": "^6.0.0", + "@types/express": "^5.0.6", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/node": "^25.0.10", + "nodemon": "^3.1.11", + "ts-node": "^10.9.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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==", + "dev": true + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "dev": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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==", + "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==" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "engines": { + "node": ">=20.19.0" + } + }, + "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==" + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "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==", + "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==", + "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, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "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==", + "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==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "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==", + "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==", + "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==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "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==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "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, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "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, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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, + "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==", + "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, + "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==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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 + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ioredis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "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==", + "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, + "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, + "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, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "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==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", + "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "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==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "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==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/meilisearch": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.55.0.tgz", + "integrity": "sha512-qSMeiezfDgIqciIeYzh5E4pXDZZD7CtHeWDCs43kN3trLgl5FtfmBAIkljL3huFaOx08feYtC8FfIFUpVwq6rg==" + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.0.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.4.tgz", + "integrity": "sha512-XNh+jiztVMddDFDCv8TWxVxi/rGx+0FfsK3Ftj6hcYzEmhTcos2uC144OJRmUFPHSu3hJr6Pgip++Ab2+Da35Q==", + "dependencies": { + "kareem": "3.2.0", + "mongodb": "~7.0", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "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==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "engines": { + "node": ">=20.19.0" + } + }, + "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==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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, + "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==", + "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==", + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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 + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/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" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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==", + "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==", + "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==", + "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==", + "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/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, + "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, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "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, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "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, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "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, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/apps/core-api/package.json b/apps/core-api/package.json index 4f9827a..2d4e21c 100644 --- a/apps/core-api/package.json +++ b/apps/core-api/package.json @@ -16,8 +16,11 @@ "license": "ISC", "packageManager": "pnpm@10.27.0", "dependencies": { + "@aws-sdk/client-s3": "^3.1006.0", + "@aws-sdk/s3-request-presigner": "^3.1006.0", "amqplib": "^0.10.9", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "dotenv": "^17.2.3", "express": "^5.2.1", "ioredis": "^5.9.2", @@ -25,15 +28,19 @@ "meilisearch": "^0.55.0", "mongoose": "^9.1.5", "morgan": "^1.10.1", + "node-cron": "^4.2.1", + "nodemailer": "^8.0.1", "winston": "^3.19.0" }, "devDependencies": { "@types/amqplib": "^0.10.8", "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/morgan": "^1.9.10", "@types/node": "^25.0.10", + "@types/nodemailer": "^7.0.9", "nodemon": "^3.1.11", "ts-node": "^10.9.2", "tsx": "^4.21.0", diff --git a/apps/core-api/src/cache/redis.ts b/apps/core-api/src/cache/redis.ts index bf9c88b..ac874b3 100644 --- a/apps/core-api/src/cache/redis.ts +++ b/apps/core-api/src/cache/redis.ts @@ -1,20 +1,20 @@ -import IORedis from "ioredis"; -import type {Redis} from "ioredis"; +// import IORedis from "ioredis"; +// import type {Redis} from "ioredis"; -import { logger } from "../utils/logger.js"; -const redis_url = process.env.REDIS_URL -if (!redis_url){ - throw new Error("REDIS_URL is not defined in environment variables") -} -export const redis:Redis = new IORedis.default(redis_url) -redis.on("connect", ()=>{ - logger.info("Redis connected successfully") -}) +// import { logger } from "../utils/logger.js"; +// const redis_url = process.env.REDIS_URL +// if (!redis_url){ +// throw new Error("REDIS_URL is not defined in environment variables") +// } +// export const redis:Redis = new IORedis.default(redis_url) +// redis.on("connect", ()=>{ +// logger.info("Redis connected successfully") +// }) -redis.on("reconnecting", () => { - logger.warn("Redis reconnecting.."); -}); +// redis.on("reconnecting", () => { +// logger.warn("Redis reconnecting.."); +// }); -redis.on("error", (err:unknown)=>{ - logger.error(`Redis error:${String(err)}`) -}) +// redis.on("error", (err:unknown)=>{ +// logger.error(`Redis error:${String(err)}`) +// }) diff --git a/apps/core-api/src/controllers/admin.controllers.ts b/apps/core-api/src/controllers/admin.controllers.ts new file mode 100644 index 0000000..497668b --- /dev/null +++ b/apps/core-api/src/controllers/admin.controllers.ts @@ -0,0 +1,48 @@ +import type { Request, Response, NextFunction } from "express"; + +import { approveSignupService, getAllPendingSignupService, rejectSignupService } from "../services/verification.service.js"; + +export const approveSignupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email } = req.body; + + const result = await approveSignupService(email); + + res.status(200).json({ success: true, data: result }); + } catch (error) { + next(error); + } +}; + +export const rejectSignupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, reason } = req.body; + + const result = await rejectSignupService(email, reason); + + res.status(200).json({ success: true, data: result }); + } catch (error) { + next(error); + } +}; + +export const getAllpendingSignupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const requests = await getAllPendingSignupService(req.query); + res.status(200).json({ success: true, data: requests }); + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/controllers/auth.controller.ts b/apps/core-api/src/controllers/auth.controller.ts index 65ba41f..5861a41 100644 --- a/apps/core-api/src/controllers/auth.controller.ts +++ b/apps/core-api/src/controllers/auth.controller.ts @@ -1,9 +1,55 @@ -import type { NextFunction, Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; -import { loginService } from "../services/auth.service.js"; +import { + logoutService, + refreshTokenService, + loginService, + signupService, + forgotPasswordService, + verifyOtpService, + resetPasswordService, + verifySignupOtpService, + +} from "../services/auth.service.js"; import type { HttpError } from "../modules/auth/http-error.js"; +// import { log } from "winston"; + + +// ================= SIGNUP ================= +export const signupController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + + const { email, password, role, documents: businessInfo } = req.body; + + if (!email || !password || !role) { + const err: HttpError = new Error("Missing required fields"); + err.statusCode = 400; + throw err; + } + + const result = await signupService({ + email, + password, + role, + documents: businessInfo, + }); + + res.status(201).json({ + success: true, + message: "Signup successful. Please verify your email.", + data: result, + }); + } catch (error) { + next(error); + } +}; +// ================= LOGIN ================= export const loginController = async ( req: Request, res: Response, @@ -20,11 +66,210 @@ export const loginController = async ( const data = await loginService(email, password); + // Set refresh token as HttpOnly cookie + res.cookie("refreshToken", data.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + // Send only access token and user info in response + res.status(200).json({ + success: true, + data: { + accessToken: data.accessToken, + user: data.user, + }, + }); + } catch (error) { + next(error); + } +}; + + + + + +export const refreshTokenController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const refreshToken = req.cookies.refreshToken; + + if (!refreshToken) { + const err: HttpError = new Error("Refresh token missing"); + err.statusCode = 401; + throw err; + } + + const result = await refreshTokenService(refreshToken); + + // Set new refresh token as HttpOnly cookie + res.cookie("refreshToken", result.refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + // Send only access token in response + res.status(200).json({ + success: true, + data: { + accessToken: result.accessToken, + user: result.user, + + }, + }); + } catch (error) { + next(error); + } +}; + + +export const logoutController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = req.user?.userId; + + if (!userId) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const result = await logoutService(userId); + + // Clear refresh token cookie + res.clearCookie("refreshToken", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + res.status(200).json({ success: true, - data, + data: result, }); } catch (error) { next(error); } }; + +// ================= VERIFY SIGNUP OTP ================= +export const verifySignupOtpController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, otp } = req.body; + + if (!email || !otp) { + const err: HttpError = new Error("Email and OTP required"); + err.statusCode = 400; + throw err; + } + + await verifySignupOtpService(email, otp); + + res.status(200).json({ + success: true, + message: "Email verified successfully", + }); + } catch (error) { + next(error); + } +}; + + +// ================= FORGOT PASSWORD ================= +export const forgotPasswordController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email } = req.body; + + if (!email) { + const err: HttpError = new Error("Email is required"); + err.statusCode = 400; + throw err; + } + + await forgotPasswordService(email); + + res.status(200).json({ + success: true, + message: "If account exists, OTP sent", + + }); + + } catch (error) { + next(error); + } +}; + +// ================= VERIFY OTP ================= +export const verifyResetOtpController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, otp } = req.body; + + if (!email || !otp) { + const err: HttpError = new Error("Email and OTP required"); + err.statusCode = 400; + throw err; + } + + const resetSessionToken = await verifyOtpService(email, otp); + + res.status(200).json({ + success: true, + message: "OTP verified", + data: { resetSessionToken }, + }); + } catch (error) { + next(error); + } +}; + +// ================= RESET PASSWORD ================= +export const resetPasswordController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { email, newPassword, resetSessionToken } = req.body; + + if (!email || !newPassword || !resetSessionToken) { + const err: HttpError = new Error("Missing required fields"); + err.statusCode = 400; + throw err; + } + + await resetPasswordService(email, newPassword, resetSessionToken); + + res.status(200).json({ + success: true, + message: "Password reset successful", + }); + } catch (error) { + next(error); + } +}; + + + diff --git a/apps/core-api/src/controllers/availability.controller.ts b/apps/core-api/src/controllers/availability.controller.ts new file mode 100644 index 0000000..fe33c71 --- /dev/null +++ b/apps/core-api/src/controllers/availability.controller.ts @@ -0,0 +1,50 @@ +import type { NextFunction, Request, Response } from "express"; + +import { + getAvailabilityService, + setAvailabilityService +} from "../services/availability.service.js"; + +export const setAvailabilityController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { userId, role } = req.user!; + + const availability = await setAvailabilityService( + userId, + role, + req.body + ); + + res.status(200).json({ + success: true, + data: availability + }); + } catch (error) { + next(error); + } +}; + +export const getAvailabilityController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const influencerProfileId = req.params.influencerProfileId as string; + + const availability = await getAvailabilityService( + influencerProfileId + ); + + res.status(200).json({ + success: true, + data: availability + }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/apps/core-api/src/controllers/collaboration.controllers.ts b/apps/core-api/src/controllers/collaboration.controllers.ts new file mode 100644 index 0000000..e1ac44f --- /dev/null +++ b/apps/core-api/src/controllers/collaboration.controllers.ts @@ -0,0 +1,55 @@ +import type { Request, Response, NextFunction } from "express"; + +import { + inviteCollaboratorsService, + respondToCollaborationService +} from "../services/collaboration.service.js"; + +export const inviteCollaboratorsController = async ( + req: Request<{ id: string }>, + res: Response, + next: NextFunction +) => { + try { + const { userId } = req.user!; + const { id } = req.params; + + const collaborations = await inviteCollaboratorsService( + id, + userId, + req.body.collaboratorIds + ); + + res.status(201).json({ + success: true, + data: collaborations + }); + } catch (err) { + next(err); + } +}; + +export const respondToCollaborationController = async ( + req: Request<{ id: string }>, + res: Response, + next: NextFunction +) => { + try { + const { userId } = req.user!; + const { id } = req.params; + const { action } = req.body; + + const result = await respondToCollaborationService( + id, + userId, + action + ); + + res.status(200).json({ + success: true, + data: result + }); + } catch (err) { + next(err); + } +}; \ No newline at end of file diff --git a/apps/core-api/src/controllers/gig.controller.ts b/apps/core-api/src/controllers/gig.controller.ts new file mode 100644 index 0000000..68aa37f --- /dev/null +++ b/apps/core-api/src/controllers/gig.controller.ts @@ -0,0 +1,325 @@ +import type { NextFunction, Request, Response } from "express"; + +import { getGigDetailsService, getMyGigsService, listGigsService, publishGigService, updateGigDeliverablesService, updateGigPricingService } from "../services/gig.service.js"; +import type { GigDeliverable } from "../types/gig.type.js"; +import { getChannel } from "../queue/rabbit.js"; +import { createGigService, deleteGigService, editGigService } from "../services/gig.service.js"; +import type { AuthRequest } from "../middlewares/auth.middleware.js"; + + + +// ================= CREATE GIG ================= + +export const GIG_CREATED_EVENT = "gig.created"; + +/* ================= CREATE GIG ================= */ + + +export const createGigController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { userId, role } = req.user!; + + const result = await createGigService( + userId, + role, + req.body + ); + + + + getChannel().sendToQueue( + GIG_CREATED_EVENT, + Buffer.from(JSON.stringify({ + gigId: result.gig._id.toString(), + title: result.gig.title, + category: result.gig.category, + pricing: result.gig.pricing.basePrice, + influencerId: result.gig.primaryInfluencerId.toString(), + createdAt: result.gig.createdAt, + })), + { persistent: true } + ); + + + + return res.status(201).json({ + success: true, + message: + result.collaborators.length > 0 + ? "Gig created as draft. Waiting for collaborator approval." + : "Gig created and published successfully.", + data: result.gig + }); + } catch (error) { + next(error); + } +}; + +/* ================= LIST GIGS ================= */ + +export const listGigsController = async ( + req: Request, + res: Response +) => { + try { + const page = Number(req.query.page) || 1; + const limit = Number(req.query.limit) || 10; + + const result = await listGigsService(req.query, page, limit); + + return res.status(200).json(result); + } catch (error: unknown) { + console.error("Error listing gigs:", error); + + return res.status(500).json({ + message: "Internal server error" + }); + } +}; + +/* ================= GET GIG DETAILS ================= */ + +interface HttpError extends Error { + statusCode?: number; +} + +export const getGigDetailsController = async ( + req: Request<{ id: string }>, + res: Response +) => { + try { + const { id } = req.params; + + const gig = await getGigDetailsService(id); + + return res.status(200).json(gig); + } catch (error: unknown) { + console.error("Error fetching gig details:", error); + + const err = error as HttpError; + + return res.status(err.statusCode || 500).json({ + message: err.message || "Internal server error" + }); + } +}; + + +// ================= EDIT GIG ================= +export const editGigController = async ( + req: AuthRequest, + res: Response, + next: NextFunction +) => { + try { + const { id } = req.params; + + // 🔥 Type-safe ID guard + if (typeof id !== "string") { + return res.status(400).json({ + message: "Invalid Gig ID" + }); + } + + if (!req.user) { + return res.status(401).json({ + message: "Unauthorized" + }); + } + + const updatedGig = await editGigService( + id, + req.user, + req.body + ); + + return res.status(200).json({ + success: true, + data: updatedGig + }); + + } catch (error) { + next(error); + } +}; + + + + +//* ================= DELETE GIG ================= + +export const deleteGigController = async ( + req: AuthRequest, + res: Response +) => { + try { + const { id } = req.params; + + // 1️⃣ Validate ID exists and is string + if (!id || Array.isArray(id)) { + return res.status(400).json({ + message: "Invalid Gig ID" + }); + } + + // 2️⃣ Ensure authenticated user exists + if (!req.user) { + return res.status(401).json({ + message: "Unauthorized" + }); + } + + await deleteGigService(id, req.user); + + return res.status(200).json({ + message: "Gig deleted successfully" + }); + + } catch (error: unknown) { + console.error("Error deleting gig:", error); + + if (error instanceof Error) { + const statusCode = + (error as { statusCode?: number }).statusCode ?? 500; + + return res.status(statusCode).json({ + message: error.message + }); + } + + return res.status(500).json({ + message: "Internal server error" + }); + } +}; + +/* ================= PUBLISH GIG ================= */ + +export const publishGigController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { userId } = req.user!; + + const id = req.params.id as string; + + if (!id) { + return res.status(400).json({ + success: false, + message: "Gig id is required" + }); + } + + const gig = await publishGigService(id, userId); + + res.status(200).json({ + success: true, + message: "Gig published successfully", + data: gig + }); + } catch (err) { + next(err); + } +}; + +/* ================= UPDATE DELIVERABLES ================= */ + +export const updateGigDeliverablesController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { userId } = req.user!; + const id = req.params.id as string; + + if (!id) { + return res.status(400).json({ + success: false, + message: "Gig id is required" + }); + } + + const deliverables = req.body as GigDeliverable[]; + + const gig = await updateGigDeliverablesService( + id, + userId, + deliverables + ); + + res.status(200).json({ + success: true, + message: "Deliverables updated successfully", + data: gig + }); + } catch (error) { + next(error); + } +}; + +/* ================= UPDATE PRICING ================= */ + +export const updateGigPricingController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { userId } = req.user!; + const id = req.params.id as string; + + if (!id) { + return res.status(400).json({ + success: false, + message: "Gig id is required" + }); + } + + const gig = await updateGigPricingService( + id, + userId, + req.body + ); + + res.status(200).json({ + success: true, + message: "Pricing updated successfully", + data: gig + }); + } catch (error) { + next(error); + } +}; + +export const getGigController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { id } = req.params; + + if (typeof id !== "string") { + return res.status(400).json({ + message: "Invalid Gig ID" + }); + } + + const gig = await getMyGigsService(id); + + res.status(200).json({ + success: true, + data: gig + }); + + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/apps/core-api/src/controllers/media.controller.ts b/apps/core-api/src/controllers/media.controller.ts new file mode 100644 index 0000000..b99d98f --- /dev/null +++ b/apps/core-api/src/controllers/media.controller.ts @@ -0,0 +1,28 @@ +import type { Request, Response, NextFunction } from "express"; + +import { generateUploadUrlService } from "../services/media.service.js"; + +export const generateUploadUrlController = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + + const { fileName, fileType, folder } = req.body; + + const result = await generateUploadUrlService( + folder, + fileName, + fileType + ); + + res.status(200).json({ + success: true, + data: result + }); + + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/controllers/profile.controller.ts b/apps/core-api/src/controllers/profile.controller.ts new file mode 100644 index 0000000..4e403d5 --- /dev/null +++ b/apps/core-api/src/controllers/profile.controller.ts @@ -0,0 +1,93 @@ +import type { Response, NextFunction } from "express"; + +import { + getMyProfileService, + updateProfileService, + getPublicInfluencerProfileService +} from "../services/profile.service.js"; +import type { AuthRequest } from "../middlewares/auth.middleware.js"; + + +export const getMyProfileController = async ( + req: AuthRequest, + res: Response, + next: NextFunction +) => { + try { + + if (!req.user) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const userId = req.user.userId; + + const profile = await getMyProfileService(userId); + + res.status(200).json({ + success: true, + data: profile + }); + + } catch (error) { + next(error); + } +}; + + +export const updateProfileController = async ( + req: AuthRequest, + res: Response, + next: NextFunction +) => { + try { + + if (!req.user) { + return res.status(401).json({ + success: false, + message: "Unauthorized", + }); + } + + const userId = req.user.userId; + + const profile = await updateProfileService(userId, req.body); + + res.status(200).json({ + success: true, + data: profile + }); + + } catch (error) { + next(error); + } +}; + +export const getPublicInfluencerProfileController = async ( + req: AuthRequest, + res: Response, + next: NextFunction +) => { + try { + const influencerId = req.params.id as string; + + if (!influencerId) { + return res.status(400).json({ + success: false, + message: "Influencer ID is required", + }); + } + + const data = await getPublicInfluencerProfileService(influencerId); + + res.status(200).json({ + success: true, + data + }); + + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/controllers/search.controller.ts b/apps/core-api/src/controllers/search.controller.ts new file mode 100644 index 0000000..cab7a05 --- /dev/null +++ b/apps/core-api/src/controllers/search.controller.ts @@ -0,0 +1,37 @@ +import type { Request, Response, NextFunction } from "express"; + +import { searchIndex } from "../search/services/search.service.js"; +import { buildGigFilters } from "../search/filters/gig.filter.js"; + +export const searchGigs = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const { q = "", page = 1, limit = 10 } = req.query; + + const filters = buildGigFilters(req.query); + + const options: { limit?: number; offset?: number; filter?: string[] } = { + limit: Number(limit), + offset: (Number(page) - 1) * Number(limit), + }; + + if (filters.length > 0) { + options.filter = filters; + } + + const result = await searchIndex("gigs", q as string, options); + + res.status(200).json({ + hits: result.hits, + total: result.estimatedTotalHits, + page: Number(page), + limit: Number(limit), + }); + + } catch (error) { + next(error); + } +}; diff --git a/apps/core-api/src/db/connect.ts b/apps/core-api/src/db/connect.ts index 701858b..70c0316 100644 --- a/apps/core-api/src/db/connect.ts +++ b/apps/core-api/src/db/connect.ts @@ -10,8 +10,10 @@ if (!mongoUri) { export const connectDB = async () => { try { - await mongoose.connect(mongoUri); + await mongoose.connect(mongoUri, { dbName: "noillin" }); logger.info("MongoDB connected successfully"); + logger.info(`CONNECTED DB: ${mongoose.connection.name}`); + } catch (err: unknown) { logger.error(`MongoDB connection failed: ${String(err)}`); process.exit(1); diff --git a/apps/core-api/src/lib/s3.client.ts b/apps/core-api/src/lib/s3.client.ts new file mode 100644 index 0000000..400c57e --- /dev/null +++ b/apps/core-api/src/lib/s3.client.ts @@ -0,0 +1,9 @@ +import { S3Client } from "@aws-sdk/client-s3"; + +export const s3Client = new S3Client({ + region: process.env.AWS_REGION!, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID!, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY! + } +}); \ No newline at end of file diff --git a/apps/core-api/src/middlewares/auth.middleware.ts b/apps/core-api/src/middlewares/auth.middleware.ts index 12edcb9..98daf1b 100644 --- a/apps/core-api/src/middlewares/auth.middleware.ts +++ b/apps/core-api/src/middlewares/auth.middleware.ts @@ -2,6 +2,8 @@ import type { Request, Response, NextFunction } from "express"; import { verifyAccessToken } from "../modules/auth/auth.utils.js"; import type { JwtPayload } from "../modules/auth/auth.utils.js"; +import { RolePermissions } from "../rbac/role-permission.js"; +import type { Permission } from "../rbac/permission.js"; export interface AuthRequest extends Request { user?: JwtPayload; @@ -43,3 +45,18 @@ export const authenticate = ( } }; +export const authorizePermission = (permission: Permission) => { + return (req: AuthRequest, _res: Response, next: NextFunction) => { + if (!req.user) { + return next(Object.assign(new Error("Unauthorized"), { statusCode: 401 })); + } + + const permissions = RolePermissions[req.user.role] ?? []; + + if (!permissions.includes(permission)) { + return next(Object.assign(new Error("Forbidden"), { statusCode: 403 })); + } + + next(); + }; +}; diff --git a/apps/core-api/src/models/availability.model.ts b/apps/core-api/src/models/availability.model.ts new file mode 100644 index 0000000..9042869 --- /dev/null +++ b/apps/core-api/src/models/availability.model.ts @@ -0,0 +1,95 @@ +import { Schema, model } from "mongoose"; + +import type { AvailabilityDocument } from "../types/availability.types.js"; + +const TimeSlotSchema = new Schema( + { + startTime: { + type: String, + required: true + }, + endTime: { + type: String, + required: true + } + }, + { _id: false } +); + +const WeeklyRuleSchema = new Schema( + { + day: { + type: String, + enum: [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday" + ], + required: true + }, + isEnabled: { + type: Boolean, + default: false + }, + slots: { + type: [TimeSlotSchema], + default: [] + } + }, + { _id: false } +); + +const DateOverrideSchema = new Schema( + { + date: { + type: String, + required: true + }, + isAvailable: { + type: Boolean, + required: true + }, + slots: { + type: [TimeSlotSchema], + default: [] + } + }, + { _id: false } +); + +const AvailabilitySchema = new Schema( + { + influencerProfileId: { + type: Schema.Types.ObjectId, + ref: "InfluencerProfile", + required: true, + unique: true, + index: true + }, + + timezone: { + type: String, + required: true + }, + + weeklyRules: { + type: [WeeklyRuleSchema], + default: [] + }, + + dateOverrides: { + type: [DateOverrideSchema], + default: [] + } + }, + { timestamps: true } +); + +export const AvailabilityModel = model( + "Availability", + AvailabilitySchema +); \ No newline at end of file diff --git a/apps/core-api/src/models/brand.model.ts b/apps/core-api/src/models/brand.model.ts new file mode 100644 index 0000000..7528d61 --- /dev/null +++ b/apps/core-api/src/models/brand.model.ts @@ -0,0 +1,89 @@ +import { Schema, model, Types, Document } from "mongoose"; + +export interface IBrandProfile extends Document { + userId: Types.ObjectId; + + companyName: string; + industry: string; + website?: string; + + contactPersonName: string; + contactEmail: string; + contactPhone?: string; + + businessRegistrationNumber?: string; + gstNumber?: string; + companySize?: string; + + documents: string[]; // S3 keys + + isProfileComplete: boolean; + isVerified: boolean; + profileImageUrl?: string, +} + +const BrandProfileSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true, + index: true, + }, + + companyName: { + type: String, + required: true, + trim: true, + }, + + industry: { + type: String, + required: true, + }, + + website: String, + + contactPersonName: { + type: String, + required: true, + }, + + contactEmail: { + type: String, + required: true, + lowercase: true, + }, + + contactPhone: String, + + businessRegistrationNumber: String, + gstNumber: String, + companySize: String, + + documents: { + type: [String], // store S3 keys + default: [], + }, + profileImageUrl: { + type: String, + default: "" + }, + isProfileComplete: { + type: Boolean, + default: false, + }, + + isVerified: { + type: Boolean, + default: false, + }, + }, + { timestamps: true } +); + +export const BrandProfile = model( + "BrandProfile", + BrandProfileSchema +); \ No newline at end of file diff --git a/apps/core-api/src/models/collaboration.model.ts b/apps/core-api/src/models/collaboration.model.ts new file mode 100644 index 0000000..074957e --- /dev/null +++ b/apps/core-api/src/models/collaboration.model.ts @@ -0,0 +1,53 @@ +import { Schema, model } from "mongoose"; + +import type { GigCollaborationDocument } from "../types/collaboration.type.js"; + + +const GigCollaborationSchema = + new Schema( + { + gigId: { + type: Schema.Types.ObjectId, + ref: "Gig", + required: true, + index: true + }, + + primaryInfluencerId: { + type: Schema.Types.ObjectId, + ref: "InfluencerProfile", + required: true, + index: true + }, + + invitedInfluencerId: { + type: Schema.Types.ObjectId, + ref: "InfluencerProfile", + required: true, + index: true + }, + + status: { + type: String, + enum: ["pending", "accepted", "rejected", "cancelled"], + default: "pending", + index: true + }, + + respondedAt: { + type: Date + } + }, + { timestamps: true } + ); + +// Prevent duplicate invitations +GigCollaborationSchema.index( + { gigId: 1, invitedInfluencerId: 1 }, + { unique: true } +); + +export const GigCollaborationModel = model( + "GigCollaboration", + GigCollaborationSchema +); diff --git a/apps/core-api/src/models/gig.model.ts b/apps/core-api/src/models/gig.model.ts new file mode 100644 index 0000000..83c235e --- /dev/null +++ b/apps/core-api/src/models/gig.model.ts @@ -0,0 +1,137 @@ +import { Schema, model } from "mongoose"; + +import type { GigDocument } from "../types/gig.type.js"; + +const DeliverableSchema = new Schema( + { + contentType: { + type: String, + required: true + }, + quantity: { + type: Number, + required: true, + min: 1 + }, + includedItems: [ + { + type: String + } + ] + }, + { _id: false } +); + +const GigSchema = new Schema( + { + title: { + type: String, + required: true, + trim: true + }, + + shortDescription: { + type: String, + required: true, + maxlength: 180 + }, + + platform: { + type: String, + enum: ["instagram", "youtube", "tiktok"], + required: true, + index: true + }, + + gigType: { + type: String, + enum: ["solo", "collaboration"], + required: true, + index: true + }, + + influencerIds: [ + { + type: Schema.Types.ObjectId, + ref: "InfluencerProfile", + required: true + } + ], + + primaryInfluencerId: { + type: Schema.Types.ObjectId, + ref: "InfluencerProfile", + required: true, + index: true + }, + + category: { + type: String, + required: true, + index: true + }, + + tags: [ + { + type: String, + index: true + } + ], + + deliverables: { + type: [DeliverableSchema], + default: [] + }, + + pricing: { + basePrice: { + type: Number, + required: true, + min: 0 + }, + currency: { + type: String, + enum: ["INR", "USD"], + required: true + }, + negotiationAllowed: { + type: Boolean, + default: false + }, + deliveryTimeInDays: { + type: Number, + required: true, + min: 1 + }, + revisionsIncluded: { + type: Number, + required: true, + min: 0 + } + }, + + maxBookingsPerSlot: { + type: Number, + min: 1 + }, + + status: { + type: String, + enum: ["draft", "published", "paused", "archived"], + default: "draft", + index: true + }, + + isDeleted: { + type: Boolean, + default: false, + index: true + } + }, + { timestamps: true } +); + +GigSchema.index({ influencerIds: 1 }); +GigSchema.index({ "pricing.basePrice": 1 }); + +export const GigModel = model("Gig", GigSchema); \ No newline at end of file diff --git a/apps/core-api/src/models/influencer.model.ts b/apps/core-api/src/models/influencer.model.ts new file mode 100644 index 0000000..c942e6b --- /dev/null +++ b/apps/core-api/src/models/influencer.model.ts @@ -0,0 +1,94 @@ +import { Schema, model, Types, Document } from "mongoose"; + +export interface IInfluencerProfile extends Document { + userId: Types.ObjectId; + + fullName: string; + username: string; + bio?: string; + + instagramUrl?: string; + youtubeUrl?: string; + tiktokUrl?: string; + + categories: string[]; + location?: string; + languages: string[]; + + followersCount?: number; + engagementRate?: number; + + isProfileComplete: boolean; + isVerified: boolean; + profileImageUrl?: string, +} + +const InfluencerProfileSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true, + index: true, + }, + + fullName: { + type: String, + default: "", + trim: true, + }, + + username: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + + bio: { + type: String, + maxlength: 500, + }, + profileImageUrl: { + type: String, + default: "" + }, + + instagramUrl: String, + youtubeUrl: String, + tiktokUrl: String, + + categories: { + type: [String], + default: [], + }, + + location: String, + + languages: { + type: [String], + default: [], + }, + + followersCount: Number, + engagementRate: Number, + + isProfileComplete: { + type: Boolean, + default: false, + }, + + isVerified: { + type: Boolean, + default: false, + }, + }, + { timestamps: true } +); + +export const InfluencerProfile = model( + "InfluencerProfile", + InfluencerProfileSchema +); \ No newline at end of file diff --git a/apps/core-api/src/models/pendingSignup.models.ts b/apps/core-api/src/models/pendingSignup.models.ts new file mode 100644 index 0000000..472ac30 --- /dev/null +++ b/apps/core-api/src/models/pendingSignup.models.ts @@ -0,0 +1,82 @@ +import { Schema, model } from "mongoose"; + +const PendingSignupSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true + }, + + passwordHash: { + type: String, + required: true + }, + + role: { + type: String, + enum: ["INFLUENCER", "BRAND", "ADMIN"], + required: true, + }, + adminLevel: { + type: String, + enum: ["SUPER", "NORMAL"], + default: null, + }, + documents: { type: String }, + + + status: { + type: String, + enum: ["PENDING", "APPROVED", "REJECTED"], + default: "PENDING", + }, + + // OTP FIELDS START HERE + + emailOtpHash: { + type: String, + select: false, + default: null, + + }, + + + emailOtpExpiresAt: { + type: Date, + default: null, + }, + + otpAttempts: { + type: Number, + default: 0, + }, + + otpResendCount: { + type: Number, + default: 0, + }, + + otpLastSentAt: { + type: Date, + default: null, + }, + + otpLockedUntil: { + type: Date, + default: null, + }, + + isEmailVerified: { + type: Boolean, + default: false, + }, + + }, + { timestamps: true } // gives createdAt & updatedAt automatically +); + +export const PendingSignup = model( + "PendingSignup", + PendingSignupSchema +); diff --git a/apps/core-api/src/models/user.model.ts b/apps/core-api/src/models/user.model.ts new file mode 100644 index 0000000..c2000d0 --- /dev/null +++ b/apps/core-api/src/models/user.model.ts @@ -0,0 +1,110 @@ +import { Schema, model, Document } from "mongoose"; + +export enum UserRole { + INFLUENCER = "INFLUENCER", + BRAND = "BRAND", + ADMIN = "ADMIN", +} + +export enum AdminLevel { + SUPER = "SUPER", + NORMAL = "NORMAL", +} + +export enum UserStatus { + PENDING = "PENDING", + ACTIVE = "ACTIVE", + SUSPENDED = "SUSPENDED", +} + +export interface IUser extends Document { + email: string; + password: string; + role: UserRole; + adminLevel?: AdminLevel; + isEmailVerified: boolean; + status: UserStatus; + refreshToken?: string; + + // Forgot Password Fields + resetOtp?: string; + resetOtpExpiry?: Date; + resetSessionToken?: string; + resetSessionExpiry?: Date; +} + +const UserSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + index: true, + immutable: true, + }, + + password: { + type: String, + required: true, + select: false, // never return password + }, + + role: { + type: String, + enum: Object.values(UserRole), + required: true, + }, + + adminLevel: { + type: String, + enum: Object.values(AdminLevel), + default: null, + }, + + isEmailVerified: { + type: Boolean, + default: false, + }, + + status: { + type: String, + enum: Object.values(UserStatus), + default: UserStatus.PENDING, + }, + + refreshToken: { + type: String, + select: false, + }, + + // ================================ + // FORGOT PASSWORD FIELDS + // ================================ + + resetOtp: { + type: String, + select: false, // never return OTP + }, + + resetOtpExpiry: { + type: Date, + }, + + resetSessionToken: { + type: String, + select: false, // never expose session token + }, + + resetSessionExpiry: { + type: Date, + }, + }, + { timestamps: true } +); + +// compound index (already yours) +UserSchema.index({ role: 1, status: 1 }); + +export const User = model("User", UserSchema); diff --git a/apps/core-api/src/modules/auth/auth.types.ts b/apps/core-api/src/modules/auth/auth.types.ts index fd13016..6dd6f27 100644 --- a/apps/core-api/src/modules/auth/auth.types.ts +++ b/apps/core-api/src/modules/auth/auth.types.ts @@ -1,4 +1,4 @@ -import { UserRole, AdminLevel } from "../users/user.model.js"; +import { UserRole, AdminLevel } from "../../models/user.model.js"; export interface JwtPayload { userId: string; diff --git a/apps/core-api/src/modules/auth/auth.utils.ts b/apps/core-api/src/modules/auth/auth.utils.ts index d39f884..d6fdac8 100644 --- a/apps/core-api/src/modules/auth/auth.utils.ts +++ b/apps/core-api/src/modules/auth/auth.utils.ts @@ -1,10 +1,10 @@ import jwt from "jsonwebtoken"; -import type { AdminLevel, UserRole } from "../users/user.model.js"; +import type { AdminLevel, UserRole } from "../../models/user.model.js"; -const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!; -const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!; +const ACCESS_SECRET = process.env.ACCESS_SECRET!; +const REFRESH_SECRET = process.env.REFRESH_SECRET!; export interface JwtPayload { userId: string; diff --git a/apps/core-api/src/modules/auth/http-error.ts b/apps/core-api/src/modules/auth/http-error.ts index 56b3949..c3cdd56 100644 --- a/apps/core-api/src/modules/auth/http-error.ts +++ b/apps/core-api/src/modules/auth/http-error.ts @@ -1,3 +1,16 @@ +// export interface HttpError extends Error { +// statusCode?: number; +// } + export interface HttpError extends Error { statusCode?: number; } + +export const createHttpError = ( + message: string, + statusCode: number +): HttpError => { + const error = new Error(message) as HttpError; + error.statusCode = statusCode; + return error; +}; \ No newline at end of file diff --git a/apps/core-api/src/modules/users/user.model.ts b/apps/core-api/src/modules/users/user.model.ts index cf162b0..92db70d 100644 --- a/apps/core-api/src/modules/users/user.model.ts +++ b/apps/core-api/src/modules/users/user.model.ts @@ -1,73 +1,110 @@ -import { Schema, model, Document } from "mongoose"; - -export enum UserRole { - INFLUENCER = "INFLUENCER", - BRAND = "BRAND", - ADMIN = "ADMIN", -} - -export enum AdminLevel { - SUPER = "SUPER", - NORMAL = "NORMAL", -} - -export enum UserStatus { - ACTIVE = "ACTIVE", - SUSPENDED = "SUSPENDED", -} - -export interface IUser extends Document { - email: string; - password: string; - role: UserRole; - adminLevel?: AdminLevel; - isEmailVerified: boolean; - status: UserStatus; - refreshToken?: string; -} - -const UserSchema = new Schema( - { - email: { - type: String, - required: true, - unique: true, - lowercase: true, - trim: true, - index: true, - immutable: true, - }, - password: { - type: String, - required: true, - select: false, - }, - role: { - type: String, - enum: Object.values(UserRole), - required: true, - }, - adminLevel: { - type: String, - enum: Object.values(AdminLevel), - default: null, - }, - isEmailVerified: { - type: Boolean, - default: false, - }, - status: { - type: String, - enum: Object.values(UserStatus), - default: UserStatus.ACTIVE, - }, - refreshToken: { - type: String, - select: false, - }, - }, - { timestamps: true } -); -UserSchema.index({ role: 1, status: 1 }); - -export const User = model("User", UserSchema) +import { Schema, model, Document } from "mongoose"; + +export enum UserRole { + INFLUENCER = "INFLUENCER", + BRAND = "BRAND", + ADMIN = "ADMIN", +} + +export enum AdminLevel { + SUPER = "SUPER", + NORMAL = "NORMAL", +} + +export enum UserStatus { + PENDING = "PENDING", + ACTIVE = "ACTIVE", + SUSPENDED = "SUSPENDED", +} + +export interface IUser extends Document { + email: string; + password: string; + role: UserRole; + adminLevel?: AdminLevel; + isEmailVerified: boolean; + status: UserStatus; + refreshToken?: string; + + // 🔐 Forgot Password Fields + resetOtp?: string; + resetOtpExpiry?: Date; + resetSessionToken?: string; + resetSessionExpiry?: Date; +} + +const UserSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + index: true, + immutable: true, + }, + + password: { + type: String, + required: true, + select: false, // 🔐 never return password + }, + + role: { + type: String, + enum: Object.values(UserRole), + required: true, + }, + + adminLevel: { + type: String, + enum: Object.values(AdminLevel), + default: null, + }, + + isEmailVerified: { + type: Boolean, + default: false, + }, + + status: { + type: String, + enum: Object.values(UserStatus), + default: UserStatus.PENDING, + }, + + refreshToken: { + type: String, + select: false, + }, + + // ================================ + // FORGOT PASSWORD FIELDS + // ================================ + + resetOtp: { + type: String, + select: false, // never return OTP + }, + + resetOtpExpiry: { + type: Date, + }, + + resetSessionToken: { + type: String, + select: false, // never expose session token + }, + + resetSessionExpiry: { + type: Date, + }, + }, + { timestamps: true } +); + +// compound index (already yours) +UserSchema.index({ role: 1, status: 1 }); + +export const User = model("User", UserSchema); diff --git a/apps/core-api/src/queue/rabbit.ts b/apps/core-api/src/queue/rabbit.ts index 2c0c074..862842b 100644 --- a/apps/core-api/src/queue/rabbit.ts +++ b/apps/core-api/src/queue/rabbit.ts @@ -1,26 +1,42 @@ -import amqp from "amqplib" +import amqp, { type Channel } from "amqplib"; -import { logger } from "../utils/logger.js" +import { logger } from "../utils/logger.js"; +import type { HttpError } from "../modules/auth/http-error.js"; -const rabbitUrl = process.env.RABBIT_URL as string +const rabbitUrl = process.env.RABBIT_URL as string; -export const connectRabbit = async() => { - if (!process.env.RABBIT_URL) { +let channel: Channel | null = null; + + +export const connectRabbit = async () => { + if (!rabbitUrl) { console.warn("RabbitMQ disabled: RABBIT_URL not set"); return; - } + } + + try { + const connection = await amqp.connect(rabbitUrl); + channel = await connection.createChannel(); + + logger.info("RabbitMQ connected (Core API)"); + + connection.on("error", (err) => { + logger.error("RabbitMQ connection error", err); + }); - try{ - const connection = await amqp.connect(rabbitUrl) - logger.info("RabbitMQ is connected") - connection.on("close", () => { - logger.warn("RabbitMQ connection closed"); -}); - - return connection - -} catch (err:unknown){ - logger.error(`RabbitMQ connection failed ${String(err)}`) -} -} + logger.warn("RabbitMQ connection closed"); + }); + + } catch (err: unknown) { + logger.error(`RabbitMQ connection failed ${String(err)}`); + } +}; + +export function getChannel():Channel{ + if(!channel){ + const err =new Error("RabbitMQ channel not initialized") as HttpError; + throw err; + } + return channel; +} \ No newline at end of file diff --git a/apps/core-api/src/rbac/permission.ts b/apps/core-api/src/rbac/permission.ts new file mode 100644 index 0000000..025fd9a --- /dev/null +++ b/apps/core-api/src/rbac/permission.ts @@ -0,0 +1,11 @@ +export enum Permission { + CREATE_PROFILE = "CREATE_PROFILE", + UPDATE_PROFILE = "UPDATE_PROFILE", + APPROVE_SIGNUP = "APPROVE_SIGNUP", + REJECT_SIGNUP = "REJECT_SIGNUP", + MANAGE_USERS = "MANAGE_USERS", + CREATE_GIG = "CREATE_GIG", + UPDATE_GIG = "UPDATE_GIG", + PUBLISH_GIG = "PUBLISH_GIG", + DELETE_GIG = "DELETE_GIG" +} diff --git a/apps/core-api/src/rbac/role-permission.ts b/apps/core-api/src/rbac/role-permission.ts new file mode 100644 index 0000000..c990df3 --- /dev/null +++ b/apps/core-api/src/rbac/role-permission.ts @@ -0,0 +1,26 @@ + +import { UserRole } from "../models/user.model.js"; + +import { Permission } from "./permission.js"; + +export const RolePermissions: Record = { + [UserRole.ADMIN]: [ + Permission.CREATE_PROFILE, + Permission.UPDATE_PROFILE, + Permission.APPROVE_SIGNUP, + Permission.REJECT_SIGNUP, + Permission.MANAGE_USERS, + ], + [UserRole.BRAND]: [ + Permission.CREATE_PROFILE, + Permission.UPDATE_PROFILE, + ], + [UserRole.INFLUENCER]: [ + Permission.CREATE_PROFILE, + Permission.UPDATE_PROFILE, + Permission.CREATE_GIG, + Permission.UPDATE_GIG, + Permission.DELETE_GIG, + Permission.PUBLISH_GIG + ], +}; diff --git a/packages/shared/src/roles.ts b/apps/core-api/src/rbac/roles.ts similarity index 84% rename from packages/shared/src/roles.ts rename to apps/core-api/src/rbac/roles.ts index 47e132f..6e3f85f 100644 --- a/packages/shared/src/roles.ts +++ b/apps/core-api/src/rbac/roles.ts @@ -1,7 +1,7 @@ export const Roles = { ADMIN: "admin", INFLUENCER: "influencer", - BRAND: "brand", + BRAND: "BRAND", } as const; export type Role = (typeof Roles)[keyof typeof Roles]; diff --git a/apps/core-api/src/repositories/Signup.repository.ts b/apps/core-api/src/repositories/Signup.repository.ts new file mode 100644 index 0000000..8e85864 --- /dev/null +++ b/apps/core-api/src/repositories/Signup.repository.ts @@ -0,0 +1,66 @@ +import { PendingSignup } from "../models/pendingSignup.models.js"; +import type { PendingSignupFilter } from "../types/pendingSignup.types.js"; + +interface CreatePendingSignupInput { + email: string; + passwordHash: string; + documents: string; + role: "INFLUENCER" | "BRAND" | "ADMIN"; + adminLevel?: "SUPER" | "NORMAL"; + + status: "PENDING" | "APPROVED" | "REJECTED"; + + // OTP fields (optional) + emailOtpHash?: string | null; + emailOtpExpiresAt?: Date | null; + otpAttempts?: number; + otpResendCount?: number; + otpLastSentAt?: Date | null; + otpLockedUntil?: Date | null; + isEmailVerified?: boolean; +} + + + +class PendingSignupRepository { + // ================= CREATE ================= + create(data: CreatePendingSignupInput) { + return PendingSignup.create(data); + } + + //==================GET ALL PENDING SIGNUPS================== + getAllPendingSignups(filter:PendingSignupFilter={}) { + return PendingSignup.find(filter).sort({ createdAt: -1 }); + } + + // ================= FIND ================= + findByEmail(email: string) { + return PendingSignup + .findOne({ email }) + .select("+emailOtpHash"); + } + + + // ================= UPDATE STATUS ================= + updateStatus(email: string, status: "APPROVED" | "REJECTED") { + return PendingSignup.findOneAndUpdate( + { email }, + { status }, + { new: true } + ); + } + + // ================= DELETE ONE ================= + deleteByEmail(email: string) { + return PendingSignup.findOneAndDelete({ email }); + } + + // ================= DELETE MANY (FOR CLEANUP) ================= + deleteMany(filter: Record): Promise { + return PendingSignup.deleteMany(filter); + } + +} + +export const pendingSignupRepository = + new PendingSignupRepository(); diff --git a/apps/core-api/src/repositories/availability.repository.ts b/apps/core-api/src/repositories/availability.repository.ts new file mode 100644 index 0000000..1f6d1ca --- /dev/null +++ b/apps/core-api/src/repositories/availability.repository.ts @@ -0,0 +1,38 @@ +import { Types } from "mongoose"; + +import type { AvailabilityDocument } from "../types/availability.types.js"; +import { AvailabilityModel } from "../models/availability.model.js"; + +export const createAvailability = async ( + data: Omit +) => { + return AvailabilityModel.create(data); +}; + +export const findAvailabilityByInfluencer = async ( + influencerProfileId: Types.ObjectId +) => { + return AvailabilityModel.findOne({ influencerProfileId }); +}; + +export const updateAvailability = async ( + influencerProfileId: Types.ObjectId, + update: Partial +) => { + return AvailabilityModel.findOneAndUpdate( + { influencerProfileId }, + update, + { new: true } + ); +}; + +export const upsertAvailability = async ( + influencerProfileId: Types.ObjectId, + data: Partial +) => { + return AvailabilityModel.findOneAndUpdate( + { influencerProfileId }, + data, + { new: true, upsert: true } + ); +}; \ No newline at end of file diff --git a/apps/core-api/src/repositories/collaboration.repository.ts b/apps/core-api/src/repositories/collaboration.repository.ts new file mode 100644 index 0000000..1f6619f --- /dev/null +++ b/apps/core-api/src/repositories/collaboration.repository.ts @@ -0,0 +1,40 @@ +import { Types } from "mongoose"; + +import type { GigCollaborationDocument } from "../types/collaboration.type.js"; +import { GigCollaborationModel } from "../models/collaboration.model.js"; + + +export const createCollaboration = async ( + data: Omit +) => { + return GigCollaborationModel.create(data); +}; + +export const findPendingCollaborationsByGig = async ( + gigId: Types.ObjectId +) => { + return GigCollaborationModel.find({ + gigId, + status: "pending" + }); +}; + +export const findCollaborationById = async ( + id: Types.ObjectId +) => { + return GigCollaborationModel.findById(id); +}; + +export const updateCollaborationStatus = async ( + id: Types.ObjectId, + status: "accepted" | "rejected" +) => { + return GigCollaborationModel.findByIdAndUpdate( + id, + { + status, + respondedAt: new Date() + }, + { new: true } + ); +}; \ No newline at end of file diff --git a/apps/core-api/src/repositories/gig.repository.ts b/apps/core-api/src/repositories/gig.repository.ts new file mode 100644 index 0000000..f5be4ab --- /dev/null +++ b/apps/core-api/src/repositories/gig.repository.ts @@ -0,0 +1,113 @@ +import type { SortOrder, Types } from "mongoose"; + +import { GigModel } from "../models/gig.model.js"; +import type { CreateGigDBInput, GigDocument } from "../types/gig.type.js"; + +export const create_gig = async ( + data: CreateGigDBInput +) => { + return GigModel.create(data); +}; +/* ================= FIND BY ID ================= */ + +export const findGigById = async ( + gigId: string | Types.ObjectId +) => { + return GigModel.findById(gigId); +}; + +/* ================= UPDATE ================= */ + +export const updateGigById = async ( + gigId: string | Types.ObjectId, + updateData: Partial +) => { + return GigModel.findByIdAndUpdate( + gigId, + updateData, + { new: true } + ); +}; + +/* ================= SOFT DELETE ================= */ + +export const softDeleteGig = async ( + gigId: string | Types.ObjectId +) => { + return GigModel.findByIdAndUpdate( + gigId, + { isDeleted: true }, + { new: true } + ); +}; +export const findPublishedGigs = async ( + filter: Partial, + sort: Record, + skip: number, + limit: number +) => { + const [gigs, total] = await Promise.all([ + GigModel.find(filter) + .select( + "title category pricing.basePrice pricing.currency primaryInfluencerId createdAt influencer" + ) + .populate("primaryInfluencerId", "displayName profileImage availableFrom platforms") + .sort(sort) + .skip(skip) + .limit(limit) + .lean(), + GigModel.countDocuments(filter) + ]); + + return { gigs, total }; +}; +// export const findPublishedGigs = async ( +// filter: any, +// sort: any, +// skip: number, +// limit: number +// ) => { +// const [gigs, total] = await Promise.all([ +// GigModel.find(filter) +// .select( +// "title category pricing.basePrice pricing.currency primaryInfluencerId createdAt" +// ) +// .sort(sort) +// .skip(skip) +// .limit(limit) +// .lean(), +// GigModel.countDocuments(filter) +// ]); + +// return { gigs, total }; +// }; +export const findPublishedGigById = async (gigId: string) => { + return GigModel.findOne({ + _id: gigId, + status: "published", + isDeleted: false + }) + .populate({ + path: "primaryInfluencerId", + select: "displayName profileImage followersCount ratingAvg" + }) + .lean(); +}; + +export const findActiveGigById = async ( + gigId: string | Types.ObjectId +) => { + return GigModel.findOne({ + _id: gigId, + isDeleted: false + }); +}; + +export const findGigsByInfluencer = async ( + influencerProfileId: string | Types.ObjectId +) => { + return GigModel.find({ + primaryInfluencerId: influencerProfileId, + isDeleted: false + }).sort({ createdAt: -1 }); +}; \ No newline at end of file diff --git a/apps/core-api/src/repositories/profile.repository.ts b/apps/core-api/src/repositories/profile.repository.ts new file mode 100644 index 0000000..de2d44a --- /dev/null +++ b/apps/core-api/src/repositories/profile.repository.ts @@ -0,0 +1,55 @@ +import { BrandProfile } from "../models/brand.model.js"; +import type { IBrandProfile } from "../models/brand.model.js" +import { InfluencerProfile} from "../models/influencer.model.js"; +import type { IInfluencerProfile } from "../models/influencer.model.js" + +export class ProfileRepository { + + async findInfluencerByUserId(userId: string): Promise { + return InfluencerProfile.findOne({ userId }); + } + + async findInfluencerById(id: string): Promise { + return InfluencerProfile.findById(id); + } + + async findBrandByUserId(userId: string): Promise { + return BrandProfile.findOne({ userId }); + } + + async createInfluencer( + data: Partial + ): Promise { + return InfluencerProfile.create(data); + } + + async createBrand( + data: Partial + ): Promise { + return BrandProfile.create(data); + } + + async updateInfluencer( + userId: string, + data: Partial + ): Promise { + return InfluencerProfile.findOneAndUpdate( + { userId }, + data, + { new: true } + ); + } + + async updateBrand( + userId: string, + data: Partial + ): Promise { + return BrandProfile.findOneAndUpdate( + { userId }, + data, + { new: true } + ); + } +} + +export const profileRepository = new ProfileRepository(); diff --git a/apps/core-api/src/repositories/user.repository.ts b/apps/core-api/src/repositories/user.repository.ts index 9e0ef02..4b8b51d 100644 --- a/apps/core-api/src/repositories/user.repository.ts +++ b/apps/core-api/src/repositories/user.repository.ts @@ -1,14 +1,130 @@ -import { User } from "../modules/users/user.model.js"; +import { User } from "../models/user.model.js"; + +class UserRepository { + + + // FIND USER WITH PASSWORD + + async findEmailWithPassword(email: string) { + const normalizedEmail = email.trim().toLowerCase(); + return User.findOne({ email: normalizedEmail }).select("+password"); + } + + + // FIND USER WITH RESET FIELDS + + async findByEmailWithResetFields(email: string) { + const normalizedEmail = email.trim().toLowerCase(); + + return User.findOne({ email: normalizedEmail }) + .select("+password +resetOtp +resetOtpExpiry +resetSessionExpiry +resetSessionToken"); + } + + + // NORMAL FIND BY EMAIL + + async findByEmail(email: string) { + const normalizedEmail = email.trim().toLowerCase(); + return User.findOne({ email: normalizedEmail }); + } + + // FIND BY ID + + async findById(userId: string) { + return User.findById(userId).select("+refreshToken"); + } + + + // SAVE REFRESH TOKEN + + async saveRefreshToken(userId: string, refreshToken: string) { + + return User.findByIdAndUpdate( + userId, + { refreshToken }, + { new: true } + ); + } + + + // SAVE RESET OTP + + async saveResetOtp( + userId: string, + hashedOtp: string, + expiry: Date + ) { + return User.findByIdAndUpdate( + userId, + { + resetOtp: hashedOtp, + resetOtpExpiry: expiry, + }, + { new: true } + ); + } + + + // SAVE RESET SESSION TOKEN + + async saveResetSession( + userId: string, + token: string, + expiry: Date + ) { + return User.findByIdAndUpdate( + userId, + { + $unset: { resetOtp: 1, resetOtpExpiry: 1 }, + $set: { resetSessionToken: token, resetSessionExpiry: expiry }, + }, + { new: true } + ); + } + + + // UPDATE PASSWORD (Production-Safe) + + async updatePassword(userId: string, hashedPassword: string) { + return User.findByIdAndUpdate( + userId, + { + password: hashedPassword, + refreshToken: "", // invalidate all sessions + }, + { + new: true, + runValidators: true, + } + ); + } + + + // CLEAR RESET SESSION + + async clearResetSession(userId: string) { + return User.findByIdAndUpdate( + userId, + { + $unset: { resetSessionToken: 1, resetSessionExpiry: 1 } + }, + { new: true } + ); + } -class UserRepository{ - async findEmailWithPassword(email: string) { - return User.findOne({ email }).select("+password"); -} + // CREATE USER - async saveRefreshToken(userId:string, refreshToken:string){ - return User.findByIdAndUpdate(userId,{refreshToken}) - } + async create(data: { + email: string; + password: string; + role: string; + adminLevel?: string; + isEmailVerified: boolean; + status: string; + }) { + return User.create(data); + } } -export const userRepository = new UserRepository() \ No newline at end of file +export const userRepository = new UserRepository(); diff --git a/apps/core-api/src/routes/admin.route.ts b/apps/core-api/src/routes/admin.route.ts new file mode 100644 index 0000000..57dc047 --- /dev/null +++ b/apps/core-api/src/routes/admin.route.ts @@ -0,0 +1,14 @@ +import { Router } from "express"; + +import { authenticate } from "../middlewares/auth.middleware.js"; +import { approveSignupController, getAllpendingSignupController, rejectSignupController } from "../controllers/admin.controllers.js"; + +const router: Router = Router(); + +router.get("/signup/", authenticate, getAllpendingSignupController); +router.post("/signup/approve", + // authenticate, + approveSignupController); +router.post("/signup/reject", authenticate, rejectSignupController); + +export default router; diff --git a/apps/core-api/src/routes/auth.routes.ts b/apps/core-api/src/routes/auth.routes.ts index 109b7d8..c793e39 100644 --- a/apps/core-api/src/routes/auth.routes.ts +++ b/apps/core-api/src/routes/auth.routes.ts @@ -1,7 +1,17 @@ -import { Router } from "express"; +import { Router } from "express"; -import { loginController } from "../controllers/auth.controller.js"; +import { forgotPasswordController, loginController, logoutController, refreshTokenController, resetPasswordController, signupController, verifyResetOtpController, verifySignupOtpController } from "../controllers/auth.controller.js"; +import { authenticate } from "../middlewares/auth.middleware.js"; const router: Router = Router() router.post("/login", loginController) -export default router \ No newline at end of file +router.post("/signup", signupController); +router.post("/refresh", refreshTokenController); +router.post("/logout", authenticate, logoutController); +router.post("/verify-signup-otp", verifySignupOtpController); +router.post("/forgot-password", forgotPasswordController); +router.post("/verify-reset-otp", verifyResetOtpController); +router.post("/reset-password", resetPasswordController); + + +export default router \ No newline at end of file diff --git a/apps/core-api/src/routes/availability.routes.ts b/apps/core-api/src/routes/availability.routes.ts index 083869f..b75ec28 100644 --- a/apps/core-api/src/routes/availability.routes.ts +++ b/apps/core-api/src/routes/availability.routes.ts @@ -1,5 +1,23 @@ -import { Router } from "express" +import { Router } from "express"; -const router : Router = Router() +import { authenticate, authorizePermission } from "../middlewares/auth.middleware.js"; +import { Permission } from "../rbac/permission.js"; +import { getAvailabilityController, setAvailabilityController } from "../controllers/availability.controller.js"; -export default router \ No newline at end of file +const router: Router = Router(); + + +router.post( + "/", + authenticate, + authorizePermission(Permission.UPDATE_PROFILE), + setAvailabilityController +); + + +router.get( + "/:influencerProfileId", + getAvailabilityController +); + +export default router; \ No newline at end of file diff --git a/apps/core-api/src/routes/collaboration.route.ts b/apps/core-api/src/routes/collaboration.route.ts new file mode 100644 index 0000000..ddbf7bc --- /dev/null +++ b/apps/core-api/src/routes/collaboration.route.ts @@ -0,0 +1,27 @@ +import { Router } from "express"; + +import { inviteCollaboratorsController, respondToCollaborationController } from "../controllers/collaboration.controllers.js"; +import { authenticate } from "../middlewares/auth.middleware.js"; + + +const router:Router = Router(); + +/** + * Invite collaborators to a gig + */ +router.post( + "/gigs/:id/collaborators", + authenticate, + inviteCollaboratorsController +); + +/** + * Accept or reject collaboration + */ +router.patch( + "/:id/respond", + authenticate, + respondToCollaborationController +); + +export default router; \ No newline at end of file diff --git a/apps/core-api/src/routes/gigs.routes.ts b/apps/core-api/src/routes/gigs.routes.ts index 9bf7f97..7c24bde 100644 --- a/apps/core-api/src/routes/gigs.routes.ts +++ b/apps/core-api/src/routes/gigs.routes.ts @@ -1,5 +1,30 @@ import { Router } from "express"; +import { authenticate, authorizePermission } from "../middlewares/auth.middleware.js"; +import { Permission } from "../rbac/permission.js"; +import { createGigController, deleteGigController, editGigController, getGigDetailsController, listGigsController, publishGigController, updateGigDeliverablesController, updateGigPricingController } from "../controllers/gig.controller.js"; + + + const router: Router = Router() +router.get("/", listGigsController); +router.get("/:id", getGigDetailsController); +router.post("/create_gig",authenticate,authorizePermission(Permission.CREATE_GIG),createGigController); +router.patch( + "/:id/deliverables", authenticate, authorizePermission(Permission.UPDATE_GIG), updateGigDeliverablesController); +router.patch( + "/:id/pricing", + authenticate, + authorizePermission(Permission.UPDATE_GIG), + updateGigPricingController +); +router.post( + "/:id/publish", + authenticate, + authorizePermission(Permission.PUBLISH_GIG), + publishGigController +); +router.patch("/:id",authenticate,authorizePermission(Permission.UPDATE_GIG),editGigController); +router.delete("/:id",authenticate,authorizePermission(Permission.DELETE_GIG),deleteGigController); export default router \ No newline at end of file diff --git a/apps/core-api/src/routes/index.ts b/apps/core-api/src/routes/index.ts index aa21217..27f31cb 100644 --- a/apps/core-api/src/routes/index.ts +++ b/apps/core-api/src/routes/index.ts @@ -1,4 +1,6 @@ -import { Router } from "express"; +import { Router } from "express"; + +import mediaRoutes from "../routes/media.routes.js"; import userRoutes from "./users.routes.js"; import authRoutes from "./auth.routes.js"; @@ -10,10 +12,12 @@ import orderRoutes from "./orders.routes.js"; import paymentRoutes from "./payments.routes.js"; import searchRoutes from "./search.routes.js"; import healthRoutes from "./health.routes.js"; +import adminRoutes from "./admin.route.js" const router:Router = Router() router.use("/health", healthRoutes) router.use("/auth", authRoutes) +router.use("/admin", adminRoutes) router.use("/users", userRoutes) router.use("/profile", profileRoutes) router.use("/gigs", gigRoutes); @@ -22,5 +26,6 @@ router.use("/bookings", bookingRoutes); router.use("/orders", orderRoutes); router.use("/payments", paymentRoutes); router.use("/search", searchRoutes); +router.use("/media", mediaRoutes); export default router \ No newline at end of file diff --git a/apps/core-api/src/routes/media.routes.ts b/apps/core-api/src/routes/media.routes.ts new file mode 100644 index 0000000..f2f08b0 --- /dev/null +++ b/apps/core-api/src/routes/media.routes.ts @@ -0,0 +1,14 @@ +import { Router } from "express"; + +import { generateUploadUrlController } from "../controllers/media.controller.js"; +import { authenticate } from "../middlewares/auth.middleware.js"; + +const router:Router = Router(); + +router.post( + "/upload-url", + authenticate, + generateUploadUrlController +); + +export default router; diff --git a/apps/core-api/src/routes/profile.routes.ts b/apps/core-api/src/routes/profile.routes.ts index ee617b7..2f63246 100644 --- a/apps/core-api/src/routes/profile.routes.ts +++ b/apps/core-api/src/routes/profile.routes.ts @@ -1,5 +1,16 @@ -import { Router } from "express"; - -const router : Router = Router() - -export default router \ No newline at end of file +import { Router } from "express"; + +import { authenticate } from "../middlewares/auth.middleware.js"; +import { + getMyProfileController, + updateProfileController, + getPublicInfluencerProfileController +} from "../controllers/profile.controller.js"; + +const router: Router = Router(); + +router.get("/get_profile", authenticate, getMyProfileController); +router.patch("/update_profile", authenticate, updateProfileController); +router.get("/influencer/:id", getPublicInfluencerProfileController); + +export default router; diff --git a/apps/core-api/src/routes/search.routes.ts b/apps/core-api/src/routes/search.routes.ts index 9bf7f97..3612195 100644 --- a/apps/core-api/src/routes/search.routes.ts +++ b/apps/core-api/src/routes/search.routes.ts @@ -1,5 +1,11 @@ import { Router } from "express"; -const router: Router = Router() +import { searchGigs } from "../controllers/search.controller.js"; + +const router: Router = Router(); + +router.get("/gigs",searchGigs); + + export default router \ No newline at end of file diff --git a/apps/core-api/src/search/filters/brand.filter.ts b/apps/core-api/src/search/filters/brand.filter.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/core-api/src/search/filters/gig.filter.ts b/apps/core-api/src/search/filters/gig.filter.ts new file mode 100644 index 0000000..0333e3e --- /dev/null +++ b/apps/core-api/src/search/filters/gig.filter.ts @@ -0,0 +1,17 @@ +export const buildGigFilters = (query: {niche?: string; minPrice?: number; maxPrice?: number}): string[] => { + const filters: string[] = []; + + if (query.niche) { + filters.push(`niche = "${query.niche}"`); + } + + if (query.minPrice) { + filters.push(`price >= ${Number(query.minPrice)}`); + } + + if (query.maxPrice) { + filters.push(`price <= ${Number(query.maxPrice)}`); + } + + return filters; +}; \ No newline at end of file diff --git a/apps/core-api/src/search/filters/influencer.filter.ts b/apps/core-api/src/search/filters/influencer.filter.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/core-api/src/search/indexes/brand.index.ts b/apps/core-api/src/search/indexes/brand.index.ts new file mode 100644 index 0000000..709257a --- /dev/null +++ b/apps/core-api/src/search/indexes/brand.index.ts @@ -0,0 +1,10 @@ +import { meili } from "../meili.js"; + +export const setupBrandIndex=async ()=>{ + const index=meili.index("brands"); + + await index.updateSettings({ + searchableAttributes:["companyName","industry"], + filterableAttributes:["industry"] + }) +} \ No newline at end of file diff --git a/apps/core-api/src/search/indexes/gig.index.ts b/apps/core-api/src/search/indexes/gig.index.ts new file mode 100644 index 0000000..d9a0217 --- /dev/null +++ b/apps/core-api/src/search/indexes/gig.index.ts @@ -0,0 +1,10 @@ +import { meili } from "../meili.js"; + +export const setupGigIndex=async () =>{ + const index=meili.index("gigs"); + + await index.updateSettings({ + searchableAttributes:["title","category","tags","price","searchMeta"], + filterableAttributes:["category","price","time"] + }) +} \ No newline at end of file diff --git a/apps/core-api/src/search/indexes/influencer.index.ts b/apps/core-api/src/search/indexes/influencer.index.ts new file mode 100644 index 0000000..09df358 --- /dev/null +++ b/apps/core-api/src/search/indexes/influencer.index.ts @@ -0,0 +1,10 @@ +import { meili } from "../meili.js"; + +export const setupInfluencerIndex= async ()=>{ + const index=meili.index("influencers"); + + await index.updateSettings({ + searchableAttributes:["fullName","username","instagram","youtube","category","location","languages","followersCount","engagementRate"], + filterableAttributes:["category","language","folllowersCount","engagementRate"] + }) +} \ No newline at end of file diff --git a/apps/core-api/src/search/meili.ts b/apps/core-api/src/search/meili.ts index c6a2664..27bf46c 100644 --- a/apps/core-api/src/search/meili.ts +++ b/apps/core-api/src/search/meili.ts @@ -17,3 +17,5 @@ if (process.env.MEILI_KEY) { export const meili = new MeiliSearch(config); logger.info("Meilisearch client initialized"); + + diff --git a/apps/core-api/src/search/services/search.service.ts b/apps/core-api/src/search/services/search.service.ts new file mode 100644 index 0000000..9353654 --- /dev/null +++ b/apps/core-api/src/search/services/search.service.ts @@ -0,0 +1,10 @@ +import { meili } from "../meili.js"; + +export const searchIndex = async ( + indexName: string, + query: string, + options: { limit?: number; offset?: number; filters?: string[] } +) => { + const index = meili.index(indexName); + return await index.search(query || "", options); +}; diff --git a/apps/core-api/src/search/setup.ts b/apps/core-api/src/search/setup.ts new file mode 100644 index 0000000..143b3fa --- /dev/null +++ b/apps/core-api/src/search/setup.ts @@ -0,0 +1,20 @@ +import { logger } from "../utils/logger.js"; + +import { setupBrandIndex } from "./indexes/brand.index.js"; +import { setupGigIndex } from "./indexes/gig.index.js"; +import { setupInfluencerIndex } from "./indexes/influencer.index.js"; + +export const setupMeili=async ()=>{ + try { + await Promise.all([ + setupBrandIndex(), + setupGigIndex(), + setupInfluencerIndex() + ]) + logger.info("Meilisearch indexes configured successfully"); + + } catch (error) { + logger.error("Meili setup failed",error) + // process.exit(1); // prevent server from crashing during local dev if Meili is down + } +} \ No newline at end of file diff --git a/apps/core-api/src/server.ts b/apps/core-api/src/server.ts index 75f7cf0..b2e1b24 100644 --- a/apps/core-api/src/server.ts +++ b/apps/core-api/src/server.ts @@ -1,34 +1,65 @@ -import "dotenv/config" -import express from "express" - -import { httpLogger } from "./middlewares/httpLogger.js" -import { logger } from "./utils/logger.js" -import { errorHandler, notFound } from "./middlewares/errorHandler.js" -import { connectRabbit } from "./queue/rabbit.js" -import "./cache/redis.js"; -import "./search/meili.js"; -import router from "./routes/index.js" -import { connectDB } from "./db/connect.js" +import cors from "cors" +import "dotenv/config"; +import express from "express"; +import cookieParser from "cookie-parser"; +import cron from "node-cron"; + +import { httpLogger } from "./middlewares/httpLogger.js"; +import { logger } from "./utils/logger.js"; +import { errorHandler, notFound } from "./middlewares/errorHandler.js"; +import { connectRabbit } from "./queue/rabbit.js"; +import "./cache/redis.js"; +import "./search/meili.js"; +import router from "./routes/index.js"; +import { connectDB } from "./db/connect.js"; +import { cleanupExpiredSignups } from "./services/verification.service.js"; +import { setupMeili } from "./search/setup.js"; +import { GIG_CREATED_EVENT } from "./controllers/gig.controller.js"; +import { getChannel } from "./queue/rabbit.js"; const app = express() const PORT = Number(process.env.PORT) || 5000 app.use(httpLogger) app.use(express.json()) -app.use("/api", router) +app.use(cookieParser()); +app.use(cors({ + origin: process.env.FRONTEND_URL || "http://localhost:3000", + credentials: true, +})) + connectDB() +app.use(httpLogger); + +app.use("/api", router); +app.use(express.json()); + +// Connect Database +connectDB(); +await connectRabbit(); + +// CRON JOB (Runs every hour) +cron.schedule("0 * * * *", async () => { + logger.info("Running cleanup for expired pending signups..."); + await cleanupExpiredSignups(); +}); + app.get("/health", (req, res) => { - res.status(200).json({ - status: "ok", - service: "core-api", - timestamp: new Date().toISOString() - }) -}) - -app.use(notFound) -app.use(errorHandler) -connectRabbit() -app.listen(PORT,"127.0.0.1", () => { - logger.info(`Core API is running at http://localhost:${PORT}`); - -}) \ No newline at end of file + res.status(200).json({ + status: "ok", + service: "core-api", + timestamp: new Date().toISOString(), + }); +}); + +app.use(notFound); +app.use(errorHandler); +await setupMeili(); + +// Ensure gig.created queue exists +await getChannel().assertQueue(GIG_CREATED_EVENT,{durable: true}); + + +app.listen(PORT, "127.0.0.1", () => { + logger.info(`Core API is running at http://localhost:${PORT}`); +}); diff --git a/apps/core-api/src/services/auth.service.ts b/apps/core-api/src/services/auth.service.ts index 2b8d7aa..559ba82 100644 --- a/apps/core-api/src/services/auth.service.ts +++ b/apps/core-api/src/services/auth.service.ts @@ -1,87 +1,429 @@ -// class AuthService { -// async loginUser(_email: string, _password: string) { -// // find user -// // compare password -// // create tokens -// // save refresh token -// // return tokens -// } +import crypto from "crypto"; -// async refreshSession(_token: string) { -// // verify refresh token -// // issue new access token -// } -// } +import bcrypt from "bcrypt"; -// export const authService = new AuthService(); -import bcrypt from "bcrypt"; - -import { signAccessToken, signRefreshToken } from "../modules/auth/auth.utils.js"; -import type { HttpError } from "../modules/auth/http-error.js"; +import { signAccessToken, signRefreshToken, verifyRefreshToken } from "../modules/auth/auth.utils.js"; +import { createHttpError } from "../modules/auth/http-error.js"; import { userRepository } from "../repositories/user.repository.js" +import { pendingSignupRepository } from "../repositories/Signup.repository.js"; +import { logger } from "../utils/logger.js"; +import { sendOtpEmail } from "../utils/sendotpEmail.js"; + + + + + +interface SignupInput { + email: string; + password: string; + role: "INFLUENCER" | "BRAND"; + documents: string; +} + +export const signupService = async (data: SignupInput) => { + const existingUser = await userRepository.findEmailWithPassword(data.email); + if (existingUser) { + throw createHttpError("User already exists", 409); + } + + const existingRequest = + await pendingSignupRepository.findByEmail(data.email); + + if (existingRequest) { + throw createHttpError("Sign up request already submitted", 409); + } + + const passwordHash = await bcrypt.hash(data.password, 10); + + // Generate OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + + + // Hash OTP before saving + const hashedOtp = await bcrypt.hash(otp, 10); + + // Save pending signup with OTP + await pendingSignupRepository.create({ + email: data.email.toLowerCase(), + passwordHash, + role: data.role, + documents: data.documents, + status: "PENDING", + + emailOtpHash: hashedOtp, + emailOtpExpiresAt: new Date(Date.now() + 5 * 60 * 1000), + otpAttempts: 0, + otpResendCount: 0, + otpLastSentAt: new Date(), + isEmailVerified: false, + }); + + // Send REAL Gmail OTP + await sendOtpEmail(data.email, otp); + + return { message: "OTP sent to your email" }; + +}; + + interface LoginResult { accessToken: string, refreshToken: string, user: { - id: string, - role: string, + id: string, + email: string, + role: string, adminLevel: string | null } } -export const loginService = async( - email : string, - password : string +export const loginService = async ( + email: string, + password: string ): Promise => { + const user = await userRepository.findEmailWithPassword(email) + logger.info(`EMAIL: ${email}`); + if (!user) { - const err: HttpError = new Error("Invalid credentials") - err.statusCode = 401 - throw err + throw createHttpError("Invalid credentials", 409); } if (user.status !== "ACTIVE") { - const err: HttpError = new Error("User is not active") - err.statusCode = 403 - throw err + throw createHttpError("User is not active", 409); } if (!user.isEmailVerified) { - const err: HttpError = new Error("User email is not verified") - err.statusCode = 403 - throw err + throw createHttpError("User email is not verified", 409); } + const isMatch = await bcrypt.compare(password, user.password) console.log("PASSWORD FROM DB:", user.password); if (!isMatch) { - const err: HttpError = new Error("Invalid credentials") - err.statusCode = 401 - throw err + throw createHttpError("Invalid credentials", 409); } const payload = { - userId : user._id.toString(), - role : user.role, - adminLevel : user.adminLevel ?? null + userId: user._id.toString(), + role: user.role, + adminLevel: user.adminLevel ?? null } const accessToken = signAccessToken(payload) const refreshToken = signRefreshToken(payload) - await userRepository.saveRefreshToken(user._id.toString(),refreshToken) + await userRepository.saveRefreshToken(user._id.toString(), refreshToken) return { accessToken, refreshToken, user: { id: user._id.toString(), + email: user.email, role: user.role, adminLevel: user.adminLevel ?? null } } -} \ No newline at end of file +} + + +// interface RefreshResult { +// accessToken: string; +// refreshToken: string; +// } +// export const refreshTokenService = async ( +// refreshToken: string +// ): Promise => { +// if (!refreshToken) { +// const err: HttpError = new Error("Refresh token required"); +// err.statusCode = 400; +// throw err; +// } + +// let payload; +// try { +// payload = verifyRefreshToken(refreshToken); +// } catch { +// const err: HttpError = new Error("Invalid refresh token"); +// err.statusCode = 401; +// throw err; +// } + +// const user = await userRepository.findById(payload.userId); + +// if (!user || !user.refreshToken) { +// const err: HttpError = new Error("Refresh token mismatch"); +// err.statusCode = 401; +// throw err; +// } + + +// if (user.refreshToken.trim() !== refreshToken.trim()) { +// const err: HttpError = new Error("Refresh token mismatch"); +// err.statusCode = 401; +// throw err; +// } + +// const newPayload = { +// userId: user._id.toString(), +// role: user.role, +// adminLevel: user.adminLevel ?? null, +// }; + +// const newAccessToken = signAccessToken(newPayload); +// const newRefreshToken = signRefreshToken(newPayload); + +// await userRepository.saveRefreshToken(user._id.toString(), newRefreshToken); + +// return { +// accessToken: newAccessToken, +// refreshToken: newRefreshToken, +// user: { +// id: user._id.toString(), +// email: user.email, +// role: user.role, +// adminLevel: user.adminLevel ?? null, +// }, +// }; +// }; + +interface RefreshResult { + accessToken: string; + refreshToken: string; + user: { + id: string; + email: string; + role: string; + adminLevel: string | null; + }; +} + +export const refreshTokenService = async ( + refreshToken: string +): Promise => { + if (!refreshToken) { + + throw createHttpError("Refresh token required", 409); + } + + let payload; + try { + payload = verifyRefreshToken(refreshToken); + } catch { + + throw createHttpError("Invalid refresh token", 409); + } + + const user = await userRepository.findById(payload.userId); + + // FIRST check user existence + if (!user || !user.refreshToken) { + + throw createHttpError("Refresh token mismatch", 409); + } + + + // Compare after narrowing + if (user.refreshToken.trim() !== refreshToken.trim()) { + + throw createHttpError("Refresh token mismatch", 409); + } + + const newPayload = { + userId: user._id.toString(), + role: user.role, + adminLevel: user.adminLevel ?? null, + }; + + const newAccessToken = signAccessToken(newPayload); + const newRefreshToken = signRefreshToken(newPayload); + + await userRepository.saveRefreshToken( + user._id.toString(), + newRefreshToken + ); + + return { + accessToken: newAccessToken, + refreshToken: newRefreshToken, + user: { + id: user._id.toString(), + email: user.email, + role: user.role, + adminLevel: user.adminLevel ?? null, + }, + }; +}; + + +export const logoutService = async (userId: string) => { + if (!userId) { + + throw createHttpError("User not authenticated", 409); + } + + // Invalidate refresh token + await userRepository.saveRefreshToken(userId, ""); + + return { message: "Logged out successfully" }; +}; + + +// ================= VERIFY SIGNUP OTP ================= +export const verifySignupOtpService = async ( + email: string, + otp: string +): Promise => { + + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + + throw createHttpError("Signup request not found", 409); + } + + if (pending.isEmailVerified) { + + throw createHttpError("Email already verified", 409); + } + + if (!pending.emailOtpHash || !pending.emailOtpExpiresAt) { + + throw createHttpError("OTP not found", 409); + } + + if (pending.emailOtpExpiresAt < new Date()) { + + throw createHttpError("OTP expired", 409); + } + + const isMatch = await bcrypt.compare( + otp, + pending.emailOtpHash + ); + + if (!isMatch) { + + throw createHttpError("Invalid OTP", 409); + } + + // SUCCESS + pending.isEmailVerified = true; + pending.emailOtpHash = null; + pending.emailOtpExpiresAt = null; + + await pending.save(); +}; + + + +// forgotPasswordService + +export const forgotPasswordService = async (email: string) => { + const user = await userRepository.findEmailWithPassword(email); + + if (!user) return; + + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + console.log("RESET OTP:", otp); + + const hashedOtp = await bcrypt.hash(otp, 10); + + const expiry = new Date(Date.now() + 10 * 60 * 1000); + + await userRepository.saveResetOtp( + user._id.toString(), + hashedOtp, + expiry + ); + + await sendOtpEmail(email, otp); + + logger.info(`Reset OTP sent to ${email}`); +}; + +// verifyOtpService + +export const verifyOtpService = async ( + email: string, + otp: string +): Promise => { + + const user = await userRepository.findByEmailWithResetFields(email); + + if (!user || !user.resetOtp || !user.resetOtpExpiry) { + + throw createHttpError("Invalid request", 409); + } + + if (user.resetOtpExpiry < new Date()) { + + throw createHttpError("OTP expired", 409); + } + + const isMatch = await bcrypt.compare(otp, user.resetOtp); + + if (!isMatch) { + + throw createHttpError("Invalid OTP", 409); + } + + const resetSessionToken = crypto.randomBytes(32).toString("hex"); + + const sessionExpiry = new Date(Date.now() + 10 * 60 * 1000); + + await userRepository.saveResetSession( + user._id.toString(), + resetSessionToken, + sessionExpiry + ); + + return resetSessionToken; +}; + +// resetPasswordService + +export const resetPasswordService = async ( + email: string, + newPassword: string, + resetSessionToken: string +) => { + + const user = await userRepository.findByEmailWithResetFields(email); + + if ( + !user || + !user.resetSessionToken || + !user.resetSessionExpiry + ) { + + throw createHttpError("Invalid request", 409); + } + + if (user.resetSessionToken !== resetSessionToken) { + + throw createHttpError("Invalid session", 409); + } + + if (user.resetSessionExpiry < new Date()) { + + throw createHttpError("Session expired", 409); + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + + await userRepository.updatePassword( + user._id.toString(), + hashedPassword + ); + + await userRepository.clearResetSession(user._id.toString()); + + logger.info(`Password reset successful for ${email}`); +}; + diff --git a/apps/core-api/src/services/availability.service.ts b/apps/core-api/src/services/availability.service.ts new file mode 100644 index 0000000..dd35684 --- /dev/null +++ b/apps/core-api/src/services/availability.service.ts @@ -0,0 +1,58 @@ +import { Types } from "mongoose"; + +import { UserRole } from "../models/user.model.js"; +import { createHttpError } from "../modules/auth/http-error.js"; +import { InfluencerProfile } from "../models/influencer.model.js"; +import { findAvailabilityByInfluencer, upsertAvailability } from "../repositories/availability.repository.js"; +import type { DateOverride, WeeklyRule } from "../types/availability.types.js"; + + +interface SetAvailabilityInput { + timezone: string; + weeklyRules: WeeklyRule[]; + dateOverrides: DateOverride[]; +} + +export const setAvailabilityService = async ( + userId: string, + role: string, + input: SetAvailabilityInput +) => { + if (role !== UserRole.INFLUENCER) { + throw createHttpError("Only influencers can set availability", 403); + } + + const influencerProfile = await InfluencerProfile.findOne({ + userId: new Types.ObjectId(userId) + }); + + if (!influencerProfile) { + throw createHttpError("Influencer profile not found", 404); + } + + const availability = await upsertAvailability( + influencerProfile._id, + { + influencerProfileId: influencerProfile._id, + timezone: input.timezone, + weeklyRules: input.weeklyRules, + dateOverrides: input.dateOverrides + } + ); + + return availability; +}; + +export const getAvailabilityService = async ( + influencerProfileId: string +) => { + const availability = await findAvailabilityByInfluencer( + new Types.ObjectId(influencerProfileId) + ); + + if (!availability) { + throw createHttpError("Availability not found", 404); + } + + return availability; +}; \ No newline at end of file diff --git a/apps/core-api/src/services/collaboration.service.ts b/apps/core-api/src/services/collaboration.service.ts new file mode 100644 index 0000000..a80be23 --- /dev/null +++ b/apps/core-api/src/services/collaboration.service.ts @@ -0,0 +1,80 @@ +import { Types } from "mongoose"; + +import { GigModel } from "../models/gig.model.js"; +import { InfluencerProfile } from "../models/influencer.model.js"; +import { + createCollaboration, + findCollaborationById, + updateCollaborationStatus +} from "../repositories/collaboration.repository.js"; + +export const inviteCollaboratorsService = async ( + gigId: string, + userId: string, + collaboratorIds: string[] +) => { + const gig = await GigModel.findById(gigId); + + if (!gig) { + throw Object.assign(new Error("Gig not found"), { statusCode: 404 }); + } + + if (gig.primaryInfluencerId.toString() !== userId) { + throw Object.assign(new Error("Unauthorized"), { statusCode: 403 }); + } + + const collaborations = []; + + for (const id of collaboratorIds) { + const profile = await InfluencerProfile.findById(id); + if (!profile) continue; + + const collab = await createCollaboration({ + gigId: gig._id, + primaryInfluencerId: gig.primaryInfluencerId, + invitedInfluencerId: new Types.ObjectId(id), + status: "pending" + }); + + collaborations.push(collab); + } + + return collaborations; +}; + +export const respondToCollaborationService = async ( + collaborationId: string, + userId: string, + action: "accepted" | "rejected" +) => { + const collaboration = await findCollaborationById( + new Types.ObjectId(collaborationId) + ); + + if (!collaboration) { + throw Object.assign(new Error("Collaboration not found"), { + statusCode: 404 + }); + } + + if (collaboration.invitedInfluencerId.toString() !== userId) { + throw Object.assign(new Error("Unauthorized"), { + statusCode: 403 + }); + } + + const updated = await updateCollaborationStatus( + collaboration._id, + action + ); + + if (action === "accepted") { + await GigModel.findByIdAndUpdate(collaboration.gigId, { + $addToSet: { + influencerIds: collaboration.invitedInfluencerId + } + }); + } + + return updated; +}; \ No newline at end of file diff --git a/apps/core-api/src/services/gig.service.ts b/apps/core-api/src/services/gig.service.ts new file mode 100644 index 0000000..f768bdb --- /dev/null +++ b/apps/core-api/src/services/gig.service.ts @@ -0,0 +1,472 @@ +import mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { JwtPayload } from "jsonwebtoken"; + +import { create_gig, findActiveGigById, findGigById, findGigsByInfluencer, findPublishedGigById, findPublishedGigs, softDeleteGig } from "../repositories/gig.repository.js"; +import { type GigDeliverable, type GigType, type Platform } from "../types/gig.type.js"; +import { type GigDocument } from "../types/gig.type.js"; +import { InfluencerProfile, type IInfluencerProfile } from "../models/influencer.model.js"; +import { UserRole } from "../models/user.model.js"; + + +/* ================= TYPES ================= */ + +interface GigQuery { + category?: string; + minPrice?: string; + maxPrice?: string; + sort?: "price_asc" | "price_desc"; +} + +interface HttpError extends Error { + statusCode?: number; +} + +/* ================= LIST GIGS ================= */ + +export const listGigsService = async ( + query: GigQuery, + page: number, + limit: number +) => { + const skip = (page - 1) * limit; + + const filter: Record = { + + status: "published", + isDeleted: false + }; + + // Category filter + if (query.category) { + filter.category = query.category; + } + + // Price filter + if (query.minPrice || query.maxPrice) { + const priceFilter: { + $gte?: number; + $lte?: number; + } = {}; + + if (query.minPrice) { + priceFilter.$gte = Number(query.minPrice); + } + + if (query.maxPrice) { + priceFilter.$lte = Number(query.maxPrice); + } + + filter["pricing.basePrice"] = priceFilter; + } + + // Sorting + let sort: Record = { createdAt: -1 }; + + if (query.sort === "price_asc") { + sort = { "pricing.basePrice": 1 }; + } + + if (query.sort === "price_desc") { + sort = { "pricing.basePrice": -1 }; + } + + const { gigs, total } = await findPublishedGigs( + filter, + sort, + skip, + limit +); + + return { + data: gigs, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }; +}; + +/* ================= GET GIG DETAILS ================= */ + +export const getGigDetailsService = async (gigId: string) => { + if (!mongoose.Types.ObjectId.isValid(gigId)) { + const err = new Error("Invalid gig ID") as HttpError; + err.statusCode = 400; + throw err; + } + + const gig = await findPublishedGigById(gigId); + if (!gig) { + const err = new Error("Gig not found") as HttpError; + err.statusCode = 404; + throw err; + } + + return gig; +}; + +/* ================= TYPES ================= */ + +interface CreateGigInput { + title: string; + shortDescription: string; + platform: Platform; + gigType: GigType; + + category: string; + tags: string[]; + + deliverables: GigDeliverable[]; + + basePrice: number; + currency: "INR" | "USD"; + negotiationAllowed?: boolean; + deliveryTimeInDays: number; + revisionsIncluded: number; + + maxBookingsPerSlot?: number; + + collaboratorIds?: string[]; +} + +/* ================= CREATE GIG ================= */ + +export const createGigService = async ( + userId: string, + role: string, + input: CreateGigInput +): Promise<{ + gig: GigDocument; + collaborators: string[]; + }> => { + if (role !== UserRole.INFLUENCER) { + throw Object.assign(new Error("only influencers can create gigs"), { + statusCode: 403 + }); + } + + const influencerProfile = await InfluencerProfile.findOne({ + userId: new Types.ObjectId(userId) + }); + + if (!influencerProfile) { + throw Object.assign(new Error("influencer profile not found"), { + statusCode: 404 + }); + } + + const gig = await create_gig({ + title: input.title, + shortDescription: input.shortDescription, + platform: input.platform, + gigType: input.gigType, + category: input.category, + tags: input.tags, + + influencerIds: [influencerProfile._id], + primaryInfluencerId: influencerProfile._id, + + pricing: { + basePrice: 0, + currency: "INR", + negotiationAllowed: false, + deliveryTimeInDays: 1, + revisionsIncluded: 0 + }, + + deliverables: [], + + status: "draft", + isDeleted: false + }); + + return { + gig, + collaborators: input.collaboratorIds ?? [] +} +}; + +export const updateGigDeliverablesService = async ( + gigId: string, + userId: string, + deliverables: GigDeliverable[] +) => { + const gig = await findGigById(gigId); + if (!gig) { + throw Object.assign(new Error("Gig not found"), { statusCode: 404 }); + } + + const influencerProfile = await InfluencerProfile.findOne({ + userId: new Types.ObjectId(userId) + }); + + if (!influencerProfile) { + throw Object.assign(new Error("Influencer profile not found"), { + statusCode: 404 + }); + } + + if (gig.primaryInfluencerId.toString() !== influencerProfile._id.toString()) { + throw Object.assign(new Error("Unauthorized"), { + statusCode: 403 + }); + } + + gig.deliverables = deliverables; + await gig.save(); + + return gig; +}; + +export const updateGigPricingService = async ( + gigId: string, + userId: string, + pricingInput: { + basePrice: number; + currency: "INR" | "USD"; + negotiationAllowed?: boolean; + deliveryTimeInDays: number; + revisionsIncluded: number; + } +) => { +const gig = await findGigById(gigId); + if (!gig) { + throw Object.assign(new Error("Gig not found"), { statusCode: 404 }); + } + + const influencerProfile = await InfluencerProfile.findOne({ + userId: new Types.ObjectId(userId) + }); + + if (!influencerProfile) { + throw Object.assign(new Error("Influencer profile not found"), { statusCode: 404 }); + } + + if (gig.primaryInfluencerId.toString() !== influencerProfile._id.toString()) { + throw Object.assign(new Error("Unauthorized"), { statusCode: 403 }); + } + + gig.pricing = { + basePrice: pricingInput.basePrice, + currency: pricingInput.currency, + negotiationAllowed: pricingInput.negotiationAllowed ?? false, + deliveryTimeInDays: pricingInput.deliveryTimeInDays, + revisionsIncluded: pricingInput.revisionsIncluded + }; + + await gig.save(); + + return gig; +}; + + + +export const publishGigService = async ( + gigId: string, + userId: string +) => { +const gig = await findGigById(gigId); + if (!gig) { + throw Object.assign(new Error("Gig not found"), { statusCode: 404 }); + } + + // 🔥 FIX: resolve influencer profile from userId + const influencerProfile = await InfluencerProfile.findOne({ + userId: new Types.ObjectId(userId) + }); + + if (!influencerProfile) { + throw Object.assign(new Error("Influencer profile not found"), { + statusCode: 404 + }); + } +} +//* ================= EDIT GIG ================= */ + + + + +type EditableGigFields = { + title?: string; + shortDescription?: string; + category?: string; + tags?: string[]; + deliverables?: GigDeliverable[]; + pricing?: { + basePrice: number; + currency: "INR" | "USD"; + negotiationAllowed?: boolean; + }; + maxBookingsPerSlot?: number; + status?: "draft" | "published" | "paused" | "archived"; +}; + +export const editGigService = async ( + gigId: string, + user: JwtPayload, + updateData: EditableGigFields +) => { + if (!mongoose.Types.ObjectId.isValid(gigId)) { + throw Object.assign(new Error("Invalid gig ID"), { statusCode: 400 }); + } + + if (user.role !== "INFLUENCER") { + throw Object.assign(new Error("Only influencers can edit gigs"), { + statusCode: 403 + }); + } + + const influencerProfile = await InfluencerProfile.findOne({ + userId: user.userId + }); + + if (!influencerProfile) { + throw Object.assign(new Error("Influencer profile not found"), { + statusCode: 404 + }); + } + + const gig = await findActiveGigById(gigId); + + if (!gig) { + throw Object.assign(new Error("Gig not found"), { statusCode: 404 }); + } + + if ( + gig.primaryInfluencerId.toString() !== + influencerProfile._id.toString() + ) { + throw Object.assign(new Error("Unauthorized"), { + statusCode: 403 + }); + } + + if (gig.status === "archived") { + throw Object.assign(new Error("Archived gig cannot be edited"), { + statusCode: 400 + }); + } + + // 🔥 SAFE FIELD UPDATES + + if (updateData.title !== undefined) { + gig.title = updateData.title; + } + + if (updateData.shortDescription !== undefined) { + gig.shortDescription = updateData.shortDescription; + } + + if (updateData.category !== undefined) { + gig.category = updateData.category; + } + + if (updateData.tags !== undefined) { + gig.tags = updateData.tags; + } + + if (updateData.deliverables !== undefined) { + gig.deliverables = updateData.deliverables; + } + + if (updateData.pricing !== undefined) { + if (updateData.pricing.basePrice !== undefined) { + gig.pricing.basePrice = updateData.pricing.basePrice; + } + + if (updateData.pricing.currency !== undefined) { + gig.pricing.currency = updateData.pricing.currency; + } + + if (updateData.pricing.negotiationAllowed !== undefined) { + gig.pricing.negotiationAllowed = + updateData.pricing.negotiationAllowed; + } + } + + if (updateData.maxBookingsPerSlot !== undefined) { + gig.maxBookingsPerSlot = updateData.maxBookingsPerSlot; + } + + if (updateData.status !== undefined) { + gig.status = updateData.status; + } + + await gig.save(); + + return gig; +}; + + +//* ================= DELETE GIG ================= + + + +export const deleteGigService = async ( + gigId: string, + user: JwtPayload +): Promise => { + // Validate ObjectId + if (!mongoose.Types.ObjectId.isValid(gigId)) { + const err: HttpError = new Error("Invalid gig ID"); + err.statusCode = 400; + throw err; + } + + // Only influencers allowed + if (user.role !== "INFLUENCER") { + const err: HttpError = new Error("Only influencers can delete gigs"); + err.statusCode = 403; + throw err; + } + + // Find influencer profile + const influencerProfile: IInfluencerProfile | null = + await InfluencerProfile.findOne({ + userId: user.userId + }); + + if (!influencerProfile) { + const err: HttpError = new Error("Influencer profile not found"); + err.statusCode = 404; + throw err; + } + + // Find gig + const gig = await findActiveGigById(gigId); + + + if (!gig) { + const err: HttpError = new Error("Gig not found or already deleted"); + err.statusCode = 404; + throw err; + } + + // Ownership check + if ( + gig.primaryInfluencerId.toString() !== + influencerProfile._id.toString() + ) { + const err: HttpError = new Error( + "You are not allowed to delete this gig" + ); + err.statusCode = 403; + throw err; + } + + // Soft delete + await softDeleteGig(gigId); +}; + +export const getMyGigsService = async (userId: string) => { + const influencerProfile = await InfluencerProfile.findOne({ userId }); + + if (!influencerProfile) { + throw Object.assign(new Error("Influencer profile not found"), { statusCode: 404 }); + } + + return findGigsByInfluencer(influencerProfile._id); +}; \ No newline at end of file diff --git a/apps/core-api/src/services/media.service.ts b/apps/core-api/src/services/media.service.ts new file mode 100644 index 0000000..b182fcd --- /dev/null +++ b/apps/core-api/src/services/media.service.ts @@ -0,0 +1,33 @@ +import { PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +import { s3Client } from "../lib/s3.client.js"; + +export const generateUploadUrlService = async ( + folder: string, + fileName: string, + fileType: string +) => { + + const uniqueFileName = `${Date.now()}-${fileName}`; + + const key = `${folder}/${uniqueFileName}`; + + const command = new PutObjectCommand({ + Bucket: process.env.AWS_BUCKET_NAME!, + Key: key, + ContentType: fileType + }); + + const uploadUrl = await getSignedUrl(s3Client, command, { + expiresIn: 300 + }); + + const fileUrl = + `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`; + + return { + uploadUrl, + fileUrl + }; +}; \ No newline at end of file diff --git a/apps/core-api/src/services/profile.service.ts b/apps/core-api/src/services/profile.service.ts new file mode 100644 index 0000000..270101f --- /dev/null +++ b/apps/core-api/src/services/profile.service.ts @@ -0,0 +1,89 @@ +import { profileRepository } from "../repositories/profile.repository.js"; +import { userRepository } from "../repositories/user.repository.js"; +import type { HttpError } from "../modules/auth/http-error.js"; +import type { IInfluencerProfile } from "../models/influencer.model.js"; +import type { IBrandProfile } from "../models/brand.model.js"; +import { findGigsByInfluencer } from "../repositories/gig.repository.js"; + + +export const getMyProfileService = async (userId: string) => { + + const user = await userRepository.findById(userId); + + if (!user) { + const err: HttpError = new Error("User not found"); + err.statusCode = 404; + throw err; + } + + if (user.role === "INFLUENCER") { + + const profile = await profileRepository.findInfluencerByUserId(userId); + + if (!profile) { + const err: HttpError = new Error("Profile not found"); + err.statusCode = 404; + throw err; + } + + + return profile; + } + + if (user.role === "BRAND") { + + const profile = await profileRepository.findBrandByUserId(userId); + + if (!profile) { + const err: HttpError = new Error("Profile not found"); + err.statusCode = 404; + throw err; + } + + + return profile; + } + + const err: HttpError = new Error("Invalid role"); + err.statusCode = 400; + throw err; +}; + + +export const updateProfileService = async ( + userId: string, + data: unknown + +) => { + + const user = await userRepository.findById(userId); + if (!user) { + const err: HttpError = new Error("User not found"); + err.statusCode = 404; + throw err; + } + + if (user.role === "INFLUENCER") { + return profileRepository.updateInfluencer(userId, data as Partial); + } + + if (user.role === "BRAND") { + return profileRepository.updateBrand(userId, data as Partial); + } + + throw new Error("Invalid role"); +}; + +export const getPublicInfluencerProfileService = async (influencerId: string) => { + const profile = await profileRepository.findInfluencerById(influencerId); + + if (!profile) { + const err: HttpError = new Error("Influencer profile not found"); + err.statusCode = 404; + throw err; + } + + const gigs = await findGigsByInfluencer(influencerId); + + return { profile, gigs }; +}; diff --git a/apps/core-api/src/services/verification.service.ts b/apps/core-api/src/services/verification.service.ts new file mode 100644 index 0000000..f585bc3 --- /dev/null +++ b/apps/core-api/src/services/verification.service.ts @@ -0,0 +1,238 @@ +import bcrypt from "bcrypt"; + +import type { HttpError } from "../modules/auth/http-error.js"; +import { pendingSignupRepository } from "../repositories/Signup.repository.js"; +import { userRepository } from "../repositories/user.repository.js"; +import { profileRepository } from "../repositories/profile.repository.js"; +import type { PendingSignupFilter, PendingSignupQuery } from "../types/pendingSignup.types.js"; + +// ================= VERIFY OTP ================= +export const verifyOtpService = async ( + email: string, + otp: string +) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Signup request not found"); + err.statusCode = 404; + throw err; + } + + if (pending.isEmailVerified) { + const err: HttpError = new Error("Email already verified"); + err.statusCode = 400; + throw err; + } + + const now = new Date(); + + // 🔒 Lock check + if (pending.otpLockedUntil && pending.otpLockedUntil > now) { + const err: HttpError = new Error("Too many attempts. Try again later."); + err.statusCode = 403; + throw err; + } + + // ⏳ Expiry check + if (!pending.emailOtpExpiresAt || pending.emailOtpExpiresAt < now) { + const err: HttpError = new Error("OTP expired"); + err.statusCode = 400; + throw err; + } + + const isMatch = await bcrypt.compare( + otp, + pending.emailOtpHash as string + ); + + if (!isMatch) { + pending.otpAttempts = (pending.otpAttempts || 0) + 1; + + if (pending.otpAttempts >= 5) { + pending.otpLockedUntil = new Date( + now.getTime() + 15 * 60 * 1000 // lock for 15 minutes + ); + } + + await pending.save(); + + const err: HttpError = new Error("Invalid OTP"); + err.statusCode = 401; + throw err; + } + + // ✅ SUCCESS + pending.isEmailVerified = true; + pending.emailOtpHash = null; + pending.emailOtpExpiresAt = null; + pending.otpAttempts = 0; + pending.otpLockedUntil = null; + + await pending.save(); + + return { message: "Email verified successfully" }; +}; + +// ================= RESEND OTP ================= +export const resendOtpService = async (email: string) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Signup request not found"); + err.statusCode = 404; + throw err; + } + + if (pending.isEmailVerified) { + const err: HttpError = new Error("Email already verified"); + err.statusCode = 400; + throw err; + } + + const now = new Date(); + + // ⏳ Cooldown check (60 seconds) + if ( + pending.otpLastSentAt && + now.getTime() - pending.otpLastSentAt.getTime() < 60 * 1000 + ) { + const err: HttpError = new Error("Please wait before requesting another OTP"); + err.statusCode = 429; + throw err; + } + + // 🔁 Max resend limit + if ((pending.otpResendCount || 0) >= 5) { + const err: HttpError = new Error("Maximum resend attempts reached"); + err.statusCode = 403; + throw err; + } + + // 🔐 Generate new OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const hashedOtp = await bcrypt.hash(otp, 10); + + pending.emailOtpHash = hashedOtp; + pending.emailOtpExpiresAt = new Date(now.getTime() + 5 * 60 * 1000); + pending.otpResendCount = (pending.otpResendCount || 0) + 1; + pending.otpLastSentAt = now; + + await pending.save(); + + // TODO: Send OTP email here + // await sendOtpEmail(email, otp); + + return { message: "OTP resent successfully" }; +}; + +// ================= APPROVE SIGNUP ================= +export const approveSignupService = async (email: string) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Pending signup not found"); + err.statusCode = 404; + throw err; + } + + if (pending.status !== "PENDING") { + const err: HttpError = new Error("Signup already processed"); + err.statusCode = 400; + throw err; + } + + // Check if user already exists + const existingUser = await userRepository.findByEmail(pending.email); + if (existingUser) { + const err: HttpError = new Error("User with this email already exists"); + err.statusCode = 409; + throw err; + } + + const user = await userRepository.create({ + email: pending.email, + password: pending.passwordHash, + role: pending.role, + // @ts-expect-error - adminLevel not in user create type yet + adminLevel: pending.adminLevel || null, + isEmailVerified: true, + status: "ACTIVE", + }); + + if (user.role === "INFLUENCER") { + await profileRepository.createInfluencer({ + userId: user._id, + fullName: "", + username: user.email?.split("@")[0] || user.email, + categories: [], + languages: [], + isProfileComplete: false, + isVerified: false, + }); + } + + if (user.role === "BRAND") { + await profileRepository.createBrand({ + userId: user._id, + companyName: "Pending Setup", + industry: "Not Specified", + contactPersonName: user.email?.split("@")[0] || "Pending", + contactEmail: user.email, + documents: [], + isProfileComplete: false, + isVerified: false, + }); + } + + + await pendingSignupRepository.updateStatus(email, "APPROVED"); + + return { message: "Signup approved successfully" }; +}; + +// ================= REJECT SIGNUP ================= +export const rejectSignupService = async ( + email: string, + _reason?: string +) => { + const pending = await pendingSignupRepository.findByEmail(email); + + if (!pending) { + const err: HttpError = new Error("Pending signup not found"); + err.statusCode = 404; + throw err; + } + + await pendingSignupRepository.updateStatus(email, "REJECTED"); + + return { message: "Signup rejected successfully" }; +}; + +export const cleanupExpiredSignups = async () => { + const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000); + + await pendingSignupRepository.deleteMany({ + isEmailVerified: false, + createdAt: { $lt: cutoff }, + }); +}; + + +export const getAllPendingSignupService = async (query: PendingSignupQuery = {}) => { + const { search, role, status = "PENDING" } = query; + const filter: PendingSignupFilter = { status }; + + if (role) { + filter.role = role.toUpperCase(); + } + + if (search) { + filter.$or = [ + { email: { $regex: search, $options: "i" } }, + { documents: { $regex: search, $options: "i" } }, + ]; + } + + return pendingSignupRepository.getAllPendingSignups(filter); +}; \ No newline at end of file diff --git a/apps/core-api/src/types/availability.types.ts b/apps/core-api/src/types/availability.types.ts new file mode 100644 index 0000000..44417f2 --- /dev/null +++ b/apps/core-api/src/types/availability.types.ts @@ -0,0 +1,40 @@ +import { Types } from "mongoose"; + +export type Weekday = + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday"; + +export interface TimeSlot { + startTime: string; + endTime: string; +} + +export interface WeeklyRule { + day: Weekday; + isEnabled: boolean; + slots: TimeSlot[]; +} + +export interface DateOverride { + date: string; + isAvailable: boolean; + slots: TimeSlot[]; +} + +export interface AvailabilityDocument { + influencerProfileId: Types.ObjectId; + + timezone: string; + + weeklyRules: WeeklyRule[]; + + dateOverrides: DateOverride[]; + + createdAt: Date; + updatedAt: Date; +} \ No newline at end of file diff --git a/apps/core-api/src/types/collaboration.type.ts b/apps/core-api/src/types/collaboration.type.ts new file mode 100644 index 0000000..bf92100 --- /dev/null +++ b/apps/core-api/src/types/collaboration.type.ts @@ -0,0 +1,22 @@ +import { Types } from "mongoose"; + +export type CollaborationStatus = + | "pending" + | "accepted" + | "rejected" + | "cancelled"; + +export interface GigCollaborationDocument { + gigId: Types.ObjectId; + + primaryInfluencerId: Types.ObjectId; + + invitedInfluencerId: Types.ObjectId; + + status: CollaborationStatus; + + respondedAt?: Date; + + createdAt: Date; + updatedAt: Date; +} diff --git a/apps/core-api/src/types/express.d.ts b/apps/core-api/src/types/express.d.ts new file mode 100644 index 0000000..89e7d9f --- /dev/null +++ b/apps/core-api/src/types/express.d.ts @@ -0,0 +1,11 @@ +import type { JwtPayload } from "../modules/auth/auth.utils.ts"; + +declare global { + namespace Express { + interface Request { + user?: JwtPayload; + } + } +} + +export {}; diff --git a/apps/core-api/src/types/gig.type.ts b/apps/core-api/src/types/gig.type.ts new file mode 100644 index 0000000..40ee58a --- /dev/null +++ b/apps/core-api/src/types/gig.type.ts @@ -0,0 +1,63 @@ +import { Types } from "mongoose"; + +export type GigStatus = + | "draft" + | "published" + | "paused" + | "archived"; + +export type Platform = + | "instagram" + | "youtube" + | "tiktok"; + +export type GigType = + | "solo" + | "collaboration"; + +export interface GigDeliverable { + contentType: string; + quantity: number; + includedItems: string[]; +} + +export interface GigPricing { + basePrice: number; + currency: "INR" | "USD"; + negotiationAllowed: boolean; + deliveryTimeInDays: number; + revisionsIncluded: number; +} + +export interface GigDocument { + _id: Types.ObjectId; + title: string; + shortDescription: string; + + platform: Platform; + gigType: GigType; + + influencerIds: Types.ObjectId[]; + primaryInfluencerId: Types.ObjectId; + + category: string; + tags: string[]; + + deliverables: GigDeliverable[]; + + pricing: GigPricing; + + maxBookingsPerSlot?: number; + + status: GigStatus; + + isDeleted: boolean; + + createdAt: Date; + updatedAt: Date; +} + +export type CreateGigDBInput = Omit< + GigDocument, + "_id" | "createdAt" | "updatedAt" +>; \ No newline at end of file diff --git a/apps/core-api/src/types/pendingSignup.types.ts b/apps/core-api/src/types/pendingSignup.types.ts new file mode 100644 index 0000000..3346ec2 --- /dev/null +++ b/apps/core-api/src/types/pendingSignup.types.ts @@ -0,0 +1,15 @@ +export interface PendingSignupQuery{ + search?:string; + role?:string; + status?:string; + +} + +export interface PendingSignupFilter{ + status?:string; + role?:string; + $or?:Array<{ + email?:{$regex:string,$options:string}; + documents?:{$regex:string,$options:string}; + }>; +} \ No newline at end of file diff --git a/apps/core-api/src/utils/sendotpEmail.ts b/apps/core-api/src/utils/sendotpEmail.ts new file mode 100644 index 0000000..e27e681 --- /dev/null +++ b/apps/core-api/src/utils/sendotpEmail.ts @@ -0,0 +1,18 @@ +import nodemailer from "nodemailer"; + +export const sendOtpEmail = async (to: string,otp: string) => { + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + await transporter.sendMail({ + from: process.env.EMAIL_USER, + to, + subject: "Your OTP Code", + text: `Your verification OTP is: ${otp}`, + }); +}; diff --git a/apps/realtime/src/server.ts b/apps/realtime/src/server.ts index 8dd1d2c..83573eb 100644 --- a/apps/realtime/src/server.ts +++ b/apps/realtime/src/server.ts @@ -1,13 +1,15 @@ -import { createServer } from "http"; +import { createServer } from "http"; import express from "express"; -import { Server } from "socket.io"; +import cors from "cors"; +import { Server } from "socket.io"; import { httpLogger } from "./middlewares/httpLogger"; import { logger } from "./utils/logger"; import { errorHandler, notFound } from "./middlewares/errorHandler"; const app = express(); +app.use(cors()) app.use(express.json()) app.use(httpLogger) const httpServer = createServer(app); diff --git a/apps/web/app/(admin)/adminAuth/login/page.tsx b/apps/web/app/(admin)/adminAuth/login/page.tsx new file mode 100644 index 0000000..1b81ac4 --- /dev/null +++ b/apps/web/app/(admin)/adminAuth/login/page.tsx @@ -0,0 +1,66 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; + +import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; +import AuthForm from "@/components/adminAuth/AuthForm"; + +export default function AdminLoginPage() { + const router = useRouter(); + const setAuth = useAuthStore((state) => state.setAuth); + + const [role, setRole] = useState<"ADMIN" | "SUPER_ADMIN">("ADMIN"); + const [formData, setFormData] = useState({ email: "", password: "" }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const response = await api.post("/auth/login", formData); + const { accessToken, user } = response.data.data; + + if (accessToken) { + // 1. Role Check: Must be ADMIN + if (user.role !== "ADMIN") { + setError("Access denied: You do not have admin privileges."); + return; + } + + // 2. Admin Level Check: If SUPER_ADMIN selected, verify it + if (role === "SUPER_ADMIN" && user.adminLevel !== "SUPER") { + setError("Access denied: You do not have super admin privileges."); + return; + } + + setAuth(accessToken, user); + router.push("/admindashboard"); + } else { + setError("Login failed: No access token returned."); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + const msg = err.response?.data?.message || err.message || "Login failed. Please try again."; + setError(msg); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/apps/web/app/(admin)/adminAuth/signup/page.tsx b/apps/web/app/(admin)/adminAuth/signup/page.tsx new file mode 100644 index 0000000..59b775c --- /dev/null +++ b/apps/web/app/(admin)/adminAuth/signup/page.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; + +import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; +import AuthForm from "@/components/adminAuth/AuthForm"; + +export default function AdminSignupPage() { + const router = useRouter(); + const setAuth = useAuthStore((state) => state.setAuth); + + const [role, setRole] = useState<"ADMIN" | "SUPER_ADMIN">("ADMIN"); + const [formData, setFormData] = useState({ email: "", password: "", confirmPassword: "" }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (formData.password !== formData.confirmPassword) { + setError("Passwords do not match."); + return; + } + setLoading(true); + setError(""); + try { + const payload = { + email: formData.email, + password: formData.password, + role: "ADMIN", + adminLevel: role === "SUPER_ADMIN" ? "SUPER" : "NORMAL", + documents: "" // Required by backend validator + }; + const response = await api.post("/auth/signup", payload); + const { accessToken, user } = response.data.data; + if (accessToken) { + setAuth(accessToken, user); + router.push("/admindashboard"); + } else { + setError("Signup failed: No token returned."); + } + } catch (err) { + const msg = err instanceof Error ? err.message : "Signup failed. Please try again."; + setError(msg); + } finally { + setLoading(false); + } + }; + + return ( + + ); +} diff --git a/apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx b/apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx new file mode 100644 index 0000000..92ec819 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/bookings-audit/page.tsx @@ -0,0 +1,97 @@ +"use client"; +import React from "react"; +import { + Search, + CheckCircle, + Clock, + DollarSign, + RefreshCw, + Filter +} from "lucide-react"; + +import MetricCard from "@/components/admindashboard/MetricCard"; +import BookingsTabs from "@/components/admindashboard/BookingsTabs"; +import BookingsTable from "@/components/admindashboard/BookingsTable"; +import InvestigationView from "@/components/admindashboard/InvestigationView"; +import { AdminGuard } from "@/components/rbac/Guards"; + +export default function BookingsAuditPage() { + return ( + +
+ {/* Left Column: Metrics & Table */} +
+ {/* Welcome Header */} +
+

Bookings & Payments Audit

+

Investigate transaction history and booking lifecycles. Read-only access.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Moderation Controls */} +
+ +
+
+ + +
+ +
+
+ + {/* Bookings Table */} + +
+ + {/* Right Column: Investigation View */} +
+ +
+
+
+ ); +} diff --git a/apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx b/apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx new file mode 100644 index 0000000..25806c1 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/disputes-reports/page.tsx @@ -0,0 +1,86 @@ +"use client"; +import React from "react"; +import { + Search, + Clock, + CheckCircle2, + Users, + Filter +} from "lucide-react"; + +import MetricCard from "@/components/admindashboard/MetricCard"; +import DisputeTabs from "@/components/admindashboard/DisputeTabs"; +import DisputeTable from "@/components/admindashboard/DisputeTable"; +import DisputeInvestigation from "@/components/admindashboard/DisputeInvestigation"; +import { AdminGuard } from "@/components/rbac/Guards"; + +export default function DisputesReportsPage() { + return ( + +
+ {/* Left Column: Metrics & Table */} +
+ {/* Welcome Header */} +
+

Disputes & Reports

+

Resolve conflicts safely and transparently.

+
+ + {/* Metrics Grid */} +
+ + + +
+ + {/* Search & Filter Controls */} +
+ +
+
+ + +
+ +
+
+ + {/* Disputes Table */} + +
+ + {/* Right Column: Investigation View */} +
+ +
+
+
+ ); +} diff --git a/apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx b/apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx new file mode 100644 index 0000000..ad814ad --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/gig-moderation/page.tsx @@ -0,0 +1,108 @@ +"use client"; +import React, { useState } from "react"; +import { + Briefcase, + AlertCircle, + PauseCircle, + DollarSign, + Filter, + Search +} from "lucide-react"; + +import MetricCard from "@/components/admindashboard/MetricCard"; +import GigsTabs from "@/components/admindashboard/GigsTabs"; +import GigsTable from "@/components/admindashboard/GigsTable"; +import DeleteConfirmationModal from "@/components/admindashboard/DeleteConfirmationModal"; + +export default function GigsModerationPage() { + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedGig, setSelectedGig] = useState(null); + + const handleDeleteClick = (gig: unknown) => { + setSelectedGig(gig); + setIsDeleteModalOpen(true); + }; + + const handleConfirmDelete = () => { + if (selectedGig && typeof selectedGig === 'object' && 'id' in selectedGig) { + console.log("Deleting gig:", selectedGig.id); + } + // Add actual delete logic here + }; + + return ( + <> + {/* Welcome Header */} +
+

Gig Moderation

+

Review and manage influencer service listings to ensure platform compliance.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Moderation Controls */} +
+ +
+
+ + +
+ +
+
+ + {/* Gigs Table */} + + + {/* Delete Confirmation Modal */} + setIsDeleteModalOpen(false)} + onConfirm={handleConfirmDelete} + /> + + ); +} diff --git a/apps/web/app/(admin)/admindashboard/layout.tsx b/apps/web/app/(admin)/admindashboard/layout.tsx new file mode 100644 index 0000000..e865916 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/layout.tsx @@ -0,0 +1,68 @@ +"use client"; + +import React, { useState } from "react"; +import { Menu, Search, Bell, Download, Plus } from "lucide-react"; + +import { AdminGuard } from "@/components/rbac/Guards"; +import Sidebar from "@/components/admindashboard/Sidebar"; + +export default function AdminDashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + return ( + +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Topbar */} +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + {/* Page Content */} +
+ {children} +
+
+
+
+ ); +} diff --git a/apps/web/app/(admin)/admindashboard/page.tsx b/apps/web/app/(admin)/admindashboard/page.tsx new file mode 100644 index 0000000..15b984f --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/page.tsx @@ -0,0 +1,79 @@ +"use client"; +import React from "react"; +import { + Users, + UserCheck, + Briefcase, + DollarSign +} from "lucide-react"; + +import MetricCard from "@/components/admindashboard/MetricCard"; +import ActivityTable from "@/components/admindashboard/ActivityTable"; +import StatsChart from "@/components/admindashboard/StatsChart"; +import SystemHealth from "@/components/admindashboard/SystemHealth"; + +export default function DashboardPage() { + return ( + <> + {/* Welcome Header */} +
+

Platform Overview

+

Welcome back, here's what's happening today.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Main Grid: Activity and Trends */} +
+
+ +
+ +
+
+ +
+
+ +
+
+
+ + ); +} diff --git a/apps/web/app/(admin)/admindashboard/user-verification/page.tsx b/apps/web/app/(admin)/admindashboard/user-verification/page.tsx new file mode 100644 index 0000000..cb5d088 --- /dev/null +++ b/apps/web/app/(admin)/admindashboard/user-verification/page.tsx @@ -0,0 +1,153 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import { + Users, + UserCheck, + Briefcase, + DollarSign, + Search +} from "lucide-react"; + +import MetricCard from "@/components/admindashboard/MetricCard"; +import VerificationTable from "@/components/admindashboard/VerificationTable"; +import VerificationTabs from "@/components/admindashboard/VerificationTabs"; +import UserDetailsDrawer from "@/components/admindashboard/UserDetailsDrawer"; +import api from "@/lib/axios.client"; + +export default function VerificationPage() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [selectedUser, setSelectedUser] = useState(null); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [requests, setRequests] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchRequests = async () => { + setLoading(true); + try { + const response = await api.get("/admin/signup/"); + if (response.data.success) { + setRequests(response.data.data); + } + } catch (error) { + console.error("Failed to fetch requests:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchRequests(); + }, []); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const handleSelectUser = (user: any) => { + setSelectedUser(user); + setIsDrawerOpen(true); + }; + + const handleApprove = async (email: string) => { + try { + const response = await api.post("/admin/signup/approve", { email }); + if (response.data.success) { + // Refresh list + fetchRequests(); + } + } catch (error) { + console.error("Failed to approve:", error); + } + }; + + const handleReject = async (email: string) => { + try { + const response = await api.post("/admin/signup/reject", { email }); + if (response.data.success) { + // Refresh list + fetchRequests(); + } + } catch (error) { + console.error("Failed to reject:", error); + } + }; + + return ( + <> + {/* Welcome Header */} +
+

User Verification

+

Review and manage pending influencer and brand applications.

+
+ + {/* Metrics Grid */} +
+ + + + +
+ + {/* Verification Controls */} +
+ +
+
+ + +
+
+
+ + {/* Verification Table */} + + + {/* User Details Drawer */} + setIsDrawerOpen(false)} + user={selectedUser} + onApprove={handleApprove} + onReject={handleReject} + /> + + ); +} diff --git a/apps/web/app/(auth)/forget-Password/page.tsx b/apps/web/app/(auth)/forget-Password/page.tsx new file mode 100644 index 0000000..dcb7e1b --- /dev/null +++ b/apps/web/app/(auth)/forget-Password/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; + +import api from "@/lib/axios.client"; + +export default function ForgotPassword() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + await api.post("/auth/forgot-password", { email }); + router.push(`/verify-otp?email=${encodeURIComponent(email)}`); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to send OTP. Please try again."; + // Axios error objects usually have err.response?.data?.message + const errorObj = err as { response?: { data?: { message?: string } } }; + if (errorObj.response?.data?.message) { + setError(errorObj.response.data.message); + } else { + setError(errorMessage); + } + } finally { + setLoading(false); + } + }; + + return ( +
+
+ +

+ Forgot Password +

+ +

+ Enter your email to receive OTP +

+ +
+ {error && ( +
{error}
+ )} + setEmail(e.target.value)} + required + className="w-full text-black px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" + /> + + +
+ +

+ Remembered password?{" "} + + Back to Login + +

+ +
+
+ ); +} diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx new file mode 100644 index 0000000..6023293 --- /dev/null +++ b/apps/web/app/(auth)/login/page.tsx @@ -0,0 +1,156 @@ +"use client"; + +import Link from "next/link"; +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; + +import api from "@/lib/axios.client"; +import { useAuthStore } from "@/store/auth.store"; + + + +export default function LoginPage() { + const router = useRouter(); + const [role, setRole] = useState<"BRAND" | "INFLUENCER">("BRAND"); + const [formData, setFormData] = useState({ + email: "", + password: "" + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const setAuth = useAuthStore((state)=>state.setAuth) + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + + })); + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const payload = { + email: formData.email, + password: formData.password, + role: role + } + + const response = await api.post("/auth/login", payload); + + // Access token is now in response.data.data.accessToken + if (response.data.data?.accessToken) { + const {accessToken, user} = response.data.data + setAuth(accessToken, user) + router.push("/home"); ; + } + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Login failed. Please try again."; + setError(errorMessage); + } finally { + setLoading(false); + } + } + + + return ( +
+
+ +
+

+ Welcome Back +

+

+ Log in to your Noillin account as{" "} + {role} +

+
+ +
+ + + +
+ +
+ {error && ( +
{error}
+ )} +
+ + +
+ +
+
+ +

+ + forgot password + +

+
+ + +
+ + +
+ +

+ Secure login · Data protected +

+
+
+ ); +} diff --git a/apps/web/app/(auth)/register/page.tsx b/apps/web/app/(auth)/register/page.tsx new file mode 100644 index 0000000..5b98fa3 --- /dev/null +++ b/apps/web/app/(auth)/register/page.tsx @@ -0,0 +1,135 @@ + +"use client"; + +import { useState } from "react"; + +export default function LoginPage() { + const [role, setRole] = useState<"brand" | "influencer">("brand"); + + return ( +
+
+ +
+

+ Welcome Back +

+

+ Join as a Brand or Influencer + +

+
+ +
+ + + +
+ +
+ +
+ + +
+
+ + +
+ +
+
+ + +
+ + +
+
+ + +
+
+ {role === "brand" ? ( + <> + + + + ) : ( + <> + + + + )} +
+ + + +
+ +

+ Secure login · Data protected +

+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/(auth)/reset-password/page.tsx b/apps/web/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..c574fd8 --- /dev/null +++ b/apps/web/app/(auth)/reset-password/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import React, { useState, useEffect, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +import api from "@/lib/axios.client"; + +function ResetPasswordContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const email = searchParams.get("email") || ""; + const resetSessionToken = searchParams.get("token") || ""; + + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + useEffect(() => { + if (!email || !resetSessionToken) { + router.push("/forget-Password"); + } + }, [email, resetSessionToken, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (newPassword !== confirmPassword) { + setError("Passwords do not match"); + return; + } + setLoading(true); + setError(""); + + try { + await api.post("/auth/reset-password", { + email, + newPassword, + resetSessionToken + }); + setSuccess(true); + setTimeout(() => { + router.push("/login"); + }, 2000); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to reset password."; + const errorObj = err as { response?: { data?: { message?: string } } }; + if (errorObj.response?.data?.message) { + setError(errorObj.response.data.message); + } else { + setError(errorMessage); + } + } finally { + setLoading(false); + } + }; + + if (success) { + return ( +
+
+

Password Reset Successful!

+

Redirecting you to login...

+
+
+ ); + } + + return ( +
+
+ +

+ Reset Password +

+ +

+ Enter your new password +

+ +
+ {error &&
{error}
} + setNewPassword(e.target.value)} + required + className="w-full px-4 text-black py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" + /> + + setConfirmPassword(e.target.value)} + required + className="w-full px-4 py-2 text-black border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" + /> + + +
+ +
+
+ ); +} + +export default function ResetPassword() { + return ( + Loading...}> + + + ); +} \ No newline at end of file diff --git a/apps/web/app/(auth)/verify-otp/page.tsx b/apps/web/app/(auth)/verify-otp/page.tsx new file mode 100644 index 0000000..4fe7aeb --- /dev/null +++ b/apps/web/app/(auth)/verify-otp/page.tsx @@ -0,0 +1,132 @@ +"use client"; + +import React, { useState, useEffect, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +import api from "@/lib/axios.client"; + +function VerifyOtpContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const email = searchParams.get("email") || ""; + + const [otp, setOtp] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [resendLoading, setResendLoading] = useState(false); + const [resendMessage, setResendMessage] = useState(""); + + // Redirect back if no email is provided + useEffect(() => { + if (!email) { + router.push("/forget-Password"); + } + }, [email, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + const response = await api.post("/auth/verify-reset-otp", { email, otp }); + const resetSessionToken = response.data?.data?.resetSessionToken; + + if (resetSessionToken) { + router.push(`/reset-password?email=${encodeURIComponent(email)}&token=${encodeURIComponent(resetSessionToken)}`); + } else { + setError("Invalid response from server. Missing token."); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to verify OTP."; + const errorObj = err as { response?: { data?: { message?: string } } }; + if (errorObj.response?.data?.message) { + setError(errorObj.response.data.message); + } else { + setError(errorMessage); + } + } finally { + setLoading(false); + } + }; + + const handleResend = async () => { + if (!email) return; + setResendLoading(true); + setError(""); + setResendMessage(""); + try { + await api.post("/auth/forgot-password", { email }); + setResendMessage("OTP resent successfully!"); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Failed to resend OTP."; + const errorObj = err as { response?: { data?: { message?: string } } }; + if (errorObj.response?.data?.message) { + setError(errorObj.response.data.message); + } else { + setError(errorMessage); + } + } finally { + setResendLoading(false); + } + }; + + return ( +
+
+ +

+ Verify OTP +

+ +

+ Enter the 6-digit OTP sent to {email ? {email} : "your email"} +

+ +
+ {error &&
{error}
} + {resendMessage &&
{resendMessage}
} + + setOtp(e.target.value.replace(/\D/g, ''))} // only numbers + required + placeholder="Enter OTP" + className="w-full px-4 text-black py-2 text-center tracking-widest border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" + /> + + +
+ +

+ Didn’t receive OTP?{" "} + +

+ +
+
+ ); +} + +export default function VerifyOtp() { + return ( + Loading...}> + + + ); +} \ No newline at end of file diff --git a/apps/web/app/brand-dashboard/page.tsx b/apps/web/app/brand-dashboard/page.tsx new file mode 100644 index 0000000..9bcb608 --- /dev/null +++ b/apps/web/app/brand-dashboard/page.tsx @@ -0,0 +1,424 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { + Bell, + ChevronDown, + UserCheck, + CalendarCheck, + Clock, + CircleDollarSign, + ChevronRight, + CheckCircle2, + Calendar, +} from "lucide-react"; + +import { useAuthStore } from "@/store/auth.store"; +import api from "@/lib/axios.client"; + +// Define the Gig interfaces similar to how gig-list works +interface GigPricing { + basePrice: number; + currency: string; +} + +interface Gig { + _id: string; + title: string; + category: string; + pricing: GigPricing; + primaryInfluencerId: { + _id: string; + displayName: string; + profileImage?: string; + availableFrom?: string; + platforms?: ("IG" | "YT" | "TT" | "PT")[]; + }; +} + +const formatCurrency = (amount: number, currency = "INR") => { + if (currency === "INR") return `₹${amount.toLocaleString("en-IN")}`; + return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount); +}; + +export default function Dashboard() { + const { user } = useAuthStore(); + const isAuthenticated = !!user; + const [gigs, setGigs] = useState([]); + const [loadingGigs, setLoadingGigs] = useState(true); + + useEffect(() => { + // Fetch a few gigs for the "Explore Gigs" section + const fetchGigs = async () => { + try { + const res = await api.get("/gigs?limit=4"); + setGigs(res.data.data); + } catch (error) { + console.error("Failed to fetch gigs:", error); + } finally { + setLoadingGigs(false); + } + }; + + fetchGigs(); + }, []); + return ( +
+ {/* Navbar */} + + + {/* Main Content */} +
+ {/* Header */} +
+

Dashboard

+

+ Manage your gigs, bookings, and requests +

+
+ + {/* Stats Grid */} +
+ {/* Card 1 */} +
+
+ +
+

Requests

+
+ + {/* Card 2 */} +
+
+ +
+

+ Accepted Bookings +

+
+ + {/* Card 3 */} +
+
+ +
+

+ Pending Requests +

+
+ + {/* Card 4 */} +
+
+ +
+

+ Transaction History +

+
+
+ + {/* Middle Section: Requests & Messages */} +
+ {/* Requests */} +
+

Requests

+
+ {/* Item 1 */} +
+
+
+ N +
+
+

+ Aura Fashion +

+

+ Req: Fall Collection Reel +

+
+
+
+ + Pending + + +
+
+ + {/* Item 2 */} +
+
+ GreenLife +
+

+ GreenLife Organics +

+

+ Req: Morning Routine Story +

+
+
+
+ + Accepted + + +
+
+
+
+ +
+
+ + {/* Recent Messages */} +
+
+

+ Recent Messages +

+ +
+
+ {/* Msg 1 */} +
+
+ Marcus +
+
+
+
+

+ Marcus Chen (TechNova) +

+ + 10m ago + +
+

+ Hi Jane, I've reviewed the script you sent over. Looks great! Just one small change regarding the intro... +

+
+
+ + {/* Msg 2 */} +
+
+ Sarah +
+
+
+

+ Sarah (Lumina) +

+ + 1h ago + +
+

+ Thanks for the quick delivery! The photos look amazing. We'll process the payment today. +

+
+
+
+
+
+ + {/* Explore Gigs */} +
+

Explore Gigs

+

+ Find recommended gigs based on your search +

+ +
+ {loadingGigs ? ( + // Simple skeleton + Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ )) + ) : gigs.length === 0 ? ( +
+ No gigs available right now. +
+ ) : ( + gigs.map((gig) => { + const influencer = gig.primaryInfluencerId; + const name = influencer?.displayName || "Unknown Creator"; + const platforms = influencer?.platforms || []; + const availableLabel = influencer?.availableFrom + ? new Date(influencer.availableFrom).toLocaleDateString("en-US", { month: "short", day: "numeric" }) + : "Available"; + + return ( +
+
+
+
+ {name.substring(0, 2).toUpperCase()} +
+
+

+ {name} +

+

{gig.category}

+
+
+

+ {gig.title} +

+ {platforms.length > 0 && ( +
+ {platforms.map(p => ( +
+ {p} +
+ ))} +
+ )} +
+ +
+
+ Starting at + {formatCurrency(gig.pricing.basePrice, gig.pricing.currency)} +
+
+ + Next available: + {availableLabel} +
+
+

+ Booking confirmed after payment +

+ + View Gig + +
+
+
+ ); + }) + )} + +
+
+
+
+ ); +} diff --git a/apps/web/app/brand-profile-page/page.tsx b/apps/web/app/brand-profile-page/page.tsx new file mode 100644 index 0000000..61f9681 --- /dev/null +++ b/apps/web/app/brand-profile-page/page.tsx @@ -0,0 +1,536 @@ +import Link from 'next/link'; +import { + BadgeCheck, + Globe, + MessageSquare, + Send, + Megaphone, + CheckCircle2, + Star, + ShieldCheck, + RefreshCcw, + Clock, + Users, + Calendar, + MapPin, + Bookmark, + Share2, + Flag, + ChevronRight, + ArrowRight, + Video, + FileImage, + Building2 +} from 'lucide-react'; + +export default function BrandProfilePage() { + return ( +
+ {/* Header Profile Section */} +
+
+
+ +
+
+ GC +
+
+
+

Glow Cosmetics Pvt Ltd

+ + Verified Brand + +
+
+
+ + + + Beauty & Skincare +
+
+ Member since Jan 2021 +
+
+
+ + www.glowcosmetics.com + +
+
+
+ +
+ + +
+ +
+
+
+ +
+ + {/* Trust & Performance Metrics */} +
+

Trust & Performance

+
+
+
+ Campaigns Posted +
+
47
+
+
+
+ Completed Collabs +
+
128
+
+
+
+ Avg. Rating +
+
4.8/5
+
+
+
+ Payment Score +
+
98%
+
+
+
+ Repeat Rate +
+
72%
+
+
+
+ Avg. Response +
+
<4h
+
+
+
+ +
+ + {/* Main Content Column */} +
+ + {/* About the Brand */} +
+

About the Brand

+

+ Glow Cosmetics is a premium beauty and skincare brand dedicated to creating clean, sustainable, and effective products. We believe in empowering individuals through self-care and natural beauty. Our product line includes serums, moisturizers, and makeup essentials crafted with ethically sourced ingredients. +

+ +
+
+
+ +
+
+

Company Size

+

50-200 employees

+
+
+
+
+ +
+
+

Years in Business

+

6 years

+
+
+
+
+ +
+
+

Headquarters

+

Mumbai, India

+
+
+
+
+ + {/* Active Campaigns */} +
+
+

Active Campaigns

+ 3 open opportunities +
+ +
+ {/* Campaign 1 */} +
+
+
+

Summer Glow Collection Launch

+ Skincare +
+
+ ₹25K - ₹50K +
+
+ +
+
+
2 weeks
+
10K+ followers
+
+ + + View Details + +
+ + {/* Campaign 2 */} +
+
+
+

Vitamin C Serum Awareness

+ Beauty +
+
+ ₹15K - ₹30K +
+
+ +
+
5 Feed Posts
+
1 month
+
5K+ followers
+
+ + + View Details + +
+ + {/* Campaign 3 */} +
+
+
+

Sustainable Beauty Ambassador

+ Lifestyle +
+
+ ₹50K - ₹1L +
+
+ +
+
Long-term partnership
+
3 months
+
50K+ followers
+
+ + + View Details + +
+
+
+ + {/* Past Collaborations */} +
+

Past Collaborations

+ +
+ {/* Collab 1 */} +
+
+
+ PS +
+
+

Priya Sharma @priyabeauty

+

Winter Skincare Routine Campaign

+
+
+
+
+
₹35,000
+
+ {[...Array(5)].map((_, i) => )} +
+
+ Completed +
+
+ + {/* Collab 2 */} +
+
+
+ RV +
+
+

Rahul Verma @rahullifestyle

+

Men's Grooming Essentials

+
+
+
+
+
₹22,000
+
+ {[...Array(5)].map((_, i) => )} +
+
+ Completed +
+
+ + {/* Collab 3 */} +
+
+
+ AI +
+
+

Ananya Iyer @ananyaskincare

+

Anti-Aging Serum Review

+
+
+
+
+
₹18,000
+
+ {[...Array(5)].map((_, i) => )} +
+
+ Completed +
+
+
+ +
+ +
+
+ + {/* Reviews */} +
+
+

Reviews from Influencers

+
+
+ {[...Array(5)].map((_, i) => )} +
+ 4.8 + (89 reviews) +
+
+ +
+ {/* Review 1 */} +
+
+
+
+ MK +
+
+

Meera Kapoor

+
+ {[...Array(5)].map((_, i) => )} +
+
+
+ 2 weeks ago +
+

+ "Amazing experience working with Glow Cosmetics! They were professional, clear about deliverables, and paid on time. The products were high quality and my audience loved them. Highly recommend collaborating with this brand!" +

+
+ + {/* Review 2 */} +
+
+
+
+ AM +
+
+

Arjun Mehta

+
+ {[...Array(4)].map((_, i) => )} + +
+
+
+ 1 month ago +
+

+ "Great communication throughout the campaign. The team was responsive and provided all the creative assets I needed. Payment was processed within 48 hours of campaign completion. Will definitely work with them again!" +

+
+ + {/* Review 3 */} +
+
+
+
+ SR +
+
+

Sneha Reddy

+
+ {[...Array(5)].map((_, i) => )} +
+
+
+ 2 months ago +
+

+ "One of the best brands I've collaborated with on Noillin. They value influencer creativity and give creative freedom. The escrow payment system gave me peace of mind. Looking forward to more campaigns!" +

+
+
+ +
+ +
+
+ +
+ + {/* Sidebar Column */} +
+ + {/* Verification Section */} +
+

Verification & Safety

+
+
+
+ +
+
+

Business Verified

+

GST & PAN confirmed

+
+
+
+
+ +
+
+

Company Registered

+

Pvt Ltd entity verified

+
+
+
+
+ +
+
+

Secure Escrow

+

Protected payments

+
+
+
+
+ +

Platform Protection

+
+

+ All collaborations are protected by Noillin's secure payment system. Funds are held in escrow until you complete your deliverables. +

+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ +
+ + + +
+
+ + {/* Similar Brands */} +
+

Similar Brands

+
+ +
+
RS
+
+

Radiant Skin Co.

+

Beauty & Skincare

+
+
+ + + + +
+
PB
+
+

Pure Botanics

+

Organic Beauty

+
+
+ + + + +
+
LH
+
+

Luxe Herbals

+

Ayurvedic Skincare

+
+
+ + +
+
+ +
+ +
+
+ +
+
+
+ Noillin © 2024. All rights reserved. +
+
+ Privacy Policy + Terms of Service + Support +
+
+
+ +
+ ); +} diff --git a/apps/web/app/gig-details/page.tsx b/apps/web/app/gig-details/page.tsx new file mode 100644 index 0000000..40d52bf --- /dev/null +++ b/apps/web/app/gig-details/page.tsx @@ -0,0 +1,508 @@ +import Image from "next/image"; +import Link from "next/link"; +import { + Star, CheckCircle2, ChevronRight, Play, Heart, + MessageSquare, Check, + Share, ShieldCheck, CheckCircle, + BarChart, Users, Instagram, Info +} from "lucide-react"; + + +export default function GigDetailsPage() { + const portfolio = [ + { type: 'video', src: 'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?auto=format&fit=crop&q=80&w=400' }, + { type: 'video', src: 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?auto=format&fit=crop&q=80&w=400' }, + { type: 'video', src: 'https://images.unsplash.com/photo-1529139574466-a303027c1d8b?auto=format&fit=crop&q=80&w=400' }, + { type: 'image', src: 'https://images.unsplash.com/photo-1584273143981-41c073dfe8f8?auto=format&fit=crop&q=80&w=400' }, + { type: 'image', src: 'https://images.unsplash.com/photo-1550614000-4b9015c9a09e?auto=format&fit=crop&q=80&w=400' }, + { type: 'image', src: 'https://images.unsplash.com/photo-1504198458649-3128b932f49e?auto=format&fit=crop&q=80&w=400', count: "+12 More" }, + ]; + + const reviews = [ + { name: "Mikaela Smith", time: "1 month ago", text: "Priya delivered exceptionally well. The content was highly engaging and resonated perfectly with our brand's audience. Cannot recommend her enough!", rating: 5, avatar: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=100" }, + { name: "Ryan Carter", time: "2 months ago", text: "Incredible eye for detail! We were stunned by the quality of her videos. Will definitely rehire for upcoming campaigns.", rating: 5, avatar: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&q=80&w=100" }, + { name: "Sophia Martinez", time: "3 months ago", text: "Great communication and very timely delivery. The reels were creative and brought in a lot of new followers for our page.", rating: 5, avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=100" } + ]; + + const similarGigs = [ + { name: "Emma Red", title: "I will create stunning Instagram reels for your fashion brand", price: "12,000", rating: "4.8", reviews: "124", img: "https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?auto=format&fit=crop&q=80&w=400", avatar: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=100", type: "Instagram Reel" }, + { name: "Maya Patel", title: "Beauty product review and tutorial on Instagram", price: "18,500", rating: "4.9", reviews: "89", img: "https://images.unsplash.com/photo-1504198458649-3128b932f49e?auto=format&fit=crop&q=80&w=400", avatar: "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=100", type: "Story Post" }, + { name: "Zoe Miller", title: "Lifestyle product placement in my daily vlogs", price: "22,000", rating: "4.7", reviews: "210", img: "https://images.unsplash.com/photo-1584273143981-41c073dfe8f8?auto=format&fit=crop&q=80&w=400", avatar: "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&q=80&w=100", type: "Instagram Post" } + ]; + + return ( +
+ {/* Navigation Bar */} + + +
+ {/* Breadcrumb */} +
+ Home + + Beauty Influencers + + Priya Sharma +
+ +
+ + {/* Main Left Column */} +
+ + {/* Header / Basic Info */} +
+
+ Priya Sharma +
+
+

Priya Sharma

+ + + Verified + +
+
+ Fashion & Beauty | Vlogger + + + + 4.9 (120 Reviews) + +
+
+
+ +

+ I will promote your brand with 2 Instagram reels and story highlights +

+ +
+ + + Top Rated Influencer + + + + Save to favorites + + + + Share Gig + +
+
+ + {/* About this Gig */} +
+

About this Gig

+

+ Are you looking to skyrocket your brand's presence on Instagram? Look no further! I specialize in creating highly engaging, trendy, and authentic content tailored for the fashion and beauty industry. With over 245K active followers, I guarantee your product will be showcased to a vibrant and responsive audience. +

+ +

What You Will Receive:

+
    +
  • 2 High-quality, professionally edited Instagram Reels featuring your product prominently.
  • +
  • 3 Story frames with interactive elements (polls/questions) to drive engagement.
  • +
  • Story Highlight placement on my profile for 30 days.
  • +
  • Performance report with detailed analytics delivered 7 days after posting.
  • +
+ +

Custom Offers:

+

+ Need more reels or a long-term partnership? I am open to discussing custom packages that suit your brand's specific needs. Feel free to shoot me a message before placing an order to discuss your strategy and goals. +

+
+ + {/* Deliverables */} +
+

Deliverables

+
+ {[ + { icon: , title: 'Instagram Video', platforms: 'Instagram, Facebook' }, + { icon: , title: 'Story Posts', platforms: 'Instagram' }, + { icon: , title: 'Carousel Posts', platforms: 'Instagram' }, + ].map((item, i) => ( +
+
+ {item.icon} +
+

{item.title}

+

DELIVERY PLATFORM

+

{item.platforms}

+
+ ))} +
+
+ + {/* Portfolio & Past Work */} +
+
+

Portfolio & Past Work

+ View All +
+
+ {portfolio.map((item, i) => ( +
+ Portfolio item + {item.type === 'video' && ( +
+
+ +
+
+ )} + {item.count && ( +
+ {item.count} +
+ )} +
+ ))} +
+
+ + {/* Audience Insights */} +
+

Audience Insights

+
+
+
+ + Followers +
+
245K
+
+1.5% this month
+
+
+
+ + Engagement rate +
+
4.8%
+
Above average
+
+
+
+ + Avg. Likes +
+
11.8K
+
Per post average
+
+
+
+ + Avg. Comments +
+
487
+
Per post average
+
+
+ +
+
+

Top Countries

+
+
+
United States54%
+
+
+
+
United Kingdom18%
+
+
+
+
India11%
+
+
+
+
Australia5%
+
+
+
+
+
+

Age Range

+
+
+
18-2442%
+
+
+
+
25-3438%
+
+
+
+
35-4412%
+
+
+
+
Under 188%
+
+
+
+
+
+

Gender Split

+
+
+
+
+
+
Female (68%)
+
Male (32%)
+
+
+
+
+ + {/* Reviews Section */} +
+
+
4.9
+
+ {[1, 2, 3, 4, 5].map(i => )} +
+
Based on 120 reviews
+ +
+ {[5, 4, 3, 2, 1].map((stars, i) => { + const pct = stars === 5 ? 85 : stars === 4 ? 12 : 3; + return ( +
+
{stars}
+ +
+
+
+
{pct}%
+
+ ) + })} +
+
+
+ {reviews.map((rev, i) => ( +
+
+
+ {rev.name} +
+
+ {rev.name} + + VERIFIED BUYER + +
+
{rev.time}
+
+
+
+ {[1, 2, 3, 4, 5].map((s) => ( + + ))} +
+
+

{rev.text}

+
+ ))} +
+ View All Verified Reviews +
+
+
+ + {/* About the Influencer */} +
+

About the Influencer

+
+
+

+ A vibrant fashion and lifestyle content creator based in London. My mission is to empower individuals to express their authentic selves through style. I blend high-street finds with luxury statement pieces to create accessible yet aspirational looks. My content is bright, energetic, and highly interactive. +

+

+ I believe in genuine partnerships. When I recommend a product, it's because I truly use and love it, which reflects directly on the engagement and trust my audience places in my sponsored content. Let's create something beautiful together! +

+
+
+
+
MEMBER SINCE
+
May 12, 2021
+
+
+
LANGUAGES
+
English, Hindi
+
+
+
RESPONSE RATE
+
98% (Within 2 hrs)
+
+
+
+
+ + {/* Similar Gigs */} +
+
+

Similar Gigs You May Like

+ View All +
+
+ {similarGigs.map((gig, i) => ( +
+
+ {gig.title} +
+ {gig.type} +
+
+
+
+ {gig.name} + {gig.name} + +
+

{gig.title}

+
+
+ {gig.rating} ({gig.reviews}) +
+
+ From + ₹{gig.price} +
+
+
+
+ ))} +
+
+ +
+ + {/* Right Sidebar (Sticky) */} +
+
+ + {/* Tab headers */} +
+ + + +
+ +
+
+ ₹15,000 + / project +
+

2x Reels & Story Shoutouts

+ +
+
+ + Unlimited revisions before upload +
+
+ + 2 Days Delivery Period +
+
+ + Story placement for 24h +
+
+ + Detailed Analytics Report +
+
+ +
+
Total Deliverables
+
Reels + Story
+
+ + + + + +
+ Secure payment handled by Nollin +
+
+
+ +
+ Report this Gig for policy violation +
+
+ +
+
+ + {/* Footer */} +
+
+
+
+
Nollin
+

+ Connecting top brands with leading influencers to create authentic, engaging content that drives real results. +

+
+
+

For Brands

+
    +
  • Find Influencers
  • +
  • Post a Campaign
  • +
  • Pricing
  • +
+
+
+

For Influencers

+
    +
  • Join Network
  • +
  • Brand Guidelines
  • +
  • Success Stories
  • +
+
+
+

Support

+
    +
  • Help Center
  • +
  • Terms of Service
  • +
  • Privacy Policy
  • +
+
+
+
+
© 2026 Nollin. All rights reserved.
+
+ +
+
+
+
+
+ ); +} diff --git a/apps/web/app/gig-list/page.tsx b/apps/web/app/gig-list/page.tsx new file mode 100644 index 0000000..710db57 --- /dev/null +++ b/apps/web/app/gig-list/page.tsx @@ -0,0 +1,718 @@ +"use client"; +import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; + +import { useAuthStore } from "@/store/auth.store"; +import api from "@/lib/axios.client"; +// ─── Types ─────────────────────────────────────────────────────────────────── + +type Platform = "IG" | "YT" | "TT" | "PT"; +type ViewMode = "grid" | "list"; +type SortOption = "recommended" | "price_asc" | "price_desc" | "next_available"; + +interface GigPricing { + basePrice: number; + currency: string; +} + +interface Gig { + _id: string; + title: string; + category: string; + pricing: GigPricing; + primaryInfluencerId: string; + createdAt: string; + // Enriched fields from influencer populate (adjust to your actual schema) + influencer?: { + name: string; + avatar?: string; + niche?: string; + platforms?: Platform[]; + availableFrom?: string; + }; +} + +interface Pagination { + page: number; + limit: number; + total: number; + totalPages: number; +} + +interface ApiResponse { + data: Gig[]; + pagination: Pagination; +} + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const LIMIT = 9; + +// Replace with your actual API base URL +// const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "/api"; + +const platformColors: Record = { + IG: "bg-pink-100 text-pink-600", + YT: "bg-red-100 text-red-600", + TT: "bg-slate-100 text-slate-700", + PT: "bg-orange-100 text-orange-600", +}; + +const platformIcons: Record = { + IG: "📸", + YT: "▶", + TT: "♪", + PT: "📌", +}; + +const categories: string[] = [ + "Fashion & Beauty", + "Tech & Gadgets", + "Fitness & Health", + "Lifestyle", +]; + +const platformList: string[] = ["Instagram", "YouTube", "TikTok"]; + +const sortOptions: { label: string; value: SortOption }[] = [ + { label: "Recommended", value: "recommended" }, + { label: "Price: Low to High", value: "price_asc" }, + { label: "Price: High to Low", value: "price_desc" }, + { label: "Next Available", value: "next_available" }, +]; + +// Deterministic avatar color based on id string +const avatarColor = (id: string) => { + const colors = [ + "#e8736c", "#5b8dee", "#4db89e", "#f0a500", "#b57bee", "#e06060", + ]; + let hash = 0; + for (let i = 0; i < id.length; i++) hash = id.charCodeAt(i) + ((hash << 5) - hash); + return colors[Math.abs(hash) % colors.length]; +}; + +const initials = (name: string) => + name + .split(" ") + .slice(0, 2) + .map((w) => w[0]) + .join("") + .toUpperCase(); + +const formatCurrency = (amount: number, currency = "INR") => { + if (currency === "INR") return `₹${amount.toLocaleString("en-IN")}`; + return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount); +}; + +// ─── Skeleton Card ──────────────────────────────────────────────────────────── + +function SkeletonCard() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +// ─── Gig Card ───────────────────────────────────────────────────────────────── + +function GigCard({ gig, view }: { gig: Gig; view: ViewMode }) { + const name = gig.influencer?.name ?? "Unknown Creator"; + const niche = gig.influencer?.niche ?? gig.category; + const platforms = gig.influencer?.platforms ?? []; + const availableFrom = gig.influencer?.availableFrom; + const color = avatarColor(gig._id); + + const availableLabel = availableFrom + ? new Date(availableFrom).toLocaleDateString("en-US", { month: "short", day: "numeric" }) + : "Available"; + + if (view === "list") { + return ( +
+
+
+ {initials(name)} +
+
+
+ {name} + + + +
+

{gig.title}

+
+
+ {platforms.map((p) => ( + + {platformIcons[p]} {p} + + ))} +
+
+

Starting at

+

+ {formatCurrency(gig.pricing.basePrice, gig.pricing.currency)} +

+
+
+ + + + {availableLabel} +
+ +
+ ); + } + + return ( +
+
+
+
+
+ {initials(name)} +
+
+
+ {name} + + + +
+

{niche}

+
+
+ +

{gig.title}

+ +
+ {platforms.length > 0 + ? platforms.map((p) => ( + + {platformIcons[p]} {p} + + )) + : ( + + )} +
+ +
+
+

Starting at

+

+ {formatCurrency(gig.pricing.basePrice, gig.pricing.currency)} +

+
+
+
+ + + + Next:{" "} + {availableLabel} +
+
+
+ +
+

+ Booking confirmed +
+ after payment +

+ +
+
+
+ ); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +const MAX_PRICE_LIMIT = 50000; + +export default function ExploreGigs() { + useAuthStore(); + const [search, setSearch] = useState(""); + const [activeCategory, setActiveCategory] = useState(null); + const [activePlatform, setActivePlatform] = useState(null); + const [availableOnly, setAvailableOnly] = useState(false); + const [view, setView] = useState("grid"); + const [sort, setSort] = useState("recommended"); + const [maxPrice, setMaxPrice] = useState(MAX_PRICE_LIMIT); + const [page, setPage] = useState(1); + + const [gigs, setGigs] = useState([]); + const [pagination, setPagination] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // ── "committed" filters are what actually trigger API calls ────────────── + // Draft filters (search, maxPrice) are local UI state only — no fetch until + // the user finishes typing (blur / Enter) or stops dragging the slider. + const [committedSearch, setCommittedSearch] = useState(""); + const [committedMaxPrice, setCommittedMaxPrice] = useState(MAX_PRICE_LIMIT); + + const fetchGigs = useCallback(async ( + opts: { + page: number; + category: string | null; + maxPrice: number; + sort: SortOption; + search: string; + } + ) => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams(); + params.set("page", String(opts.page)); + params.set("limit", String(LIMIT)); + + if (opts.category) params.set("category", opts.category); + if (opts.maxPrice < MAX_PRICE_LIMIT) params.set("maxPrice", String(opts.maxPrice)); + if (opts.sort !== "recommended") params.set("sort", opts.sort); + if (opts.search) params.set("search", opts.search); + + const res = await api.get(`/gigs?${params.toString()}`); + const json: ApiResponse = res.data; + setGigs(json.data); + setPagination(json.pagination); + } catch (err: unknown) { + const errorObj = err as { response?: { data?: { message?: string } } }; + if (errorObj.response?.data?.message) { + setError(errorObj.response.data.message); + } else { + setError(err instanceof Error ? err.message : "Something went wrong"); + } + } finally { + setLoading(false); + } + }, []); + + // Only fetch when committed values or page/sort/category change + useEffect(() => { + fetchGigs({ page, category: activeCategory, maxPrice: committedMaxPrice, sort, search: committedSearch }); + }, [page, activeCategory, committedMaxPrice, sort, committedSearch, fetchGigs]); + + // ── Category: immediate commit (it's a click, not a drag/type) ──────────── + const handleCategoryChange = (cat: string) => { + setActiveCategory((prev) => (prev === cat ? null : cat)); + setPage(1); + }; + + const handlePlatformChange = (p: string) => { + setActivePlatform((prev) => (prev === p ? null : p)); + setPage(1); + }; + + const handleSortChange = (e: React.ChangeEvent) => { + setSort(e.target.value as SortOption); + setPage(1); + }; + + // ── Price slider: update display instantly, fetch only on mouse/touch release + const handleMaxPriceChange = (e: React.ChangeEvent) => { + setMaxPrice(Number(e.target.value)); // updates label only, no fetch + }; + const handleMaxPriceCommit = () => { + setCommittedMaxPrice(maxPrice); + setPage(1); + }; + + // ── Search: update input instantly, fetch on Enter or blur ─────────────── + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + setCommittedSearch(search); + setPage(1); + } + }; + const handleSearchBlur = () => { + if (search !== committedSearch) { + setCommittedSearch(search); + setPage(1); + } + }; + const handleSearchClear = () => { + setSearch(""); + setCommittedSearch(""); + setPage(1); + }; + + const totalPages = pagination?.totalPages ?? 1; + + const pageNumbers = (): (number | "…")[] => { + if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1); + const pages: (number | "…")[] = [1]; + if (page > 3) pages.push("…"); + for (let i = Math.max(2, page - 1); i <= Math.min(totalPages - 1, page + 1); i++) { + pages.push(i); + } + if (page < totalPages - 2) pages.push("…"); + pages.push(totalPages); + return pages; + }; + + return ( +
+ {/* Navbar */} + + +
+ {/* Sidebar */} + + + {/* Main */} +
+ {/* Header */} +
+

Explore Influencer Gigs

+

Browse verified creators and book based on real availability.

+
+ + {/* Search */} +
+ + + + setSearch(e.target.value)} + onKeyDown={handleSearchKeyDown} + onBlur={handleSearchBlur} + className="w-full pl-10 pr-10 py-3 bg-white border border-gray-200 rounded-xl text-sm text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent shadow-sm" + /> + {search && ( + + )} +
+ + {/* Toolbar */} +
+
+ {loading ? ( + + ) : pagination ? ( + <> + Showing{" "} + {gigs.length}{" "} + of{" "} + {pagination.total}{" "} + verified gigs + + ) : null} +
+
+ {/* Grid/List toggle */} +
+ + +
+ + +
+
+ + {/* Error state */} + {error && ( +
+ {error} + +
+ )} + + {/* Cards */} +
+ {loading + ? Array.from({ length: LIMIT }).map((_, i) => ) + : gigs.map((gig) => ( + + ))} +
+ + {/* Empty state */} + {!loading && !error && gigs.length === 0 && ( +
+
🔍
+

No gigs found

+

Try adjusting your filters or search query.

+ +
+ )} + {/* Pagination */} + {!loading && totalPages > 1 && ( +
+ + + {pageNumbers().map((n, i) => + n === "…" ? ( + + … + + ) : ( + + ) + )} + + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/gig/[id]/edit/page.tsx b/apps/web/app/gig/[id]/edit/page.tsx new file mode 100644 index 0000000..606b659 --- /dev/null +++ b/apps/web/app/gig/[id]/edit/page.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useParams } from "next/navigation"; +import { FaCheck } from "react-icons/fa"; + +import api from "@/lib/axios.client"; +import Navbar from "@/components/Navbar"; +import { StepIndicator } from "@/components/gig-create/StepIndicator"; +import { GigDetails } from "@/components/gig-create/GigDetails"; +import { Deliverables } from "@/components/gig-create/Deliverables"; +import { Pricing } from "@/components/gig-create/Pricing"; +import { Availability } from "@/components/gig-create/Availability"; +import { ReviewAndPublish } from "@/components/gig-create/ReviewAndPublish"; +import { useGigCreateStore } from "@/store/gigCreate.store"; + +export default function EditGigPage() { + const params = useParams(); + const [currentStep, setCurrentStep] = useState(1); + const totalSteps = 6; + const setMode = useGigCreateStore((s) => s.setMode); + const { + setGigId, + setDetails, + setDeliverables, + setPricing, + setAvailability + } = useGigCreateStore(); + + useEffect(() => { + setMode("edit"); + const fetchGig = async () => { + const res = await api.get(`/gigs/${params.id}`); + const gig = res.data; + + setGigId(gig._id); + + setDetails({ + title: gig.title, + shortDescription: gig.shortDescription, + category: gig.category, + platform: gig.platform, + tags: gig.tags + }); + + setDeliverables(gig.deliverables); + setPricing(gig.pricing); + console.log(gig.primaryInfluencerId) + const influencerId = + typeof gig.primaryInfluencerId === "string" + ? gig.primaryInfluencerId + : gig.primaryInfluencerId?._id; + + if (influencerId) { + const availabilityRes = await api.get( + `/availability/${influencerId}` + ); + + setAvailability(availabilityRes.data.data); + } + }; + + fetchGig(); + }, [params.id, setAvailability, setDeliverables, setDetails, setGigId, setMode, setPricing]); + + const nextStep = () => { + if (currentStep < totalSteps) { + setCurrentStep((prev) => prev + 1); + } + }; + + const prevStep = () => { + if (currentStep > 1) { + setCurrentStep((prev) => prev - 1); + } + }; + + const goToStep = (step: number) => { + if (step >= 1 && step <= totalSteps) { + setCurrentStep(step); + } + }; + + return ( +
+ + +
+
+

+ Edit Gig +

+
+ +
+
+ +
+ +
+ {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} + {currentStep === 3 && ( + + )} + {currentStep === 4 && ( + + )} + {currentStep === 5 && ( + + )} + {currentStep === 6 && ( +
+
+ +
+

Gig Published Successfully!

+

+ Your gig is now live and visible to brands. You can manage it from your dashboard. +

+ +
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/app/gig/create/page.tsx b/apps/web/app/gig/create/page.tsx new file mode 100644 index 0000000..0fee504 --- /dev/null +++ b/apps/web/app/gig/create/page.tsx @@ -0,0 +1,109 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { FaCheck } from "react-icons/fa"; + +import { StepIndicator } from "@/components/gig-create/StepIndicator"; +import Navbar from "@/components/Navbar"; +import { GigDetails } from "@/components/gig-create/GigDetails"; +import { Deliverables } from "@/components/gig-create/Deliverables"; +import { Pricing } from "@/components/gig-create/Pricing"; +import { Availability } from "@/components/gig-create/Availability"; +import { ReviewAndPublish } from "@/components/gig-create/ReviewAndPublish"; +import { useGigCreateStore } from "@/store/gigCreate.store"; + +export default function GigCreatePage() { + const [currentStep, setCurrentStep] = useState(1); + const totalSteps = 6; + const reset = useGigCreateStore((s) => s.reset); + const setMode = useGigCreateStore((s) => s.setMode); + + useEffect(() => { + reset(); + setMode("create"); + }, [reset, setMode]); + + const nextStep = () => { + if (currentStep < totalSteps) { + setCurrentStep((prev) => prev + 1); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + }; + + const prevStep = () => { + if (currentStep > 1) { + setCurrentStep((prev) => prev - 1); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + } + + const goToStep = (step: number) => { + if (step >= 1 && step <= totalSteps) { + setCurrentStep(step); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + }; + return ( + +
+ + +
+ + {/* Header Section */} +
+

Create a New Gig

+

+ {currentStep === 1 && "Set up your gig details to start connecting with brands."} + {currentStep === 2 && "Define clearly what you will deliver to the client."} + {currentStep === 3 && "Set your pricing and packages."} +

+
+ +
+ {/* Progress Indicator Container */} +
+ +
+ + {/* Main Form Content */} +
+ {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} + {currentStep === 3 && ( + + )} + {currentStep === 4 && ( + + )} + {currentStep === 5 && ( + + )} + {currentStep === 6 && ( +
+
+ +
+

Gig Published Successfully!

+

+ Your gig is now live and visible to brands. You can manage it from your dashboard. +

+ +
+ )} + {/* Future steps will go here */} +
+
+
+
+ ); +} diff --git a/apps/web/app/gigs/page.tsx b/apps/web/app/gigs/page.tsx deleted file mode 100644 index 5a6c3e4..0000000 --- a/apps/web/app/gigs/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function GigsPage() { - return

Gigs Page

; -} diff --git a/apps/web/app/home/page.tsx b/apps/web/app/home/page.tsx new file mode 100644 index 0000000..ad83933 --- /dev/null +++ b/apps/web/app/home/page.tsx @@ -0,0 +1,46 @@ +"use client"; +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +import { useAuthStore } from "@/store/auth.store"; +import RoleGuard from "@/components/rbac/RoleGuard"; + +export default function HomePage() { + const router = useRouter(); + const { user } = useAuthStore(); + + useEffect(() => { + if (user?.role === "ADMIN") { + router.replace("/admindashboard"); + } + }, [user, router]); + + if (user?.role === "ADMIN") return null; + + return ( + +
+

Dashboard

+

Welcome {user?.email} ({user?.role})

+ +
+ + + +
+
+
+ ); +} diff --git a/apps/web/app/influencer-profile-page/page.tsx b/apps/web/app/influencer-profile-page/page.tsx new file mode 100644 index 0000000..7414951 --- /dev/null +++ b/apps/web/app/influencer-profile-page/page.tsx @@ -0,0 +1,482 @@ +"use client"; + +import { useEffect, useState, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import Link from 'next/link'; +import Image from 'next/image'; +import { + CheckCircle2, + MapPin, + Star, + MessageSquare, + Lock, + Instagram, + Youtube, + Twitter, + Linkedin, + ArrowRight +} from 'lucide-react'; + +import api from "@/lib/axios.client"; + +interface GigData { + _id: string; + title: string; + shortDescription?: string; + pricing?: { + currency: string; + basePrice: number; + }; + status?: string; +} + +interface ProfileData { + displayName?: string; + username?: string; + bio?: string; + profileImage?: string; + isVerified?: boolean; + categories?: string[]; + instagramUrl?: string; + youtubeUrl?: string; + followersCount?: number; + location?: string; + ratingAvg?: number; +} + +function ProfileContent() { + const searchParams = useSearchParams(); + const id = searchParams.get("id"); + const [data, setData] = useState<{ profile: ProfileData; gigs: GigData[] } | null>(null); + const [loading, setLoading] = useState(!!id); + + useEffect(() => { + if (!id) { + return; + } + api.get(`/profile/influencer/${id}`) + .then(res => { + setData(res.data.data || res.data); + setLoading(false); + }) + .catch(err => { + console.error(err); + setLoading(false); + }); + }, [id]); + + if (loading) return
Loading profile...
; + if (!data || !data.profile) return
Profile not found.
; + + const { profile, gigs } = data; + const name = profile.displayName || profile.username || "Unknown Influencer"; + const bio = profile.bio || "No bio available."; + const avatar = profile.profileImage || "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=150"; + + return ( +
+ {/* Navigation */} + + +
+ + {/* Profile Header Card */} +
+
+
+ {name} +
+ {profile.isVerified && ( +
+ + + +
+ )} +
+ +
+
+
+

{name}

+ {profile.isVerified && ( + + Verified + + )} +
+

{profile.categories?.join(" & ") || "Creator"}

+
+ +
+ {profile.instagramUrl && ( +
+ IG +
+ )} + {profile.youtubeUrl && ( +
+ YT +
+ )} +
+ {(profile.followersCount || 0).toLocaleString()} Followers +
+
+ +
+
+ {profile.location || "Global"} +
+
+ + +
+
+
+
+ + {/* Stats Grid */} +
+
+
{gigs?.length || 0}
+

Active Gigs

+
+
+
+ {[...Array(5)].map((_, i) => )} +
+
{profile.ratingAvg?.toFixed(1) || "New"}
+

Avg. Rating

+
+
+
< 2h
+

Response Time

+
+
+
68%
+

Repeat Brands

+
+
+ + {/* About Section */} +
+

About

+

+ {bio} +

+
+ + {/* Performance & Demographics Row */} +
+
+

Platform Performance

+
+ {/* Instagram */} +
+
+
+ +
+
+

Instagram

+

Reels & Stories

+
+
+
+

{(profile.followersCount || 0).toLocaleString()} Followers

+

Good Engagement

+
+
+ + {/* YouTube */} +
+
+
+ +
+
+

YouTube

+

Long-form Video

+
+
+
+

Verified Subs

+

Good Engagement

+
+
+ +
+
+ +
+

Audience Demographics

+ +
+ {/* Top Countries */} +
+

Top Countries

+
+ United States (45%) + UK (15%) + Canada (10%) +
+
+ + {/* Age Range */} +
+

Age Range

+
+
+
+
+
+
+
+ 18-24 + 25-34 (Primary) + 35-44 + 45+ +
+
+ + {/* Gender Split */} +
+

Gender Split

+
+
+
+ Female + 68% +
+
+
+
+
+
+
+ Male + 32% +
+
+
+
+
+
+
+
+
+
+ + {/* Active Gigs Section */} +
+
+

Active Gigs

+ View all +
+ +
+ {gigs?.map((gig: GigData, idx: number) => ( +
+ {gig.status === 'published' && Active} +
+ +
+

{gig.title}

+

+ {gig.shortDescription || "Gig details currently unavailable."} +

+
+
+
+

Starting at

+

+ {gig.pricing?.currency === "INR" ? "₹" : "$"} + {gig.pricing?.basePrice?.toLocaleString("en-IN") || 0} +

+
+
+ + View Gig + +
+
+ ))} + + {(!gigs || gigs.length === 0) && ( +

No active gigs available for this creator.

+ )} +
+
+ + {/* Reviews Section */} +
+

Reviews

+ +
+ {/* Review 1 */} +
+
+
+
+ G +
+
+

Glow Cosmetics

+

2 weeks ago

+
+
+
+ {[...Array(5)].map((_, i) => )} +
+
+

+ "The content created was incredibly professional and delivered ahead of schedule. Highly recommended!" +

+
+ + {/* Review 2 */} +
+
+
+
+ M +
+
+

Mode Fashion

+

1 month ago

+
+
+
+ {[...Array(5)].map((_, i) => )} +
+
+

+ "Great communication throughout the process. Our audience loved the styling tips incorporated into the post." +

+
+
+
+ + {/* Availability & Safety Banner */} +
+
+

Availability

+

Availability varies by gig. Check individual gig pages for specific dates.

+ Check availability inside gig +
+
+
+
+ Verified Profile +
+
+ Secure Payments +
+
+ Direct In-App Chat +
+
+
+
+ +
+ + {/* Footer */} +
+
+
+
+
+
+ N +
+ Noillin +
+

+ The professional marketplace for influencer-brand collaborations. +

+
+ +
+

Platform

+
    +
  • Browse Influencers
  • +
  • Explore Gigs
  • +
  • How it Works
  • +
+
+ +
+

Company

+
    +
  • About Us
  • +
  • Careers
  • +
  • Blog
  • +
+
+ +
+

Support

+
    +
  • Help Center
  • +
  • Terms of Service
  • +
  • Privacy Policy
  • +
+
+
+ +
+

© 2024 Noillin. All rights reserved.

+
+ + + +
+
+
+
+
+ ); +} + +export default function ProfilePage() { + return ( + Loading Configuration...
}> + + + ) +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index f7fa87e..fb785c8 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; + import "./globals.css"; +import AuthInitializer from "@/components/AuthInitializer"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,6 +29,7 @@ export default function RootLayout({ + {children} diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx deleted file mode 100644 index 1f0079e..0000000 --- a/apps/web/app/login/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function LoginPage() { - return

Login Page

; -} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index c09c252..a9d59d5 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,3 +1,391 @@ +"use client"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; + export default function HomePage() { - return

Welcome to Noillin Platform

; + const router = useRouter(); + const handleLoginClick = () => { + router.push("/login"); + }; + const handleSignupClick = () => { + router.push("/signup"); + } + return ( +
+ {/* Navigation */} + + + {/* Hero Section */} +
+ + {/* Left Column - Text Content */} +
+

+ Book Trusted Influencers.
+ Pay Securely. +

+

+ Discover, schedule, and collaborate with verified creators — all in one place. +

+ +
+ + +
+ +
+
+ {[1, 2, 3].map((i) => ( +
+ avatar +
+ ))} +
+ Join 2,000+ creators today +
+
+ + {/* Right Column - Mockup Card */} +
+ {/* Decorative Green Background Shape */} +
+ + {/* Main Card */} +
+ + {/* Lock Icon */} +
+ + + +
+ + {/* Profile Info */} +
+
+ Sarah Jenkins +
+
+

Sarah Jenkins

+

Lifestyle & Beauty

+
+ Instagram + TikTok +
+
+
+ + {/* Calendar */} +
+

Select Availability

+
+ {['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((day, i) => ( +
{day}
+ ))} + {['1', '2', '3'].map((date) => ( +
{date}
+ ))} + {/* Active Date */} +
4
+ {['5', '6', '7'].map((date) => ( +
{date}
+ ))} +
+
+ + {/* Price Breakdown */} +
+
+ Service Fee + $150.00 +
+
+ Total + $150.00 +
+
+ + {/* CTA Button */} + +
+
+
+ + {/* Stats Section */} +
+

Trusted by brands and creators

+
+
+

1,000+

+

Influencers

+
+
+

$2M+

+

Processed

+
+
+

100%

+

Secure

+
+
+
+ + {/* How it Works Section */} +
+
+
+

How Noillin Works

+

A simple, secure process to connect brands with creators.

+
+ +
+ {/* Connecting line for desktop (behind cards) */} +
+ + {/* Step 1 */} +
+
+ + + +
+

Discover

+

Browse verified influencer gigs based on your niche and budget.

+
+ + {/* Step 2 */} +
+
+ + + +
+

Book

+

Select availability and packages that fit your campaign needs.

+
+ + {/* Step 3 */} +
+
+ + + +
+

Pay Securely

+

Funds are held securely until deliverables are confirmed.

+
+ +
+
+
+ {/* Platform Features */} +
+
+

Platform Features

+
+ +
+ {[ + { title: "Availability Booking", desc: "See real-time calendar slots before you book. No more back-and-forth emails.", icon: "calendar" }, + { title: "Secure Payments", desc: "Escrow-style protection ensures your money is safe until the job is done.", icon: "shield-check" }, + { title: "Direct Chat", desc: "Communicate directly with influencers or brands via our secure platform.", icon: "chat" }, + { title: "Transparent Pricing", desc: "No hidden fees. What you see is exactly what you pay.", icon: "cash" }, + { title: "Group Collabs", desc: "Book multiple influencers for the same campaign effortlessly.", icon: "users" }, + { title: "Fast Turnaround", desc: "Get confirmed bookings in minutes, not days.", icon: "lightning" }, + ].map((feature, idx) => ( +
+
+ {/* Simplified Icon mapping */} +
+
+

{feature.title}

+

{feature.desc}

+
+ ))} +
+
+ + {/* Segmented Section: Brands & Creators */} +
+ {/* Brands */} +
+
+

Built for Brands

+
    + {["Filter influencers by niche, platform, and follower count", "Clear deliverables and transparent pricing packages", "Secure escrow-style payments protect your budget"].map((item, i) => ( +
  • + + {item} +
  • + ))} +
+ +
+ + {/* Creators */} +
+
+

Built for Creators

+
    + {["Create single or group gigs with custom packages", "Manage availability like a pro calendar", "Get paid instantly once the brand confirms receipt"].map((item, i) => ( +
  • + + {item} +
  • + ))} +
+ +
+
+ + {/* Community Stories */} +
+
+

Community Stories

+
+ +
+ {[ + { + name: "Alex Rivera", + role: "Marketing Director, TechFlow", + img: "https://i.pravatar.cc/150?img=11", + quote: "Noillin streamlined our influencer outreach. We found the perfect micro-influencers within hours and the booking process was seamless." + }, + { + name: "Sarah Jenkins", + role: "Lifestyle Creator", + img: "https://i.pravatar.cc/150?img=5", + quote: "Finally, a platform that treats creators as professionals. The calendar sync and secure payments give me peace of mind." + }, + { + name: "Michael Chen", + role: "Founder, BloomSkincare", + img: "https://i.pravatar.cc/150?img=8", + quote: "The group collaboration feature is a game changer. We coordinated 10 influencers for a product launch in one afternoon." + } + ].map((story, i) => ( +
+
+ {[...Array(5)].map((_, i) => )} +
+

{story.quote}

+
+
+ {story.name} +
+
+

{story.name}

+

{story.role}

+
+
+
+ ))} +
+
+ {/* CTA Section */} +
+ {/* Decorative Circles */} +
+
+ +
+

Start Collaborating Today

+

+ Join thousands of brands and creators building the future of influence. +

+
+ + +
+
+
+ + {/* Footer */} +
+
+ + {/* Logo */} +
+
+ + + +
+ Noillin +
+ + {/* Links */} + + + {/* Trust Badges */} +
+
+ 🔒 + SECURE PAYMENTS +
+
+ + VERIFIED PROFILES +
+
+ 💬 + REAL-TIME CHAT +
+
+ +
+
+ +
+ ); } + + + + diff --git a/apps/web/app/profile-setup/page.tsx b/apps/web/app/profile-setup/page.tsx new file mode 100644 index 0000000..2189f0b --- /dev/null +++ b/apps/web/app/profile-setup/page.tsx @@ -0,0 +1,476 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Image from "next/image"; + +import { uploadToS3 } from "@/lib/s3-uploads"; +import Navbar from "@/components/Navbar"; +import { useAuthStore } from "@/store/auth.store"; +import api from "@/lib/axios.client"; + +export default function ProfileSetupPage() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + + const user = useAuthStore((state) => state.user); + + const userType = user?.role as "INFLUENCER" | "BRAND" | undefined; + + const [commonData, setCommonData] = useState({ + profilePicture: null as File | null, + bio: "", + location: "", + phoneNumber: "", + }); + + const [influencerData, setInfluencerData] = useState({ + fullName: "", + username: "", + niche: "", + gender: "", + dob: "", + instagram: "", + youtube: "", + tiktok: "", + }); + + const [brandData, setBrandData] = useState({ + companyName: "", + industry: "", + website: "", + companySize: "", + }); + + useEffect(() => { + if (!userType) return; + + const fetchProfile = async () => { + try { + const res = await api.get("/profile/get_profile"); + const data = res.data.data; + + if (userType === "INFLUENCER") { + setInfluencerData({ + fullName: data.fullName || "", + username: data.username || "", + niche: data.categories?.[0] || "", + gender: "", + dob: "", + instagram: data.instagramUrl || "", + youtube: data.youtubeUrl || "", + tiktok: data.tiktokUrl || "", + }); + + setCommonData((prev) => ({ + ...prev, + bio: data.bio || "", + location: data.location || "", + })); + } + + if (userType === "BRAND") { + setBrandData({ + companyName: data.companyName || "", + industry: data.industry || "", + website: data.website || "", + companySize: data.companySize || "", + }); + } + } catch (err) { + console.error(err); + } + }; + + fetchProfile(); + }, [userType]); + + const handleCommonChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setCommonData((prev) => ({ ...prev, [name]: value })); + }; + + const handleInfluencerChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setInfluencerData((prev) => ({ ...prev, [name]: value })); + }; + + const handleBrandChange = ( + e: React.ChangeEvent + ) => { + const { name, value } = e.target; + setBrandData((prev) => ({ ...prev, [name]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + if (!userType) return; + type InfluencerPayload = { + bio?: string; + location?: string; + fullName?: string; + username?: string; + categories?: string[]; + instagramUrl?: string; + youtubeUrl?: string; + tiktokUrl?: string; + languages?: string[]; + }; + + type BrandPayload = { + bio?: string; + location?: string; + companyName?: string; + industry?: string; + website?: string; + companySize?: string; + }; + + let payload: InfluencerPayload | BrandPayload = { + + + bio: commonData.bio, + location: commonData.location, + }; + + if (userType === "INFLUENCER") { + payload = { + ...payload, + fullName: influencerData.fullName, + username: influencerData.username, + categories: influencerData.niche + ? [influencerData.niche] + : [], + instagramUrl: influencerData.instagram, + youtubeUrl: influencerData.youtube, + tiktokUrl: influencerData.tiktok, + languages: [], + }; + } + + if (userType === "BRAND") { + payload = { + ...payload, + companyName: brandData.companyName, + industry: brandData.industry, + website: brandData.website, + companySize: brandData.companySize, + }; + } + + await api.patch("/profile/update_profile", payload); + + alert("Profile Updated Successfully"); + router.push("/home"); + } catch (err) { + console.error(err); + alert("Failed to update profile"); + } finally { + setLoading(false); + } + }; + + if (!user) { + return
Loading profile...
; + } + + + return ( +
+ + +
+
+ {/* Header */} + + + {/* Main Card */} +
+
+
+ + {/* Section: Basic Info */} +
+
+

Basic Information

+

This will be displayed on your public profile.

+
+ +
+ {/* Avatar Upload */} +
+
+ {commonData.profilePicture ? ( + Preview + ) : ( +
+ + + + Upload Photo +
+ )} + {/* */} +
+
+ + {/* Basic Inputs */} +
+
+ +