commit 3561c09e2d99090bb7d32c260a7a67c9aed91e6a Author: Mojtaba Khorshidkolah Date: Thu Mar 26 08:14:56 2026 +0330 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d513bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +# Keep environment variables out of version control +.env + +/generated/prisma + +/src/generated/prisma diff --git a/mongodb/config/index.ts b/mongodb/config/index.ts new file mode 100644 index 0000000..5811115 --- /dev/null +++ b/mongodb/config/index.ts @@ -0,0 +1,20 @@ +import mongoose from "mongoose"; + +export async function connectMongo() { + if (mongoose.connection.readyState === 1) return; + + await mongoose.connect(process.env.MONGO_URI!, { + maxPoolSize: 20, + serverSelectionTimeoutMS: 5000, + }); + + console.log("MongoDB connected"); + + mongoose.connection.on("error", (err) => { + console.error("MongoDB connection error:", err); + }); + + mongoose.connection.on("disconnected", () => { + console.warn("MongoDB disconnected"); + }); +} diff --git a/mongodb/models/index.ts b/mongodb/models/index.ts new file mode 100644 index 0000000..1c35a80 --- /dev/null +++ b/mongodb/models/index.ts @@ -0,0 +1,53 @@ +import mongoose, {Schema} from "mongoose"; + +// API Request Logs +const ApiRequestLogSchema = new Schema({ + method: {type: String, required: true}, + path: {type: String, required: true}, + status: {type: Number, required: true}, + durationMs: {type: Number, required: true}, + userId: {type: String, required: false}, + role: {type: String}, + createdAt: {type: Date, default: Date.now}, +}); + +export const ApiRequestLog = mongoose.model( + "ApiRequestLog", + ApiRequestLogSchema +); + +// Error Logs +const ErrorLogSchema = new Schema({ + message: {type: String, required: true}, + stack: {type: String}, + severity: {type: String, default: "error"}, + createdAt: {type: Date, default: Date.now}, +}); + +export const ErrorLog = mongoose.model("ErrorLog", ErrorLogSchema); + +// Security Event Logs +const SecurityEventSchema = new Schema({ + type: {type: String, required: true}, + userId: {type: String}, + ip: {type: String}, + createdAt: {type: Date, default: Date.now}, +}); + +export const SecurityEventLog = mongoose.model( + "SecurityEventLog", + SecurityEventSchema +); + +// Performance Logs +const PerformanceLogSchema = new Schema({ + metric: {type: String, required: true}, + valueMs: {type: Number, required: true}, + endpoint: {type: String}, + createdAt: {type: Date, default: Date.now}, +}); + +export const PerformanceLog = mongoose.model( + "PerformanceLog", + PerformanceLogSchema +); diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..68636bf --- /dev/null +++ b/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["src"], + "ext": "ts", + "exec": "ts-node-esm src/index.ts" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..271c900 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5661 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/adapter-pg": "^7.0.1", + "@prisma/client": "^7.0.1", + "auto-bind": "^4.0.0", + "axios": "^1.13.2", + "bcrypt": "^6.0.0", + "busboy": "^1.6.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "dotenv": "^17.2.3", + "exceljs": "^4.4.0", + "express": "^5.1.0", + "express-rate-limit": "^8.2.1", + "file-type": "^21.1.1", + "form-data": "^4.0.5", + "helmet": "^8.1.0", + "hpp": "^0.2.3", + "http-errors": "^2.0.1", + "joi": "^18.0.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^9.0.2", + "multer": "^2.0.2", + "node-fetch": "^3.3.2", + "nodemailer": "^7.0.11", + "slugify": "^1.6.6", + "transliteration": "^2.3.5" + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/busboy": "^1.5.4", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/csurf": "^1.11.5", + "@types/express": "^5.0.5", + "@types/hpp": "^0.2.7", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "nodemon": "^3.1.11", + "prisma": "^7.0.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz", + "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", + "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz", + "integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.2" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz", + "integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hono/node-server": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz", + "integrity": "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", + "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz", + "integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.0.1.tgz", + "integrity": "sha512-01GpPPhLMoDMF4ipgfZz0L87fla/TV/PBQcmHy+9vV1ml6gUoqF8dUIRNI5Yf2YKpOwzQg9sn8C7dYD1Yio9Ug==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.0.1", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.0.1.tgz", + "integrity": "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.0.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.0.1.tgz", + "integrity": "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.0.1.tgz", + "integrity": "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz", + "integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.13.0.tgz", + "integrity": "sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.2", + "@electric-sql/pglite-socket": "0.0.6", + "@electric-sql/pglite-tools": "0.2.7", + "@hono/node-server": "1.14.2", + "@mrleebo/prisma-ast": "0.12.1", + "@prisma/get-platform": "6.8.2", + "@prisma/query-plan-executor": "6.18.0", + "foreground-child": "3.3.1", + "get-port-please": "3.1.2", + "hono": "4.7.10", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.21.3", + "std-env": "3.9.0", + "valibot": "1.1.0", + "zeptomatch": "2.0.2" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.0.1.tgz", + "integrity": "sha512-sBbxm/yysHLLF2iMAB+qcX/nn3WFgsiC4DQNz0uM6BwGSIs8lIvgo0u8nR9nxe5gvFgKiIH8f4z2fgOEMeXc8w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/engines": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.0.1.tgz", + "integrity": "sha512-f+D/vdKeImqUHysd5Bgv8LQ1whl4sbLepHyYMQQMK61cp4WjwJVryophleLUrfEJRpBLGTBI/7fnLVENxxMFPQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1", + "@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "@prisma/fetch-engine": "7.0.1", + "@prisma/get-platform": "7.0.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6.tgz", + "integrity": "sha512-RA7pShKvijHib4USRB3YuLTQamHKJPkTRDc45AwxfahUQngiGVMlIj4ix4emUxkrum4o/jwn82WIwlG57EtgiQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz", + "integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.0.1.tgz", + "integrity": "sha512-5DnSairYIYU7dcv/9pb1KCwIRHZfhVOd34855d01lUI5QdF9rdCkMywPQbBM67YP7iCgQoEZO0/COtOMpR4i9A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1", + "@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "@prisma/get-platform": "7.0.1" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz", + "integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", + "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.8.2" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", + "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz", + "integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.8.2.tgz", + "integrity": "sha512-/iAEWEUpTja+7gVMu1LtR2pPlvDmveAwMHdTWbDeGlT7yiv0ZTCPpmeAGdq/Y9aJ9Zj1cEGBXGRbmmNPj022PQ==", + "devOptional": true, + "license": "UNLICENSED", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/csurf": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.5.tgz", + "integrity": "sha512-5rw87+5YGixyL2W8wblSUl5DSZi5YOlXE6Awwn2ofLvqKr/1LruKffrQipeJKUX44VaxKj8m5es3vfhltJTOoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/hpp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@types/hpp/-/hpp-0.2.7.tgz", + "integrity": "sha512-YSQBkTwZepklRez0wgsljeewMytGNKgBAZR1YbmE0X49+elqkZ+fr/gvB407wL9Dl7a/Kv3W04yJueRmEHytBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/auto-bind": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", + "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz", + "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "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, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-type": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.1.1.tgz", + "integrity": "sha512-ifJXo8zUqbQ/bLbl9sFoqHNTNWbnPY1COImFfM6CCy7z+E+jC1eY9YfOKkx0fckIg+VljAy2/87T61fp0+eEkg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.11.tgz", + "integrity": "sha512-HNwLkgRg9SqTAd1N3Uh/MnKwTBTzwBxTOPbXQ8pb0tpwydjk90k4zRE8JUn9fMUiRwKtXFZ1TWFmms3dZHN+Fg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/hono": { + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.10.tgz", + "integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "license": "ISC", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hpp/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/hpp/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz", + "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.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==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "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, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mongodb": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", + "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "license": "Apache-2.0", + "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.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz", + "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.0.2.tgz", + "integrity": "sha512-+GCaqwE+X//yN9eo2M2L/n+mVti9J6vH5iQKbhD+2AArZd5iaZqK/DkmkE4S6/iYYMyVQPTXsRk7jyVOYEtJzA==", + "license": "MIT", + "dependencies": { + "kareem": "3.0.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/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prisma": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.0.1.tgz", + "integrity": "sha512-zp93MdFMSU1IHPEXbUHVUuD8wauh2BUm14OVxhxGrWJQQpXpda0rW4VSST2bci4raoldX64/wQxHKkl/wqDskQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/config": "7.0.1", + "@prisma/dev": "0.13.0", + "@prisma/engines": "7.0.1", + "@prisma/studio-core": "0.8.2", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "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/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/remeda": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz", + "integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.39.1" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz", + "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.1.0", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/transliteration": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz", + "integrity": "sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==", + "license": "MIT", + "dependencies": { + "yargs": "^17.5.1" + }, + "bin": { + "slugify": "dist/bin/slugify", + "transliterate": "dist/bin/transliterate" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "devOptional": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.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==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", + "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": 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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/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==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zeptomatch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz", + "integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.10" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2d9593d --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "dev": "nodemon --watch src --exec ts-node -r tsconfig-paths/register src/index.ts", + "dev:ts": "nodemon", + "build": "tsc", + "start": "node dist/index.js" + }, + "prisma": { + "schema": "./prisma" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/busboy": "^1.5.4", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/csurf": "^1.11.5", + "@types/express": "^5.0.5", + "@types/hpp": "^0.2.7", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "nodemon": "^3.1.11", + "prisma": "^7.0.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.0.1", + "@prisma/client": "^7.0.1", + "auto-bind": "^4.0.0", + "axios": "^1.13.2", + "bcrypt": "^6.0.0", + "busboy": "^1.6.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "csurf": "^1.11.0", + "dotenv": "^17.2.3", + "exceljs": "^4.4.0", + "express": "^5.1.0", + "express-rate-limit": "^8.2.1", + "file-type": "^21.1.1", + "form-data": "^4.0.5", + "helmet": "^8.1.0", + "hpp": "^0.2.3", + "http-errors": "^2.0.1", + "joi": "^18.0.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^9.0.2", + "multer": "^2.0.2", + "node-fetch": "^3.3.2", + "nodemailer": "^7.0.11", + "slugify": "^1.6.6", + "transliteration": "^2.3.5" + } +} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..dbf7747 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,13 @@ +import 'dotenv/config' +import { defineConfig, env } from 'prisma/config' + +export default defineConfig({ + schema: 'prisma/schema.prisma', + migrations: { + path: 'prisma/migrations', + }, + datasource: { + url: env('DATABASE_URL'), + }, + +}) \ No newline at end of file diff --git a/prisma/migrations/20251127094339_init/migration.sql b/prisma/migrations/20251127094339_init/migration.sql new file mode 100644 index 0000000..9176da7 --- /dev/null +++ b/prisma/migrations/20251127094339_init/migration.sql @@ -0,0 +1,225 @@ +-- CreateEnum +CREATE TYPE "UserRoles" AS ENUM ('DOCTOR', 'TEAM'); + +-- CreateEnum +CREATE TYPE "PatientStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "PageType" AS ENUM ('STATIC', 'SERVICE', 'PACKAGE', 'FORM_PAGE', 'CONTACT'); + +-- CreateEnum +CREATE TYPE "PageStatus" AS ENUM ('DRAFT', 'PUBLISHED'); + +-- CreateEnum +CREATE TYPE "BlockType" AS ENUM ('TEXT', 'TEAM', 'CONTACT_INFO', 'MAP', 'PACKAGE_LIST', 'ACCORDION', 'FORM', 'GALLERY'); + +-- CreateEnum +CREATE TYPE "UploadedBy" AS ENUM ('PATIENT', 'STAFF'); + +-- CreateEnum +CREATE TYPE "FormStatus" AS ENUM ('PENDING', 'REVIEWED', 'REJECTED', 'APPROVED'); + +-- CreateTable +CREATE TABLE "Users" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL, + "bio" TEXT, + "position" TEXT, + "expertise" TEXT, + "phone_number" TEXT, + "email" TEXT, + "image" TEXT, + "socials" JSONB, + "profile_photo_id" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Patient" ( + "id" SERIAL NOT NULL, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + "dob" TIMESTAMP(3) NOT NULL, + "email" TEXT, + "phone_number" TEXT, + "country_name" TEXT, + "country_code" TEXT, + "status" "PatientStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Patient_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Service" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "intro" JSONB, + "image" TEXT, + "description" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Service_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Page" ( + "id" SERIAL NOT NULL, + "type" "PageType" NOT NULL, + "featured_image" TEXT, + "status" "PageStatus" NOT NULL DEFAULT 'PUBLISHED', + + CONSTRAINT "Page_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PageBlock" ( + "id" SERIAL NOT NULL, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "blockType" "BlockType" NOT NULL, + "data" JSONB NOT NULL, + "pageId" INTEGER NOT NULL, + + CONSTRAINT "PageBlock_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Package" ( + "id" SERIAL NOT NULL, + "service_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "price" DOUBLE PRECISION, + "image" TEXT, + "sort_order" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Package_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PackageItem" ( + "id" SERIAL NOT NULL, + "package_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "content" JSONB, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PackageItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MedicalDocument" ( + "id" SERIAL NOT NULL, + "patient_id" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "uploadedBy" "UploadedBy" NOT NULL DEFAULT 'PATIENT', + "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MedicalDocument_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Language" ( + "id" SERIAL NOT NULL, + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Language_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Image" ( + "id" SERIAL NOT NULL, + "code" TEXT, + "url" TEXT NOT NULL, + "name" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + + CONSTRAINT "Image_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "File" ( + "id" SERIAL NOT NULL, + "code" TEXT, + "url" TEXT NOT NULL, + "name" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "uploadedBy" "UploadedBy" NOT NULL DEFAULT 'PATIENT', + "uploadedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "documentId" INTEGER, + + CONSTRAINT "File_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DefaultInfo" ( + "id" SERIAL NOT NULL, + "address" TEXT, + "phone_number" TEXT, + "email" TEXT, + "mapEmbed" JSONB, + "ipd_technician_phone" TEXT NOT NULL, + "ipd_email" TEXT, + "instagram_link" TEXT, + "linkedin_link" TEXT, + "under_logo_description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DefaultInfo_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AdmissionForm" ( + "id" SERIAL NOT NULL, + "patient_id" INTEGER NOT NULL, + "data" JSONB NOT NULL, + "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "FormStatus" NOT NULL DEFAULT 'PENDING', + + CONSTRAINT "AdmissionForm_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_email_key" ON "Patient"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Service_slug_key" ON "Service"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Language_code_key" ON "Language"("code"); + +-- AddForeignKey +ALTER TABLE "Users" ADD CONSTRAINT "Users_profile_photo_id_fkey" FOREIGN KEY ("profile_photo_id") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PageBlock" ADD CONSTRAINT "PageBlock_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Package" ADD CONSTRAINT "Package_service_id_fkey" FOREIGN KEY ("service_id") REFERENCES "Service"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PackageItem" ADD CONSTRAINT "PackageItem_package_id_fkey" FOREIGN KEY ("package_id") REFERENCES "Package"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicalDocument" ADD CONSTRAINT "MedicalDocument_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Patient"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "File" ADD CONSTRAINT "File_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "MedicalDocument"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AdmissionForm" ADD CONSTRAINT "AdmissionForm_patient_id_fkey" FOREIGN KEY ("patient_id") REFERENCES "Patient"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20251130105147_init/migration.sql b/prisma/migrations/20251130105147_init/migration.sql new file mode 100644 index 0000000..c107906 --- /dev/null +++ b/prisma/migrations/20251130105147_init/migration.sql @@ -0,0 +1,284 @@ +/* + Warnings: + + - You are about to drop the column `code` on the `Image` table. All the data in the column will be lost. + - You are about to drop the column `name` on the `Image` table. All the data in the column will be lost. + - You are about to drop the column `url` on the `Image` table. All the data in the column will be lost. + - The primary key for the `Patient` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `country_code` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `country_name` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `dob` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `first_name` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `last_name` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `phone_number` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `status` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `bio` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `createdAt` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `image` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `name` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `phone_number` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `profile_photo_id` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `role` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `socials` on the `Users` table. All the data in the column will be lost. + - You are about to drop the `AdmissionForm` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `DefaultInfo` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `File` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Language` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `MedicalDocument` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Package` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `PackageItem` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Page` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `PageBlock` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Service` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `firstName` to the `Patient` table without a default value. This is not possible if the table is not empty. + - Added the required column `lastName` to the `Patient` table without a default value. This is not possible if the table is not empty. + - Added the required column `firstName` to the `Users` table without a default value. This is not possible if the table is not empty. + - Added the required column `lastName` to the `Users` table without a default value. This is not possible if the table is not empty. + - Made the column `position` on table `Users` required. This step will fail if there are existing NULL values in that column. + +*/ +-- CreateEnum +CREATE TYPE "CaseStatus" AS ENUM ('NEW', 'CONTACTED', 'DOCS_PENDING', 'REVIEWING', 'PRE_APPROVED', 'REJECTED', 'CLOSED', 'CONVERTED_TO_HIS'); + +-- CreateEnum +CREATE TYPE "DocumentType" AS ENUM ('PASSPORT', 'MEDICAL_RECORD', 'IMAGING', 'LAB_RESULT', 'OTHER'); + +-- CreateEnum +CREATE TYPE "InteractionType" AS ENUM ('PHONE', 'WHATSAPP', 'EMAIL', 'SYSTEM'); + +-- CreateEnum +CREATE TYPE "UsersType" AS ENUM ('DOCTOR', 'TRANSFER_TEAM', 'DEPARTMENT'); + +-- DropForeignKey +ALTER TABLE "AdmissionForm" DROP CONSTRAINT "AdmissionForm_patient_id_fkey"; + +-- DropForeignKey +ALTER TABLE "File" DROP CONSTRAINT "File_documentId_fkey"; + +-- DropForeignKey +ALTER TABLE "MedicalDocument" DROP CONSTRAINT "MedicalDocument_patient_id_fkey"; + +-- DropForeignKey +ALTER TABLE "Package" DROP CONSTRAINT "Package_service_id_fkey"; + +-- DropForeignKey +ALTER TABLE "PackageItem" DROP CONSTRAINT "PackageItem_package_id_fkey"; + +-- DropForeignKey +ALTER TABLE "PageBlock" DROP CONSTRAINT "PageBlock_pageId_fkey"; + +-- DropForeignKey +ALTER TABLE "Users" DROP CONSTRAINT "Users_profile_photo_id_fkey"; + +-- DropIndex +DROP INDEX "Patient_email_key"; + +-- AlterTable +ALTER TABLE "Image" DROP COLUMN "code", +DROP COLUMN "name", +DROP COLUMN "url", +ADD COLUMN "filename" TEXT, +ALTER COLUMN "mimeType" DROP NOT NULL, +ALTER COLUMN "size" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Patient" DROP CONSTRAINT "Patient_pkey", +DROP COLUMN "country_code", +DROP COLUMN "country_name", +DROP COLUMN "dob", +DROP COLUMN "first_name", +DROP COLUMN "last_name", +DROP COLUMN "phone_number", +DROP COLUMN "status", +ADD COLUMN "age" INTEGER, +ADD COLUMN "countryCode" TEXT, +ADD COLUMN "firstName" TEXT NOT NULL, +ADD COLUMN "lastName" TEXT NOT NULL, +ADD COLUMN "nationality" TEXT, +ADD COLUMN "phone" TEXT, +ADD COLUMN "preferredLanguage" TEXT, +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "Patient_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "Patient_id_seq"; + +-- AlterTable +ALTER TABLE "Users" DROP COLUMN "bio", +DROP COLUMN "createdAt", +DROP COLUMN "image", +DROP COLUMN "name", +DROP COLUMN "phone_number", +DROP COLUMN "profile_photo_id", +DROP COLUMN "role", +DROP COLUMN "socials", +ADD COLUMN "firstName" TEXT NOT NULL, +ADD COLUMN "imageId" INTEGER, +ADD COLUMN "lastName" TEXT NOT NULL, +ADD COLUMN "phone" TEXT, +ADD COLUMN "type" "UsersType" NOT NULL DEFAULT 'DOCTOR', +ALTER COLUMN "position" SET NOT NULL; + +-- DropTable +DROP TABLE "AdmissionForm"; + +-- DropTable +DROP TABLE "DefaultInfo"; + +-- DropTable +DROP TABLE "File"; + +-- DropTable +DROP TABLE "Language"; + +-- DropTable +DROP TABLE "MedicalDocument"; + +-- DropTable +DROP TABLE "Package"; + +-- DropTable +DROP TABLE "PackageItem"; + +-- DropTable +DROP TABLE "Page"; + +-- DropTable +DROP TABLE "PageBlock"; + +-- DropTable +DROP TABLE "Service"; + +-- DropEnum +DROP TYPE "BlockType"; + +-- DropEnum +DROP TYPE "FormStatus"; + +-- DropEnum +DROP TYPE "PageStatus"; + +-- DropEnum +DROP TYPE "PageType"; + +-- DropEnum +DROP TYPE "PatientStatus"; + +-- DropEnum +DROP TYPE "UploadedBy"; + +-- DropEnum +DROP TYPE "UserRoles"; + +-- CreateTable +CREATE TABLE "OnlineCase" ( + "id" TEXT NOT NULL, + "patientId" TEXT NOT NULL, + "trackingCode" TEXT NOT NULL, + "message" TEXT, + "specialty" TEXT, + "formData" JSONB, + "status" "CaseStatus" NOT NULL DEFAULT 'NEW', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OnlineCase_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Document" ( + "id" TEXT NOT NULL, + "caseId" TEXT, + "patientId" TEXT, + "uploadedById" TEXT, + "type" "DocumentType" NOT NULL, + "fileKey" TEXT NOT NULL, + "filename" TEXT, + "mimeType" TEXT, + "size" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Document_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Interaction" ( + "id" TEXT NOT NULL, + "caseId" TEXT NOT NULL, + "staffId" TEXT, + "type" "InteractionType" NOT NULL, + "message" TEXT, + "direction" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Interaction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Review" ( + "id" TEXT NOT NULL, + "caseId" TEXT NOT NULL, + "doctorId" TEXT, + "note" TEXT, + "result" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CaseStatusHistory" ( + "id" TEXT NOT NULL, + "caseId" TEXT NOT NULL, + "from" "CaseStatus", + "to" "CaseStatus" NOT NULL, + "changedBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CaseStatusHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Staff" ( + "id" TEXT NOT NULL, + "username" TEXT NOT NULL, + "role" TEXT NOT NULL, + "email" TEXT NOT NULL, + + CONSTRAINT "Staff_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "OnlineCase_trackingCode_key" ON "OnlineCase"("trackingCode"); + +-- CreateIndex +CREATE UNIQUE INDEX "Staff_email_key" ON "Staff"("email"); + +-- AddForeignKey +ALTER TABLE "OnlineCase" ADD CONSTRAINT "OnlineCase_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "Patient"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_patientId_fkey" FOREIGN KEY ("patientId") REFERENCES "Patient"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Interaction" ADD CONSTRAINT "Interaction_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Interaction" ADD CONSTRAINT "Interaction_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_doctorId_fkey" FOREIGN KEY ("doctorId") REFERENCES "Staff"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CaseStatusHistory" ADD CONSTRAINT "CaseStatusHistory_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "OnlineCase"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Users" ADD CONSTRAINT "Users_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20251201053031_init/migration.sql b/prisma/migrations/20251201053031_init/migration.sql new file mode 100644 index 0000000..5df710b --- /dev/null +++ b/prisma/migrations/20251201053031_init/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the column `email` on the `Staff` table. All the data in the column will be lost. + - A unique constraint covering the columns `[username]` on the table `Staff` will be added. If there are existing duplicate values, this will fail. + - Added the required column `password` to the `Staff` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "Staff_email_key"; + +-- AlterTable +ALTER TABLE "Staff" DROP COLUMN "email", +ADD COLUMN "password" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Staff_username_key" ON "Staff"("username"); diff --git a/prisma/migrations/20260210101431_init/migration.sql b/prisma/migrations/20260210101431_init/migration.sql new file mode 100644 index 0000000..fbd8e53 --- /dev/null +++ b/prisma/migrations/20260210101431_init/migration.sql @@ -0,0 +1,711 @@ +/* + Warnings: + + - The values [MEDICAL_RECORD,IMAGING] on the enum `DocumentType` will be removed. If these variants are still used in the database, this will fail. + - You are about to drop the column `countryCode` on the `Patient` table. All the data in the column will be lost. + - You are about to drop the column `nationality` on the `Patient` table. All the data in the column will be lost. + - You are about to alter the column `email` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`. + - You are about to alter the column `firstName` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`. + - You are about to alter the column `lastName` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`. + - You are about to alter the column `phone` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(20)`. + - You are about to alter the column `preferredLanguage` on the `Patient` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(10)`. + - You are about to drop the column `note` on the `Review` table. All the data in the column will be lost. + - You are about to drop the column `result` on the `Review` table. All the data in the column will be lost. + - You are about to drop the column `expertise` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `firstName` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `lastName` on the `Users` table. All the data in the column will be lost. + - You are about to drop the column `position` on the `Users` table. All the data in the column will be lost. + - You are about to drop the `Interaction` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[pid]` on the table `Patient` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[nationalityCode]` on the table `Patient` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[passportCode,nationalityId]` on the table `Patient` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[phone]` on the table `Patient` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[email]` on the table `Patient` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[firstName,lastName,birthDate]` on the table `Patient` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[email]` on the table `Staff` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[slug]` on the table `Users` will be added. If there are existing duplicate values, this will fail. + - Added the required column `fileUrl` to the `Document` table without a default value. This is not possible if the table is not empty. + - Made the column `filename` on table `Document` required. This step will fail if there are existing NULL values in that column. + - Added the required column `fileKey` to the `Image` table without a default value. This is not possible if the table is not empty. + - Added the required column `pid` to the `Patient` table without a default value. This is not possible if the table is not empty. + - Changed the type of `role` on the `Staff` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Added the required column `slug` to the `Users` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "DocStatus" AS ENUM ('NEW', 'PENDING_SCAN', 'SAFE', 'INFECTED', 'INVALID'); + +-- CreateEnum +CREATE TYPE "Sex" AS ENUM ('male', 'female', 'other'); + +-- CreateEnum +CREATE TYPE "UploadStatus" AS ENUM ('PENDING', 'UPLOADING', 'UPLOADED', 'VERIFIED', 'FAILED', 'EXPIRED'); + +-- CreateEnum +CREATE TYPE "StaffRoles" AS ENUM ('developer', 'admin', 'doctor', 'coordinator'); + +-- CreateEnum +CREATE TYPE "StaffStatus" AS ENUM ('ACTIVE', 'WARNED', 'RESTRICTED', 'BANNED'); + +-- CreateEnum +CREATE TYPE "ViolationType" AS ENUM ('SPAM', 'RATE_LIMIT', 'CONTENT', 'ABUSE'); + +-- CreateEnum +CREATE TYPE "RestrictionType" AS ENUM ('TEMP', 'SHADOW', 'PERMANENT'); + +-- CreateEnum +CREATE TYPE "ConfigType" AS ENUM ('BOOLEAN', 'NUMBER', 'STRING', 'JSON', 'STRING_ARRAY'); + +-- CreateEnum +CREATE TYPE "AuditAction" AS ENUM ('CREATE', 'UPDATE', 'DELETE', 'READ'); + +-- AlterEnum +BEGIN; +CREATE TYPE "DocumentType_new" AS ENUM ('MEDICAL_IMAGE', 'LAB_RESULT', 'PRESCRIPTION', 'PASSPORT', 'NATIONAL_ID', 'OTHER', 'OTHER_FILE'); +ALTER TABLE "Document" ALTER COLUMN "type" TYPE "DocumentType_new" USING ("type"::text::"DocumentType_new"); +ALTER TYPE "DocumentType" RENAME TO "DocumentType_old"; +ALTER TYPE "DocumentType_new" RENAME TO "DocumentType"; +DROP TYPE "public"."DocumentType_old"; +COMMIT; + +-- DropForeignKey +ALTER TABLE "Interaction" DROP CONSTRAINT "Interaction_caseId_fkey"; + +-- DropForeignKey +ALTER TABLE "Interaction" DROP CONSTRAINT "Interaction_staffId_fkey"; + +-- AlterTable +ALTER TABLE "CaseStatusHistory" ALTER COLUMN "to" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Document" ADD COLUMN "checksum" TEXT, +ADD COLUMN "fileUrl" TEXT NOT NULL, +ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "status" "DocStatus" NOT NULL DEFAULT 'NEW', +ALTER COLUMN "filename" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Image" ADD COLUMN "fileKey" TEXT NOT NULL, +ADD COLUMN "fileUrl" TEXT; + +-- AlterTable +ALTER TABLE "Patient" DROP COLUMN "countryCode", +DROP COLUMN "nationality", +ADD COLUMN "address" VARCHAR(500), +ADD COLUMN "birthDate" TIMESTAMP(3), +ADD COLUMN "deletedAt" TIMESTAMP(3), +ADD COLUMN "nationalityCode" CHAR(3), +ADD COLUMN "nationalityId" INTEGER, +ADD COLUMN "passportCode" VARCHAR(20), +ADD COLUMN "pid" VARCHAR(10) NOT NULL, +ADD COLUMN "postalCode" VARCHAR(20), +ADD COLUMN "sex" "Sex", +ALTER COLUMN "email" SET DATA TYPE VARCHAR(255), +ALTER COLUMN "firstName" SET DATA TYPE VARCHAR(100), +ALTER COLUMN "lastName" SET DATA TYPE VARCHAR(100), +ALTER COLUMN "phone" SET DATA TYPE VARCHAR(20), +ALTER COLUMN "preferredLanguage" SET DATA TYPE VARCHAR(10); + +-- AlterTable +ALTER TABLE "Review" DROP COLUMN "note", +DROP COLUMN "result", +ADD COLUMN "deletedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "Staff" ADD COLUMN "email" TEXT, +ADD COLUMN "is_verified" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "resetPasswordExpires" TIMESTAMP(3), +ADD COLUMN "resetPasswordToken" TEXT, +ADD COLUMN "send_notif_with_email" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "status" "StaffStatus" NOT NULL DEFAULT 'ACTIVE', +ADD COLUMN "strikes" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "trustScore" INTEGER NOT NULL DEFAULT 100, +DROP COLUMN "role", +ADD COLUMN "role" "StaffRoles" NOT NULL; + +-- AlterTable +ALTER TABLE "Users" DROP COLUMN "expertise", +DROP COLUMN "firstName", +DROP COLUMN "lastName", +DROP COLUMN "position", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "expertiseId" INTEGER, +ADD COLUMN "medicalNumber" TEXT, +ADD COLUMN "slug" TEXT NOT NULL, +ADD COLUMN "teamName" TEXT; + +-- DropTable +DROP TABLE "Interaction"; + +-- CreateTable +CREATE TABLE "ReviewTranslation" ( + "id" SERIAL NOT NULL, + "reviewId" TEXT NOT NULL, + "lang_id" INTEGER, + "note" TEXT, + "result" TEXT, + + CONSTRAINT "ReviewTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UploadSession" ( + "id" TEXT NOT NULL, + "uploadKey" TEXT NOT NULL, + "nonce" TEXT NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + "createdById" TEXT, + "purpose" TEXT NOT NULL, + "allowedTypes" TEXT, + "maxSize" INTEGER, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expiresAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "UploadSession_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Violation" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" "ViolationType" NOT NULL, + "severity" INTEGER NOT NULL, + "reason" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Violation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Restriction" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" "RestrictionType" NOT NULL, + "expiresAt" TIMESTAMP(3), + + CONSTRAINT "Restriction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "StaffTranslation" ( + "id" SERIAL NOT NULL, + "staffId" TEXT NOT NULL, + "lang_id" INTEGER, + "displayName" TEXT, + "position" TEXT, + "description" TEXT, + + CONSTRAINT "StaffTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserTranslation" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "lang_id" INTEGER, + "firstName" TEXT, + "lastName" TEXT, + "position" TEXT, + + CONSTRAINT "UserTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Expertise" ( + "id" SERIAL NOT NULL, + "slug" VARCHAR(100) NOT NULL, + + CONSTRAINT "Expertise_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ExpertiseTranslation" ( + "id" SERIAL NOT NULL, + "expertiseId" INTEGER NOT NULL, + "lang_id" INTEGER, + "displayName" TEXT, + + CONSTRAINT "ExpertiseTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PanelConfig" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "type" "ConfigType" NOT NULL, + "description" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PanelConfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Default" ( + "id" TEXT NOT NULL DEFAULT 'SITE_DEFAULTS', + "email" TEXT NOT NULL, + "hospitalPhone" TEXT NOT NULL, + "logoUrl" TEXT, + "mapAddress" TEXT NOT NULL, + "instagramLink" TEXT, + "linkedinLink" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Default_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DefaultTranslation" ( + "id" SERIAL NOT NULL, + "defaultId" TEXT NOT NULL, + "languageId" INTEGER NOT NULL, + "address" TEXT NOT NULL, + "underLogoText" TEXT, + + CONSTRAINT "DefaultTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Page" ( + "id" SERIAL NOT NULL, + "slug" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Page_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PageBlock" ( + "id" SERIAL NOT NULL, + "pageId" INTEGER NOT NULL, + "type" TEXT NOT NULL, + "sort" INTEGER NOT NULL, + + CONSTRAINT "PageBlock_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PageBlockTranslation" ( + "id" SERIAL NOT NULL, + "blockId" INTEGER NOT NULL, + "lang_id" INTEGER, + "field" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "PageBlockTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Language" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "slug" TEXT NOT NULL, + + CONSTRAINT "Language_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RefreshToken" ( + "id" SERIAL NOT NULL, + "token" TEXT NOT NULL, + "staffId" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Countries" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "callCode" TEXT NOT NULL, + "coverId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Countries_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AuditLog" ( + "id" SERIAL NOT NULL, + "actorId" TEXT, + "actorRole" "StaffRoles", + "action" "AuditAction" NOT NULL, + "entity" TEXT NOT NULL, + "entityId" INTEGER, + "before" JSONB, + "after" JSONB, + "ip" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AccessLog" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "userRole" "StaffRoles" NOT NULL, + "resource" TEXT NOT NULL, + "resourceId" INTEGER, + "reason" TEXT, + "ip" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AccessLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "DecisionLog" ( + "id" SERIAL NOT NULL, + "decisionType" TEXT NOT NULL, + "input" JSONB NOT NULL, + "output" JSONB NOT NULL, + "algorithmVersion" TEXT NOT NULL, + "actorId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DecisionLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TermsOfService" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "version" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TermsOfService_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TosAcceptanceLog" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "policyType" TEXT NOT NULL, + "policyVersion" TEXT NOT NULL, + "acceptedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ip" TEXT, + + CONSTRAINT "TosAcceptanceLog_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PrivacyPolicy" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "PrivacyPolicy_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MedicalPackage" ( + "id" SERIAL NOT NULL, + "thumbnail_id" INTEGER, + "icon" TEXT, + "priority" INTEGER, + "parent_id" INTEGER, + + CONSTRAINT "MedicalPackage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MedicalPackagesTranslation" ( + "id" SERIAL NOT NULL, + "title" VARCHAR(50) NOT NULL, + "content" TEXT, + "lang_id" INTEGER, + "medicalPackageId" INTEGER, + + CONSTRAINT "MedicalPackagesTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TransferPackage" ( + "id" SERIAL NOT NULL, + "location" TEXT NOT NULL, + "price" TEXT NOT NULL, + + CONSTRAINT "TransferPackage_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TransferPackageTranslations" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "content" TEXT NOT NULL, + "lang_id" INTEGER, + "transferPackageId" INTEGER, + + CONSTRAINT "TransferPackageTranslations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TransferTeam" ( + "id" SERIAL NOT NULL, + + CONSTRAINT "TransferTeam_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TransferTeamTranslation" ( + "id" SERIAL NOT NULL, + "lang_id" INTEGER, + "transferTeamId" INTEGER, + "name" VARCHAR(50) NOT NULL, + "introduction" TEXT NOT NULL, + "duties" TEXT NOT NULL, + "services" TEXT NOT NULL, + + CONSTRAINT "TransferTeamTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_TransferPackageLocationImages" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_TransferPackageLocationImages_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_TransferPackageGalleryImages" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_TransferPackageGalleryImages_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_TransferPackageToTransferTeam" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_TransferPackageToTransferTeam_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_TransferTeamToUsers" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_TransferTeamToUsers_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "ReviewTranslation_reviewId_lang_id_key" ON "ReviewTranslation"("reviewId", "lang_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "UploadSession_uploadKey_key" ON "UploadSession"("uploadKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "UploadSession_nonce_key" ON "UploadSession"("nonce"); + +-- CreateIndex +CREATE INDEX "UploadSession_createdById_idx" ON "UploadSession"("createdById"); + +-- CreateIndex +CREATE INDEX "UploadSession_status_idx" ON "UploadSession"("status"); + +-- CreateIndex +CREATE UNIQUE INDEX "StaffTranslation_staffId_lang_id_key" ON "StaffTranslation"("staffId", "lang_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserTranslation_userId_lang_id_key" ON "UserTranslation"("userId", "lang_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "ExpertiseTranslation_expertiseId_lang_id_key" ON "ExpertiseTranslation"("expertiseId", "lang_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "PanelConfig_key_key" ON "PanelConfig"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "DefaultTranslation_defaultId_languageId_key" ON "DefaultTranslation"("defaultId", "languageId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Page_slug_key" ON "Page"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Language_slug_key" ON "Language"("slug"); + +-- CreateIndex +CREATE INDEX "Language_id_slug_idx" ON "Language"("id", "slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token"); + +-- CreateIndex +CREATE INDEX "AuditLog_entity_entityId_idx" ON "AuditLog"("entity", "entityId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TermsOfService_version_key" ON "TermsOfService"("version"); + +-- CreateIndex +CREATE UNIQUE INDEX "MedicalPackagesTranslation_medicalPackageId_lang_id_key" ON "MedicalPackagesTranslation"("medicalPackageId", "lang_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "TransferPackageTranslations_transferPackageId_lang_id_key" ON "TransferPackageTranslations"("transferPackageId", "lang_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "TransferTeamTranslation_transferTeamId_lang_id_key" ON "TransferTeamTranslation"("transferTeamId", "lang_id"); + +-- CreateIndex +CREATE INDEX "_TransferPackageLocationImages_B_index" ON "_TransferPackageLocationImages"("B"); + +-- CreateIndex +CREATE INDEX "_TransferPackageGalleryImages_B_index" ON "_TransferPackageGalleryImages"("B"); + +-- CreateIndex +CREATE INDEX "_TransferPackageToTransferTeam_B_index" ON "_TransferPackageToTransferTeam"("B"); + +-- CreateIndex +CREATE INDEX "_TransferTeamToUsers_B_index" ON "_TransferTeamToUsers"("B"); + +-- CreateIndex +CREATE INDEX "CaseStatusHistory_id_caseId_idx" ON "CaseStatusHistory"("id", "caseId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_pid_key" ON "Patient"("pid"); + +-- CreateIndex +CREATE INDEX "Patient_pid_idx" ON "Patient"("pid"); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_nationalityCode_key" ON "Patient"("nationalityCode"); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_passportCode_nationalityId_key" ON "Patient"("passportCode", "nationalityId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_phone_key" ON "Patient"("phone"); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_email_key" ON "Patient"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Patient_firstName_lastName_birthDate_key" ON "Patient"("firstName", "lastName", "birthDate"); + +-- CreateIndex +CREATE UNIQUE INDEX "Staff_email_key" ON "Staff"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Users_slug_key" ON "Users"("slug"); + +-- CreateIndex +CREATE INDEX "Users_id_slug_idx" ON "Users"("id", "slug"); + +-- AddForeignKey +ALTER TABLE "Patient" ADD CONSTRAINT "Patient_nationalityId_fkey" FOREIGN KEY ("nationalityId") REFERENCES "Countries"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ReviewTranslation" ADD CONSTRAINT "ReviewTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ReviewTranslation" ADD CONSTRAINT "ReviewTranslation_reviewId_fkey" FOREIGN KEY ("reviewId") REFERENCES "Review"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Violation" ADD CONSTRAINT "Violation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Restriction" ADD CONSTRAINT "Restriction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StaffTranslation" ADD CONSTRAINT "StaffTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StaffTranslation" ADD CONSTRAINT "StaffTranslation_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Users" ADD CONSTRAINT "Users_expertiseId_fkey" FOREIGN KEY ("expertiseId") REFERENCES "Expertise"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserTranslation" ADD CONSTRAINT "UserTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserTranslation" ADD CONSTRAINT "UserTranslation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExpertiseTranslation" ADD CONSTRAINT "ExpertiseTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExpertiseTranslation" ADD CONSTRAINT "ExpertiseTranslation_expertiseId_fkey" FOREIGN KEY ("expertiseId") REFERENCES "Expertise"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DefaultTranslation" ADD CONSTRAINT "DefaultTranslation_defaultId_fkey" FOREIGN KEY ("defaultId") REFERENCES "Default"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DefaultTranslation" ADD CONSTRAINT "DefaultTranslation_languageId_fkey" FOREIGN KEY ("languageId") REFERENCES "Language"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PageBlock" ADD CONSTRAINT "PageBlock_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PageBlockTranslation" ADD CONSTRAINT "PageBlockTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PageBlockTranslation" ADD CONSTRAINT "PageBlockTranslation_blockId_fkey" FOREIGN KEY ("blockId") REFERENCES "PageBlock"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_staffId_fkey" FOREIGN KEY ("staffId") REFERENCES "Staff"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Countries" ADD CONSTRAINT "Countries_coverId_fkey" FOREIGN KEY ("coverId") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TosAcceptanceLog" ADD CONSTRAINT "TosAcceptanceLog_policyVersion_fkey" FOREIGN KEY ("policyVersion") REFERENCES "TermsOfService"("version") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicalPackage" ADD CONSTRAINT "MedicalPackage_thumbnail_id_fkey" FOREIGN KEY ("thumbnail_id") REFERENCES "Image"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicalPackage" ADD CONSTRAINT "MedicalPackage_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "MedicalPackage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicalPackagesTranslation" ADD CONSTRAINT "MedicalPackagesTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MedicalPackagesTranslation" ADD CONSTRAINT "MedicalPackagesTranslation_medicalPackageId_fkey" FOREIGN KEY ("medicalPackageId") REFERENCES "MedicalPackage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TransferPackageTranslations" ADD CONSTRAINT "TransferPackageTranslations_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TransferPackageTranslations" ADD CONSTRAINT "TransferPackageTranslations_transferPackageId_fkey" FOREIGN KEY ("transferPackageId") REFERENCES "TransferPackage"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TransferTeamTranslation" ADD CONSTRAINT "TransferTeamTranslation_lang_id_fkey" FOREIGN KEY ("lang_id") REFERENCES "Language"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TransferTeamTranslation" ADD CONSTRAINT "TransferTeamTranslation_transferTeamId_fkey" FOREIGN KEY ("transferTeamId") REFERENCES "TransferTeam"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferPackageLocationImages" ADD CONSTRAINT "_TransferPackageLocationImages_A_fkey" FOREIGN KEY ("A") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferPackageLocationImages" ADD CONSTRAINT "_TransferPackageLocationImages_B_fkey" FOREIGN KEY ("B") REFERENCES "TransferPackage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferPackageGalleryImages" ADD CONSTRAINT "_TransferPackageGalleryImages_A_fkey" FOREIGN KEY ("A") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferPackageGalleryImages" ADD CONSTRAINT "_TransferPackageGalleryImages_B_fkey" FOREIGN KEY ("B") REFERENCES "TransferPackage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferPackageToTransferTeam" ADD CONSTRAINT "_TransferPackageToTransferTeam_A_fkey" FOREIGN KEY ("A") REFERENCES "TransferPackage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferPackageToTransferTeam" ADD CONSTRAINT "_TransferPackageToTransferTeam_B_fkey" FOREIGN KEY ("B") REFERENCES "TransferTeam"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferTeamToUsers" ADD CONSTRAINT "_TransferTeamToUsers_A_fkey" FOREIGN KEY ("A") REFERENCES "TransferTeam"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_TransferTeamToUsers" ADD CONSTRAINT "_TransferTeamToUsers_B_fkey" FOREIGN KEY ("B") REFERENCES "Users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..230cb09 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,682 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +enum CaseStatus { + NEW + CONTACTED + DOCS_PENDING + REVIEWING + PRE_APPROVED + REJECTED + CLOSED + CONVERTED_TO_HIS +} + +// enum DocumentType { +// PASSPORT +// MEDICAL_RECORD +// IMAGING +// LAB_RESULT +// OTHER +// } + +enum InteractionType { + PHONE + WHATSAPP + EMAIL + SYSTEM +} + +model Patient { + id String @id @default(uuid()) + + pid String @unique @db.VarChar(10) + // -------- Identity -------- + firstName String @db.VarChar(100) + lastName String @db.VarChar(100) + + birthDate DateTime? + sex Sex? + age Int? + // -------- Nationality -------- + nationality Countries? @relation(fields: [nationalityId], references: [id]) + nationalityId Int? + nationalityCode String? @db.Char(3) + + passportCode String? @db.VarChar(20) + + // -------- Contact -------- + phone String? @db.VarChar(20) + email String? @db.VarChar(255) + preferredLanguage String? @db.VarChar(10) + + // -------- Address -------- + address String? @db.VarChar(500) + postalCode String? @db.VarChar(20) + + // -------- System -------- + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + // -------- Relations -------- + cases OnlineCase[] + documents Document[] + + // -------- Indexes & Constraints -------- + @@unique([nationalityCode]) // کد ملی + @@unique([passportCode, nationalityId]) // پاسپورت + @@unique([phone]) + @@unique([email]) + @@unique([firstName, lastName, birthDate]) + @@index([pid]) +} + +model Document { + id String @id @default(uuid()) + caseId String? + patientId String? + uploadedById String? + + type DocumentType + filename String + fileUrl String + fileKey String + mimeType String? + size Int? + checksum String? + status DocStatus @default(NEW) + + is_deleted Boolean @default(false) + createdAt DateTime @default(now()) + + case OnlineCase? @relation(fields: [caseId], references: [id]) + patient Patient? @relation(fields: [patientId], references: [id]) + uploadedBy Staff? @relation(fields: [uploadedById], references: [id]) +} + +enum DocumentType { + MEDICAL_IMAGE + LAB_RESULT + PRESCRIPTION + PASSPORT + NATIONAL_ID + OTHER + OTHER_FILE +} + +enum DocStatus { + NEW + PENDING_SCAN + SAFE + INFECTED + INVALID +} + +enum Sex { + male + female + other +} + +model OnlineCase { + id String @id @default(uuid()) + patientId String + trackingCode String @unique + + message String? + specialty String? + formData Json? + + status CaseStatus @default(NEW) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + patient Patient @relation(fields: [patientId], references: [id]) + documents Document[] + // interactions Interaction[] + reviews Review[] + statusHistory CaseStatusHistory[] +} + +model Review { + id String @id @default(uuid()) + caseId String + doctorId String? + + createdAt DateTime @default(now()) + deletedAt DateTime? + + case OnlineCase @relation(fields: [caseId], references: [id]) + doctor Staff? @relation(fields: [doctorId], references: [id]) + + translations ReviewTranslation[] +} + +model ReviewTranslation { + id Int @id @default(autoincrement()) + reviewId String + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + + note String? + result String? // eligible, needs more docs, not eligible (می‌تونه ترجمه شود) + + review Review @relation(fields: [reviewId], references: [id]) + + @@unique([reviewId, lang_id]) // هر Review در هر زبان فقط یک ترجمه دارد +} + +model CaseStatusHistory { + id String @id @default(uuid()) + caseId String + from CaseStatus? + to CaseStatus? + changedBy String? + + createdAt DateTime @default(now()) + + case OnlineCase @relation(fields: [caseId], references: [id]) + + @@index([id, caseId]) +} + +model UploadSession { + id String @id @default(uuid()) + uploadKey String @unique // path/key to store file on CDN (e.g. documents/{uuid}.pdf) + nonce String @unique + used Boolean @default(false) + createdById String? // user id + purpose String // "document" | "image" + allowedTypes String? // JSON string array + maxSize Int? + status String @default("PENDING") + createdAt DateTime @default(now()) + expiresAt DateTime + + // indexes + @@index([createdById]) + @@index([status]) +} + +enum UploadStatus { + PENDING + UPLOADING + UPLOADED + VERIFIED + FAILED + EXPIRED +} + +enum StaffRoles { + developer + admin + doctor + coordinator +} + +model Staff { + id String @id @default(uuid()) + username String @unique + password String + email String? @unique + role StaffRoles + + is_verified Boolean @default(false) + send_notif_with_email Boolean @default(false) + resetPasswordToken String? + resetPasswordExpires DateTime? + status StaffStatus @default(ACTIVE) + trustScore Int @default(100) + strikes Int @default(0) + + restrictions Restriction[] + violations Violation[] + documents Document[] + reviews Review[] + + profilePicture Image? @relation(fields: [profilePictureID],references: [id]) + profilePictureID Int? + + translations StaffTranslation[] + tokens RefreshToken[] +} + +enum StaffStatus { + ACTIVE + WARNED + RESTRICTED + BANNED +} + +model Violation { + id String @id @default(uuid()) + userId String + type ViolationType + severity Int + reason String + + user Staff @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) +} + +enum ViolationType { + SPAM + RATE_LIMIT + CONTENT + ABUSE +} + +model Restriction { + id String @id @default(uuid()) + userId String + type RestrictionType + expiresAt DateTime? + + user Staff @relation(fields: [userId], references: [id]) +} + +enum RestrictionType { + TEMP + SHADOW + PERMANENT +} + +model StaffTranslation { + id Int @id @default(autoincrement()) + staffId String + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + + // فیلدهایی که نیاز به ترجمه دارند + displayName String? + position String? + description String? + + staff Staff @relation(fields: [staffId], references: [id]) + + @@unique([staffId, lang_id]) // هر کارمند در هر زبان فقط یک ترجمه دارد +} + +model Image { + id Int @id @default(autoincrement()) + fileKey String + filename String? + fileUrl String? + mimeType String? + size Int? + usersProfile Users[] + countriesCover Countries[] + medicalPackagesThumbnails MedicalPackage[] + transferPackageLocationImages TransferPackage[] @relation(name: "TransferPackageLocationImages") + transferPackageGalleryImages TransferPackage[] @relation(name: "TransferPackageGalleryImages") + staffProfilePictures Staff[] +} + +model Users { + id Int @id @default(autoincrement()) + slug String @unique + type UsersType @default(DOCTOR) + + displayInMainPage Boolean @default(false) + phone String? + email String? + teamName String? + medicalNumber String? + expertise Expertise? @relation(fields: [expertiseId], references: [id]) + expertiseId Int? + image Image? @relation(fields: [imageId], references: [id]) + imageId Int? + + createdAt DateTime @default(now()) + + translations UserTranslation[] + transerTeamMembers TransferTeam[] + + @@index([id, slug]) +} + +model UserTranslation { + id Int @id @default(autoincrement()) + userId Int + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + firstName String? + lastName String? + position String? + bio String? + excerpt String? + user Users @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد +} + +model Expertise { + id Int @id @default(autoincrement()) + slug String @db.VarChar(100) + users Users[] + translations ExpertiseTranslation[] +} + +model ExpertiseTranslation { + id Int @id @default(autoincrement()) + expertiseId Int + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + displayName String? + level EducationLevel? + expertise Expertise @relation(fields: [expertiseId], references: [id]) + + @@unique([expertiseId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد +} +enum EducationLevel { + GP + SPECIALIST + SUBSPECIALIST + FELLOWSHIP +} +enum UsersType { + DOCTOR + TRANSFER_TEAM + DEPARTMENT +} + +model PanelConfig { + id String @id @default(uuid()) + key String @unique + value String // مقدار خام (string) + type ConfigType // نوع داده + description String? + updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) +} + +model Default { + id String @id @default("SITE_DEFAULTS") + + email String + hospitalPhone String + logoUrl String? + mapAddress String + instagramLink String? + linkedinLink String? + ipdNumber String? + updatedAt DateTime @updatedAt + + translations DefaultTranslation[] +} + +model DefaultTranslation { + id Int @id @default(autoincrement()) + default Default @relation(fields: [defaultId], references: [id]) + defaultId String + language Language @relation(fields: [languageId], references: [id]) + languageId Int + + address String + underLogoText String? + aboutUsText String? + patientsRights String? + @@unique([defaultId, languageId]) +} + +model Statics { + id String @id @default(uuid()) + key String @unique @db.VarChar(191) + group String @db.VarChar(100) + description String? @db.Text + + translations StaticsTranslation[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([group]) +} + +model StaticsTranslation { + id String @id @default(uuid()) + + languageId Int + staticsKeyId String + + value String @db.Text + + language Language @relation(fields: [languageId], references: [id], onDelete: Cascade) + staticsKey Statics @relation(fields: [staticsKeyId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([languageId, staticsKeyId]) + @@index([languageId]) + @@index([staticsKeyId]) +} + +enum ConfigType { + BOOLEAN + NUMBER + STRING + JSON + STRING_ARRAY +} + +model Page { + id Int @id @default(autoincrement()) + slug String @unique + createdAt DateTime @default(now()) + pageBlocks PageBlock[] +} + +model PageBlock { + id Int @id @default(autoincrement()) + pageId Int + type String // hero, text, image, faq, etc + sort Int + + page Page @relation(fields: [pageId], references: [id]) + pageBlockTranslations PageBlockTranslation[] +} + +model PageBlockTranslation { + id Int @id @default(autoincrement()) + blockId Int + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + field String // title, subtitle, description, button_text + value String + + block PageBlock @relation(fields: [blockId], references: [id]) +} + +model Language { + id Int @id @default(autoincrement()) + title String + slug String @unique + + pageBlockTranslations PageBlockTranslation[] + // translations Translation[] + reviewTranslations ReviewTranslation[] + staffTranslations StaffTranslation[] + userTranslations UserTranslation[] + expertiseTranslations ExpertiseTranslation[] + defaultTranslations DefaultTranslation[] + medicalPackagesTranslations MedicalPackagesTranslation[] + transferPackageTranslations TransferPackageTranslations[] + transferTeamTranslations TransferTeamTranslation[] + staticsTranslations StaticsTranslation[] + + @@index([id, slug]) +} + +model RefreshToken { + id Int @id @default(autoincrement()) + token String @unique + staff Staff @relation(fields: [staffId], references: [id]) + staffId String + + expiresAt DateTime + createdAt DateTime @default(now()) +} + +model Countries { + id Int @id @default(autoincrement()) + name String + callCode String + cover Image? @relation(fields: [coverId], references: [id]) + coverId Int? + createdAt DateTime @default(now()) + patients Patient[] +} + +enum AuditAction { + CREATE + UPDATE + DELETE + READ +} + +model AuditLog { + id Int @id @default(autoincrement()) + actorId String? + actorRole StaffRoles? + action AuditAction + entity String + entityId Int? + before Json? + after Json? + ip String? + createdAt DateTime @default(now()) + + @@index([entity, entityId]) +} + +model AccessLog { + id Int @id @default(autoincrement()) + userId String + userRole StaffRoles + resource String + resourceId Int? + reason String? + ip String? + createdAt DateTime @default(now()) +} + +model DecisionLog { + id Int @id @default(autoincrement()) + decisionType String + input Json + output Json + algorithmVersion String + actorId String? + createdAt DateTime @default(now()) +} + +model TermsOfService { + id String @id @default(uuid()) // شناسه یکتا + title String // عنوان سند، مثلا "Terms of Service" + content String // متن کامل TOS (Markdown یا HTML) + version String @unique // نسخه سند، مثلا "v1.0.0" + isActive Boolean @default(true) // آیا این نسخه فعال است؟ + createdAt DateTime @default(now()) // زمان ایجاد نسخه + updatedAt DateTime @updatedAt // زمان آخرین آپدیت + + /// روابط اختیاری، اگر بخواهید لاگ پذیرش کاربران را نگه دارید + acceptances TosAcceptanceLog[] // ثبت کاربرانی که این نسخه را پذیرفته‌اند +} + +model TosAcceptanceLog { + id Int @id @default(autoincrement()) + userId String // شناسه کاربر + policyType String // نوع سند (TOS یا Privacy Policy) + policyVersion String // نسخه سند پذیرفته شده + acceptedAt DateTime @default(now()) // زمان پذیرش + ip String? // آی‌پی کاربر برای ثبت لاگ + tos TermsOfService? @relation(fields: [policyVersion], references: [version]) +} + +model PrivacyPolicy { + id Int @id @default(autoincrement()) // شناسه یکتا + content String // متن کامل TOS (Markdown یا HTML) + createdAt DateTime @default(now()) // زمان ایجاد نسخه + updatedAt DateTime @updatedAt // زمان آخرین آپدیت +} + +model MedicalPackage { + id Int @id @default(autoincrement()) + + thumbnail Image? @relation(fields: [thumbnail_id], references: [id]) + thumbnail_id Int? + icon String? + priority Int? + price String + parent MedicalPackage? @relation("MedicalPackageHierarchy", fields: [parent_id], references: [id], onDelete: SetNull) + parent_id Int? // برای اشاره به دسته‌بندی والد + children MedicalPackage[] @relation("MedicalPackageHierarchy") + translations MedicalPackagesTranslation[] +} + +model MedicalPackagesTranslation { + id Int @id @default(autoincrement()) + title String @db.VarChar(50) + content String? + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + medicalPackage MedicalPackage? @relation(fields: [medicalPackageId], references: [id]) + medicalPackageId Int? + + @@unique([medicalPackageId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد +} + +model TransferPackage { + id Int @id @default(autoincrement()) + + location String + price String + locationImages Image[] @relation(name: "TransferPackageLocationImages") + galleryImages Image[] @relation(name: "TransferPackageGalleryImages") + transferTeam TransferTeam[] + translations TransferPackageTranslations[] +} + +model TransferPackageTranslations { + id Int @id @default(autoincrement()) + + name String + content String + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + transferPackage TransferPackage? @relation(fields: [transferPackageId], references: [id]) + transferPackageId Int? + + @@unique([transferPackageId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد +} + +model TransferTeam { + id Int @id @default(autoincrement()) + + members Users[] + packages TransferPackage[] + translations TransferTeamTranslation[] +} + +model TransferTeamTranslation { + id Int @id @default(autoincrement()) + lang Language? @relation(fields: [lang_id], references: [id]) + lang_id Int? + transferTeam TransferTeam? @relation(fields: [transferTeamId], references: [id]) + transferTeamId Int? + name String @db.VarChar(50) + introduction String + duties String + services String + + @@unique([transferTeamId, lang_id]) // هر کاربر در هر زبان فقط یک ترجمه دارد +} diff --git a/src/app/server.ts b/src/app/server.ts new file mode 100644 index 0000000..27b9fa6 --- /dev/null +++ b/src/app/server.ts @@ -0,0 +1,90 @@ +import dotenv from "dotenv"; +import path from "path"; +import cookieParser from "cookie-parser"; +import {NextFunction, Request, Response} from "express"; +import createHttpError from "http-errors"; +import {secureApp} from "@/core/secure-app"; +import mainRouter from "@/core/router/main.router"; +import {ServerErrorsObject} from "@/common/types"; +// import {connectRabbit} from "../common/configs/rabbit"; +import {connectMongo} from "../../mongodb/config/index"; +import { + ErrorLog, +} from "../../mongodb/models/index"; +import {apiLoggingMiddleware} from "@/core/middlewares/logs.middleware"; + +const express = require("express") as typeof import("express"); +dotenv.config(); + +export default class ServerApplication { + #PORT = process.env.PORT || 3500; + #APP = express(); + constructor() { + this.AppConfiguration(); + this.StartApplication(); + this.InitClientSession(); + this.RoutesConfiguration(); + this.ErrorHandlingConfiguration(); + } + async AppConfiguration() { + this.#APP.use(secureApp); + this.#APP.use(express.json()); + this.#APP.use(express.urlencoded({extended: true})); + this.#APP.set("json spaces", 2); + this.#APP.use( + "/resources/images", + express.static(path.join(__dirname, "..", "media", "images")) + ); + this.#APP.use( + "/resources/videos", + express.static(path.join(__dirname, "..", "media", "videos")) + ); + } + async StartApplication() { + // await database_connection(); + // await connectRabbit(); + await connectMongo(); + this.#APP.listen(this.#PORT, () => { + console.log( + `Server Running on PORT ${this.#PORT} url : ${"http://localhost:"}${ + this.#PORT + }` + ); + }); + } + InitClientSession() { + this.#APP.use(cookieParser(process.env.COOKIE_PARSER_SECRET_KEY)); + } + RoutesConfiguration() { + this.#APP.use(apiLoggingMiddleware()); + + this.#APP.get("/", (req, res) => res.send("")); + this.#APP.use("/api/v1", mainRouter); + } + ErrorHandlingConfiguration() { + this.#APP.use((req: any, res: Response, next: NextFunction) => { + next(createHttpError.NotFound("این آدرس یافت نشد")); + }); + this.#APP.use( + async (error: any, req: Request, res: Response, next: NextFunction) => { + await ErrorLog.create({ + message: error.message, + stack: error.stack, + severity: "HIGH", + }); + const serverError = createHttpError.InternalServerError(); + const statusCode = error.status || serverError.status; + const message: string = error.message || serverError.message; + + const errorObject: ServerErrorsObject = { + status: statusCode, + error: { + message, + }, + }; + + return res.status(statusCode).json(errorObject); + } + ); + } +} diff --git a/src/common/configs/file-multer.ts b/src/common/configs/file-multer.ts new file mode 100644 index 0000000..a6fe753 --- /dev/null +++ b/src/common/configs/file-multer.ts @@ -0,0 +1,40 @@ +import multer, { FileFilterCallback } from "multer"; +import { Request } from "express"; + +// Accepted document mime types +const allowedDocTypes: string[] = [ + "application/pdf", + "application/zip", + "application/x-zip-compressed", + "application/x-rar-compressed", + "application/octet-stream", // For DICOM files sometimes +]; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, "uploads/documents"); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +// Main multer config +export const uploadDocument = multer({ + storage, + limits: { + fileSize: 10 * 1024 * 1024, // 10 MB + }, + fileFilter: ( + req: Request, + file: Express.Multer.File, + cb: FileFilterCallback + ) => { + if (!allowedDocTypes.includes(file.mimetype)) { + return cb( + new Error("فرمت فایل معتبر نیست (فقط PDF, ZIP, RAR, DICOM)") + ); + } + cb(null, true); + }, +}); diff --git a/src/common/configs/image-multer.ts b/src/common/configs/image-multer.ts new file mode 100644 index 0000000..156ba53 --- /dev/null +++ b/src/common/configs/image-multer.ts @@ -0,0 +1,33 @@ +import multer, { FileFilterCallback } from "multer"; +import { Request } from "express"; + +// Allowed image formats +const allowedImageTypes: string[] = ["image/jpeg", "image/png", "image/webp"]; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, "uploads/profileImages"); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +export const uploadProfileImage = multer({ + storage, + limits: { + fileSize: 2 * 1024 * 1024, // 2 MB + }, + fileFilter: ( + req: Request, + file: Express.Multer.File, + cb: FileFilterCallback + ) => { + if (!allowedImageTypes.includes(file.mimetype)) { + return cb( + new Error("فقط فرمت‌های JPG, PNG, WEBP برای تصویر پروفایل معتبر است") + ); + } + cb(null, true); + }, +}); diff --git a/src/common/constants/config.ts b/src/common/constants/config.ts new file mode 100644 index 0000000..074a524 --- /dev/null +++ b/src/common/constants/config.ts @@ -0,0 +1,141 @@ +import {CorsOptions} from "cors"; +import dotenv from "dotenv"; +import {NextFunction} from "express"; +import {Options} from "express-rate-limit"; +import {HelmetOptions} from "helmet"; +import createHttpError from "http-errors"; +import {VALID_ORIGINS} from "./variables"; +import {Request} from "express"; + +dotenv.config(); + +const multer = require("multer"); +const path = require("path"); +const fs = require("fs"); +export const config = { + port: process.env.PORT || 3500, + db: { + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }, + jwtSecret: process.env.JWT_SECRET || "secret", +}; + +export const limiter_option: Partial = { + windowMs: 15 * 60 * 1000, + max: 4000, + standardHeaders: true, + legacyHeaders: false, + + handler: (req: any, res: any, next: NextFunction) => { + next( + new createHttpError.TooManyRequests( + "تعداد درخواست شما بیشتر از حد مجاز است ، در زمان دیگری مجدد درخواست دهید" + ) + ); + }, + // message: {error: "Too many requests, please try again later."}, +}; + +export const helmet_option: HelmetOptions = { + contentSecurityPolicy: { + useDefaults: true, + directives: { + defaultSrc: ["'self'"], + // scriptSrc: ["'self'", "'unsafe-inline'", "https://trusted.cdn.com"], + // styleSrc: ["'self'", "'unsafe-inline'"], + // imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'"], + // fontSrc: ["'self'", "https://fonts.gstatic.com"], + objectSrc: ["'none'"], + upgradeInsecureRequests: [], // تبدیل اتومات http به https + }, + }, + crossOriginEmbedderPolicy: true, + crossOriginResourcePolicy: {policy: "same-origin"}, + frameguard: {action: "deny"}, // جلوگیری از Clickjacking + referrerPolicy: {policy: "no-referrer"}, // جلوگیری از لو رفتن referrer + xssFilter: true, // فعال کردن فیلتر XSS + hsts: {maxAge: 63072000, includeSubDomains: true, preload: true}, // HSTS +}; + +export const cors_option: CorsOptions = { + origin: true, + credentials: true, + allowedHeaders: [ + "Content-Type", + "Authorization", + "x-upload-token", // 👈 اینو اضافه کن + ], + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], + maxAge: 600, + // origin:["http://localhost:3000"] +}; + +async function createDirectoryRoute(req: Request) { + const date = new Date(); + + const directory = path.join(__dirname, "..", "..", "..", "public", "images"); + req.body.fileUploadPath = path.join(directory, "original"); + fs.mkdirSync(directory, {recursive: true}); + return directory; +} + +// const storage = multer.diskStorage({ +// destination: async ( +// req: Request, +// file: any, +// cb: (a?: any, b?: any) => void +// ) => { +// if (file?.originalname) { +// const filePath = await createDirectoryRoute(req); +// return cb(null, filePath); +// } +// cb(null, null); +// }, +// filename: (req: Request, file: any, cb: (a?: any, b?: any) => void) => { +// if (file?.originalname) { +// const ext = path.extname(file.originalname); +// const fileName = String(new Date().getTime() + ext); +// req.body.filename = fileName; +// return cb(null, fileName); +// } +// cb(null, null); +// }, +// }); +// function fileFilter( +// req: Request, +// file: any, +// cb: (a?: any, b?: any) => void +// ) { +// const ext = path.extname(file.originalname); +// if (ValidExtNames.includes(ext)) { +// return cb(null, true); +// } + +// return cb(createHttpError.BadRequest("فرمت ارسالی صحیح نمی باشد.")); +// } + +// const storage = multer.diskStorage({ +// destination: async (req: Request, file: any, cb: Function) => { +// // const root_directory = await createDirectoryRoute(req); +// // const originalDirectory = path.join(root_directory, "original"); +// // مشخص کردن پوشه‌ای که فایل‌ها ذخیره شوند +// cb(null, true); // پوشه اصلی +// }, +// filename: (req: Request, file: any, cb: Function) => { +// cb(null, file.filename); // نام فایل بر اساس زمان فعلی +// }, +// }); +// const uploadFile = multer({ +// storage: storage, +// // fileFilter, +// // limits: {fileSize: 1000000}, +// }); +const storage = multer.memoryStorage(); // Keep files in memory (instead of disk) +const uploadFile = multer({storage: storage}); + +export {uploadFile}; diff --git a/src/common/constants/models-enums.ts b/src/common/constants/models-enums.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/common/constants/policy.ts b/src/common/constants/policy.ts new file mode 100644 index 0000000..cfee05b --- /dev/null +++ b/src/common/constants/policy.ts @@ -0,0 +1,64 @@ +import {CookieOptions} from "express"; + +// interface registration_policy_types { +// user: { +// defaultStatus: UserSt; +// emailVerificationRequired: boolean; +// otpVerificationRequired: boolean; +// }; +// business: { +// defaultStatus: UserSt; +// emailVerificationRequired: boolean; +// otpVerificationRequired: boolean; +// adminApprovalRequired: boolean; +// callApprovalRequired: boolean; +// }; +// } + +// export const registration_policy: Readonly = +// Object.freeze({ +// user: { +// defaultStatus: "active" as const, +// emailVerificationRequired: false, +// otpVerificationRequired: true, +// }, +// business: { +// defaultStatus: "suspended" as const, +// emailVerificationRequired: false, +// otpVerificationRequired: true, +// adminApprovalRequired: true, +// callApprovalRequired: true, +// }, +// }); + +interface token_policy_types { + r_token_expire: `${number}${"s" | "m" | "h" | "d" | "y"}`; + a_token_expire: `${number}${"s" | "m" | "h" | "d" | "y"}`; + access_token_options: CookieOptions; + refresh_token_options: CookieOptions; +} +const isProd = process.env.NODE_ENV === "production"; + +console.log(isProd) +export const token_policy: Readonly = Object.freeze({ + r_token_expire: "7d", + a_token_expire: "1h", + access_token_options: { + httpOnly: true, + signed: true, + secure: isProd, + sameSite: isProd ? "none" : "lax", + maxAge: 60 * 60 * 1000, // 1 ساعت + } as CookieOptions, + refresh_token_options: { + httpOnly: true, + signed: true, + secure: isProd, + sameSite: isProd ? "none" : "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 روز + } as const, +}); + +export const otp_code_length = 6; + +export const price_policy_max = 990000000000; diff --git a/src/common/constants/regex.ts b/src/common/constants/regex.ts new file mode 100644 index 0000000..964fba9 --- /dev/null +++ b/src/common/constants/regex.ts @@ -0,0 +1,4 @@ +export const mobile_regex = /^(\+98|0)?9\d{9}$/; +export const otp_code_regex = /^\d{6}$/; +export const password_regex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; \ No newline at end of file diff --git a/src/common/constants/variables.js b/src/common/constants/variables.js new file mode 100644 index 0000000..a6976c9 --- /dev/null +++ b/src/common/constants/variables.js @@ -0,0 +1,228 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.allCountries = exports.SELECT_STAFF_OUT_DATA = exports.VALID_ORIGINS = void 0; +// export const projectName="shomalhospital" +exports.VALID_ORIGINS = [ + "http://localhost:3000", + "https://ipd.shomalhospital.com", + "http://ipd.shomalhospital.com", +]; +exports.SELECT_STAFF_OUT_DATA = Object.freeze({ + id: true, + username: true, + email: true, + is_verified: true, + role: true, + send_notif_with_email: true, +}); +exports.allCountries = [ + { label: "Afghanistan", value: "afghanistan", code: "+93" }, + { label: "Albania", value: "albania", code: "+355" }, + { label: "Algeria", value: "algeria", code: "+213" }, + { label: "American Samoa", value: "american-samoa", code: "+1-684" }, + { label: "Andorra", value: "andorra", code: "+376" }, + { label: "Angola", value: "angola", code: "+244" }, + { label: "Anguilla", value: "anguilla", code: "+1-264" }, + { label: "Antigua and Barbuda", value: "antigua-and-barbuda", code: "+1-268" }, + { label: "Argentina", value: "argentina", code: "+54" }, + { label: "Armenia", value: "armenia", code: "+374" }, + { label: "Aruba", value: "aruba", code: "+297" }, + { label: "Australia", value: "australia", code: "+61" }, + { label: "Austria", value: "austria", code: "+43" }, + { label: "Azerbaijan", value: "azerbaijan", code: "+994" }, + { label: "Bahamas", value: "bahamas", code: "+1-242" }, + { label: "Bahrain", value: "bahrain", code: "+973" }, + { label: "Bangladesh", value: "bangladesh", code: "+880" }, + { label: "Barbados", value: "barbados", code: "+1-246" }, + { label: "Belarus", value: "belarus", code: "+375" }, + { label: "Belgium", value: "belgium", code: "+32" }, + { label: "Belize", value: "belize", code: "+501" }, + { label: "Benin", value: "benin", code: "+229" }, + { label: "Bhutan", value: "bhutan", code: "+975" }, + { label: "Bolivia", value: "bolivia", code: "+591" }, + { + label: "Bosnia and Herzegovina", + value: "bosnia-and-herzegovina", + code: "+387", + }, + { label: "Botswana", value: "botswana", code: "+267" }, + { label: "Brazil", value: "brazil", code: "+55" }, + { label: "Brunei", value: "brunei", code: "+673" }, + { label: "Bulgaria", value: "bulgaria", code: "+359" }, + { label: "Burkina Faso", value: "burkina-faso", code: "+226" }, + { label: "Burundi", value: "burundi", code: "+257" }, + { label: "Cabo Verde", value: "cabo-verde", code: "+238" }, + { label: "Cambodia", value: "cambodia", code: "+855" }, + { label: "Cameroon", value: "cameroon", code: "+237" }, + { label: "Canada", value: "canada", code: "+1" }, + { + label: "Central African Republic", + value: "central-african-republic", + code: "+236", + }, + { label: "Chad", value: "chad", code: "+235" }, + { label: "Chile", value: "chile", code: "+56" }, + { label: "China", value: "china", code: "+86" }, + { label: "Colombia", value: "colombia", code: "+57" }, + { label: "Comoros", value: "comoros", code: "+269" }, + { label: "Congo", value: "congo", code: "+242" }, + { + label: "Congo, Democratic Republic of the", + value: "congo-democratic-republic", + code: "+243", + }, + { label: "Costa Rica", value: "costa-rica", code: "+506" }, + { label: "Croatia", value: "croatia", code: "+385" }, + { label: "Cuba", value: "cuba", code: "+53" }, + { label: "Cyprus", value: "cyprus", code: "+357" }, + { label: "Czech Republic", value: "czech-republic", code: "+420" }, + { label: "Denmark", value: "denmark", code: "+45" }, + { label: "Djibouti", value: "djibouti", code: "+253" }, + { label: "Dominica", value: "dominica", code: "+1-767" }, + { label: "Dominican Republic", value: "dominican-republic", code: "+1-809" }, + { label: "Ecuador", value: "ecuador", code: "+593" }, + { label: "Egypt", value: "egypt", code: "+20" }, + { label: "El Salvador", value: "el-salvador", code: "+503" }, + { label: "Equatorial Guinea", value: "equatorial-guinea", code: "+240" }, + { label: "Eritrea", value: "eritrea", code: "+291" }, + { label: "Estonia", value: "estonia", code: "+372" }, + { label: "Eswatini", value: "eswatini", code: "+268" }, + { label: "Ethiopia", value: "ethiopia", code: "+251" }, + { label: "Fiji", value: "fiji", code: "+679" }, + { label: "Finland", value: "finland", code: "+358" }, + { label: "France", value: "france", code: "+33" }, + { label: "Gabon", value: "gabon", code: "+241" }, + { label: "Gambia", value: "gambia", code: "+220" }, + { label: "Georgia", value: "georgia", code: "+995" }, + { label: "Germany", value: "germany", code: "+49" }, + { label: "Ghana", value: "ghana", code: "+233" }, + { label: "Greece", value: "greece", code: "+30" }, + { label: "Grenada", value: "grenada", code: "+1-473" }, + { label: "Guatemala", value: "guatemala", code: "+502" }, + { label: "Guinea", value: "guinea", code: "+224" }, + { label: "Guinea-Bissau", value: "guinea-bissau", code: "+245" }, + { label: "Guyana", value: "guyana", code: "+592" }, + { label: "Haiti", value: "haiti", code: "+509" }, + { label: "Honduras", value: "honduras", code: "+504" }, + { label: "Hungary", value: "hungary", code: "+36" }, + { label: "Iceland", value: "iceland", code: "+354" }, + { label: "India", value: "india", code: "+91" }, + { label: "Indonesia", value: "indonesia", code: "+62" }, + { label: "Iran", value: "iran", code: "+98" }, + { label: "Iraq", value: "iraq", code: "+964" }, + { label: "Ireland", value: "ireland", code: "+353" }, + { label: "Israel", value: "israel", code: "+972" }, + { label: "Italy", value: "italy", code: "+39" }, + { label: "Jamaica", value: "jamaica", code: "+1-876" }, + { label: "Japan", value: "japan", code: "+81" }, + { label: "Jordan", value: "jordan", code: "+962" }, + { label: "Kazakhstan", value: "kazakhstan", code: "+7" }, + { label: "Kenya", value: "kenya", code: "+254" }, + { label: "Kiribati", value: "kiribati", code: "+686" }, + { label: "Kuwait", value: "kuwait", code: "+965" }, + { label: "Kyrgyzstan", value: "kyrgyzstan", code: "+996" }, + { label: "Laos", value: "laos", code: "+856" }, + { label: "Latvia", value: "latvia", code: "+371" }, + { label: "Lebanon", value: "lebanon", code: "+961" }, + { label: "Lesotho", value: "lesotho", code: "+266" }, + { label: "Liberia", value: "liberia", code: "+231" }, + { label: "Libya", value: "libya", code: "+218" }, + { label: "Liechtenstein", value: "liechtenstein", code: "+423" }, + { label: "Lithuania", value: "lithuania", code: "+370" }, + { label: "Luxembourg", value: "luxembourg", code: "+352" }, + { label: "Macau", value: "macau", code: "+853" }, + { label: "North Macedonia", value: "north-macedonia", code: "+389" }, + { label: "Madagascar", value: "madagascar", code: "+261" }, + { label: "Malawi", value: "malawi", code: "+265" }, + { label: "Malaysia", value: "malaysia", code: "+60" }, + { label: "Maldives", value: "maldives", code: "+960" }, + { label: "Mali", value: "mali", code: "+223" }, + { label: "Malta", value: "malta", code: "+356" }, + { label: "Marshall Islands", value: "marshall-islands", code: "+692" }, + { label: "Mauritania", value: "mauritania", code: "+222" }, + { label: "Mauritius", value: "mauritius", code: "+230" }, + { label: "Mexico", value: "mexico", code: "+52" }, + { label: "Micronesia", value: "micronesia", code: "+691" }, + { label: "Moldova", value: "moldova", code: "+373" }, + { label: "Monaco", value: "monaco", code: "+377" }, + { label: "Mongolia", value: "mongolia", code: "+976" }, + { label: "Montenegro", value: "montenegro", code: "+382" }, + { label: "Morocco", value: "morocco", code: "+212" }, + { label: "Mozambique", value: "mozambique", code: "+258" }, + { label: "Myanmar", value: "myanmar", code: "+95" }, + { label: "Namibia", value: "namibia", code: "+264" }, + { label: "Nauru", value: "nauru", code: "+674" }, + { label: "Nepal", value: "nepal", code: "+977" }, + { label: "Netherlands", value: "netherlands", code: "+31" }, + { label: "New Zealand", value: "new-zealand", code: "+64" }, + { label: "Nicaragua", value: "nicaragua", code: "+505" }, + { label: "Niger", value: "niger", code: "+227" }, + { label: "Nigeria", value: "nigeria", code: "+234" }, + { label: "Norway", value: "norway", code: "+47" }, + { label: "Oman", value: "oman", code: "+968" }, + { label: "Pakistan", value: "pakistan", code: "+92" }, + { label: "Palau", value: "palau", code: "+680" }, + { label: "Palestine", value: "palestine", code: "+970" }, + { label: "Panama", value: "panama", code: "+507" }, + { label: "Papua New Guinea", value: "papua-new-guinea", code: "+675" }, + { label: "Paraguay", value: "paraguay", code: "+595" }, + { label: "Peru", value: "peru", code: "+51" }, + { label: "Philippines", value: "philippines", code: "+63" }, + { label: "Poland", value: "poland", code: "+48" }, + { label: "Portugal", value: "portugal", code: "+351" }, + { label: "Qatar", value: "qatar", code: "+974" }, + { label: "Romania", value: "romania", code: "+40" }, + { label: "Russia", value: "russia", code: "+7" }, + { label: "Rwanda", value: "rwanda", code: "+250" }, + { label: "Samoa", value: "samoa", code: "+685" }, + { label: "San Marino", value: "san-marino", code: "+378" }, + { label: "Saudi Arabia", value: "saudi-arabia", code: "+966" }, + { label: "Senegal", value: "senegal", code: "+221" }, + { label: "Serbia", value: "serbia", code: "+381" }, + { label: "Seychelles", value: "seychelles", code: "+248" }, + { label: "Sierra Leone", value: "sierra-leone", code: "+232" }, + { label: "Singapore", value: "singapore", code: "+65" }, + { label: "Slovakia", value: "slovakia", code: "+421" }, + { label: "Slovenia", value: "slovenia", code: "+386" }, + { label: "Solomon Islands", value: "solomon-islands", code: "+677" }, + { label: "Somalia", value: "somalia", code: "+252" }, + { label: "South Africa", value: "south-africa", code: "+27" }, + { label: "South Sudan", value: "south-sudan", code: "+211" }, + { label: "Spain", value: "spain", code: "+34" }, + { label: "Sri Lanka", value: "sri-lanka", code: "+94" }, + { label: "Sudan", value: "sudan", code: "+249" }, + { label: "Suriname", value: "suriname", code: "+597" }, + { label: "Sweden", value: "sweden", code: "+46" }, + { label: "Switzerland", value: "switzerland", code: "+41" }, + { label: "Syria", value: "syria", code: "+963" }, + { label: "Taiwan", value: "taiwan", code: "+886" }, + { label: "Tajikistan", value: "tajikistan", code: "+992" }, + { label: "Tanzania", value: "tanzania", code: "+255" }, + { label: "Thailand", value: "thailand", code: "+66" }, + { label: "Timor‑Leste", value: "timor-leste", code: "+670" }, + { label: "Togo", value: "togo", code: "+228" }, + { label: "Tonga", value: "tonga", code: "+676" }, + { label: "Trinidad and Tobago", value: "trinidad-and-tobago", code: "+1-868" }, + { label: "Tunisia", value: "tunisia", code: "+216" }, + { label: "Turkey", value: "turkey", code: "+90" }, + { label: "Turkmenistan", value: "turkmenistan", code: "+993" }, + { label: "Tuvalu", value: "tuvalu", code: "+688" }, + { label: "Uganda", value: "uganda", code: "+256" }, + { label: "Ukraine", value: "ukraine", code: "+380" }, + { label: "United Arab Emirates", value: "united-arab-emirates", code: "+971" }, + { label: "United Kingdom", value: "united-kingdom", code: "+44" }, + { label: "United States", value: "united-states", code: "+1" }, + { label: "Uruguay", value: "uruguay", code: "+598" }, + { label: "Uzbekistan", value: "uzbekistan", code: "+998" }, + { label: "Vanuatu", value: "vanuatu", code: "+678" }, + { + label: "Vatican City", + value: "vatican-city", + code: "+379" /* برخی منابع مختلف است */, + }, + { label: "Venezuela", value: "venezuela", code: "+58" }, + { label: "Vietnam", value: "vietnam", code: "+84" }, + { label: "Yemen", value: "yemen", code: "+967" }, + { label: "Zambia", value: "zambia", code: "+260" }, + { label: "Zimbabwe", value: "zimbabwe", code: "+263" }, +]; diff --git a/src/common/constants/variables.ts b/src/common/constants/variables.ts new file mode 100644 index 0000000..f648ca4 --- /dev/null +++ b/src/common/constants/variables.ts @@ -0,0 +1,238 @@ +// export const projectName="shomalhospital" +export const VALID_ORIGINS = [ + /^http:\/\/localhost:\d+$/, + "https://ipd.shomalhospital.com", + "http://ipd.shomalhospital.com", +]; +export const SELECT_STAFF_OUT_DATA = Object.freeze({ + id: true, + username: true, + email: true, + is_verified: true, + role: true, + send_notif_with_email: true, +}); + +export const allCountries = [ + { label: "Afghanistan", value: "afghanistan", code: "+93" }, + { label: "Albania", value: "albania", code: "+355" }, + { label: "Algeria", value: "algeria", code: "+213" }, + { label: "American Samoa", value: "american-samoa", code: "+1-684" }, + { label: "Andorra", value: "andorra", code: "+376" }, + { label: "Angola", value: "angola", code: "+244" }, + { label: "Anguilla", value: "anguilla", code: "+1-264" }, + { + label: "Antigua and Barbuda", + value: "antigua-and-barbuda", + code: "+1-268", + }, + { label: "Argentina", value: "argentina", code: "+54" }, + { label: "Armenia", value: "armenia", code: "+374" }, + { label: "Aruba", value: "aruba", code: "+297" }, + { label: "Australia", value: "australia", code: "+61" }, + { label: "Austria", value: "austria", code: "+43" }, + { label: "Azerbaijan", value: "azerbaijan", code: "+994" }, + { label: "Bahamas", value: "bahamas", code: "+1-242" }, + { label: "Bahrain", value: "bahrain", code: "+973" }, + { label: "Bangladesh", value: "bangladesh", code: "+880" }, + { label: "Barbados", value: "barbados", code: "+1-246" }, + { label: "Belarus", value: "belarus", code: "+375" }, + { label: "Belgium", value: "belgium", code: "+32" }, + { label: "Belize", value: "belize", code: "+501" }, + { label: "Benin", value: "benin", code: "+229" }, + { label: "Bhutan", value: "bhutan", code: "+975" }, + { label: "Bolivia", value: "bolivia", code: "+591" }, + { + label: "Bosnia and Herzegovina", + value: "bosnia-and-herzegovina", + code: "+387", + }, + { label: "Botswana", value: "botswana", code: "+267" }, + { label: "Brazil", value: "brazil", code: "+55" }, + { label: "Brunei", value: "brunei", code: "+673" }, + { label: "Bulgaria", value: "bulgaria", code: "+359" }, + { label: "Burkina Faso", value: "burkina-faso", code: "+226" }, + { label: "Burundi", value: "burundi", code: "+257" }, + { label: "Cabo Verde", value: "cabo-verde", code: "+238" }, + { label: "Cambodia", value: "cambodia", code: "+855" }, + { label: "Cameroon", value: "cameroon", code: "+237" }, + { label: "Canada", value: "canada", code: "+1" }, + { + label: "Central African Republic", + value: "central-african-republic", + code: "+236", + }, + { label: "Chad", value: "chad", code: "+235" }, + { label: "Chile", value: "chile", code: "+56" }, + { label: "China", value: "china", code: "+86" }, + { label: "Colombia", value: "colombia", code: "+57" }, + { label: "Comoros", value: "comoros", code: "+269" }, + { label: "Congo", value: "congo", code: "+242" }, + { + label: "Congo, Democratic Republic of the", + value: "congo-democratic-republic", + code: "+243", + }, + { label: "Costa Rica", value: "costa-rica", code: "+506" }, + { label: "Croatia", value: "croatia", code: "+385" }, + { label: "Cuba", value: "cuba", code: "+53" }, + { label: "Cyprus", value: "cyprus", code: "+357" }, + { label: "Czech Republic", value: "czech-republic", code: "+420" }, + { label: "Denmark", value: "denmark", code: "+45" }, + { label: "Djibouti", value: "djibouti", code: "+253" }, + { label: "Dominica", value: "dominica", code: "+1-767" }, + { label: "Dominican Republic", value: "dominican-republic", code: "+1-809" }, + { label: "Ecuador", value: "ecuador", code: "+593" }, + { label: "Egypt", value: "egypt", code: "+20" }, + { label: "El Salvador", value: "el-salvador", code: "+503" }, + { label: "Equatorial Guinea", value: "equatorial-guinea", code: "+240" }, + { label: "Eritrea", value: "eritrea", code: "+291" }, + { label: "Estonia", value: "estonia", code: "+372" }, + { label: "Eswatini", value: "eswatini", code: "+268" }, + { label: "Ethiopia", value: "ethiopia", code: "+251" }, + { label: "Fiji", value: "fiji", code: "+679" }, + { label: "Finland", value: "finland", code: "+358" }, + { label: "France", value: "france", code: "+33" }, + { label: "Gabon", value: "gabon", code: "+241" }, + { label: "Gambia", value: "gambia", code: "+220" }, + { label: "Georgia", value: "georgia", code: "+995" }, + { label: "Germany", value: "germany", code: "+49" }, + { label: "Ghana", value: "ghana", code: "+233" }, + { label: "Greece", value: "greece", code: "+30" }, + { label: "Grenada", value: "grenada", code: "+1-473" }, + { label: "Guatemala", value: "guatemala", code: "+502" }, + { label: "Guinea", value: "guinea", code: "+224" }, + { label: "Guinea-Bissau", value: "guinea-bissau", code: "+245" }, + { label: "Guyana", value: "guyana", code: "+592" }, + { label: "Haiti", value: "haiti", code: "+509" }, + { label: "Honduras", value: "honduras", code: "+504" }, + { label: "Hungary", value: "hungary", code: "+36" }, + { label: "Iceland", value: "iceland", code: "+354" }, + { label: "India", value: "india", code: "+91" }, + { label: "Indonesia", value: "indonesia", code: "+62" }, + { label: "Iran", value: "iran", code: "+98" }, + { label: "Iraq", value: "iraq", code: "+964" }, + { label: "Ireland", value: "ireland", code: "+353" }, + { label: "Israel", value: "israel", code: "+972" }, + { label: "Italy", value: "italy", code: "+39" }, + { label: "Jamaica", value: "jamaica", code: "+1-876" }, + { label: "Japan", value: "japan", code: "+81" }, + { label: "Jordan", value: "jordan", code: "+962" }, + { label: "Kazakhstan", value: "kazakhstan", code: "+7" }, + { label: "Kenya", value: "kenya", code: "+254" }, + { label: "Kiribati", value: "kiribati", code: "+686" }, + { label: "Kuwait", value: "kuwait", code: "+965" }, + { label: "Kyrgyzstan", value: "kyrgyzstan", code: "+996" }, + { label: "Laos", value: "laos", code: "+856" }, + { label: "Latvia", value: "latvia", code: "+371" }, + { label: "Lebanon", value: "lebanon", code: "+961" }, + { label: "Lesotho", value: "lesotho", code: "+266" }, + { label: "Liberia", value: "liberia", code: "+231" }, + { label: "Libya", value: "libya", code: "+218" }, + { label: "Liechtenstein", value: "liechtenstein", code: "+423" }, + { label: "Lithuania", value: "lithuania", code: "+370" }, + { label: "Luxembourg", value: "luxembourg", code: "+352" }, + { label: "Macau", value: "macau", code: "+853" }, + { label: "North Macedonia", value: "north-macedonia", code: "+389" }, + { label: "Madagascar", value: "madagascar", code: "+261" }, + { label: "Malawi", value: "malawi", code: "+265" }, + { label: "Malaysia", value: "malaysia", code: "+60" }, + { label: "Maldives", value: "maldives", code: "+960" }, + { label: "Mali", value: "mali", code: "+223" }, + { label: "Malta", value: "malta", code: "+356" }, + { label: "Marshall Islands", value: "marshall-islands", code: "+692" }, + { label: "Mauritania", value: "mauritania", code: "+222" }, + { label: "Mauritius", value: "mauritius", code: "+230" }, + { label: "Mexico", value: "mexico", code: "+52" }, + { label: "Micronesia", value: "micronesia", code: "+691" }, + { label: "Moldova", value: "moldova", code: "+373" }, + { label: "Monaco", value: "monaco", code: "+377" }, + { label: "Mongolia", value: "mongolia", code: "+976" }, + { label: "Montenegro", value: "montenegro", code: "+382" }, + { label: "Morocco", value: "morocco", code: "+212" }, + { label: "Mozambique", value: "mozambique", code: "+258" }, + { label: "Myanmar", value: "myanmar", code: "+95" }, + { label: "Namibia", value: "namibia", code: "+264" }, + { label: "Nauru", value: "nauru", code: "+674" }, + { label: "Nepal", value: "nepal", code: "+977" }, + { label: "Netherlands", value: "netherlands", code: "+31" }, + { label: "New Zealand", value: "new-zealand", code: "+64" }, + { label: "Nicaragua", value: "nicaragua", code: "+505" }, + { label: "Niger", value: "niger", code: "+227" }, + { label: "Nigeria", value: "nigeria", code: "+234" }, + { label: "Norway", value: "norway", code: "+47" }, + { label: "Oman", value: "oman", code: "+968" }, + { label: "Pakistan", value: "pakistan", code: "+92" }, + { label: "Palau", value: "palau", code: "+680" }, + { label: "Palestine", value: "palestine", code: "+970" }, + { label: "Panama", value: "panama", code: "+507" }, + { label: "Papua New Guinea", value: "papua-new-guinea", code: "+675" }, + { label: "Paraguay", value: "paraguay", code: "+595" }, + { label: "Peru", value: "peru", code: "+51" }, + { label: "Philippines", value: "philippines", code: "+63" }, + { label: "Poland", value: "poland", code: "+48" }, + { label: "Portugal", value: "portugal", code: "+351" }, + { label: "Qatar", value: "qatar", code: "+974" }, + { label: "Romania", value: "romania", code: "+40" }, + { label: "Russia", value: "russia", code: "+7" }, + { label: "Rwanda", value: "rwanda", code: "+250" }, + { label: "Samoa", value: "samoa", code: "+685" }, + { label: "San Marino", value: "san-marino", code: "+378" }, + { label: "Saudi Arabia", value: "saudi-arabia", code: "+966" }, + { label: "Senegal", value: "senegal", code: "+221" }, + { label: "Serbia", value: "serbia", code: "+381" }, + { label: "Seychelles", value: "seychelles", code: "+248" }, + { label: "Sierra Leone", value: "sierra-leone", code: "+232" }, + { label: "Singapore", value: "singapore", code: "+65" }, + { label: "Slovakia", value: "slovakia", code: "+421" }, + { label: "Slovenia", value: "slovenia", code: "+386" }, + { label: "Solomon Islands", value: "solomon-islands", code: "+677" }, + { label: "Somalia", value: "somalia", code: "+252" }, + { label: "South Africa", value: "south-africa", code: "+27" }, + { label: "South Sudan", value: "south-sudan", code: "+211" }, + { label: "Spain", value: "spain", code: "+34" }, + { label: "Sri Lanka", value: "sri-lanka", code: "+94" }, + { label: "Sudan", value: "sudan", code: "+249" }, + { label: "Suriname", value: "suriname", code: "+597" }, + { label: "Sweden", value: "sweden", code: "+46" }, + { label: "Switzerland", value: "switzerland", code: "+41" }, + { label: "Syria", value: "syria", code: "+963" }, + { label: "Taiwan", value: "taiwan", code: "+886" }, + { label: "Tajikistan", value: "tajikistan", code: "+992" }, + { label: "Tanzania", value: "tanzania", code: "+255" }, + { label: "Thailand", value: "thailand", code: "+66" }, + { label: "Timor‑Leste", value: "timor-leste", code: "+670" }, + { label: "Togo", value: "togo", code: "+228" }, + { label: "Tonga", value: "tonga", code: "+676" }, + { + label: "Trinidad and Tobago", + value: "trinidad-and-tobago", + code: "+1-868", + }, + { label: "Tunisia", value: "tunisia", code: "+216" }, + { label: "Turkey", value: "turkey", code: "+90" }, + { label: "Turkmenistan", value: "turkmenistan", code: "+993" }, + { label: "Tuvalu", value: "tuvalu", code: "+688" }, + { label: "Uganda", value: "uganda", code: "+256" }, + { label: "Ukraine", value: "ukraine", code: "+380" }, + { + label: "United Arab Emirates", + value: "united-arab-emirates", + code: "+971", + }, + { label: "United Kingdom", value: "united-kingdom", code: "+44" }, + { label: "United States", value: "united-states", code: "+1" }, + { label: "Uruguay", value: "uruguay", code: "+598" }, + { label: "Uzbekistan", value: "uzbekistan", code: "+998" }, + { label: "Vanuatu", value: "vanuatu", code: "+678" }, + { + label: "Vatican City", + value: "vatican-city", + code: "+379" /* برخی منابع مختلف است */, + }, + { label: "Venezuela", value: "venezuela", code: "+58" }, + { label: "Vietnam", value: "vietnam", code: "+84" }, + { label: "Yemen", value: "yemen", code: "+967" }, + { label: "Zambia", value: "zambia", code: "+260" }, + { label: "Zimbabwe", value: "zimbabwe", code: "+263" }, +]; diff --git a/src/common/lib/prisma.js b/src/common/lib/prisma.js new file mode 100644 index 0000000..14adcc7 --- /dev/null +++ b/src/common/lib/prisma.js @@ -0,0 +1,10 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.prisma = void 0; +require("dotenv/config"); +var adapter_pg_1 = require("@prisma/adapter-pg"); +var client_1 = require("../../generated/prisma/client"); +var connectionString = "".concat(process.env.DATABASE_URL); +var adapter = new adapter_pg_1.PrismaPg({ connectionString: connectionString }); +var prisma = new client_1.PrismaClient({ adapter: adapter }); +exports.prisma = prisma; diff --git a/src/common/lib/prisma.ts b/src/common/lib/prisma.ts new file mode 100644 index 0000000..d1a491c --- /dev/null +++ b/src/common/lib/prisma.ts @@ -0,0 +1,10 @@ +import "dotenv/config"; +import { PrismaPg } from '@prisma/adapter-pg' +import { PrismaClient } from "../../generated/prisma/client"; + +const connectionString = `${process.env.DATABASE_URL}` + +const adapter = new PrismaPg({ connectionString }) +const prisma = new PrismaClient({ adapter }) + +export { prisma } \ No newline at end of file diff --git a/src/common/types/index.ts b/src/common/types/index.ts new file mode 100644 index 0000000..486bd6c --- /dev/null +++ b/src/common/types/index.ts @@ -0,0 +1,32 @@ +import {StaffRoles} from "@/generated/prisma/enums"; +import {Response} from "express"; + +export interface ServerResponseObject { + status: number; + data: any; + message?: string; +} + +export interface ServerErrorsObject { + status: number; + data?: any; + error: { + message: string; + description?: string; + }; +} + +export type ServerResponse = Response< + ServerResponseObject | ServerErrorsObject +>; + +export const CaseStatusConst = { + NEW: "NEW", + CONTACTED: "CONTACTED", + DOCS_PENDING: "DOCS_PENDING", + REVIEWING: "REVIEWING", + PRE_APPROVED: "PRE_APPROVED", + REJECTED: "REJECTED", + CLOSED: "CLOSED", + CONVERTED_TO_HIS: "CONVERTED_TO_HIS", +} as const; diff --git a/src/common/utils/exports.ts b/src/common/utils/exports.ts new file mode 100644 index 0000000..af74313 --- /dev/null +++ b/src/common/utils/exports.ts @@ -0,0 +1,71 @@ +import Excel from "exceljs"; + +import axios from "axios"; +import FormData = require("form-data"); + +function cleanRecord(record: any) { + const newRecord: any = {}; + + for (const key in record) { + const value = record[key]; + + if (Array.isArray(value)) { + // اگر آرایه از آبجکت‌هاست، مقادیر مهم را ترکیب کن + newRecord[key] = value + .map((item) => + item.firstName && item.lastName && item.position + ? `${item.firstName} ${item.lastName} (${item.position})` + : JSON.stringify(item) + ) + .join(", "); + } else if (typeof value === "object" && value !== null) { + // اگر آبجکت است، مثلا translations + if (value.translations) { + newRecord[key] = value.translations + .map((t: any) => t.displayName) + .join(", "); + } else { + newRecord[key] = JSON.stringify(value); + } + } else { + newRecord[key] = value; + } + } + + return newRecord; +} +export async function exportToExcel( + records: Record[], + columns: { header: string; key: string; width?: number }[], + fileName: string = "export" +) { + if (!records.length) throw new Error("داده‌ای برای خروجی وجود ندارد"); + + const wb = new Excel.Workbook(); + const ws = wb.addWorksheet("Export"); + + ws.columns = columns; + + ws.addRows(records); // داده‌ها را اضافه می‌کنیم + + const excelBuffer = await wb.xlsx.writeBuffer(); + + // ارسال به CDN یا ذخیره محلی + const form = new FormData(); + form.append("exports", excelBuffer, { + filename: `${fileName}-${Date.now()}.xlsx`, + contentType: + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + + const cdnRes = await axios.post(process.env.CDN_UPLOAD_EXPORTS_URL!, form, { + headers: { + ...form.getHeaders(), + Authorization: `Bearer ${process.env.CDN_SERVICE_TOKEN}`, + }, + maxBodyLength: Infinity, + maxContentLength: Infinity, + }); + + return cdnRes.data?.url || null; +} diff --git a/src/common/utils/functions.ts b/src/common/utils/functions.ts new file mode 100644 index 0000000..db3a6db --- /dev/null +++ b/src/common/utils/functions.ts @@ -0,0 +1,290 @@ +import createHttpError from "http-errors"; +import {ServerResponse} from "../types"; +import {Prisma, PrismaClient} from "@/generated/prisma/client"; +import {prisma} from "../lib/prisma"; +import {createUserTranslationsType} from "@/modules/users/types"; +import {createSlug} from "./generate"; +const crypto = require("crypto"); + +const algorithm = "aes-256-cbc"; +const key = Buffer.from(process.env.ENCRYPTION_KEY!); +const nodemailer = require("nodemailer"); +interface SlugTranslation { + lang_id: number; + langSlug?: string; // مثلا fa, en (اختیاری ولی ترجیحی) + [key: string]: string | number | null | undefined; +} + +interface GenerateSlugOptions { + preferredLangSlug?: string; // پیش‌فرض: 'fa' + fieldsPriority: string[]; // مثلا ['firstName', 'lastName'] + fallback?: string; // اگر همه‌چی خالی بود +} + +export async function sendEmail({ + to, + subject, + text, +}: { + to: string; + subject: string; + text: string; +}) { + const transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT, + secure: true, + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASS, + }, + tls: { + rejectUnauthorized: false, // اگر SSL سرور قدیمی بود مشکل حل می‌کند + }, + }); + + await transporter.sendMail({ + from: `"${process.env.PROJECT_NAME}" `, + to, + subject, + text, + }); +} + +export async function id_validation(id: any) { + if (!id) { + throw new createHttpError.BadRequest("درخواست نامعتبر است"); + } + // else if (isNaN(id)) { + // throw new createHttpError.BadRequest("درخواست نامعتبر است"); + // } + + return true; +} +export function maskMobile(mobile: string) { + // اطمینان از اینکه شماره فقط ارقام هست + const digits = mobile.replace(/\D/g, ""); + + // بررسی فرمت شماره ایران (11 رقمی، شروع با 09) + if (!/^09\d{9}$/.test(digits)) { + throw new Error("شماره موبایل معتبر ایران نیست"); + } + + // ماسک‌کردن سه رقم وسط + return digits.replace(/^(\d{4})(\d{3})(\d{4})$/, "$1***$3"); +} + +export function addMinutes(date: Date, minutes: number) { + return new Date(date.getTime() + minutes * 60 * 1000); +} + +export function handlePrismaError(err: any) { + if (err instanceof Prisma.PrismaClientKnownRequestError) { + console.log(err) + switch (err.code) { + case "P2002": { + const target = err.meta?.target; + + const fields = Array.isArray(target) + ? target.join(", ") + : target ?? "field"; + throw new createHttpError.Conflict(`این دیتا قبلا ثبت شده است`); + } + + case "P2025": + throw new createHttpError.NotFound("پیدا نشد"); + + case "P2003": + throw new createHttpError.InternalServerError( + "امکان انجام این عملیات وجود ندارد" + ); + + default: + throw new createHttpError.InternalServerError("خطایی رخ داده است"); + } + } + + throw new createHttpError.InternalServerError("خطایی رخ داده است"); +} + +export function buildPaginationResult( + page: number, + limit: number, + total: number, + data: T[] +) { + return { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1, + data, + }; +} + +interface PaginationOptions { + include?: Record; + where?: Record; + orderBy?: Record; +} + +export async function buildPagination( + req: any, + res: ServerResponse, + table: keyof PrismaClient, + options: PaginationOptions = {} +) { + const page = Number(req?.query?.page ?? 1); + const limit = Number(req?.query?.limit ?? 10); + const skip = (page - 1) * limit; + + const model = prisma[table] as any; + + if (!model?.findMany) { + return res.status(400).json({ + status: 400, + error: {message: `Model '${table.toString()}' is not queryable.`}, + }); + } + + const {include, where, orderBy} = options; + + const [data, total] = await Promise.all([ + model.findMany({ + skip, + take: limit, + where, + include, + orderBy: orderBy ?? {createdAt: "desc"}, + }), + model.count({where}), + ]); + + return buildPaginationResult(page, limit, total, data); +} + +export function mapUserTranslationsToPrismaCreate( + translations: createUserTranslationsType[] +) { + return translations.map((tr) => ({ + lang: { + connect: { + id: tr.lang_id, + }, + }, + firstName: tr.firstName, + lastName: tr.lastName, + position: tr.position, + })); +} +export async function generateSlugFromTranslations( + translations: SlugTranslation[], + {preferredLangSlug = "fa", fieldsPriority, fallback = ""}: GenerateSlugOptions +) { + if (!translations?.length) return fallback; + + // 1. انتخاب ترجمه مرجع + let main = + translations.find((t) => t.langSlug === preferredLangSlug) ?? + translations.find((t) => t.langSlug === "en") ?? + translations[0]; + + // 2. استخراج فیلدهای معتبر + const parts = fieldsPriority + .map((field) => main[field]) + .filter((v): v is string => typeof v === "string" && v.trim().length > 0); + + if (!parts.length) return fallback; + + // 3. ساخت slug + return await createSlug(parts.join(" ")); +} + +export function mapDbTranslationsForSlug( + translations: Array<{ + lang_id: number | null; + lang?: {slug: string} | null; + [key: string]: any; + }> +) { + return translations.map((t) => ({ + ...t, + lang_id: t.lang_id!, + langSlug: t.lang?.slug, + })); +} + +export function encrypt(text: string) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, key, iv); + let encrypted = cipher.update(text, "utf8", "hex"); + encrypted += cipher.final("hex"); + return iv.toString("hex") + ":" + encrypted; +} + +export function decrypt(text: string) { + const [ivHex, encrypted] = text.split(":"); + const iv = Buffer.from(ivHex, "hex"); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + let decrypted = decipher.update(encrypted, "hex", "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; +} + + + +function luhnCheckDigit(input: string): number { + let sum = 0; + let shouldDouble = true; + + for (let i = input.length - 1; i >= 0; i--) { + let digit = parseInt(input[i], 10); + + if (shouldDouble) { + digit *= 2; + if (digit > 9) digit -= 9; + } + + sum += digit; + shouldDouble = !shouldDouble; + } + + return (10 - (sum % 10)) % 10; +} +export async function generatePatientPID(): Promise { + const now = new Date(); + + const year = String(now.getFullYear()).slice(2); // YY + const month = String(now.getMonth() + 1).padStart(2, "0"); // MM + + const prefix = `${year}${month}`; // YYMM + + const lastPatient = await prisma.patient.findFirst({ + where: { + pid: { + startsWith: prefix, + }, + }, + orderBy: { + pid: "desc", + }, + select: { + pid: true, + }, + }); + + let nextSeq = 1; + + if (lastPatient) { + nextSeq = parseInt(lastPatient.pid.slice(4, 9), 10) + 1; + } + + const sequence = String(nextSeq).padStart(5, "0"); + + const base = `${prefix}${sequence}`; // 9 رقم + const checkDigit = luhnCheckDigit(base); + + return `${base}${checkDigit}`; // 10 رقم +} diff --git a/src/common/utils/generate.ts b/src/common/utils/generate.ts new file mode 100644 index 0000000..c567b7e --- /dev/null +++ b/src/common/utils/generate.ts @@ -0,0 +1,178 @@ +import crypto from "crypto"; +import bcrypt from "bcrypt"; +import jwt, {Secret, SignOptions} from "jsonwebtoken"; +import dotenv from "dotenv"; +import {token_policy} from "../constants/policy"; + +import slugify from "slugify"; + +dotenv.config(); + +export const HashPassword = async (password: string): Promise => { + const saltRounds = 10; + try { + const hashedPassword = await bcrypt.hash(password, saltRounds); + return hashedPassword; + } catch (error) { + throw new Error("hashing Error"); + } +}; + +export async function comparePassword(password: string, hash: string) { + return await bcrypt.compare(password, hash); +} + +export async function generateSecureToken( + length: number = 32, + encoding: BufferEncoding = "hex" +): Promise { + return await crypto.randomBytes(length).toString(encoding); +} + +// token +export interface jwt_token_payload { + id: string; + role?: string; +} +const secretKey: Secret = + process.env.JWT_SECRET || crypto.randomBytes(64).toString("hex"); +const RefreshSecretKey: Secret = + process.env.JWT_SECRET || crypto.randomBytes(64).toString("hex"); + +export async function generateAccessToken( + payload: jwt_token_payload, + expiresIn: `${number}${ + | "s" + | "m" + | "h" + | "d" + | "y"}` = token_policy.a_token_expire +): Promise { + const options: SignOptions = {expiresIn}; + + return new Promise((resolve, reject) => { + jwt.sign(payload, secretKey, options, (err, token) => { + if (err || !token) { + return reject(err || new Error("Token generation failed")); + } + resolve(token); + }); + }); +} + +export async function generateRefreshToken( + payload: jwt_token_payload, + rememberMe: boolean +): Promise { + return new Promise((resolve, reject) => { + jwt.sign( + payload, + RefreshSecretKey, + {expiresIn: rememberMe ? "30d" : token_policy.r_token_expire}, + (err, token) => { + if (err || !token) { + return reject(err || new Error("Refresh Token generation failed")); + } + resolve(token); + } + ); + }); +} +export async function verifyAccessToken(token: string) { + return new Promise((resolve, reject) => { + jwt.verify(token, secretKey, (err, result) => { + if (err) { + return reject(new Error("verify access token failed")); + } + resolve(result); + }); + }); +} +export async function verifyRefreshToken(token: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, RefreshSecretKey, (err, result) => { + if (err) { + return reject(new Error("verify refresh token failed")); + } + resolve(result); + }); + }); +} + +export async function generateSlug(text: string): Promise { + return await text + .toLowerCase() + .trim() + .replace(/[\u064B-\u0652]/g, "") // حذف حرکات عربی + .replace(/[^\w\u0600-\u06FF\s-]/g, "") // حذف کاراکترهای غیرمجاز (غیر از حروف، اعداد، خط تیره و فاصله) + .replace(/[\s_-]+/g, "-") // تبدیل فاصله‌ها و خط زیر به خط تیره + .replace(/^-+|-+$/g, ""); // حذف خط‌تیره‌های اضافی اول و آخر +} + +export interface UploadTokenPayload { + userId: string; + type: "document" | "image"; + mime: string; + maxSize: number; + exp: number; + nonce: string; +} + +/** + * تولید توکن یک‌بار مصرف برای آپلود + */ +export function generateUploadToken( + payload: Omit +) { + const fullPayload: UploadTokenPayload = { + ...payload, + exp: Date.now() + 30_000, // 30 ثانیه اعتبار + nonce: crypto.randomBytes(16).toString("hex"), // یک‌بار مصرف + }; + + const payloadString = JSON.stringify(fullPayload); + + const signature = crypto + .createHmac("sha256", process.env.UPLOAD_SECRET!) + .update(payloadString) + .digest("hex"); + + return Buffer.from( + JSON.stringify({payload: fullPayload, signature}) + ).toString("base64"); +} + +/** + * اعتبارسنجی توکن یک‌بار مصرف + */ +export function verifyUploadToken(token: string): UploadTokenPayload { + const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf-8")); + const {payload, signature} = decoded; + + const expectedSig = crypto + .createHmac("sha256", process.env.UPLOAD_SECRET!) + .update(JSON.stringify(payload)) + .digest("hex"); + + if (expectedSig !== signature) throw new Error("Invalid token"); + if (payload.exp < Date.now()) throw new Error("Token expired"); + + return payload; +} +export function generateIPDReceptionCode() { + const randomNumber = Math.floor(100000 + Math.random() * 900000); // عدد 6 رقمی + return `ipd-r-${randomNumber}`; +} +export function generateIPDConsultantCode() { + const randomNumber = Math.floor(100000 + Math.random() * 900000); // عدد 6 رقمی + return `ipd-c-${randomNumber}`; +} + +export async function createSlug(input: string) { + // const latin = transliterate(input); // تبدیل از فارسی/عربی → لاتین + return await slugify(input, { + lower: true, + strict: true, + trim: true, + }); +} diff --git a/src/common/utils/logger.ts b/src/common/utils/logger.ts new file mode 100644 index 0000000..6bba537 --- /dev/null +++ b/src/common/utils/logger.ts @@ -0,0 +1,17 @@ +import {prisma} from "../lib/prisma"; + +export async function createAuditLog(data: any) { + return prisma.auditLog.create({data}); +} + +export async function createAccessLog(data: any) { + return prisma.accessLog.create({data}); +} + +export async function createDecisionLog(data: any) { + return prisma.decisionLog.create({data}); +} + +export async function createPolicyAcceptanceLog(data: any) { + return prisma.tosAcceptanceLog.create({data}); +} diff --git a/src/core/controller/main.controller.ts b/src/core/controller/main.controller.ts new file mode 100644 index 0000000..05b08f3 --- /dev/null +++ b/src/core/controller/main.controller.ts @@ -0,0 +1,97 @@ +import {prisma} from "@/common/lib/prisma"; +import {CaseStatus} from "@/generated/prisma/enums"; +import createHttpError from "http-errors"; + +const autoBind = require("auto-bind"); + +export class Controller { + constructor() { + autoBind(this); + } + async updateCaseStatus( + caseId: string, + newStatus: CaseStatus, + staffId?: string + ) { + // وضعیت فعلی را بگیر + const existing = await prisma.onlineCase.findUnique({ + where: {id: caseId}, + select: {status: true}, + }); + + if (!existing) { + throw new Error("Case Not Found"); + } + + // اگر تغییری نکرده بود، بی‌خود تاریخچه نساز + if (existing.status === newStatus) { + return; + } + + // تغییر وضعیت Case + await prisma.onlineCase.update({ + where: {id: caseId}, + data: {status: newStatus}, + }); + + // ساخت رکورد تاریخچه + await prisma.caseStatusHistory.create({ + data: { + caseId: caseId, + from: existing.status, + to: newStatus, + changedBy: staffId || null, + }, + }); + + return true; + } + async isPatientExist(id: string) { + const patient = await prisma.patient.findUnique({ + where: {id}, + include: { + cases: { + select: { + status: true, + }, + }, + }, + }); + + if (!patient) { + throw new createHttpError.NotFound("بیمار یافت نشد"); + } + + return patient; + } + async isOnlineCaseExist(id: string) { + const OnlineCase = await prisma.onlineCase.findUnique({ + where: {id}, + }); + + if (!OnlineCase) { + throw new createHttpError.NotFound("پرونده بیمار یافت نشد"); + } + + return OnlineCase; + } + + async isExistLanguage(id: number) { + const language = await prisma.language.findUnique({where: {id}}); + + if (!language) { + return false; + } + + return language; + } + async findLanguageBySlug(slug:string) { + const language = await prisma.language.findUnique({where: {slug}}); + + if (!language) { + return false; + } + + return language; + } +} diff --git a/src/core/getconfig.ts b/src/core/getconfig.ts new file mode 100644 index 0000000..6775db2 --- /dev/null +++ b/src/core/getconfig.ts @@ -0,0 +1,20 @@ +import {prisma} from "@/common/lib/prisma"; + +export async function getConfig(key: string) { + const item = await prisma.panelConfig.findUnique({where: {key}}); + + if (!item) return null; + + switch (item.type) { + case "BOOLEAN": + return item.value === "true"; + case "NUMBER": + return Number(item.value); + case "JSON": + return JSON.parse(item.value); + case "STRING_ARRAY": + return item.value; + default: + return item.value; + } +} diff --git a/src/core/middlewares/auth.middleware.ts b/src/core/middlewares/auth.middleware.ts new file mode 100644 index 0000000..f07326e --- /dev/null +++ b/src/core/middlewares/auth.middleware.ts @@ -0,0 +1,66 @@ +import {NextFunction} from "express"; + +import {HttpStatusCode} from "axios"; +import {ServerResponse} from "@/common/types"; +import {jwt_token_payload, verifyAccessToken} from "@/common/utils/generate"; +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; + +export const authMiddleware = async ( + req: any, + res: ServerResponse, + next: NextFunction +) => { + const token = req.signedCookies?.accessToken; + if (!token || typeof token !== "string") + return res.status(HttpStatusCode.Forbidden).json({ + status: HttpStatusCode.Forbidden, + error: { + message: "دسترسی شما غیرمجاز است", + description: "denied", + }, + }); + + try { + const decoded = (await verifyAccessToken(token)) as jwt_token_payload; + + try { + const user = await prisma.staff.findUnique({ + where: { + id: decoded.id, + }, + }); + req.user = user; + } catch (error) { + handlePrismaError(error); + } + next(); + } catch { + next(new Error("دسترسی شما منقضی شده و یا نامعتبر است")); + } +}; +export const optionalAuthMiddleware = async ( + req: any, + res: ServerResponse, + next: NextFunction +) => { + const token = req.signedCookies?.accessToken; + if (!token) { + return next(); + } + if (token && typeof token !== "string") + return res.status(HttpStatusCode.Unauthorized).json({ + status: HttpStatusCode.Unauthorized, + error: { + message: "دسترسی شما غیرمجاز است", + description: "unauthorized", + }, + }); + + try { + const decoded = await verifyAccessToken(token); + req.user = decoded; + return next(); + } catch {} + return next(); +}; diff --git a/src/core/middlewares/check-language.middleware.ts b/src/core/middlewares/check-language.middleware.ts new file mode 100644 index 0000000..3f6518c --- /dev/null +++ b/src/core/middlewares/check-language.middleware.ts @@ -0,0 +1,43 @@ +import {prisma} from "@/common/lib/prisma"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; + +export async function checkLanguageSlug( + req: any, + res: ServerResponse, + next: NextFunction +) { + try { + const langSlug = req.params?.lang; + + if (!langSlug) { + return res.status(400).json({ + status: 400, + error: { + message: "Language slug is required", + }, + }); + } + + const language = await prisma.language.findUnique({ + where: {slug: langSlug}, + select: {id: true}, + }); + + if (!language) { + return res.status(404).json({ + status: 404, + error: { + message: "Language not found", + }, + }); + } + + // اگر خواستی بعداً استفاده کنی + req.langId = language.id; + + next(); + } catch (error) { + next(error); + } +} diff --git a/src/core/middlewares/check-tos.middleware.ts b/src/core/middlewares/check-tos.middleware.ts new file mode 100644 index 0000000..52ff46e --- /dev/null +++ b/src/core/middlewares/check-tos.middleware.ts @@ -0,0 +1,48 @@ +import {ServerResponse} from "@/common/types"; +import {Staff} from "@/generated/prisma/client"; +import {NextFunction} from "express"; + +export async function tosGuard( + req: any, + res: ServerResponse, + next: NextFunction +) { + const user = req.user as Staff; + + if (!user) { + return res + .status(401) + .json({status: 401, error: {message: "User not authenticated"}}); + } + + // banned + if (user.status === "BANNED") { + return res.status(403).json({ + status: 403, + error: { + message: "Account banned", + description: "Violation of terms or strikes exceeded", + }, + }); + } + + // restricted (مثلاً وقتی strikes >= 3) + if (user.status === "RESTRICTED") { + return res.status(429).json({ + status: 429, + data: { + strikes: user.strikes, + trustScore: user.trustScore, + }, + error: {message: "Account temporarily restricted"}, + }); + } + + // warned (اختیاری می‌تونی alert بدی) + if (user.status === "WARNED") { + req.user.isWarned = true; // می‌تونی جلوی UI هشدار بدی + } + + // همه چی اوکی + next(); +} diff --git a/src/core/middlewares/config-check.middleware.ts b/src/core/middlewares/config-check.middleware.ts new file mode 100644 index 0000000..e3ab54d --- /dev/null +++ b/src/core/middlewares/config-check.middleware.ts @@ -0,0 +1,36 @@ +import {ServerResponse} from "@/common/types"; +import {getConfig} from "../getconfig"; +import {NextFunction} from "express"; + +export function configCheck(configKeys: string[]) { + return async function (req: any, res: ServerResponse, next: NextFunction) { + try { + for (const key of configKeys) { + const value = await getConfig(key); + + // اگر اصلاً کانفیگ وجود نداشت یا false بود + if (!value) { + return res.status(503).json({ + status: 503, + data: { + success: false, + config: key, + }, + message: `ماژول «${key}» غیرفعال است`, + }); + } + } + + // همه OK بودند → ادامه بده + next(); + } catch (error) { + return res.status(500).json({ + status: 500, + data: { + success: false, + }, + message: "خطا در بررسی کانفیگ‌ها", + }); + } + }; +} diff --git a/src/core/middlewares/logs.middleware.ts b/src/core/middlewares/logs.middleware.ts new file mode 100644 index 0000000..4e38cf2 --- /dev/null +++ b/src/core/middlewares/logs.middleware.ts @@ -0,0 +1,36 @@ +import { ServerResponse } from "@/common/types"; +import { createAccessLog } from "@/common/utils/logger"; +import { NextFunction } from "express"; +import { ApiRequestLog } from "mongodb/models"; + +export function apiLoggingMiddleware() { + return async (req: any & { user?: { id: number; role: string } }, res: ServerResponse, next: NextFunction) => { + const start = Date.now(); + + res.on("finish", async () => { + // Mongo async (حجم بالا، غیر حساس) + ApiRequestLog.create({ + method: req.method, + path: req.path, + status: res.statusCode, + durationMs: Date.now() - start, + userId: req.user?.id, + role: req.user?.role, + }).catch(console.error); + + // Prisma async (حساس، حقوقی) + if (req.user) { + createAccessLog({ + userId: req.user.id, + userRole: req.user.role, + resource: req.path, + resourceId: req.params.id ? Number(req.params.id) : null, + reason: "ACCESS_CHECK", + ip: req.ip, + }).catch(console.error); + } + }); + + next(); + }; +} \ No newline at end of file diff --git a/src/core/middlewares/role-authorize.middleware.ts b/src/core/middlewares/role-authorize.middleware.ts new file mode 100644 index 0000000..e394a1d --- /dev/null +++ b/src/core/middlewares/role-authorize.middleware.ts @@ -0,0 +1,76 @@ + + +// async function getPermissionsForRole(roleId: number): Promise> { +// const role = db.role.findUnique({ +// where: { id: roleId }, +// include: { +// policies: { include: { policy: true } }, +// policy_sets: { +// include: { +// set: { include: { policies: true } } +// } +// } +// } +// }); + +import { prisma } from "@/common/lib/prisma"; +import { ServerResponse } from "@/common/types"; +import { Staff } from "@/generated/prisma/client"; +import { StaffRoles } from "@/generated/prisma/enums"; +import { NextFunction } from "express"; +import createHttpError from "http-errors"; + +// if (!role) return new Set(); + +// const directPermissions = Object.values(role.policies).map(rp => rp.policy.action); +// const setPermissions = Object.values(role.policy_sets).flatMap(rps => Object.values(rps.set.policies).map((p:any) => p.action)); + +// return new Set([...directPermissions, ...setPermissions]); +// } + +// // میدل‌ویر چک مجوز خاص +// export function role_authorize(action: string) { +// return async function (req: any, res: Response, next: NextFunction) { +// try { +// // فرض: کاربر تو req.user ذخیره شده و roleId داره +// const user = req.user as { id: number; roleId: number } | undefined; + +// if (!user) { +// return res.status(401).json({ message: 'Unauthorized: User not logged in' }); +// } + +// const permissions = await getPermissionsForRole(user.roleId); + +// if (!permissions.has(action)) { +// return res.status(403).json({ message: 'Forbidden: Access denied' }); +// } + +// next(); +// } catch (error) { +// res.status(500).json({ message: 'Internal server error' }); +// } +// }; +// } + +export function role_authorize(...allowed_roles: StaffRoles[]) { + return async (req: any, res: ServerResponse, next: NextFunction) => { + const user_data = req.user as Staff; + + if (!user_data.id) { + throw new createHttpError.Unauthorized("حساب کاربری شما نامعتبر است"); + } + + + if (user_data?.role && !allowed_roles.includes(user_data?.role)) { + return res + .status(403) + .json({ + status: 403, + error: {message: "دسترسی شما به این بخش غیرمجاز است"}, + }); + } + + + next(); + }; +} diff --git a/src/core/middlewares/verify.middleware.ts b/src/core/middlewares/verify.middleware.ts new file mode 100644 index 0000000..f76d93f --- /dev/null +++ b/src/core/middlewares/verify.middleware.ts @@ -0,0 +1,20 @@ +import {ServerResponse} from "@/common/types"; +import {Staff} from "@/generated/prisma/client"; +import {NextFunction} from "express"; +import createHttpError from "http-errors"; + +export async function checkVerify( + req: any, + res: ServerResponse, + next: NextFunction +) { + const user = req.user as Staff; + + if (!user.is_verified) { + throw new createHttpError.Unauthorized( + "حساب کاربری شما تایید نشده است ، لطفا با مدیر سیستم تماس بگیرید" + ); + } + + next(); +} diff --git a/src/core/policies.ts b/src/core/policies.ts new file mode 100644 index 0000000..c964b1b --- /dev/null +++ b/src/core/policies.ts @@ -0,0 +1,6 @@ +export const policies = { + SPAM: {severity: 10, strike: 1}, + RATE_LIMIT: {severity: 5, strike: 1}, + CONTENT: {severity: 20, strike: 2}, + ABUSE: {severity: 50, strike: 3}, +}; diff --git a/src/core/router/main.router.ts b/src/core/router/main.router.ts new file mode 100644 index 0000000..27dad3f --- /dev/null +++ b/src/core/router/main.router.ts @@ -0,0 +1,41 @@ +import auth_router from "@/modules/auth/routes/index.router"; +import onlineCase_router from "@/modules/online-case/router"; +import patients_router from "@/modules/patient/router/index.router"; +import review_router from "@/modules/review/router"; +import staff_router from "@/modules/staff/router/index.router"; +import statistics_router from "@/modules/statistics/index.router"; +import upload_router from "@/modules/upload/routes/index.router"; +import users_router from "@/modules/users/router/index.router"; +import router from "express"; +import expertise_router from "@/modules/expertise/router/index.router"; +import language_router from "@/modules/language/router"; +import country_router from "@/modules/country/router"; +import default_info_router from "@/modules/default/router/index.router"; +import configs_router from "@/modules/configs/router/index.router"; +import tos_router from "@/modules/tos/router"; +import privacy_policy_router from "@/modules/privacy-policy/router"; +import medical_package_router from "@/modules/medical-packages/router/index.router"; +import transfer_team_router from "@/modules/transfer-team/router"; +import publicApisRouter from "@/modules/public-apis/index.router"; + +const mainRouter = router.Router(); +mainRouter.use("/auth", auth_router); + +mainRouter.use("/user", users_router); +mainRouter.use("/staff", staff_router); +mainRouter.use("/upload", upload_router); +mainRouter.use("/patient", patients_router); +mainRouter.use("/case", onlineCase_router); +mainRouter.use("/review", review_router); +mainRouter.use("/statistics", statistics_router); +mainRouter.use('/expertise', expertise_router) +mainRouter.use('/language',language_router) +mainRouter.use('/country',country_router) +mainRouter.use('/configs',configs_router) +mainRouter.use('/default-info',default_info_router) +mainRouter.use('/tos',tos_router) +mainRouter.use('/pp',privacy_policy_router) +mainRouter.use('/medical-packages',medical_package_router) +mainRouter.use('/transfer-team',transfer_team_router) +mainRouter.use('/public-apis',publicApisRouter) +export default mainRouter; diff --git a/src/core/secure-app.ts b/src/core/secure-app.ts new file mode 100644 index 0000000..93c2636 --- /dev/null +++ b/src/core/secure-app.ts @@ -0,0 +1,29 @@ +import helmet from "helmet"; +import rateLimit from "express-rate-limit"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import csurf from "csurf"; +import hpp from "hpp"; +import { cors_option, helmet_option, limiter_option } from "@/common/constants/config"; + +export const rateLimiter = rateLimit(limiter_option); + +export const corsOptions = cors(cors_option); + +export const securityHeaders = helmet(helmet_option); + +export const csrfProtection = [ + cookieParser(), + csurf({cookie: {httpOnly: true, secure: true, sameSite: "strict"}}), +]; + +export const sanitizeData = [ + hpp(), +]; + +export const secureApp = [ + corsOptions, + rateLimiter, + securityHeaders, + ...sanitizeData, +]; diff --git a/src/core/service/register-violation.service.ts b/src/core/service/register-violation.service.ts new file mode 100644 index 0000000..cbee54f --- /dev/null +++ b/src/core/service/register-violation.service.ts @@ -0,0 +1,47 @@ +import { prisma } from "@/common/lib/prisma"; +import { policies } from "./policies"; + + +export async function registerViolation( + userId: string, + type: keyof typeof policies, + reason: string +) { + const policy = policies[type]; + + const user = await prisma.staff.findUnique({where: {id: userId}}); + if (!user) throw new Error("User not found"); + + // ثبت Violation + await prisma.violation.create({ + data: { + userId, + type, + severity: policy.severity, + reason, + }, + }); + + // به‌روزرسانی Strikes و TrustScore + const newStrikes = user.strikes + policy.strike; + const newTrustScore = user.trustScore - policy.severity; + + let newStatus: typeof user.status = user.status; + + if (newTrustScore <= 0 || newStrikes >= 5) { + newStatus = "BANNED"; + } else if (newStrikes >= 3) { + newStatus = "RESTRICTED"; + } else if (newStrikes >= 1) { + newStatus = "WARNED"; + } + + await prisma.staff.update({ + where: {id: userId}, + data: { + strikes: newStrikes, + trustScore: newTrustScore, + status: newStatus, + }, + }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..46eabef --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +import ServerApplication from "./app/server"; + +new ServerApplication() diff --git a/src/modules/auth/controller/AuthController.ts b/src/modules/auth/controller/AuthController.ts new file mode 100644 index 0000000..600eda4 --- /dev/null +++ b/src/modules/auth/controller/AuthController.ts @@ -0,0 +1,150 @@ +import {Controller} from "@/core/controller/main.controller"; +import {NextFunction, Request} from "express"; +import AuthService from "../service/AuthService"; +import { + ResetPasswordRequestValidationSchema, + SigninFormDataValidationSchema, +} from "../validation"; +import {AUTH_MESSAGES} from "../messages"; +import {ServerResponse} from "@/common/types"; +import {getConfig} from "@/core/getconfig"; +import {token_policy} from "@/common/constants/policy"; + +class AuthControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = AuthService; + } + async login(req: Request, res: ServerResponse, next: NextFunction) { + try { + await SigninFormDataValidationSchema.validateAsync(req.body ?? {}, { + abortEarly: true, + stripUnknown: true, + }); + + const {username, password, rememberMe} = req.body; + const {accessToken, refreshToken} = await this.#service.login( + username, + password, + rememberMe ?? false + ); + + if (rememberMe) { + return res + .cookie("accessToken", accessToken, token_policy.access_token_options) + .cookie( + "refreshToken", + refreshToken, + token_policy.refresh_token_options + ) + .json({ + status: 200, + data: {}, + message: AUTH_MESSAGES.success[200], + }); + } else { + return res + .cookie("accessToken", accessToken, token_policy.access_token_options) + + .json({ + status: 200, + data: {}, + message: AUTH_MESSAGES.success[200], + }); + } + } catch (error) { + console.log(error); + next(error); + } + } + + async forgotPassword(req: any, res: ServerResponse, next: NextFunction) { + const emailEnabled = await getConfig("email.enabled"); + + if (!emailEnabled) { + return res.status(500).json({ + status: 500, + error: {message: "امکان ارسال ایمیل غیرفعال است"}, + }); + } + try { + await ResetPasswordRequestValidationSchema.validateAsync(req.body ?? {}, { + abortEarly: true, + stripUnknown: true, + }); + const {username, email} = req.body; + + await this.#service.forgot_password(username, email); + + return res.status(200).json({ + status: 200, + data: {}, + message: "", + }); + } catch (error) { + next(error); + } + } + async resetPassword(req: any, res: ServerResponse, next: NextFunction) { + try { + const {token} = req.params; + const {newPassword} = req.body; + await this.#service.resetPasswordWithToken(token, newPassword); + res.json({ + status: 200, + data: {}, + message: "رمز عبور ویرایش شد", + }); + } catch (err) { + next(err); + } + } + async logout(req: any, res: ServerResponse, next: NextFunction) { + try { + res.clearCookie("accessToken").clearCookie("refreshToken").json({ + status: 200, + data: {}, + message: "خروج از حساب موفقیت آمیز بود.", + }); + } catch (error) { + next(error); + } + } + async refreshToken(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.refreshToken(req, res); + + } catch (err) { + next(err); + } + } + async getMe(req: any, res: ServerResponse, next: NextFunction) { + try { + const user = req?.user; + + const data = await this.#service.getMe(user); + + // if (!data) { + // return res.status(400).json({ + // status: 400, + // error: { + // message: "کاربر یافت نشد", + // description: "unauthorized", + // }, + // }); + // } + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } +} + +const AuthController = new AuthControllerClass(); + +export default AuthController; diff --git a/src/modules/auth/messages/index.ts b/src/modules/auth/messages/index.ts new file mode 100644 index 0000000..7c54052 --- /dev/null +++ b/src/modules/auth/messages/index.ts @@ -0,0 +1,11 @@ +export const AUTH_MESSAGES = Object.freeze({ + errors:{ + 401:"حساب کاربری نامعتبر است، لطفا وارد اکانت خود شوید", + 400:"دیتای فرم ارسالی نامعتبر است", + 404:"نام کاربری یا رمز عبور اشتباه است", + password_failed:"رمز عبور اشتباه است", + }, + success:{ + 200:"با موفقیت وارد شدید" + } +}) \ No newline at end of file diff --git a/src/modules/auth/routes/index.router.ts b/src/modules/auth/routes/index.router.ts new file mode 100644 index 0000000..3603728 --- /dev/null +++ b/src/modules/auth/routes/index.router.ts @@ -0,0 +1,13 @@ +import { authMiddleware, optionalAuthMiddleware } from "@/core/middlewares/auth.middleware"; +import AuthController from "../controller/AuthController"; +import express from 'express' +const auth_router = express.Router(); + + +auth_router.post("/login", AuthController.login); +auth_router.post("/logout", authMiddleware,AuthController.logout); +auth_router.post("/forgot-password", optionalAuthMiddleware,AuthController.forgotPassword); +auth_router.post("/reset-password/:token", AuthController.resetPassword); +auth_router.get('/get/me',authMiddleware,AuthController.getMe) +auth_router.post('/refresh-token',authMiddleware,AuthController.refreshToken) +export default auth_router; diff --git a/src/modules/auth/service/AuthService.ts b/src/modules/auth/service/AuthService.ts new file mode 100644 index 0000000..e40b05e --- /dev/null +++ b/src/modules/auth/service/AuthService.ts @@ -0,0 +1,225 @@ +import {prisma} from "@/common/lib/prisma"; +import { + comparePassword, + generateAccessToken, + generateRefreshToken, + HashPassword, + verifyRefreshToken, +} from "@/common/utils/generate"; +import {Controller} from "@/core/controller/main.controller"; +import createHttpError from "http-errors"; +import {AUTH_MESSAGES} from "../messages"; +import crypto from "crypto"; +import {handlePrismaError, sendEmail} from "@/common/utils/functions"; +import {ServerResponse} from "@/common/types"; +import {token_policy} from "@/common/constants/policy"; +import {userRequested} from "@/modules/users/types"; +class AuthServiceClass extends Controller { + constructor() { + super(); + } + + async login(username: string, password: string, rememberMe: boolean) { + const user = await prisma.staff.findUnique({ + where: {username}, + select: {id: true, is_verified: true, password: true, role: true}, + }); + + if (!user?.id) { + throw new createHttpError.NotFound("نام کاربری یا رمز عبور اشتباه است"); + } + + if (!user.is_verified) { + throw new createHttpError.Unauthorized("حساب کاربری شما مسدود است"); + } + + const password_match = await comparePassword(password, user.password!); + + if (!password_match) { + throw new createHttpError.BadRequest(AUTH_MESSAGES.errors[404]); + } + + const accessToken = await generateAccessToken({ + id: user.id, + role: user.role, + }); + if (rememberMe) { + const refreshToken = await generateRefreshToken( + {id: user.id, role: user.role}, + rememberMe + ); + + try { + await prisma.refreshToken.create({ + data: { + expiresAt: new Date(Date.now()), + staffId: user.id, + token: refreshToken, + }, + }); + } catch (error) { + handlePrismaError(error); + } + + return {accessToken, refreshToken}; + } + return {accessToken}; + } + async forgot_password(username: string, email: string) { + await this.check_user_exist_username(username); + + await this.sendResetLink(email); + } + async sendResetLink(email: string) { + const user = await prisma.staff.findFirst({where: {email}}); + if (!user) throw new Error("User not found"); + if (!user.is_verified) { + throw new createHttpError.Unauthorized("حساب کاربری شما مسدود است"); + } + + const token = crypto.randomBytes(32).toString("hex"); + + await prisma.staff.update({ + where: { + username: user.username, + id: user.id, + }, + data: { + resetPasswordExpires: new Date(Date.now() + 1000 * 60 * 10), + resetPasswordToken: token, + }, + }); + + const resetURL = `${process.env.BASE_DOMAIN}/reset-password/${token}`; + + await sendEmail({ + to: user.email ?? "", + subject: "Reset Password", + text: `Click to reset your password: ${resetURL}`, + }); + + return true; + } + async refreshToken(req: any, res: ServerResponse) { + const refreshToken = req.signedCookies?.refreshToken; + + if (!refreshToken) { + return res.status(404).json({ + status: 404, + error: { + message: "توکنی وجود ندارد", + description: "unauthorized", + }, + }); + } + + const storedToken = await prisma.refreshToken.findUnique({ + where: {token: refreshToken}, + include: {staff: true}, + }); + + if (!storedToken) { + return res.status(404).json({ + status: 404, + error: { + message: "توکن یافت نشد", + description: "unauthorized", + }, + }); + } + + // expire + if (storedToken.expiresAt < new Date()) { + await prisma.refreshToken.delete({ + where: {token: refreshToken}, + }); + return res.status(404).json({ + status: 404, + error: { + message: "توکن نامعتبر است", + description: "unauthorized", + }, + }); + } + + const decoded = await verifyRefreshToken(refreshToken); + const accessToken = await generateAccessToken(decoded.user); + + res + .cookie("accessToken", accessToken, token_policy.access_token_options) + + .status(200) + .json({status: 200, data: {}, message: "Ok"}); + } + async resetPasswordWithToken(token: string, newPassword: string) { + const user = await prisma.staff.findFirst({ + where: { + resetPasswordToken: token, + resetPasswordExpires: {gt: Date.now().toString()}, + }, + }); + + if (!user) + throw new Error( + "مهلت تغییر رمز عبور به پایان رسیده است ، مجددا اقدام کنید" + ); + if (!user.is_verified) { + throw new createHttpError.Unauthorized("حساب کاربری شما مسدود است"); + } + + const newPasswordHash = await HashPassword(newPassword); + await prisma.staff.update({ + where: { + username: user.username, + id: user.id, + }, + data: { + password: newPasswordHash, + resetPasswordExpires: "", + resetPasswordToken: "", + }, + }); + + return true; + } + async check_user_exist_username(username: string) { + const is_exist = await prisma.staff.findUnique({ + where: {username}, + }); + + if (!is_exist) { + return false; + } + + return true; + } + async getMe(user: userRequested) { + try { + const data = await prisma.staff.findUnique({ + where: { + id: user.id, + }, + select: { + id: true, + role: true, + email: true, + username: true, + send_notif_with_email: true, + translations: { + select: { + displayName: true, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } +} + +const AuthService = new AuthServiceClass(); + +export default AuthService; diff --git a/src/modules/auth/validation/index.ts b/src/modules/auth/validation/index.ts new file mode 100644 index 0000000..a07c9e2 --- /dev/null +++ b/src/modules/auth/validation/index.ts @@ -0,0 +1,33 @@ +import {password_regex} from "@/common/constants/regex"; +import Joi from "joi"; + +export const SigninFormDataValidationSchema = Joi.object({ + username: Joi.string().alphanum().required().messages({ + "string.base": "نام کاربری نامعتبر است", + "string.empty": "نام کاربری نمیتواند خالی باشد", + "any.required": "نام کاربری الزامیست", + "alphunum": "نام کاربری مطابق استاندارد نیست", + }), + password: Joi.string().required().messages({ + "string.base": "رمز عبور الزامیست", + "string.empty": "رمز عبور الزامیست", + "any.required": "رمز عبور الزامیست", + }), + rememberMe: Joi.boolean().optional().messages({ + "boolean.base": "مرا بخاطر بسپار نامعتبر است", + }), +}); + +export const ResetPasswordRequestValidationSchema = Joi.object({ + username: Joi.string().alphanum().required().messages({ + "string.base": "نام کاربری نامعتبر است", + "string.empty": "نام کاربری نمیتواند خالی باشد", + "any.required": "نام کاربری الزامیست", + alphunum: "نام کاربری مطابق استاندارد نیست", + }), + email: Joi.string().email().required().messages({ + "string.base":"فرمت ایمیل نامعتبر است", + "string.email":"ایمیل صحیح نیست", + "any.required":"ایمیل وارد نشده است" + }), +}); diff --git a/src/modules/configs/controller/configs.controller.ts b/src/modules/configs/controller/configs.controller.ts new file mode 100644 index 0000000..a09dfb7 --- /dev/null +++ b/src/modules/configs/controller/configs.controller.ts @@ -0,0 +1,54 @@ +import {Controller} from "@/core/controller/main.controller"; +import ConfigsService from "../service/configs.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; + +class ConfigsControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = ConfigsService; + } + async init(req:any,res:ServerResponse,next:NextFunction){ + try { + await this.#service.init(); + return res.status(200).json({ + status:200, + data:{}, + message:"Ok" + }) + } catch (error) { + next(error) + } + } + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAll(); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.update(req.body); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } +} + +const ConfigsController = new ConfigsControllerClass(); + +export default ConfigsController; diff --git a/src/modules/configs/router/index.router.ts b/src/modules/configs/router/index.router.ts new file mode 100644 index 0000000..c10ac45 --- /dev/null +++ b/src/modules/configs/router/index.router.ts @@ -0,0 +1,9 @@ +import express from "express"; +import ConfigsController from "../controller/configs.controller"; + +const configs_router = express.Router(); + +configs_router.get("/get/all", ConfigsController.getAll); +configs_router.put("/update", ConfigsController.update); +configs_router.get('/init',ConfigsController.init) +export default configs_router; diff --git a/src/modules/configs/service/configs.service.ts b/src/modules/configs/service/configs.service.ts new file mode 100644 index 0000000..4979258 --- /dev/null +++ b/src/modules/configs/service/configs.service.ts @@ -0,0 +1,88 @@ +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import {Controller} from "@/core/controller/main.controller"; +export interface ConfigFormItem { + key: string; + value: string; + description?: string; +} +class ConfigsServiceClass extends Controller { + async init() { + try { + await prisma.panelConfig.createMany({ + data: [ + { + key: "email.enabled", + value: "true", + type: "BOOLEAN", + description: "Enable/disable email sending module", + }, + { + key: "sms.enabled", + value: "false", + type: "BOOLEAN", + description: "Enable/disable SMS sending module", + }, + { + key: "upload.document.maxSize", + value: "5242880", + type: "NUMBER", + description: "Max upload Documents size in bytes (5MB)", + }, + + { + key: "upload.document.formats", + value: + "[application/pdf,application/zip,application/x-rar-compressed,application/dicom,text/plain]", + type: "STRING_ARRAY", + description: "Max upload size in bytes (5MB)", + }, + { + key: "upload.profile.maxSize", + value: "5242880", + type: "NUMBER", + description: "Max upload profile picture size in bytes (5MB)", + }, + { + key: "upload.profile.formats", + value: "[png,jpg,webp]", + type: "STRING_ARRAY", + description: "Max upload profile picture size in bytes (5MB)", + }, + ], + }); + } catch (error) { + handlePrismaError(error); + } + } + async update(data: ConfigFormItem[]) { + // console.log(data); + try { + await prisma.$transaction( + data.map((item) => + prisma.panelConfig.update({ + where: {key: item.key}, + data: { + value: item.value, + description: item.description, + }, + }) + ) + ); + } catch (error) { + handlePrismaError(error); + } + } + async getAll() { + try { + const data = await prisma.panelConfig.findMany(); + return data; + } catch (error) { + handlePrismaError(error); + } + } +} + +const ConfigsService = new ConfigsServiceClass(); + +export default ConfigsService; diff --git a/src/modules/country/countroller/country.controller.ts b/src/modules/country/countroller/country.controller.ts new file mode 100644 index 0000000..2bdb01b --- /dev/null +++ b/src/modules/country/countroller/country.controller.ts @@ -0,0 +1,62 @@ +import {Controller} from "@/core/controller/main.controller"; +import CountryService from "../service/country.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import {CreateCountryValidationSchema} from "../validation"; + +class CountryControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = CountryService; + } + async init(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.init(); + return res.status(200).json({ + status: 200, + data: {}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateCountryValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + await this.#service.create(req.body); + } catch (error) { + next(error); + } + } + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.getAllCountries(req, res); + } catch (error) { + next(error); + } + } + async getAllWithoutPagination( + req: any, + res: ServerResponse, + next: NextFunction + ) { + try { + const data = await this.#service.getAllCountriesWithoutPagination(); + return res.status(200).json({ + status:200, + data, + message:"Ok" + }) + } catch (error) { + next(error); + } + } +} + +const CountryController = new CountryControllerClass(); +export default CountryController; diff --git a/src/modules/country/router/index.ts b/src/modules/country/router/index.ts new file mode 100644 index 0000000..b00378e --- /dev/null +++ b/src/modules/country/router/index.ts @@ -0,0 +1,28 @@ +import {authMiddleware} from "@/core/middlewares/auth.middleware"; +import {role_authorize} from "@/core/middlewares/role-authorize.middleware"; +import express from "express"; +import CountryController from "../countroller/country.controller"; + +const country_router = express.Router(); + +country_router.post( + "/create", + authMiddleware, + role_authorize("developer", "admin"), + CountryController.create +); +country_router.get( + "/get/items", + authMiddleware, + role_authorize("developer", "admin"), + CountryController.getAllWithoutPagination +); +country_router.get( + "/get/all", + authMiddleware, + role_authorize("developer", "admin"), + CountryController.getAll +); +country_router.get("/init", CountryController.init); + +export default country_router; diff --git a/src/modules/country/service/country.service.ts b/src/modules/country/service/country.service.ts new file mode 100644 index 0000000..6be334c --- /dev/null +++ b/src/modules/country/service/country.service.ts @@ -0,0 +1,49 @@ +import {Controller} from "@/core/controller/main.controller"; +import {CreateCountryDataTypes} from "../types"; +import {buildPagination, handlePrismaError} from "@/common/utils/functions"; +import {prisma} from "@/common/lib/prisma"; +import {ServerResponse} from "@/common/types"; +import { allCountries } from "@/common/constants/variables"; + +class CountryServiceClass extends Controller { + async init() { + try { + await prisma.countries.createMany({ + data: allCountries.map((country) => ({ + name: country.label, + callCode: country.code, + })), + }); + } catch (error) { + handlePrismaError(error); + } + } + async create(data: CreateCountryDataTypes) { + const {name, callCode} = data; + try { + await prisma.countries.create({ + data: { + name, + callCode, + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async getAllCountriesWithoutPagination() { + try { + const data = await prisma.countries.findMany(); + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getAllCountries(req: any, res: ServerResponse) { + await buildPagination(req, res, "countries"); + } +} + +const CountryService = new CountryServiceClass(); + +export default CountryService; diff --git a/src/modules/country/types/index.ts b/src/modules/country/types/index.ts new file mode 100644 index 0000000..49de8f9 --- /dev/null +++ b/src/modules/country/types/index.ts @@ -0,0 +1,4 @@ +export interface CreateCountryDataTypes{ + name:string, + callCode:string +} \ No newline at end of file diff --git a/src/modules/country/validation/index.ts b/src/modules/country/validation/index.ts new file mode 100644 index 0000000..6912cfc --- /dev/null +++ b/src/modules/country/validation/index.ts @@ -0,0 +1,16 @@ +import Joi from "joi"; + +export const CreateCountryValidationSchema = Joi.object({ + name: Joi.string().required().messages({ + "string.base": "فرمت نام کشور اشتباه است", + "string.empty": "نام کشور نمیتواند خالی باشد", + "any.required": "نام کشور وارد نشده است", + }), + callCode: Joi.string() + .required() + .messages({ + "string.base": "فرمت کد کشور اشتباه است", + "string.empty": "کد کشور نمیتواند خالی باشد", + "any.required": "کد کشور وارد نشده است", + }), +}); diff --git a/src/modules/default/controller/default-info.controller.ts b/src/modules/default/controller/default-info.controller.ts new file mode 100644 index 0000000..1c35535 --- /dev/null +++ b/src/modules/default/controller/default-info.controller.ts @@ -0,0 +1,61 @@ +import {Controller} from "@/core/controller/main.controller"; +import DefaultInfoService from "../service/default-info.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import UpdateDefaultInfoValidationSchema from "../validation"; + +class DefaultInfoControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = DefaultInfoService; + } + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAll(); + + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + await UpdateDefaultInfoValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + + await this.#service.update("SITE_DEFAULTS", req.body); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ثبت شد", + }); + } catch (error) { + next(error); + } + } + async initDefaultInfo(req:any,res:ServerResponse,next:NextFunction){ + try { + await this.#service.initDefaultInfo(); + + return res.status(200).json({ + status:200, + data:{}, + message:"ویرایش شد" + }) + } catch (error) { + next(error) + } + } +} + +const DefaultInfoController = new DefaultInfoControllerClass(); + +export default DefaultInfoController; diff --git a/src/modules/default/router/index.router.ts b/src/modules/default/router/index.router.ts new file mode 100644 index 0000000..10010fc --- /dev/null +++ b/src/modules/default/router/index.router.ts @@ -0,0 +1,24 @@ +import {authMiddleware} from "@/core/middlewares/auth.middleware"; +import {role_authorize} from "@/core/middlewares/role-authorize.middleware"; +import express from "express"; +import DefaultInfoController from "../controller/default-info.controller"; + +const default_info_router = express.Router(); + +default_info_router.get( + "/get/all", + authMiddleware, + role_authorize("admin", "developer"), + DefaultInfoController.getAll +); +default_info_router.put( + "/update", + authMiddleware, + role_authorize("admin", "developer"), + DefaultInfoController.update +); + +default_info_router.get('/init',DefaultInfoController.initDefaultInfo) + + +export default default_info_router; diff --git a/src/modules/default/service/default-info.service.ts b/src/modules/default/service/default-info.service.ts new file mode 100644 index 0000000..a03600b --- /dev/null +++ b/src/modules/default/service/default-info.service.ts @@ -0,0 +1,125 @@ +import { prisma } from "@/common/lib/prisma"; +import { handlePrismaError } from "@/common/utils/functions"; +import { Controller } from "@/core/controller/main.controller"; +export interface DefaultTranslationInput { + languageId: number; + address: string; + underLogoText: string; + aboutUsText: string; + patientsRights: string; +} +export interface DefaultFormValues { + email: string; + instagramLink: string; + linkedinLink: string; + hospitalPhone: string; + mapAddress: string; + logo?: string | null; + translations: DefaultTranslationInput[]; // به جای فیلد تکی +} + +class DefaultInfoServiceClass extends Controller { + async getAll() { + try { + const data = await prisma.default.findUnique({ + where: { id: "SITE_DEFAULTS" }, + include: { + translations: { + select: { + address: true, + aboutUsText: true, + underLogoText: true, + languageId: true, + language: { + select: { + title: true, + slug: true, + }, + }, + }, + }, + }, + }); + + // console.log(data) + return data; + } catch (error) { + handlePrismaError(error); + } + } + + async update(id: "SITE_DEFAULTS", data: DefaultFormValues) { + try { + await prisma.default.upsert({ + where: { id }, + update: { + email: data.email, + hospitalPhone: data.hospitalPhone, + instagramLink: data.instagramLink, + linkedinLink: data.linkedinLink, + mapAddress: data.mapAddress, + logoUrl: data.logo, + }, + create: { + id, + email: data.email, + hospitalPhone: data.hospitalPhone, + instagramLink: data.instagramLink, + linkedinLink: data.linkedinLink, + mapAddress: data.mapAddress, + logoUrl: data.logo, + }, + }); + + // بروزرسانی یا ایجاد ترجمه‌ها + for (const translation of data.translations) { + await prisma.defaultTranslation.upsert({ + where: { + defaultId_languageId: { + defaultId: id, + languageId: translation.languageId, + }, + }, + update: { + address: translation.address, + underLogoText: translation.underLogoText, + aboutUsText: translation.aboutUsText, + patientsRights: translation.patientsRights, + }, + create: { + defaultId: id, + languageId: translation.languageId, + address: translation.address, + underLogoText: translation.underLogoText, + aboutUsText: translation.aboutUsText, + patientsRights: translation.patientsRights, + }, + }); + } + + return true; + } catch (error) { + handlePrismaError(error); + } + } + async initDefaultInfo() { + try { + await prisma.default.create({ + data: { + email: "ipd@shomal.hospital", + hospitalPhone: "011-4492", + instagramLink: "https://instagram.com/shomalhospital", + mapAddress: "asd", + linkedinLink: + "https://www.linkedin.com/in/shomal-amol-hospital-7a1699392/", + logoUrl: "", + }, + }); + } catch (error) { + handlePrismaError(error); + } + } +} + +const DefaultInfoService = new DefaultInfoServiceClass(); +export default DefaultInfoService; diff --git a/src/modules/default/validation/index.ts b/src/modules/default/validation/index.ts new file mode 100644 index 0000000..eab6827 --- /dev/null +++ b/src/modules/default/validation/index.ts @@ -0,0 +1,62 @@ +const Joi = require("joi"); + +export const translationSchema = Joi.object({ + address: Joi.string().required().messages({ + "string.base": "آدرس باید رشته باشد", + "any.required": "آدرس الزامیست", + }), + + underLogoText: Joi.string().required().messages({ + "string.base": "متن زیر لوگو باید رشته باشد", + "any.required": "متن زیر لوگو الزامیست", + }), +}); + +/* ---------- Main Schema ---------- */ + +export const UpdateDefaultInfoValidationSchema = Joi.object({ + email: Joi.string() + .email({tlds: {allow: false}}) + .required() + .messages({ + "string.email": "فرمت ایمیل صحیح نیست", + "any.required": "ایمیل الزامی است", + }), + + hospitalPhone: Joi.string() + .pattern(/^[0-9+\-() ]+$/) + .min(8) + .required() + .messages({ + "string.pattern.base": "شماره تلفن فقط می‌تواند شامل عدد و + - ( ) باشد", + "string.min": "شماره تلفن کوتاه است", + "any.required": "شماره تلفن الزامی است", + }), + + mapAddress: Joi.string().min(5).required().messages({ + "string.min": "آدرس خیلی کوتاه است", + "any.required": "آدرس الزامی است", + }), + + instagramLink: Joi.string().uri().allow(null, "").messages({ + "string.uri": "لینک اینستاگرام معتبر نیست", + }), + + linkedinLink: Joi.string().uri().allow(null, "").messages({ + "string.uri": "لینک لینکدین معتبر نیست", + }), + + logoUrl: Joi.string().uri().allow(null, "").messages({ + "string.uri": "لینک لوگو معتبر نیست", + }), + + translations: Joi.array() + .items(translationSchema) + .min(1) + .required() + .messages({ + "array.min": "حداقل یک ترجمه الزامی است", + "any.required": "وارد کردن ترجمه ها الزامیست", + }), +}); +export default UpdateDefaultInfoValidationSchema; diff --git a/src/modules/expertise/controller/expertise.controller.ts b/src/modules/expertise/controller/expertise.controller.ts new file mode 100644 index 0000000..0cd7c30 --- /dev/null +++ b/src/modules/expertise/controller/expertise.controller.ts @@ -0,0 +1,100 @@ +import {Controller} from "@/core/controller/main.controller"; +import ExpertiseService from "../service/expertise.service"; +import {createExpertiseValidationSchema} from "../validation"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; + +class ExpertiseControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = ExpertiseService; + } + async getExpertiseWithoutPagination(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getExpertiseWithoutPagination(); + return res.status(200).json({ + status: 200, + data, + message: "ok", + }); + } catch (error) { + next(error); + } + } + async getExpertise(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getExpertises(req); + return res.status(200).json({ + status: 200, + data, + message: "ok", + }); + } catch (error) { + next(error); + } + } + async getExpertiseById(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getExpertiseById(req); + + return res.status(200).json({ + status: 200, + data: {...data}, + message: "ok", + }); + } catch (error) { + next(error); + } + } + async createExpertise(req: any, res: ServerResponse, next: NextFunction) { + try { + await createExpertiseValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + + await this.#service.createExpertise(req.body || {}); + return res.status(200).json({ + status: 200, + data: {}, + message: "انجام شد", + }); + } catch (error) { + next(error); + } + } + async updateExpertise(req: any, res: ServerResponse, next: NextFunction) { + try { + await createExpertiseValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + await this.#service.updateExpertise(req); + + return res.status(200).json({ + status: 200, + data: {}, + message: "ویرایش شد", + }); + } catch (error) { + next(error); + } + } + async deleteExpertise(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id; + await this.#service.deleteExpertise(id); + return res.status(200).json({ + status: 200, + data: {}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const ExpertiseController = new ExpertiseControllerClass(); +export default ExpertiseController; diff --git a/src/modules/expertise/router/index.router.ts b/src/modules/expertise/router/index.router.ts new file mode 100644 index 0000000..759d53f --- /dev/null +++ b/src/modules/expertise/router/index.router.ts @@ -0,0 +1,13 @@ +import express from "express"; +import ExpertiseController from "../controller/expertise.controller"; +import { checkLanguageSlug } from "@/core/middlewares/check-language.middleware"; + +const expertise_router = express.Router(); +expertise_router.get("/:lang/get/all",checkLanguageSlug, ExpertiseController.getExpertise); +expertise_router.get("/:lang/get/all/list",checkLanguageSlug, ExpertiseController.getExpertiseWithoutPagination); +expertise_router.get("/get/single/:id", ExpertiseController.getExpertiseById); +expertise_router.post("/create", ExpertiseController.createExpertise); +expertise_router.put("/update/:id", ExpertiseController.updateExpertise); +expertise_router.delete("/delete/:id", ExpertiseController.deleteExpertise); + +export default expertise_router; diff --git a/src/modules/expertise/service/expertise.service.ts b/src/modules/expertise/service/expertise.service.ts new file mode 100644 index 0000000..f96382a --- /dev/null +++ b/src/modules/expertise/service/expertise.service.ts @@ -0,0 +1,217 @@ +import {Controller} from "@/core/controller/main.controller"; +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import createHttpError from "http-errors"; + +interface ExpertiseTranslation { + displayName: string; + lang_id: string; +} +interface createExpertiseDataBody { + slug: string; + translations: ExpertiseTranslation[]; +} + +class ExpertiseServiceClass extends Controller { + async createExpertise(data: createExpertiseDataBody) { + const {translations, slug} = data; + + try { + await prisma.expertise.create({ + data: { + slug, + translations: { + create: translations, + }, + }, + include: {translations: true}, + }); + + return true; + } catch (err) { + handlePrismaError(err); + } + } + async getExpertiseWithoutPagination() { + // const where: any = { + + // ...(id && {pid: id}), + // }; + try { + const data = await prisma.expertise.findMany({ + include: { + translations: { + select: { + displayName: true, + lang_id: true, + lang: true, + expertise: true, + id: true, + }, + }, + }, + }); + + return data; + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async getExpertises(req: any) { + const langId = req?.langId; + const page = req?.query?.page; + const limit = req?.query?.limit; + const slug = req?.query?.slug; + const search = req?.query?.search; + const skip = (page - 1) * limit ; + const id = req?.query?.id; + // const where: any = { + + // ...(id && {pid: id}), + // }; + try { + const [data, total] = await Promise.all([ + prisma.expertise.findMany({ + skip, + take: +limit, + orderBy: {id: "desc"}, + where: { + ...(id && {id}), + ...(slug && {slug}), + translations: { + some: { + lang_id: langId, + ...(search && { + displayName: { + contains: search, + mode: "insensitive", + }, + }), + }, + }, + }, + select: { + id: true, + slug: true, + translations: { + where: { + lang_id: langId, + }, + select: { + displayName: true, + lang: { + select: { + title: true, + }, + }, + }, + }, + }, + }), + await prisma.expertise.count({ + where: { + ...(id && {id}), + ...(slug && {slug}), + translations: { + some: { + ...(langId && {lang_id: langId}), + ...(search && { + displayName: { + contains: search, + mode: "insensitive", + }, + }), + }, + }, + }, + }), + ]); + + return { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1, + data, + }; + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async getExpertiseById(req: any) { + const id = req.params?.id; + const lang = req.query?.lang; + + try { + const expertise = await prisma.expertise.findUnique({ + where: {id: Number(id)}, + include: { + translations: lang ? {where: {lang}} : true, + }, + }); + + if (!expertise) { + throw new createHttpError.NotFound("پیدا نشد"); + } + + return expertise; + } catch (err) { + handlePrismaError(err); + } + } + async updateExpertise(req: any) { + const id = req.params?.id; + const slug = req.body?.slug; + const translations = req.body?.translations; + + try { + const expertise = await prisma.expertise.update({ + where: {id: Number(id)}, + data: { + slug, + translations: { + upsert: translations.map((t: ExpertiseTranslation) => ({ + where: { + expertiseId_lang: { + expertiseId: Number(id), + lang: t.lang_id, + }, + }, + update: { + displayName: t.displayName, + }, + create: { + lang: t.lang_id, + displayName: t.displayName, + }, + })), + }, + }, + include: {translations: true}, + }); + + return true; + } catch (err) { + console.log(err); + handlePrismaError(err); + } + } + async deleteExpertise(id: number) { + try { + await prisma.expertise.delete({ + where: {id: Number(id)}, + }); + return true; + } catch (error) { + handlePrismaError(error); + } + } +} + +const ExpertiseService = new ExpertiseServiceClass(); + +export default ExpertiseService; diff --git a/src/modules/expertise/validation/index.ts b/src/modules/expertise/validation/index.ts new file mode 100644 index 0000000..db79955 --- /dev/null +++ b/src/modules/expertise/validation/index.ts @@ -0,0 +1,30 @@ +import Joi from "joi"; + +const translationSchema = Joi.object({ + displayName: Joi.string().required().messages({ + "string.base": "display name must be a string", + "string.length": "display name code must be 2 characters", + "any.required": "display name is required", + }), + lang_id: Joi.number().required().messages({ + + "any.required": "Language is required", + }), +}); +export const createExpertiseValidationSchema = Joi.object({ + slug: Joi.string().required().messages({ + "string.base": "فرمت اسلاگ اشتباه است", + "string.empty": "اسلاگ نمیتواند خالی باشد", + "any.required": "اسلاگ وارد نشده است", + }), + translations: Joi.array() + .items(translationSchema) + .min(1) + .required() + .messages({ + "array.base": "Translations must be an array", + "array.min": "At least one translation is required", + "any.required": "Translations are required", + }), +}); + diff --git a/src/modules/language/controller/language.controller.ts b/src/modules/language/controller/language.controller.ts new file mode 100644 index 0000000..794ef68 --- /dev/null +++ b/src/modules/language/controller/language.controller.ts @@ -0,0 +1,80 @@ +import {Controller} from "@/core/controller/main.controller"; +import LanguageService from "../service/language.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import {CreateLanguageValidationSchema} from "../validation"; + +class LanguageControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = LanguageService; + } + async getAllLanguage(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAllLanguage(req); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async createLanguage(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateLanguageValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + + await this.#service.createLanguage(req.body || {}); + + return res.status(200).json({ + status: 200, + data: {}, + message: "ایجاد شد", + }); + } catch (error) { + next(error); + } + } + async updateLanguage(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id; + await CreateLanguageValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + + await this.#service.updateLanguage(req.body, +id); + return res.status(200).json({ + status: 200, + data: {}, + message: "ویرایش شد", + }); + } catch (error) { + next(error); + } + } + async deleteLanguage(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id; + await this.#service.delete(id); + + return res.status(200).json({ + status: 200, + data: {}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const LanguageController = new LanguageControllerClass(); + +export default LanguageController; diff --git a/src/modules/language/router/index.ts b/src/modules/language/router/index.ts new file mode 100644 index 0000000..43f1e62 --- /dev/null +++ b/src/modules/language/router/index.ts @@ -0,0 +1,29 @@ +import { role_authorize } from "@/core/middlewares/role-authorize.middleware"; +import { authMiddleware } from "./../../../core/middlewares/auth.middleware"; +import express from "express"; +import LanguageController from "../controller/language.controller"; + +const language_router = express.Router(); + +language_router.post( + "/create", + authMiddleware, + role_authorize("developer"), + LanguageController.createLanguage, +); +language_router.put( + "/update/:id", + authMiddleware, + role_authorize("developer"), + LanguageController.updateLanguage, +); +language_router.delete( + "/delete/:id", + authMiddleware, + role_authorize("developer"), + LanguageController.deleteLanguage, +); + +language_router.get("/get/all", LanguageController.getAllLanguage); + +export default language_router; diff --git a/src/modules/language/service/language.service.ts b/src/modules/language/service/language.service.ts new file mode 100644 index 0000000..2e73383 --- /dev/null +++ b/src/modules/language/service/language.service.ts @@ -0,0 +1,90 @@ +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import {createSlug} from "@/common/utils/generate"; +import {Controller} from "@/core/controller/main.controller"; +import { Language } from "@/generated/prisma/client"; +import createHttpError from "http-errors"; + +class LanguageServiceClass extends Controller { + async getAllLanguage(req: any) { + const search = req?.query?.search; + try { + const data = await prisma.language.findMany({ + where: { + title: {contains: search, mode: "insensitive"}, + }, + }); + return data; + } catch (error) { + handlePrismaError(error); + } + } + async createLanguage(data: Language) { + let existing_slug; + + try { + existing_slug = await prisma.language.findUnique({ + where: { + title: data.title, + slug: data.slug, + }, + }); + } catch (error) { + handlePrismaError(error); + } + + if (existing_slug) { + throw new createHttpError.Conflict("دیتای تکراری"); + } + + try { + await prisma.language.create({ + data: { + title: data.title, + slug: data.slug, + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async updateLanguage(data: Language, id: number) { + const language = await this.isExistLanguage(id); + + if (!language) { + throw new createHttpError.NotFound("شناسه یافت نشد"); + } + + const slug = await createSlug(data.title); + + if (language.slug === slug) { + throw new createHttpError.Conflict("دیتای تکراری"); + } + + try { + await prisma.language.update({ + where: { + id, + }, + data: { + ...(data.title && {title: data.title}), + ...(slug && {slug}), + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async delete(id: string) { + try { + await prisma.language.delete({where: {id: +id}}); + return true; + } catch (error) { + handlePrismaError(error); + } + } +} + +const LanguageService = new LanguageServiceClass(); + +export default LanguageService; diff --git a/src/modules/language/validation/index.ts b/src/modules/language/validation/index.ts new file mode 100644 index 0000000..bf83235 --- /dev/null +++ b/src/modules/language/validation/index.ts @@ -0,0 +1,14 @@ +import Joi from "joi"; + +export const CreateLanguageValidationSchema = Joi.object({ + title: Joi.string().required().messages({ + "string.base": "فرمت عنوان اشتباه است", + "string.empty": "عنوان نمیتواند خالی باشد", + "any.required": "عنوان وارد نشده است", + }), + slug: Joi.string().required().messages({ + "string.base": "فرمت اسلاگ اشتباه است", + "string.empty": "اسلاگ نمیتواند خالی باشد", + "any.required": "اسلاگ وارد نشده است", + }), +}); diff --git a/src/modules/logs/controller/logs.controller.ts b/src/modules/logs/controller/logs.controller.ts new file mode 100644 index 0000000..5d3d547 --- /dev/null +++ b/src/modules/logs/controller/logs.controller.ts @@ -0,0 +1,15 @@ +import { Controller } from "@/core/controller/main.controller"; +import LogsService from "../service/logs.service"; + + +class LogsControllerClass extends Controller{ + #service; + constructor(){ + super(); + this.#service = LogsService + } +} + +const LogsController = new LogsControllerClass(); + +export default LogsController; \ No newline at end of file diff --git a/src/modules/logs/service/logs.service.ts b/src/modules/logs/service/logs.service.ts new file mode 100644 index 0000000..adcec90 --- /dev/null +++ b/src/modules/logs/service/logs.service.ts @@ -0,0 +1,10 @@ +import { Controller } from "@/core/controller/main.controller"; + + +class LogsServiceClass extends Controller { + +} + +const LogsService = new LogsServiceClass(); + +export default LogsService \ No newline at end of file diff --git a/src/modules/medical-packages/controller/medical-package.controller.ts b/src/modules/medical-packages/controller/medical-package.controller.ts new file mode 100644 index 0000000..26a7465 --- /dev/null +++ b/src/modules/medical-packages/controller/medical-package.controller.ts @@ -0,0 +1,89 @@ +import {Controller} from "@/core/controller/main.controller"; +import MedicalPackageService from "../service/medical-package.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import CreateMedicalPackageValidationSchema from "../validation"; + +class MedicalPackageControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = MedicalPackageService; + } + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateMedicalPackageValidationSchema.validateAsync(req.body || {}); + + await this.#service.create(req.body); + return res.status(200).json({ + status: 200, + data: {}, + message: "انجام شد", + }); + } catch (error) { + next(error); + } + } + async getAllWithPagination( + req: any, + res: ServerResponse, + next: NextFunction + ) { + try { + const data = await this.#service.getAllWithPagination(req, res); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAllParentsWithoutPagination( + req: any, + res: ServerResponse, + next: NextFunction + ) { + try { + const data = await this.#service.getAllParentsWithoutPagination(); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getSingleMedicalPackage(req:any,res:ServerResponse,next:NextFunction){ + try { + const id = req?.params?.id; + const data = await this.#service.getSingleMedicalPackage(req,id) + return res.status(200).json({ + status:200, + data, + message:"Ok" + }) + } catch (error) { + next(error) + } + } + async delete(req:any,res:ServerResponse,next:NextFunction){ + try { + const id = req?.params?.id; + await this.#service.delete(id) + return res.status(200).json({ + status:200, + data:{}, + message:"حذف شد" + }) + } catch (error) { + next(error) + } + } +} + +const MedicalPackageController = new MedicalPackageControllerClass(); + +export default MedicalPackageController; diff --git a/src/modules/medical-packages/router/index.router.ts b/src/modules/medical-packages/router/index.router.ts new file mode 100644 index 0000000..83758a4 --- /dev/null +++ b/src/modules/medical-packages/router/index.router.ts @@ -0,0 +1,20 @@ +import express from "express"; +import MedicalPackageController from "../controller/medical-package.controller"; +import {checkLanguageSlug} from "@/core/middlewares/check-language.middleware"; + +const medical_package_router = express.Router(); + +medical_package_router.post("/create", MedicalPackageController.create); +medical_package_router.get( + "/:lang/get/all", + checkLanguageSlug, + MedicalPackageController.getAllWithPagination +); +medical_package_router.get( + "/:lang/get/all/parent", + checkLanguageSlug, + MedicalPackageController.getAllParentsWithoutPagination +); +medical_package_router.get('/:lang/get/single/:id',checkLanguageSlug,MedicalPackageController.getSingleMedicalPackage) +medical_package_router.delete('/delete/:id',MedicalPackageController.delete) +export default medical_package_router; diff --git a/src/modules/medical-packages/service/medical-package.service.ts b/src/modules/medical-packages/service/medical-package.service.ts new file mode 100644 index 0000000..434849c --- /dev/null +++ b/src/modules/medical-packages/service/medical-package.service.ts @@ -0,0 +1,182 @@ +import {prisma} from "@/common/lib/prisma"; +import {ServerResponse} from "@/common/types"; +import {handlePrismaError} from "@/common/utils/functions"; +import {Controller} from "@/core/controller/main.controller"; + +class MedicalPackageServiceClass extends Controller { + async create(data: any) { + const {translations, id, icon, parent_id, priority, thumbnail_id,price} = data; + try { + await prisma.medicalPackage.create({ + data: { + icon, + translations: { + create: translations, + }, + priority, + price, + parent_id: parent_id ? +parent_id : null, + ...(thumbnail_id && {thumbnail_id: +thumbnail_id}), + }, + + include: { + translations: true, + }, + }); + + return true; + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async getAllWithPagination(req: any, res: ServerResponse) { + const langId = req?.langId; + const page = req?.query?.page; + const limit = req?.query?.limit; + const skip = (page - 1) * limit; + const search = req?.query?.search; + try { + const [data, total] = await Promise.all([ + prisma.medicalPackage.findMany({ + skip, + take: +limit, + where: { + translations: { + some: { + lang_id: langId, + ...(search && { + displayName: { + contains: search, + mode: "insensitive", + }, + }), + }, + }, + }, + + select: { + id: true, + icon: true, + priority: true, + thumbnail_id: true, + parent_id: true, + price:true, + thumbnail: { + select: { + filename: true, + fileUrl:true, + fileKey:true, + }, + }, + children: true, + translations: { + where: { + lang_id: langId, + }, + select: { + lang_id: true, + content: true, + title: true, + }, + }, + }, + }), + await prisma.medicalPackage.count({ + where: { + translations: { + some: { + lang_id: langId, + ...(search && { + displayName: { + contains: search, + mode: "insensitive", + }, + }), + }, + }, + }, + }), + ]); + + return { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1, + data, + }; + } catch (error) { + handlePrismaError(error); + } + } + async getAllParentsWithoutPagination() { + try { + const data = await prisma.medicalPackage.findMany({ + where: { + parent_id: null, + }, + include: { + translations: true, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getSingleMedicalPackage(req: any, id: string) { + const langId = req?.langId; + + try { + const data = await prisma.medicalPackage.findUnique({ + where: { + id: +id, + translations: { + some: { + lang_id: +langId, + }, + }, + }, + include: { + translations: { + select: { + lang_id: true, + title: true, + content: true, + }, + }, + thumbnail:{ + select:{ + fileKey:true, + filename:true, + fileUrl:true + } + } + }, + }); + return data; + } catch (error) { + handlePrismaError(error); + } + } + async delete(id: string) { + try { + await prisma.medicalPackage.delete({ + where: { + id: +id, + }, + }); + + return true; + } catch (error) { + handlePrismaError(error); + } + } +} + +const MedicalPackageService = new MedicalPackageServiceClass(); +export default MedicalPackageService; diff --git a/src/modules/medical-packages/validation/index.ts b/src/modules/medical-packages/validation/index.ts new file mode 100644 index 0000000..00e8d10 --- /dev/null +++ b/src/modules/medical-packages/validation/index.ts @@ -0,0 +1,49 @@ +import Joi from "joi"; + +const translationSchema = Joi.object({ + title: Joi.string().required().messages({ + "string.base": "عنوان می بایست رشته باشد", + "string.empty": "عنوان نمیتواند خالی باشد", + "any.required": "عنوان الزامیست", + }), + content: Joi.string().required().messages({ + "string.base": "محتوا می بایست رشته باشد", + "string.empty": "محتوا نمیتواند خالی باشد", + "any.required": "محتوا الزامیست", + }), + lang_id: Joi.number().required().messages({ + "any.required": "Language is required", + }), +}); + +const CreateMedicalPackageValidationSchema = Joi.object({ + icon: Joi.string().optional().allow("").messages({ + "string.base": "آیکون می بایست رشته باشد", + "string.empty": "آیکون نمیتواند خالی باشد", + }), + priority: Joi.number().required().messages({ + "number.base": "اولویت نمایش می بایست عدد باشد", + "any.required": "اولویت نمایش الزامیست", + }), + parent_id: Joi.number().optional().allow(null).messages({ + "number.base": "شناسه والد می بایست عدد باشد", + }), + price: Joi.string().required().allow("").messages({ + "any.required": "قيمت الزاميست", + }), + thumbnail_id: Joi.number().optional().allow(null).messages({ + "number.base": "شناسه عکس شاخص می بایست عدد باشد", + "number.empty": "شناسه عکس شاخص نمیتواند خالی باشد", + "any.required": "عکس شاخص انتخاب نشده است", + }), + translations: Joi.array() + .items(translationSchema) + .min(1) + .required() + .messages({ + "array.base": "Translations must be an array", + "array.min": "At least one translation is required", + "any.required": "Translations are required", + }), +}); +export default CreateMedicalPackageValidationSchema; diff --git a/src/modules/online-case/controller/online-case.controller.ts b/src/modules/online-case/controller/online-case.controller.ts new file mode 100644 index 0000000..c108f4c --- /dev/null +++ b/src/modules/online-case/controller/online-case.controller.ts @@ -0,0 +1,101 @@ +import {Controller} from "@/core/controller/main.controller"; +import OnlineCaseService from "../service/online-case.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import { + createOnlineCaseValidationSchema, + createOnlineCaseWithFormValidationSchema, +} from "../validation"; + +class OnlineCaseControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = OnlineCaseService; + } + async getAllOnlineCases(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAllOnlineCases(req,res); + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getSingleOnlineCase(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getSingleOnlineCase(req); + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async createOnlineCaseWithForm( + req: any, + res: ServerResponse, + next: NextFunction + ) { + try { + const userId = req.params?.id + await createOnlineCaseWithFormValidationSchema.validateAsync( + req.body || {}, + { + abortEarly: true, + stripUnknown: true, + } + ); + await this.#service.createOnlineCaseWithForm(req.body || {},userId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ایجاد شد", + }); + } catch (error) { + next(error); + } + } + async createOnlineCase(req: any, res: ServerResponse, next: NextFunction) { + try { + const patientId = req?.params?.id; + await createOnlineCaseValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + await this.#service.createOnlineCase(req.body, patientId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ایجاد شد", + }); + } catch (error) { + next(error); + } + } + async updateOnlineCase(req: any, res: ServerResponse, next: NextFunction) { + try { + const caseId = req?.params?.id; + await createOnlineCaseValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + await this.#service.updateOnlineCase(req.body, caseId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ویرایش شد", + }); + } catch (error) { + next(error); + } + } +} + +const OnlineCaseController = new OnlineCaseControllerClass(); +export default OnlineCaseController; diff --git a/src/modules/online-case/router/index.ts b/src/modules/online-case/router/index.ts new file mode 100644 index 0000000..67b6bda --- /dev/null +++ b/src/modules/online-case/router/index.ts @@ -0,0 +1,18 @@ +import express from "express"; +import OnlineCaseController from "../controller/online-case.controller"; + +const onlineCase_router = express.Router(); + +onlineCase_router.get("/get/all", OnlineCaseController.getAllOnlineCases); +onlineCase_router.post("/create", OnlineCaseController.createOnlineCase); +onlineCase_router.post( + "/create/form", + OnlineCaseController.createOnlineCaseWithForm +); + +onlineCase_router.get( + "/get/single/:id", + OnlineCaseController.getSingleOnlineCase +); +onlineCase_router.put("/update/:id", OnlineCaseController.updateOnlineCase); +export default onlineCase_router; diff --git a/src/modules/online-case/service/online-case.service.ts b/src/modules/online-case/service/online-case.service.ts new file mode 100644 index 0000000..22f4634 --- /dev/null +++ b/src/modules/online-case/service/online-case.service.ts @@ -0,0 +1,162 @@ +import {Controller} from "@/core/controller/main.controller"; +import {OnlineCaseDataBody, UpdateOnlineCaseDataBody} from "../types"; +import {prisma} from "@/common/lib/prisma"; +import { + buildPagination, + generatePatientPID, + handlePrismaError, +} from "@/common/utils/functions"; +import {generateIPDReceptionCode} from "@/common/utils/generate"; +import {createPatientDataBody} from "@/modules/patient/types"; +import {ServerResponse} from "@/common/types"; + +class OnlineCaseServiceClass extends Controller { + constructor() { + super(); + } + async createOnlineCase(data: OnlineCaseDataBody, id: string) { + await this.isPatientExist(id); + + const trackingCode = await generateIPDReceptionCode(); + + let newOnlineCase; + try { + newOnlineCase = await prisma.onlineCase.create({ + data: { + trackingCode, + message: data.message, + patientId: id, + specialty: data.specialty, + formData: JSON.stringify(data.formData), + status: "NEW", + }, + }); + } catch (error) { + handlePrismaError(error); + } + + await this.updateCaseStatus(newOnlineCase?.id!, "NEW"); + } + async updateOnlineCase(data: UpdateOnlineCaseDataBody, caseId: string) { + const patient = await this.isPatientExist(data.patientId); + + const onlineCase = await this.isOnlineCaseExist(caseId); + + try { + await prisma.onlineCase.update({ + where: { + id: patient?.id, + }, + data: { + ...(data?.message && {message: data.message}), + ...(data.patientId && {patientId: data.patientId}), + ...(data?.specialty && {specialty: data.specialty}), + ...(data?.formData && {formData: JSON.stringify(data.formData)}), + }, + }); + } catch (error) { + handlePrismaError(error); + } + + await this.updateCaseStatus(onlineCase.id, data.status); + } + async contactedOnlineCase(caseId: string) { + await this.updateCaseStatus(caseId, "CONTACTED"); + } + async createOnlineCaseWithForm( + data: createPatientDataBody & OnlineCaseDataBody, + userId: string + ) { + let patient; + let newOnlineCase; + try { + patient = await prisma.patient.create({ + data: { + firstName: data.firstName, + lastName: data.lastName, + age: data?.age, + nationalityCode: data.nationalityCode, + address: data.address, + birthDate: data.birthDate, + nationalityId: +data.nationality!, + pid: await generatePatientPID(), + phone: data.phone, + email: data.email, + preferredLanguage: data.preferredLanguage, + }, + select: { + id: true, + }, + }); + } catch (error) { + handlePrismaError(error); + } + + const trackingCode = await generateIPDReceptionCode(); + + try { + newOnlineCase = await prisma.onlineCase.create({ + data: { + trackingCode, + message: data.message, + patientId: patient?.id!, + specialty: data.specialty, + formData: JSON.stringify(data.formData), + status: "NEW", + }, + }); + } catch (error) { + handlePrismaError(error); + } + // create new CaseStatusHistory + if (newOnlineCase?.id) { + await this.updateCaseStatus(newOnlineCase.id, "NEW"); + } + } + async getAllOnlineCases(req: any, res: ServerResponse) { + return buildPagination(req, res, "onlineCase", { + include: { + reviews: { + select: { + translations: { + select: { + lang: true, + note: true, + result: true, + id: true, + }, + }, + }, + }, + }, + }); + } + async getSingleOnlineCase(id: string) { + try { + const data = await prisma.onlineCase.findUnique({ + where: {id}, + include: { + reviews: { + select: { + caseId: true, + translations: { + select: { + lang: true, + note: true, + result: true, + }, + }, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } +} + +const OnlineCaseService = new OnlineCaseServiceClass(); +export default OnlineCaseService; diff --git a/src/modules/online-case/types/index.ts b/src/modules/online-case/types/index.ts new file mode 100644 index 0000000..2f42772 --- /dev/null +++ b/src/modules/online-case/types/index.ts @@ -0,0 +1,14 @@ +import {CaseStatus} from "@/generated/prisma/enums"; + +export interface OnlineCaseDataBody { + message?: string; + specialty?: string; + formData?: string; +} +export interface UpdateOnlineCaseDataBody { + message?: string; + specialty?: string; + formData?: string; + status: CaseStatus; + patientId: string; +} diff --git a/src/modules/online-case/validation/index.ts b/src/modules/online-case/validation/index.ts new file mode 100644 index 0000000..2f0a08b --- /dev/null +++ b/src/modules/online-case/validation/index.ts @@ -0,0 +1,83 @@ +import {CaseStatusConst} from "@/common/types"; +import Joi from "joi"; + +export const createOnlineCaseWithFormValidationSchema = Joi.object({ + fullName: Joi.string().required().messages({ + "any.required": "نام بیمار الزامیست", + "string.base": "نام بیمار نامعتبر است", + "string.empty": "نام بیمار نمی تواند خالی باشد", + }), + lastName: Joi.string().required().messages({ + "any.required": "نام بیمار الزامیست", + "string.base": "نام بیمار نامعتبر است", + "string.empty": "نام بیمار نمی تواند خالی باشد", + }), + nationality: Joi.string().optional(), + countryCode: Joi.string().optional(), + phone: Joi.string().optional(), + email: Joi.string().optional(), + preferredLanguage: Joi.string().optional().messages({ + "string.base": "زبان ترجیحی معتبر نیست", + }), + + age: Joi.number().integer().min(0).max(150).optional().messages({ + "number.base": "سن باید عدد باشد", + "number.integer": "سن باید عدد صحیح باشد", + "number.min": "سن نمی‌تواند کمتر از ۰ باشد", + "number.max": "سن نمی‌تواند بیشتر از 150 باشد", + }), + message: Joi.string().optional().allow("").messages({ + "string.base": "پیغام باید رشته باشد", + }), + + specialty: Joi.string().optional().allow("").messages({ + "string.base": "تخصص باید رشته باشد", + }), + + formData: Joi.object().optional().messages({ + "object.base": "فرم داده‌ها باید یک شیء JSON باشد", + }), + + status: Joi.string() + // .valid(...Object(CaseStatusConst).values()) + .optional() + .messages({ + "any.only": `وضعیت باید یکی از مقادیر معتبر باشد: ${Object.values( + CaseStatusConst + ).join(", ")}`, + "string.base": "وضعیت باید رشته باشد", + }), +}) + .or("phone", "email") + .messages({ + "object.missing": "حداقل یکی از فیلدهای شماره تماس یا ایمیل باید وارد شود.", + }); + +export const createOnlineCaseValidationSchema = Joi.object({ + + message: Joi.string().optional().allow("").messages({ + "string.base": "پیغام باید رشته باشد", + }), + + specialty: Joi.string().optional().allow("").messages({ + "string.base": "تخصص باید رشته باشد", + }), + + formData: Joi.object().optional().messages({ + "object.base": "فرم داده‌ها باید یک شیء JSON باشد", + }), + + status: Joi.string() + // .valid(...Object(CaseStatusConst).values()) + .optional() + .messages({ + "any.only": `وضعیت باید یکی از مقادیر معتبر باشد: ${Object.values( + CaseStatusConst + ).join(", ")}`, + "string.base": "وضعیت باید رشته باشد", + }), +}) + .or("phone", "email") + .messages({ + "object.missing": "حداقل یکی از فیلدهای شماره تماس یا ایمیل باید وارد شود.", + }); diff --git a/src/modules/patient/controllers/patient.controller.ts b/src/modules/patient/controllers/patient.controller.ts new file mode 100644 index 0000000..a7c570c --- /dev/null +++ b/src/modules/patient/controllers/patient.controller.ts @@ -0,0 +1,129 @@ +import {Controller} from "@/core/controller/main.controller"; +import PatientService from "../services/patient.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import {CreatePatientValidationSchema} from "../validation"; + +class PatientControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = PatientService; + } + async getAllPatientsWithoutPagination(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAllPatientWithoutPagination(req); + + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAllPatients(req: any, res: ServerResponse, next: NextFunction) { + try { + const user =req?.user + const data = await this.#service.getAllPatients(req,res,user); + + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getSinglePatient(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetId = req?.params?.id; + const data = await this.#service.getSinglePatient(targetId); + + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async createPatient(req: any, res: ServerResponse, next: NextFunction) { + try { + + await CreatePatientValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + + await this.#service.createPatient(req.body); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ایجاد شد", + }); + } catch (error) { + next(error); + } + } + async updatePatient(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetId = req?.params?.id; + await CreatePatientValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + + await this.#service.updatePatient(req.body, targetId); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ویرایش شد", + }); + } catch (error) { + next(error); + } + } + async deletePatient(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetId = req?.params?.id; + console.log(targetId) + await this.#service.deletePatient(targetId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت حذف گردید", + }); + } catch (error) { + next(error); + } + } + async restorePatient(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetId = req?.params?.id; + await this.#service.restorePatient(targetId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت بازگردانی شد", + }); + } catch (error) { + next(error); + } + } + // async exportPatientsExcel(req: any, res: ServerResponse, next: NextFunction) { + // try { + // await exportExcel(res, "patient"); + // } catch (error) { + // next(error); + // } + // } +} + +const PatientController = new PatientControllerClass(); +export default PatientController; diff --git a/src/modules/patient/router/index.router.ts b/src/modules/patient/router/index.router.ts new file mode 100644 index 0000000..4ea8dcd --- /dev/null +++ b/src/modules/patient/router/index.router.ts @@ -0,0 +1,39 @@ +import {role_authorize} from "@/core/middlewares/role-authorize.middleware"; +import express from "express"; +import PatientController from "../controllers/patient.controller"; +import {authMiddleware} from "@/core/middlewares/auth.middleware"; +const patients_router = express.Router(); + +patients_router.get( + "/get/all", + authMiddleware, + role_authorize("developer", "admin", "coordinator", "doctor"), + PatientController.getAllPatients +); +patients_router.get("/get/single/:id", PatientController.getSinglePatient); +patients_router.put( + "/update/:id", + authMiddleware, + role_authorize("admin", "developer", "coordinator"), + PatientController.updatePatient +); +patients_router.put( + "/restore/:id", + authMiddleware, + role_authorize("admin", "developer", "coordinator"), + PatientController.restorePatient +); +patients_router.delete( + "/delete/:id", + authMiddleware, + role_authorize("admin", "developer"), + PatientController.deletePatient +); +patients_router.post( + "/create", + authMiddleware, + role_authorize("admin", "developer", "coordinator"), + PatientController.createPatient +); + +export default patients_router; diff --git a/src/modules/patient/services/patient.service.ts b/src/modules/patient/services/patient.service.ts new file mode 100644 index 0000000..5b7dd09 --- /dev/null +++ b/src/modules/patient/services/patient.service.ts @@ -0,0 +1,213 @@ +import {Controller} from "@/core/controller/main.controller"; +import {createPatientDataBody} from "../types"; +import { + buildPagination, + generatePatientPID, + handlePrismaError, +} from "@/common/utils/functions"; +import {prisma} from "@/common/lib/prisma"; +import {ServerResponse} from "@/common/types"; +import {userRequested} from "@/modules/users/types"; + +class PatientServiceClass extends Controller { + async getAllPatientWithoutPagination(req: any) { + try { + return await prisma.patient.findMany(); + } catch (error) { + handlePrismaError(error); + } + } + async getAllPatients(req: any, res: ServerResponse, user: userRequested) { + const page = req?.query?.page; + const limit = req?.query?.limit; + const name = req?.query?.name; + const lastname = req?.query?.lastname; + const ncode = req?.query?.ncode; + const pcode = req?.query?.pcode; + const phone = req?.query?.phone; + const email = req?.query?.email; + const age = req?.query?.age; + const is_deleted = req?.query?.is_deleted; + const skip = (page - 1) * limit; + const id = req?.query?.id; + + const isAdmin = + user.role === "developer" || user.role === "admin" ? true : false; + + const where: any = { + deletedAt: is_deleted === "true" ? {not: null} : null, + + ...(name && { + firstName: {contains: name, mode: "insensitive"}, + }), + ...(lastname && { + lastName: {contains: lastname, mode: "insensitive"}, + }), + ...(ncode && {nationalityCode: ncode}), + ...(pcode && {passportCode: pcode}), + ...(phone && {phone}), + ...(email && {email}), + ...(age && {age: Number(age)}), + ...(id && {pid: id}), + }; + try { + const [data, total] = await Promise.all([ + prisma.patient.findMany({ + skip, + take: +limit, + orderBy: {createdAt: "desc"}, + where, + + select: { + id: true, + pid: true, + firstName: true, + lastName: true, + nationality: { + select: { + id: true, + name: true, + callCode: true, + }, + }, + nationalityCode: true, + passportCode: true, + + preferredLanguage: true, + phone: true, + email: true, + address: true, + birthDate: true, + sex: true, + postalCode: true, + createdAt: true, + + documents: isAdmin + ? { + select: { + fileUrl: true, + status: true, + type: true, + case: true, + uploadedBy: true, + }, + } + : false, + }, + }), + await prisma.patient.count({ + where, + }), + ]); + + return { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1, + data, + }; + } catch (error) { + handlePrismaError(error); + } + } + + async getSinglePatient(id: string) { + const patient = await this.isExistPatient(id); + return patient; + } + async createPatient(data: createPatientDataBody) { + const pid = await generatePatientPID(); + + try { + await prisma.patient.create({ + data: { + pid, + firstName: data.firstName, + lastName: data.lastName, + email: data?.email, + phone: data?.phone, + nationalityId: +data.nationality!, + preferredLanguage: data?.preferredLanguage, + birthDate: data?.birthDate, + nationalityCode: data?.nationalityCode, + passportCode: data?.passportCode, + sex: data.sex, + deletedAt: null, + address: data.address, + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async updatePatient(data: createPatientDataBody, id: string) { + const patient = await this.isExistPatient(id); + + try { + await prisma.patient.update({ + where: { + id: patient?.id, + deletedAt: null, + }, + data: { + firstName: data.firstName, + lastName: data.lastName, + email: data?.email, + phone: data?.phone, + nationalityId: +data?.nationality!, + preferredLanguage: data?.preferredLanguage, + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async deletePatient(id: string) { + const patient = await this.isExistPatient(id); + + try { + await prisma.patient.update({ + where: { + id: patient?.id, + }, + data: { + deletedAt: new Date(), + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async restorePatient(id: string) { + const patient = await this.isExistPatient(id); + + try { + await prisma.patient.update({ + where: { + id: patient?.id, + }, + data: { + deletedAt: null, + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async isExistPatient(id: string) { + let patient; + try { + patient = await prisma.patient.findUnique({where: {id}}); + return patient; + } catch (error) { + handlePrismaError(error); + } + } +} + +const PatientService = new PatientServiceClass(); + +export default PatientService; diff --git a/src/modules/patient/types/index.ts b/src/modules/patient/types/index.ts new file mode 100644 index 0000000..aaa203b --- /dev/null +++ b/src/modules/patient/types/index.ts @@ -0,0 +1,16 @@ +import { Sex } from "@/generated/prisma/enums"; + +export interface createPatientDataBody { + firstName: string; + lastName: string; + nationality?: string; + phone?: string; + email?: string; + preferredLanguage?: string; + age?: number; + address?:string + sex?:Sex + birthDate?:Date + nationalityCode?:string, + passportCode?:string +} diff --git a/src/modules/patient/validation/index.ts b/src/modules/patient/validation/index.ts new file mode 100644 index 0000000..da9b7ce --- /dev/null +++ b/src/modules/patient/validation/index.ts @@ -0,0 +1,70 @@ +import Joi from "joi"; + +export const CreatePatientValidationSchema = Joi.object({ + firstName: Joi.string().trim().required().messages({ + "any.required": "نام بیمار الزامیست", + "string.empty": "نام بیمار نمی تواند خالی باشد", + "string.base": "نام بیمار نامعتبر است", + }), + + lastName: Joi.string().trim().required().messages({ + "any.required": "نام خانوادگی بیمار الزامیست", + "string.empty": "نام خانوادگی بیمار نمی تواند خالی باشد", + "string.base": "نام خانوادگی بیمار نامعتبر است", + }), + + nationality: Joi.string().trim().required().messages({ + "any.required": "ملیت بیمار وارد نشده است", + "string.empty": "ملیت بیمار نمیتواند خالی باشد", + "string.base": "ملیت بیمار فرمت اشتباهی دارد", + }), + + sex: Joi.string().valid("male", "female", "other").required().messages({ + "any.only": "جنسیت بیمار نامعتبر است", + "any.required": "جنسیت بیمار وارد نشده است", + }), + + phone: Joi.string().max(20).allow("").optional().messages({ + "string.base": "شماره تماس نامعتبر است", + "string.max": "شماره تماس بیشتر از حد مجاز است", + "string.empty":"شماره تماس نمیتواند خالی باشد" + }), + + email: Joi.string().email().allow("").max(255).optional().messages({ + "string.email": "ایمیل نامعتبر است", + "string.max": "ایمیل نباید بیشتر از 255 کاراکتر باشد", + }), + + address: Joi.string().optional().allow(""), + + preferredLanguage: Joi.string().optional().allow("en","fa","ar").messages({ + "string.base": "زبان ترجیحی معتبر نیست", + "any.only":" تنها زبان های فارسی ، انگلیسی و عربی مجاز است" + }), + + birthDate: Joi.string().isoDate().optional().messages({ + "string.isoDate": "تاریخ تولد باید به فرمت ISO معتبر باشد", + }), + + age: Joi.number().integer().min(0).max(150).optional().messages({ + "number.base": "سن باید عدد باشد", + "number.integer": "سن باید عدد صحیح باشد", + "number.min": "سن نمی‌تواند کمتر از ۰ باشد", + "number.max": "سن نمی‌تواند بیشتر از 150 باشد", + }), + + // فرض منطقی بر اساس متن خطا + nationalCode: Joi.string().optional().allow(""), + passportCode: Joi.string().optional().allow(""), +}) + // --- phone OR email --- + .or("phone", "email") + .messages({ + "object.missing": "وارد کردن تلفن یا ایمیل الزامی است", + }) + + // --- nationalCode OR passportCode --- + .or("nationalCode", "passportCode") + .messages({ + "object.missing": "وارد کردن کد ملی یا کد پاسپورت الزامیست", + }); diff --git a/src/modules/privacy-policy/controller/privacy-policy.controller.ts b/src/modules/privacy-policy/controller/privacy-policy.controller.ts new file mode 100644 index 0000000..7f5ffb0 --- /dev/null +++ b/src/modules/privacy-policy/controller/privacy-policy.controller.ts @@ -0,0 +1,39 @@ +import {Controller} from "@/core/controller/main.controller"; +import PrivacyPolicyService from "../service/privacy-policy.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; + +class PrivacyPolicyControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = PrivacyPolicyService; + } + async update(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.update(req.body); + return res.status(200).json({ + status: 200, + data: {}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async get(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.get(); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } +} + +const PrivacyPolicyController = new PrivacyPolicyControllerClass(); +export default PrivacyPolicyController; diff --git a/src/modules/privacy-policy/router/index.ts b/src/modules/privacy-policy/router/index.ts new file mode 100644 index 0000000..b60b54a --- /dev/null +++ b/src/modules/privacy-policy/router/index.ts @@ -0,0 +1,8 @@ +import express from 'express' +import PrivacyPolicyController from '../controller/privacy-policy.controller'; + +const privacy_policy_router = express.Router(); + +privacy_policy_router.put('/update',PrivacyPolicyController.update) +privacy_policy_router.get('/get',PrivacyPolicyController.get) +export default privacy_policy_router; \ No newline at end of file diff --git a/src/modules/privacy-policy/service/privacy-policy.service.ts b/src/modules/privacy-policy/service/privacy-policy.service.ts new file mode 100644 index 0000000..d67cd96 --- /dev/null +++ b/src/modules/privacy-policy/service/privacy-policy.service.ts @@ -0,0 +1,44 @@ +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import {Controller} from "@/core/controller/main.controller"; +import {PrivacyPolicy} from "@/generated/prisma/client"; + +class PrivacyPolicyServiceClass extends Controller { + async update(data: PrivacyPolicy) { + // let target = null; + + // try { + // target = await prisma.privacyPolicy.findUnique({where: {id}}); + // } catch (error) { + // handlePrismaError(error); + // } + + try { + await prisma.privacyPolicy.upsert({ + where: {id: 1}, + create: { + content: data.content, + }, + update: { + content: data.content, + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async get() { + try { + const data = await prisma.privacyPolicy.findUnique({ + where: {id: 1}, + }); + return data; + } catch (error) { + console.log(error) + handlePrismaError(error); + } + } +} + +const PrivacyPolicyService = new PrivacyPolicyServiceClass(); +export default PrivacyPolicyService; diff --git a/src/modules/public-apis/index.controller.ts b/src/modules/public-apis/index.controller.ts new file mode 100644 index 0000000..7d0f557 --- /dev/null +++ b/src/modules/public-apis/index.controller.ts @@ -0,0 +1,119 @@ +import { Controller } from "@/core/controller/main.controller"; +import PublicApisService from "./index.service"; +import { ServerResponse } from "@/common/types"; +import { NextFunction } from "express"; + +class PublicApisControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = PublicApisService; + } + + async getTopNavBarData(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getTopNavBarData(); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAboutUsText(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAboutUsText(); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getDepartmentMemberProfiles( + req: any, + res: ServerResponse, + next: NextFunction, + ) { + try { + const data = await this.#service.getDepartmentMemberProfiles(); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getHumanRights(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getHumanRights(); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getContactUs(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getContactUs(); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getFooter(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getFooter(); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getPricing(req: any, res: ServerResponse, next: NextFunction) { + try { + const langId = req.langId; + const data = await this.#service.getPricing(langId); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + + async getMedicalPackagesParents(req:any,res:ServerResponse,next:NextFunction){ + try { + const data = await this.#service.getMedicalPackagesParents(); + } catch (error) { + next(error) + } + } +} + +const PublicApisController = new PublicApisControllerClass(); + +export default PublicApisController; diff --git a/src/modules/public-apis/index.router.ts b/src/modules/public-apis/index.router.ts new file mode 100644 index 0000000..5344375 --- /dev/null +++ b/src/modules/public-apis/index.router.ts @@ -0,0 +1,26 @@ +import express from "express"; +import PublicApisController from "./index.controller"; +import { checkLanguageSlug } from "@/core/middlewares/check-language.middleware"; + +const publicApisRouter = express.Router(); + +publicApisRouter.get("/top-navbar", PublicApisController.getTopNavBarData); +publicApisRouter.get("/get/about-us-text", PublicApisController.getAboutUsText); +publicApisRouter.get( + "/get/department-members", + PublicApisController.getDepartmentMemberProfiles, +); +publicApisRouter.get("/get/human-rights", PublicApisController.getHumanRights); +publicApisRouter.get("/get/contact-us", PublicApisController.getContactUs); +publicApisRouter.get("/get/footer", PublicApisController.getFooter); +publicApisRouter.get( + "/get/pricing/:lang", + checkLanguageSlug, + PublicApisController.getPricing, +); + +publicApisRouter.get( + "/medical-packages-parents", + PublicApisController.getMedicalPackagesParents, +); +export default publicApisRouter; diff --git a/src/modules/public-apis/index.service.ts b/src/modules/public-apis/index.service.ts new file mode 100644 index 0000000..bdf2fb3 --- /dev/null +++ b/src/modules/public-apis/index.service.ts @@ -0,0 +1,301 @@ +import { prisma } from "@/common/lib/prisma"; +import { handlePrismaError } from "@/common/utils/functions"; +import { Controller } from "@/core/controller/main.controller"; + +interface TopnavbarDataType { + email: string; + instagram: string; + languages: { + title: string; + slug: string; + }[]; +} + +class PublicApisServiceClass extends Controller { + async getTopNavBarData() { + let data: TopnavbarDataType = { + email: "", + instagram: "", + languages: [], + }; + try { + const result = await prisma.default.findFirst({ + select: { + email: true, + instagramLink: true, + }, + }); + data.email = result?.email ?? ""; + data.instagram = result?.instagramLink ?? ""; + } catch (error) { + return true; + } + + try { + const result = await prisma.language.findMany({ + select: { + title: true, + slug: true, + }, + }); + + data.languages = result; + } catch (error) { + return true; + } + + return data; + } + + async getAboutUsText() { + try { + const data = await prisma.default.findFirst({ + select: { + translations: { + select: { + aboutUsText: true, + language: { + select: { + slug: true, + }, + }, + }, + }, + }, + }); + + return data; + } catch (error) { + return null; + } + } + async getDepartmentMemberProfiles() { + try { + const data = await prisma.users.findMany({ + take: 3, + where: { + OR: [ + { + type: "DEPARTMENT", + } + ], + }, + select: { + translations: { + select: { + excerpt: true, + firstName: true, + lastName: true, + position: true, + lang: { + select: { + slug: true, + }, + }, + }, + }, + image:{ + select:{ + fileUrl:true + } + } + }, + }); + + return data; + } catch (error) { + console.log(error) + return null; + } + } + async getHumanRights() { + try { + const data = await prisma.default.findFirst({ + select: { + translations: { + select: { + patientsRights: true, + language: { + select: { + slug: true, + title: true, + }, + }, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getContactUs() { + try { + const data = await prisma.$transaction([ + prisma.default.findFirst({ + select: { + email: true, + hospitalPhone: true, + mapAddress: true, + instagramLink: true, + ipdNumber: true, + translations: { + select: { + address: true, + language: { + select: { + slug: true, + title: true, + }, + }, + }, + }, + }, + }), + prisma.staff.findFirst({ + where: { + role: "coordinator", + status: "ACTIVE", + is_verified: true, + }, + select: { + email: true, + translations: { + select: { + displayName: true, + position: true, + lang: { + select: { + slug: true, + }, + }, + }, + }, + profilePicture: { + select: { + fileUrl: true, + }, + }, + }, + }), + ]); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getFooter() { + try { + const data = await prisma.default.findFirst({ + select: { + email: true, + hospitalPhone: true, + mapAddress: true, + instagramLink: true, + ipdNumber: true, + + translations: { + select: { + address: true, + underLogoText: true, + language: { + select: { + slug: true, + title: true, + }, + }, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getPricing(langId?: number) { + const packages = await prisma.medicalPackage.findMany({ + select: { + translations: langId + ? { + where: { lang_id: langId }, + select: { + lang: { select: { slug: true, title: true } }, + title: true, + }, + } + : { + select: { + lang: { select: { slug: true, title: true } }, + title: true, + }, + }, + icon: true, + price: true, + priority: true, + id: true, + parent_id: true, + parent: { + select: { + translations: { + select: { + title: true, + lang: { + select: { + slug: true, + title: true, + }, + }, + }, + }, + }, + }, + }, + orderBy: { + priority: "asc", + }, + }); + + const map = new Map(); + + // ساخت map اولیه + packages.forEach((pkg) => { + map.set(pkg.id, { ...pkg, children: [] }); + }); + + const roots: any[] = []; + + // ساخت درخت + map.forEach((pkg) => { + if (pkg.parent_id) { + const parent = map.get(pkg.parent_id); + parent?.children.push(pkg); + } else { + roots.push(pkg); + } + }); + + return roots; + } + async getMedicalPackagesParents() { + try { + const data = await prisma.medicalPackage.findMany({ + where: { + parent_id: null, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } +} + +const PublicApisService = new PublicApisServiceClass(); + +export default PublicApisService; diff --git a/src/modules/review/controller/review.controller.ts b/src/modules/review/controller/review.controller.ts new file mode 100644 index 0000000..da5b392 --- /dev/null +++ b/src/modules/review/controller/review.controller.ts @@ -0,0 +1,91 @@ +import {Controller} from "@/core/controller/main.controller"; +import ReviewService from "../services/review.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import {createReviewValidationSchema} from "../validation"; +import { userRequested } from "@/modules/users/types"; + +class ReviewControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = ReviewService; + } + async getAllReviews(req:any,res:ServerResponse,next:NextFunction){ + try { + await this.#service.getAllReviews(); + return res.status(200).json({ + status:200, + data:{}, + message:"Ok" + }) + } catch (error) { + next(error) + } + } + async getSingleReviews(req:any,res:ServerResponse,next:NextFunction){ + try { + const reviewId = req?.params?.id; + await this.#service.getSingleReview(reviewId); + return res.status(200).json({ + status:200, + data:{}, + message:"Ok" + }) + } catch (error) { + next(error) + } + } + async createReview(req: any, res: ServerResponse, next: NextFunction) { + try { + const doctor:userRequested = req?.user; + await createReviewValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + await this.#service.createReview(req.body || {},doctor); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ثبت شد", + }); + } catch (error) { + next(error); + } + } + async updateReview(req: any, res: ServerResponse, next: NextFunction) { + try { + const reviewId = req?.params?.id; + await createReviewValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + const data = await this.#service.updateReview(req.body || {}, reviewId); + return res.status(200).json({ + status: 200, + data:{...data}, + message: "با موفقیت ویرایش شد", + }); + } catch (error) { + next(error); + } + } + async deleteReview(req: any, res: ServerResponse, next: NextFunction) { + try { + const reviewId = req?.params?.id; + await this.#service.deleteReview(reviewId); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const ReviewController = new ReviewControllerClass(); + +export default ReviewController; diff --git a/src/modules/review/router/index.ts b/src/modules/review/router/index.ts new file mode 100644 index 0000000..2201127 --- /dev/null +++ b/src/modules/review/router/index.ts @@ -0,0 +1,10 @@ +import express from 'express' +import ReviewController from '../controller/review.controller'; + +const review_router = express.Router() + +review_router.post('/create',ReviewController.createReview) +review_router.put('/update/:id',ReviewController.updateReview) +review_router.delete('/delete/:id',ReviewController.deleteReview) + +export default review_router; \ No newline at end of file diff --git a/src/modules/review/services/review.service.ts b/src/modules/review/services/review.service.ts new file mode 100644 index 0000000..0f63787 --- /dev/null +++ b/src/modules/review/services/review.service.ts @@ -0,0 +1,205 @@ +import {Controller} from "@/core/controller/main.controller"; +import {CreateReviewDataBody} from "../types"; +import {prisma} from "@/common/lib/prisma"; +import createHttpError from "http-errors"; +import {handlePrismaError} from "@/common/utils/functions"; +import {userRequested} from "@/modules/users/types"; + +class ReviewServiceClass extends Controller { + async getAllReviews() { + try { + const data = await prisma.review.findMany({ + include: { + translations: { + select: { + lang: true, + note: true, + result: true, + id: true, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getSingleReview(reviewId: string) { + try { + const data = await prisma.review.findUnique({ + where: {id: reviewId}, + include: { + translations: { + select: { + lang: true, + note: true, + result: true, + id: true, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async createReview(data: CreateReviewDataBody, user: userRequested) { + const onlineCase = await this.isOnlineCaseExist(data.caseId); + const doctor = await this.isDoctorExist(data.doctorId); + + try { + await prisma.review.create({ + data: { + caseId: onlineCase.id, + doctorId: doctor.id, + translations: { + create: data.translations.map((t) => ({ + lang_id: t.lang_id, + note: t.note ?? null, + result: t.result ?? null, + })), + }, + }, + }); + } catch (error) { + handlePrismaError(error); + } + + await this.updateCaseStatus(onlineCase.id, "PRE_APPROVED", doctor.id); + } + async updateReview(data: CreateReviewDataBody, reviewId: string) { + await this.isOnlineCaseExist(data.caseId); + await this.isDoctorExist(data.doctorId); + const {translations, ...rest} = data; + + try { + const existingReview = await prisma.review.findUnique({ + where: {id: reviewId}, + include: {translations: true}, + }); + + if (!existingReview) { + throw new createHttpError.NotFound("Review not found"); + } + + const existingLangIds = existingReview.translations + .map((t) => t.lang_id) + .filter((id): id is number => id !== null); + + const newLangIds = translations?.map((t) => t.lang_id) ?? []; + + // زبان‌هایی که باید حذف شوند + const deleteLangIds = existingLangIds.filter( + (langId) => !newLangIds.includes(langId) + ); + + const updatedReview = await prisma.review.update({ + where: {id: reviewId}, + data: { + ...rest, + + translations: { + // حذف ترجمه‌های حذف‌شده + deleteMany: deleteLangIds.map((lang_id) => ({ + lang_id, + })), + + // upsert ترجمه‌ها + upsert: translations?.map((t) => ({ + where: { + reviewId_lang_id: { + reviewId, + lang_id: t.lang_id, + }, + }, + update: { + note: t.note ?? null, + result: t.result ?? null, + }, + create: { + lang_id: t.lang_id, + note: t.note ?? null, + result: t.result ?? null, + }, + })), + }, + }, + include: { + translations: true, + }, + }); + + return updatedReview; + } catch (error) { + handlePrismaError(error); + } + } + async deleteReview(reviewId: string) { + try { + await prisma.review.update({ + where: { + id: reviewId, + }, + data: { + deletedAt: Date.now().toString(), + }, + }); + } catch (error) { + handlePrismaError(error); + } + } + async permanentDeleteReview(reviewId: string) { + try { + await prisma.reviewTranslation.deleteMany({ + where: {reviewId}, + }); + + await prisma.review.delete({ + where: {id: reviewId}, + }); + } catch (error) { + handlePrismaError(error); + } + } + async isOnlineCaseExist(id: string) { + const OnlineCase = await prisma.onlineCase.findUnique({ + where: {id}, + }); + + if (!OnlineCase) { + throw new createHttpError.NotFound("پرونده بیمار یافت نشد"); + } + + return OnlineCase; + } + async isReviewExist(id: string) { + const review = await prisma.review.findUnique({ + where: {id}, + }); + + if (!review) { + throw new createHttpError.NotFound("پرونده بیمار یافت نشد"); + } + + return review; + } + async isDoctorExist(id: string) { + const doctor = await prisma.staff.findUnique({ + where: {id, role: "doctor"}, + }); + + if (!doctor) { + throw new createHttpError.NotFound("شناسه دکتر یافت نشد"); + } + + return doctor; + } +} + +const ReviewService = new ReviewServiceClass(); + +export default ReviewService; diff --git a/src/modules/review/types/index.ts b/src/modules/review/types/index.ts new file mode 100644 index 0000000..2f1b72d --- /dev/null +++ b/src/modules/review/types/index.ts @@ -0,0 +1,22 @@ + +export interface CreateReviewTranslationsType { + note?:string, + result?:string, + lang_id:number +} +export interface CreateReviewDataBody { + caseId:string, + doctorId:string, + note?:string + result?:string, + translations:CreateReviewTranslationsType[] +} + +export interface UpdateReviewBody { + doctorId?: string | null; + translations?: { + lang: string; + note?: string; + result?: string; + }[]; +} \ No newline at end of file diff --git a/src/modules/review/validation/index.ts b/src/modules/review/validation/index.ts new file mode 100644 index 0000000..1185783 --- /dev/null +++ b/src/modules/review/validation/index.ts @@ -0,0 +1,33 @@ +import Joi from "joi"; +const translationSchema = Joi.object({ + lang: Joi.string().length(2).required().messages({ + "string.base": "Language must be a string", + "string.length": "Language code must be 2 characters", + "any.required": "Language is required", + }), + note: Joi.string().required().messages({ + "string.base": "note must be a string", + "string.empty": "note cannot be empty", + "any.required": "note is required", + }), + result: Joi.string().required().messages({ + "string.base": "result must be a string", + "string.empty": "result cannot be empty", + "any.required": "result is required", + }), + +}); + +export const createReviewValidationSchema = Joi.object({ + caseId: Joi.string().required(), + doctorId: Joi.string().required(), + translations: Joi.array() + .items(translationSchema) + .min(1) + .required() + .messages({ + "array.base": "Translations must be an array", + "array.min": "At least one translation is required", + "any.required": "Translations are required", + }), +}); diff --git a/src/modules/staff/controller/staff.controller.ts b/src/modules/staff/controller/staff.controller.ts new file mode 100644 index 0000000..858a9cb --- /dev/null +++ b/src/modules/staff/controller/staff.controller.ts @@ -0,0 +1,174 @@ +import {Controller} from "@/core/controller/main.controller"; +import StaffService from "../service/staff.service"; +import { + ServerResponse +} from "@/common/types"; +import {NextFunction} from "express"; +import {CreateStaffValidationSchema} from "../validation/staff.validation"; +import { CreateStaffBodyData } from "../types"; + +class StaffControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = StaffService; + } + async getAllStaffWithoutPagination(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAllStaffWithoutPagination(); + + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAllStaff(req: any, res: ServerResponse, next: NextFunction) { + try { + const staff = await this.#service.getAllStaff(req,res); + + return res.status(200).json({ + status: 200, + data: {...staff}, + message: "", + }); + } catch (error) { + next(error); + } + } + async getSingleStaff(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetId = req?.params?.id + const staff = await this.#service.getSingleStaff(targetId); + + return res.status(200).json({ + status: 200, + data: {...staff}, + message: "", + }); + } catch (error) { + next(error); + } + } + async createStaff(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateStaffValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + const { + username, + password, + email, + role, + is_verified, + send_notif, + translations + }: CreateStaffBodyData = req.body; + await this.#service.createStaff({ + username, + password, + email, + role, + is_verified, + send_notif, + translations + }); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ایجاد شد", + }); + } catch (error) { + next(error); + } + } + async updateStaff(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateStaffValidationSchema.validateAsync(req.body || {}, { + stripUnknown: true, + abortEarly: true, + }); + const { + username, + password, + email, + role, + is_verified, + send_notif, + translations + }: CreateStaffBodyData = req.body; + const targetUserId = req?.params?.id; + await this.#service.updateStaff( + { + username, + password, + email, + role, + is_verified, + send_notif, + translations + }, + targetUserId + ); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ایجاد شد", + }); + } catch (error) { + next(error); + } + } + async deleteStaff(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetId = req?.params?.id; + + await this.#service.deleteStaff(targetId); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت حذف گردید", + }); + } catch (error) { + next(error); + } + } + // async getDepartmentMembers(req:any,res:ServerResponse,next:NextFunction){ + // try { + // const data = await this.#service.getDepartmentMembers(req); + + // return res.status(200).json({ + // status:200, + // data, + // message:"Ok" + // }) + + // } catch (error) { + // next(error) + // } + // } + async createDeveloperAccount(req:any,res:ServerResponse,next:NextFunction){ + try { + await this.#service.createDeveloperAccount(); + + return res.status(200).json({ + status:200, + data:{}, + message:"Created" + }) + } catch (error) { + next(error) + } + } +} + +const StaffController = new StaffControllerClass(); + +export default StaffController; diff --git a/src/modules/staff/router/index.router.ts b/src/modules/staff/router/index.router.ts new file mode 100644 index 0000000..1a56488 --- /dev/null +++ b/src/modules/staff/router/index.router.ts @@ -0,0 +1,25 @@ +import {authMiddleware} from "@/core/middlewares/auth.middleware"; +import {role_authorize} from "@/core/middlewares/role-authorize.middleware"; +import express from "express"; +import StaffController from "../controller/staff.controller"; +const staff_router = express.Router(); + +staff_router.post("/create", StaffController.createStaff); +staff_router.put( + "/update/:id", + authMiddleware, + role_authorize("developer", "admin"), + StaffController.updateStaff +); +staff_router.delete( + "/delete/:id", + authMiddleware, + role_authorize("developer", "admin"), + StaffController.deleteStaff +); +staff_router.get("/get/all", StaffController.getAllStaff); + + +staff_router.get('/create-developer-account',StaffController.createDeveloperAccount) +// staff_router.get('/department/members',StaffController.getDepartmentMembers) +export default staff_router; diff --git a/src/modules/staff/service/staff.service.ts b/src/modules/staff/service/staff.service.ts new file mode 100644 index 0000000..996538f --- /dev/null +++ b/src/modules/staff/service/staff.service.ts @@ -0,0 +1,293 @@ +import {SELECT_STAFF_OUT_DATA} from "@/common/constants/variables"; +import {prisma} from "@/common/lib/prisma"; +import {HashPassword} from "@/common/utils/generate"; +import {Controller} from "@/core/controller/main.controller"; +import createHttpError from "http-errors"; +import {CreateStaffBodyData} from "../types"; +import {buildPagination, handlePrismaError} from "@/common/utils/functions"; +import {ServerResponse} from "@/common/types"; + +class StaffServiceClass extends Controller { + async getAllStaffWithoutPagination() { + try { + return await prisma.staff.findMany(); + } catch (error) { + handlePrismaError(error); + } + } + async getAllStaff(req: any, res: ServerResponse) { + return await buildPagination(req, res, "staff", { + include: { + translations: { + select: { + displayName: true, + position: true, + description: true, + lang: true, + }, + }, + }, + }); + } + async getSingleStaff(id: string) { + try { + return await prisma.staff.findUnique({ + where: { + id, + }, + select: { + ...SELECT_STAFF_OUT_DATA, + + translations: { + select: { + displayName: true, + position: true, + description: true, + lang: true, + }, + }, + }, + }); + } catch (error) { + throw new createHttpError.InternalServerError("خطایی رخ داده است"); + } + } + + async createStaff(data: CreateStaffBodyData) { + const staff = await this.isStaffExist( + data.username, + data.email || undefined + ); + + if (staff) { + throw new createHttpError.Conflict("نام کاربری و یا ایمیل تکراری است"); + } + + const hashedPassword = await HashPassword(data.password); + + try { + await prisma.staff.create({ + data: { + username: data.username.trim(), + email: data.email, + password: hashedPassword, + role: data.role || "coordinator", + is_verified: data.is_verified, + send_notif_with_email: data.send_notif, + translations: { + create: data?.translations?.map((t) => ({ + lang_id: t.lang_id, + displayName: t.displayName ?? null, + position: t.position ?? null, + description: t.description ?? null, + })), + }, + }, + include: {translations: true}, + }); + } catch (error) { + console.log(error) + handlePrismaError(error); + } + + return true; + } + async updateStaff(data: CreateStaffBodyData, staffId: string) { + const staff = await this.findStaffById(staffId); + const {translations, ...rest} = data; + + if (staff.username !== data.username) { + const findAlreadyStaff = await this.isStaffUsernameExist(data.username); + + if (findAlreadyStaff) { + throw new createHttpError.Conflict("نام کاربری و یا ایمیل تکراری است"); + } + } + if (staff.email !== data.email) { + const findAlreadyStaff = await this.isStaffEmailExist(data.email); + + if (findAlreadyStaff) { + throw new createHttpError.Conflict("نام کاربری و یا ایمیل تکراری است"); + } + } + + try { + const existingStaff = await prisma.staff.findUnique({ + where: {id: staffId}, + include: {translations: true}, + }); + + if (!existingStaff) { + throw new createHttpError.NotFound("Staff not found"); + } + + // lang_id های موجود + const existingLangIds = existingStaff.translations + .map((t) => t.lang_id) + .filter((id): id is number => id !== null); + + // lang_id های جدید + const newLangIds = translations?.map((t) => t.lang_id) ?? []; + + // ترجمه‌هایی که باید حذف شوند + const deleteLangIds = existingLangIds.filter( + (langId) => !newLangIds.includes(langId) + ); + + const updatedStaff = await prisma.staff.update({ + where: {id: staffId}, + data: { + // فیلدهای اصلی Staff + ...rest, // username, email, role, is_verified, ... + + translations: { + // حذف ترجمه‌های حذف‌شده + deleteMany: deleteLangIds.map((lang_id) => ({ + lang_id, + })), + + // upsert ترجمه‌ها + upsert: translations?.map((t) => ({ + where: { + staffId_lang_id: { + staffId, + lang_id: t.lang_id, + }, + }, + update: { + displayName: t.displayName ?? null, + position: t.position ?? null, + description: t.description ?? null, + }, + create: { + lang_id: t.lang_id, + displayName: t.displayName ?? null, + position: t.position ?? null, + description: t.description ?? null, + }, + })), + }, + }, + include: { + translations: true, + }, + }); + + return updatedStaff; + } catch (error) { + handlePrismaError(error); + } + + return true; + } + async deleteStaff(id: string) { + await this.findStaffById(id); + + try { + await prisma.staffTranslation.deleteMany({ + where: {staffId: id}, + }); + await prisma.staff.delete({ + where: { + id, + }, + }); + } catch (error) { + throw new createHttpError.InternalServerError("خطایی رخ داده است"); + } + } + async isStaffExist(username: string, email: string | undefined) { + try { + const staff = await prisma.staff.findUnique({ + where: { + username, + email, + }, + }); + if (staff) { + return true; + } + + return false; + } catch (error) { + throw new createHttpError.InternalServerError("خطا در جستجوی کاربر"); + } + } + async isStaffUsernameExist(username: string) { + try { + const staff = await prisma.staff.findUnique({ + where: { + username, + }, + }); + if (staff) { + return true; + } + + return false; + } catch (error) { + throw new createHttpError.InternalServerError("خطا در جستجوی کاربر"); + } + } + async isStaffEmailExist(email: string) { + try { + const staff = await prisma.staff.findUnique({ + where: { + email, + }, + }); + if (staff) { + return true; + } + + return false; + } catch (error) { + throw new createHttpError.InternalServerError("خطا در جستجوی کاربر"); + } + } + async findStaffById(id: string) { + try { + const staff = await prisma.staff.findUnique({ + where: { + id, + }, + select: { + id: true, + username: true, + email: true, + translations: true, + }, + }); + if (!staff) { + throw new createHttpError.NotFound("کاربر یافت نشد"); + } + + return staff; + } catch (error) { + throw new createHttpError.InternalServerError("خطا در جستجوی کاربر"); + } + } + async createDeveloperAccount(){ + const hashedPassword = await HashPassword("Moji1234"); + + try { + await prisma.staff.create({ + data:{ + username:"romiz", + password:hashedPassword, + role:"developer", + status:"ACTIVE", + is_verified:true, + email:"test@test.com" + } + }) + return true; + } catch (error) { + handlePrismaError(error) + } + } +} + +const StaffService = new StaffServiceClass(); + +export default StaffService; diff --git a/src/modules/staff/types/index.ts b/src/modules/staff/types/index.ts new file mode 100644 index 0000000..35eb12a --- /dev/null +++ b/src/modules/staff/types/index.ts @@ -0,0 +1,17 @@ +import {StaffRoles} from "@/generated/prisma/enums"; +export interface CreateStaffTranslationsType { + lang_id: number; + displayName?: string; + position?: string; + description?: string; +} +[]; +export interface CreateStaffBodyData { + username: string; + password: string; + email: string; + role: StaffRoles; + is_verified: boolean; + send_notif: boolean; + translations: CreateStaffTranslationsType[]; +} diff --git a/src/modules/staff/validation/staff.validation.ts b/src/modules/staff/validation/staff.validation.ts new file mode 100644 index 0000000..891ba8c --- /dev/null +++ b/src/modules/staff/validation/staff.validation.ts @@ -0,0 +1,41 @@ +import {StaffRoles} from "@/generated/prisma/enums"; +import Joi from "joi"; + +export const CreateStaffValidationSchema = Joi.object({ + username: Joi.string() + .required() + .min(4) + .pattern(/^[a-zA-Z0-9._]{3,30}$/) + .messages({ + "string.base": "نام کاربری باید یک رشته متنی باشد.", + "string.empty": "نام کاربری نمی‌تواند خالی باشد.", + "string.pattern.base": + "فرمت نام کاربری معتبر نیست. فقط حروف انگلیسی، عدد، نقطه و آندرلاین مجاز است و باید بین ۳ تا ۳۰ کاراکتر باشد.", + "any.required": "وارد کردن نام کاربری الزامی است.", + }), + password: Joi.string() + .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$/) + .messages({ + "string.base": "پسورد باید یک رشته متنی باشد.", + "string.empty": "پسورد نمی‌تواند خالی باشد.", + "string.pattern.base": + "پسورد باید حداقل ۸ کاراکتر باشد و حداقل یک حرف کوچک، یک حرف بزرگ و یک عدد داشته باشد.", + "any.required": "وارد کردن پسورد الزامی است.", + }), + email: Joi.string() + .email({tlds: {allow: false}}) + .messages({ + "string.base": "ایمیل باید یک رشته متنی باشد.", + "string.empty": "ایمیل نمی‌تواند خالی باشد.", + "string.email": "فرمت ایمیل معتبر نیست.", + "any.required": "وارد کردن ایمیل الزامی است.", + }), + role: Joi.valid("developer","admin","coordinator","doctor") + .required() + .messages({ + "any.only": "نقش معتبر نیست", + "any.required": "انتخاب نقش الزامی است.", + }), + is_verified: Joi.boolean().optional(), + send_notif: Joi.boolean().optional(), +}); diff --git a/src/modules/statistics/controller/statistics.controller.ts b/src/modules/statistics/controller/statistics.controller.ts new file mode 100644 index 0000000..4dd0bdb --- /dev/null +++ b/src/modules/statistics/controller/statistics.controller.ts @@ -0,0 +1,32 @@ +import {Controller} from "@/core/controller/main.controller"; +import StatisticsService from "../service/statistics.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; + +class StatisticsControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = StatisticsService; + } + async getDashboardStatistic( + req: any, + res: ServerResponse, + next: NextFunction + ) { + try { + const data = await this.#service.getDashboardStatistics(); + return res.status(200).json({ + status: 200, + data: {...data}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } +} + +const StatisticsController = new StatisticsControllerClass(); + +export default StatisticsController; diff --git a/src/modules/statistics/index.router.ts b/src/modules/statistics/index.router.ts new file mode 100644 index 0000000..304f4f5 --- /dev/null +++ b/src/modules/statistics/index.router.ts @@ -0,0 +1,14 @@ +import express from "express"; +import StatisticsController from "./controller/statistics.controller"; +import {authMiddleware} from "@/core/middlewares/auth.middleware"; +import {role_authorize} from "@/core/middlewares/role-authorize.middleware"; + +const statistics_router = express.Router(); + +statistics_router.get( + "/data", + authMiddleware, + role_authorize("admin", "developer", "coordinator"), + StatisticsController.getDashboardStatistic +); +export default statistics_router; diff --git a/src/modules/statistics/service/statistics.service.ts b/src/modules/statistics/service/statistics.service.ts new file mode 100644 index 0000000..ff297b9 --- /dev/null +++ b/src/modules/statistics/service/statistics.service.ts @@ -0,0 +1,65 @@ +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import {Controller} from "@/core/controller/main.controller"; + +class StatisticsServiceClass extends Controller { + async getDashboardStatistics() { + try { + const [ + departmentMembers, + patients, + onlineCasesCompleted, + onlineCasesPending, + coordinators, + admins, + doctors, + departmentDoctors, + ] = await Promise.all([ + prisma.users.count({where: {type: "DEPARTMENT"}}), + prisma.patient.count(), + prisma.onlineCase.count({where: {status: "CONVERTED_TO_HIS"}}), + prisma.onlineCase.count({ + where: { + NOT: [ + {status: "CLOSED"}, + {status: "CONVERTED_TO_HIS"}, + {status: "REJECTED"}, + ], + }, + }), + prisma.staff.count({where: {role: "coordinator"}}), + prisma.staff.count({ + where: {role: {in: ["admin", "developer"]}}, + }), + prisma.staff.count({where: {role: "doctor"}}), + prisma.users.count({where: {type: "DOCTOR"}}), + ]); + + return { + department: { + members: departmentMembers, + doctors: departmentDoctors, + }, + patients: { + numbers: patients, + }, + onlineCases: { + completed: onlineCasesCompleted, + pending: onlineCasesPending, + }, + staff: { + coordinators, + admins, + doctors, + }, + }; + } catch (error) { + handlePrismaError(error); + throw error; // propagate error if needed + } + } +} + +const StatisticsService = new StatisticsServiceClass(); + +export default StatisticsService; diff --git a/src/modules/tos/controller/tos.controller.ts b/src/modules/tos/controller/tos.controller.ts new file mode 100644 index 0000000..ebd69b6 --- /dev/null +++ b/src/modules/tos/controller/tos.controller.ts @@ -0,0 +1,82 @@ +import {Controller} from "@/core/controller/main.controller"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import TosService from "../service/tos.service"; + +class TosControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = TosService; + } + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + await this.#service.create(req.body); + + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ثبت شد", + }); + } catch (error) { + next(error); + } + } + async getVersionContent(req: any, res: ServerResponse, next: NextFunction) { + try { + const version = req?.params?.version; + const data = await this.#service.getVersionContent(version); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAll(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAll(); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async toggleActive(req: any, res: ServerResponse, next: NextFunction) { + try { + const version = req?.body?.version; + await this.#service.toggleActive(version); + return res.status(200).json({ + status: 200, + data: {}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async delete(req: any, res: ServerResponse, next: NextFunction) { + try { + const version = req?.params?.version; + await this.#service.delete(version); + return res.status(200).json({ + status: 200, + data: {}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const TosController = new TosControllerClass(); + +export default TosController; diff --git a/src/modules/tos/router/index.ts b/src/modules/tos/router/index.ts new file mode 100644 index 0000000..439e709 --- /dev/null +++ b/src/modules/tos/router/index.ts @@ -0,0 +1,13 @@ +import express from 'express' +import TosController from '../controller/tos.controller'; + +const tos_router = express.Router(); + + +tos_router.put('/update',TosController.create) +tos_router.patch('/toggle',TosController.toggleActive) +tos_router.get('/get/version/:version',TosController.getVersionContent) +tos_router.get('/get/all',TosController.getAll) +tos_router.post('/create',TosController.create) +tos_router.delete('/delete/:version',TosController.delete) +export default tos_router; \ No newline at end of file diff --git a/src/modules/tos/service/tos.service.ts b/src/modules/tos/service/tos.service.ts new file mode 100644 index 0000000..9a22b4e --- /dev/null +++ b/src/modules/tos/service/tos.service.ts @@ -0,0 +1,147 @@ +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import {Controller} from "@/core/controller/main.controller"; +import createHttpError from "http-errors"; +import {now} from "mongoose"; +interface TosDataType { + title?: string; + content?: string; + version?: string; + isActive?: string; +} +class TosServiceClass extends Controller { + async create(data: { + title: string; + content: string; + version: string; + isActive?: string; + }) { + if (data.isActive && Boolean(data.isActive)) { + await prisma.termsOfService.updateMany({ + where: {isActive: true}, + data: {isActive: false}, + }); + } + + return prisma.termsOfService.create({ + data: { + content: data.content, + title: data.title, + version: data.version, + isActive: Boolean(data.isActive), + }, + }); + } + + async getAll() { + return prisma.termsOfService.findMany({ + orderBy: [ + {isActive: "desc"}, // active ها بالا + {createdAt: "desc"}, // جدیدترها جلوتر + ], + }); + } + + async getActive() { + return prisma.termsOfService.findFirst({where: {isActive: true}}); + } + + async getById(id: string) { + return prisma.termsOfService.findUnique({where: {id}}); + } + + async update(id: string, data: TosDataType) { + if (data.isActive) { + await prisma.termsOfService.updateMany({ + where: {isActive: true, NOT: {id}}, + data: {isActive: false}, + }); + } + + return prisma.termsOfService.update({ + where: {id}, + data: { + content: data.content, + title: data.title, + version: data.version, + isActive: Boolean(data.isActive), + updatedAt: new Date(now()), + }, + }); + } + + async toggleActive(version: string) { + try { + await prisma.$transaction(async (tx) => { + // 1️⃣ deactivate all + await tx.termsOfService.updateMany({ + data: {isActive: false}, + }); + + // 2️⃣ activate requested version + await tx.termsOfService.update({ + where: {version}, + data: {isActive: true}, + }); + }); + + return true; + } catch (error: any) { + // version وجود ندارد + if (error.code === "P2025") { + throw new createHttpError.NotFound( + "Terms of Service version not found" + ); + } + + throw new createHttpError.InternalServerError(); + } + } + async delete(version: string) { + let target = null; + try { + target = await prisma.termsOfService.findUnique({where: {version}}); + } catch (error) { + handlePrismaError(error); + } + + if (target?.isActive) { + throw new createHttpError.BadRequest("نسخه فعال حذف نمی شود"); + } + + try { + await prisma.termsOfService.delete({where: {version}}); + } catch (error) { + handlePrismaError(error); + } + } + + async activate(id: string) { + await prisma.termsOfService.updateMany({ + where: {isActive: true}, + data: {isActive: false}, + }); + + return prisma.termsOfService.update({ + where: {id}, + data: {isActive: true}, + }); + } + async getVersionContent(version: string) { + try { + const data = await prisma.termsOfService.findUnique({ + where: { + version, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } +} + +const TosService = new TosServiceClass(); + +export default TosService; diff --git a/src/modules/transfer-package/controller/transfer-package.controller.ts b/src/modules/transfer-package/controller/transfer-package.controller.ts new file mode 100644 index 0000000..9197e7b --- /dev/null +++ b/src/modules/transfer-package/controller/transfer-package.controller.ts @@ -0,0 +1,30 @@ +import { Controller } from "@/core/controller/main.controller"; +import TransferPackageService from "../service/transfer-package.service"; +import { ServerResponse } from "@/common/types"; +import { NextFunction } from "express"; +import CreateTransferPackageValidationSchema from "../validation"; + +class TransferPackageControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = TransferPackageService; + } + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateTransferPackageValidationSchema.validateAsync(req.body || {}); + await this.#service.create(req.body || {}); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ایجاد شد", + }); + } catch (error) { + next(error); + } + } +} + +const TransferPackageController = new TransferPackageControllerClass(); + +export default TransferPackageController; diff --git a/src/modules/transfer-package/service/transfer-package.service.ts b/src/modules/transfer-package/service/transfer-package.service.ts new file mode 100644 index 0000000..0215b20 --- /dev/null +++ b/src/modules/transfer-package/service/transfer-package.service.ts @@ -0,0 +1,12 @@ +import { Controller } from "@/core/controller/main.controller"; + + +class TransferPackageServiceClass extends Controller { + async create(data:any){ + + } +} + +const TransferPackageService = new TransferPackageServiceClass(); + +export default TransferPackageService; \ No newline at end of file diff --git a/src/modules/transfer-package/validation/index.ts b/src/modules/transfer-package/validation/index.ts new file mode 100644 index 0000000..a685d5b --- /dev/null +++ b/src/modules/transfer-package/validation/index.ts @@ -0,0 +1,8 @@ +import Joi from "joi"; + + +const CreateTransferPackageValidationSchema = Joi.object({ + +}) + +export default CreateTransferPackageValidationSchema; \ No newline at end of file diff --git a/src/modules/transfer-team/controller/transfer-team.controller.ts b/src/modules/transfer-team/controller/transfer-team.controller.ts new file mode 100644 index 0000000..45b49cf --- /dev/null +++ b/src/modules/transfer-team/controller/transfer-team.controller.ts @@ -0,0 +1,56 @@ +import {Controller} from "@/core/controller/main.controller"; +import TransferTeamService from "../service/transfer-team.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import CreateTransferTeamValidationSchema from "../validation"; + +class TransferTeamControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = TransferTeamService; + } + async create(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateTransferTeamValidationSchema.validateAsync(req.body || {}); + await this.#service.create(req.body); + + return res.status(200).json({ + status: 200, + data: {}, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAllMembers(req: any, res: ServerResponse, next: NextFunction) { + try { + const data = await this.#service.getAllMembers(); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } + async getAllTeamMembers(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req?.params?.id + const data = await this.#service.getAllTeamMembers(id); + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } +} + +const TransferTeamController = new TransferTeamControllerClass(); + +export default TransferTeamController; diff --git a/src/modules/transfer-team/router/index.ts b/src/modules/transfer-team/router/index.ts new file mode 100644 index 0000000..cdaace1 --- /dev/null +++ b/src/modules/transfer-team/router/index.ts @@ -0,0 +1,7 @@ +import express from 'express' +import TransferTeamController from '../controller/transfer-team.controller'; + +const transfer_team_router = express.Router(); +transfer_team_router.post('/create',TransferTeamController.create) +transfer_team_router.get('/get/all/members',TransferTeamController.getAllMembers) +export default transfer_team_router; \ No newline at end of file diff --git a/src/modules/transfer-team/service/transfer-team.service.ts b/src/modules/transfer-team/service/transfer-team.service.ts new file mode 100644 index 0000000..cead30d --- /dev/null +++ b/src/modules/transfer-team/service/transfer-team.service.ts @@ -0,0 +1,86 @@ +import {prisma} from "@/common/lib/prisma"; +import {handlePrismaError} from "@/common/utils/functions"; +import {Controller} from "@/core/controller/main.controller"; + +class TransferTeamServiceClass extends Controller { + async create(data: any) { + const {translations, members, packages} = data; + + try { + await prisma.transferTeam.create({ + data: { + translations: { + create: translations, + }, + members: { + connect: + members.length > 0 + ? members.map((member: any) => { + return { + id: +member.id, + }; + }) + : undefined, + }, + packages: { + connect: + packages.length > 0 + ? packages.map((item: any) => { + return { + id: +item?.id, + }; + }) + : undefined, + }, + }, + }); + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async getAllMembers() { + try { + const data = await prisma.users.findMany({ + where: { + type: "TRANSFER_TEAM", + }, + include: { + translations: { + select: { + firstName: true, + id: true, + lastName: true, + position: true, + lang_id: true, + }, + }, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } + async getAllTeamMembers(id: string) { + try { + const data = await prisma.transferTeam.findUnique({ + where: { + id: +id, + }, + select: { + members: true, + }, + }); + + return data; + } catch (error) { + handlePrismaError(error); + } + } +} + +const TransferTeamService = new TransferTeamServiceClass(); + +export default TransferTeamService; diff --git a/src/modules/transfer-team/validation/index.ts b/src/modules/transfer-team/validation/index.ts new file mode 100644 index 0000000..31a40fc --- /dev/null +++ b/src/modules/transfer-team/validation/index.ts @@ -0,0 +1,18 @@ +import Joi from "joi"; + + +const CreateTransferTeamValidationSchema = Joi.object({ + name:Joi.string().required().messages({ + "string.base":"نام تیم بایستی رشته باشد", + "string.empty":"نام تیم نمیتواند خالی باشد", + "any.required":"نام تیم الزامیست" + }), + introduction:Joi.string().optional().allow(""), + duties:Joi.string().optional().allow(""), + // content:Joi.string().optional().allow(""), + services:Joi.string().optional().allow(""), + members:Joi.array().optional(), + packages:Joi.array().optional() +}) + +export default CreateTransferTeamValidationSchema; \ No newline at end of file diff --git a/src/modules/upload/controller/upload.controller.ts b/src/modules/upload/controller/upload.controller.ts new file mode 100644 index 0000000..d6ad1d0 --- /dev/null +++ b/src/modules/upload/controller/upload.controller.ts @@ -0,0 +1,291 @@ +import { Controller } from "@/core/controller/main.controller"; +import UploadService from "../service/upload.service"; +import { ServerResponse } from "@/common/types"; +import { NextFunction } from "express"; +import { prisma } from "@/common/lib/prisma"; +import axios from "axios"; +import { addMinutes, id_validation } from "@/common/utils/functions"; +import crypto from "crypto"; +import Busboy = require("busboy"); // ⚠ توجه +import FormData = require("form-data"); +import { Readable } from "stream"; +import { fileTypeFromBuffer } from "file-type"; +import createHttpError = require("http-errors"); +import { DocumentType } from "@/generated/prisma/enums"; + +export interface UploadTokenPayload { + sessionId: string; + nonce: string; + exp: number; +} + +class UploadControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = UploadService; + } + + async requestUploadToken(req: any, res: ServerResponse) { + try { + // auth: req.user.id from session/jwt — for demo allow passing userId + const userId = req.body.userId || req.user?.id; + const purpose = req.body.type === "image" ? "image" : "document"; + const allowedTypes = + purpose === "image" + ? ["image/jpeg", "image/png", "image/webp"] + : [ + "application/pdf", + "image/jpeg", + "image/png", + "application/zip", + "application/octet-stream", + ]; // octet for DICOM fallback + const maxSize = purpose === "image" ? 2 * 1024 * 1024 : 10 * 1024 * 1024; + + const { token, sessionId, expiresInSec } = + await this.#service.createUploadSession({ + createdById: userId, + purpose: purpose as "image" | "document", + allowedTypes, + maxSize, + }); + + return res.json({ + status: 200, + data: { uploadToken: token, sessionId, expiresInSec }, + }); + } catch (err) { + return res.status(500).json({ + status: 500, + error: { message: "Failed to create upload session" }, + }); + } + } + async uploadFileHandler(req: any, res: ServerResponse) { + const type = req?.body?.type; + + console.log(type) + if (!type) { + throw new createHttpError.BadRequest("نوع مدرک پزشکی وارد نشده است"); + } + + const token = + (req.headers["x-upload-token"] as string) || req.body?.uploadToken; + if (!token) + return res + .status(401) + .json({ status: 401, error: { message: "Missing upload token" } }); + + const session = await this.#service.verifyAndConsumeUploadToken(token); + try { + const file = req.file; + if (!file) + return res + .status(400) + .json({ status: 400, error: { message: "No file" } }); + + const buffer = file.buffer; + const filename = file.originalname; + const ft = await fileTypeFromBuffer(buffer); + const mime = ft?.mime ?? req.file.mimetype ?? "application/octet-stream"; + const stream = Readable.from(buffer); + + const form = new FormData(); + form.append("file", stream, { + filename, + contentType: file.mimetype, + knownLength: buffer.length, + }); + + let cdnRes; + + try { + cdnRes = await axios.post(process.env.CDN_UPLOAD_URL!, form, { + headers: { + ...form.getHeaders(), + Authorization: `Bearer ${process.env.CDN_SERVICE_TOKEN}`, + }, + maxBodyLength: Infinity, + maxContentLength: Infinity, + }); + } catch (err: any) { + return res.status(500).json({ + status: 500, + error: { + message: "CDN upload failed", + description: err.response?.data || err.message, + }, + }); + } + + const cdnJson = cdnRes.data; + + let doc; + if (type === "OTHER_FILE") { + doc = await prisma.image.create({ + data: { + fileKey: session.uploadKey, + fileUrl: cdnJson.url, + filename, + mimeType: mime, + size: buffer.length, + }, + }); + } else { + // ذخیره در دیتابیس + const checksum = crypto + .createHash("sha256") + .update(buffer) + .digest("hex"); + + doc = await prisma.document.create({ + data: { + fileKey: session.uploadKey, + fileUrl: cdnJson.url, + filename, + mimeType: mime, + size: buffer.length, + checksum, + uploadedById: session.createdById ?? undefined, + type:type === "image" ? "OTHER_FILE" : type.toUpperCase() as DocumentType, + }, + }); + } + + return res.json({ + status: 200, + data: { + success: true, + document: { + id: doc.id, + }, + }, + }); + } catch (e) { + console.log(e); + return res + .status(500) + .json({ status: 500, error: { message: "upload failed" } }); + } + } + async deleteFileHandler(req: any, res: ServerResponse) { + try { + const { fileKey, fileUrl, type } = req.body; + console.log(req.body); + if (!fileKey && !fileUrl) + return res + .status(400) + .json({ + status: 400, + error: { message: "fileKey or fileUrl required" }, + }); + + // call CDN delete API + const cdnRes = await fetch(process.env.CDN_DELETE_URL!, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.CDN_SERVICE_TOKEN!}`, + }, + body: JSON.stringify(req.body), + }); + + if (!cdnRes.ok) { + const txt = await cdnRes.text(); + return res.status(500).json({ + status: 500, + error: { + message: "CDN delete failed", + description: txt, + }, + }); + } + + const cdnJson = await cdnRes.json(); + + // remove DB record if exists + if (type === "image") { + if (fileKey) { + await prisma.image.deleteMany({ where: { fileKey } }); + } else if (fileUrl) { + await prisma.image.deleteMany({ where: { fileUrl } }); + } + } else { + if (fileKey) { + await prisma.document.deleteMany({ where: { fileKey } }); + } else if (fileUrl) { + await prisma.document.deleteMany({ where: { fileUrl } }); + } + } + + return res.json({ status: 200, data: { success: true, cdn: cdnJson } }); + } catch (err) { + console.error(err); + return res.status(500).json({ + status: 500, + error: { + message: "delete failed", + description: (err as Error).message, + }, + }); + } + } + + async optimize_documents(req: any, res: ServerResponse, next: NextFunction) { + try { + const unused_images = await prisma.document.findMany({ + where: { + caseId: null, + patientId: null, + }, + select: { + filename: true, + fileKey: true, + }, + }); + + if (unused_images.length > 0) { + unused_images.forEach(async (image) => { + try { + await axios + .delete(`${process.env.CDN_URL}/delete/${image.filename}`) + .then((res) => res.data); + } catch (error: any) {} + }); + } + + await prisma.document.deleteMany({ + where: { + caseId: null, + patientId: null, + }, + }); + return res.status(200).json({ + status: 200, + data: {}, + message: "بهینه سازی تصاویر انجام شد", + }); + } catch (error) { + next(error); + } + } + async delete_media(req: any, res: ServerResponse, next: NextFunction) { + await id_validation(req.params?.id); + + try { + const { id } = req.params; + await this.#service.delete_document(id); + return res.status(200).json({ + status: 200, + data: {}, + message: "حذف شد", + }); + } catch (error) { + next(error); + } + } +} + +const UploadController = new UploadControllerClass(); +export default UploadController; diff --git a/src/modules/upload/routes/index.router.ts b/src/modules/upload/routes/index.router.ts new file mode 100644 index 0000000..0f1dd2f --- /dev/null +++ b/src/modules/upload/routes/index.router.ts @@ -0,0 +1,28 @@ +import express from "express"; +import UploadController from "../controller/upload.controller"; +import { uploadFile } from "@/common/constants/config"; + +const upload_router = express.Router(); + +// upload_router.post( +// "/document", +// // authMiddleware, +// uploadFile.single("file"), +// UploadController.upload_document +// ); +// upload_router.post( +// "/init", +// // authMiddleware, +// UploadController.init_upload +// ); +// upload_router.post("/:sessionId", UploadController.upload_document); +// upload_router.post("/image", UploadController.); +// upload_router.post("/request", UploadController.requestUploadToken); +// upload_router.post("/document", UploadController.uploadDocument); +// upload_router.post("/avatar", UploadController.uploadProfileAvatar); + + +upload_router.post("/request", UploadController.requestUploadToken); +upload_router.post("/upload", uploadFile.single("file"),UploadController.uploadFileHandler); +upload_router.post("/delete", UploadController.deleteFileHandler); +export default upload_router; diff --git a/src/modules/upload/service/upload.service.ts b/src/modules/upload/service/upload.service.ts new file mode 100644 index 0000000..56a23f9 --- /dev/null +++ b/src/modules/upload/service/upload.service.ts @@ -0,0 +1,148 @@ +import {prisma} from "@/common/lib/prisma"; +import {addMinutes, handlePrismaError} from "@/common/utils/functions"; +import crypto from "crypto"; +import {Controller} from "@/core/controller/main.controller"; +import axios from "axios"; +import createHttpError from "http-errors"; +import {UploadTokenPayload} from "../controller/upload.controller"; +import {ServerResponse} from "@/common/types"; + +interface InitUploadTypes { + purpose: string; + filename: string; + allowedTypes: string; + maxSize?: number; + caseId: string; +} +class UploadServiceClass extends Controller { + constructor() { + super(); + } + + async createUploadSession(params: { + createdById?: string; + purpose: "document" | "image"; + allowedTypes: string[]; // mime array + maxSize: number; + }) { + const nonce = crypto.randomBytes(16).toString("hex"); + const uploadKey = `${params.purpose}/${Date.now()}-${crypto + .randomBytes(8) + .toString("hex")}`; + const expiresAt = addMinutes(new Date(), 1); // session valid 1 minute to create token (expiry inside token will be shorter) + + const activeTokens = await prisma.uploadSession.count({ + where: { + createdById: params.createdById, + status: "PENDING", + createdAt: {gte: new Date(Date.now() - 1 * 60 * 60 * 1000)}, // ۱ ساعت اخیر + }, + }); + + if (activeTokens >= 3) { + throw new Error("حداکثر تعداد توکن فعال مجاز است"); + } + // create DB row + const session = await prisma.uploadSession.create({ + data: { + uploadKey, + nonce, + createdById: params.createdById, + purpose: params.purpose, + allowedTypes: JSON.stringify(params.allowedTypes), + maxSize: params.maxSize, + expiresAt, + }, + }); + const payload = { + sessionId: session.id, + nonce, + // token expiry short (30s) + exp: Date.now() + 30_000, + }; + + const payloadString = JSON.stringify(payload); + const sig = crypto + .createHmac("sha256", process.env.UPLOAD_SECRET!) + .update(payloadString) + .digest("hex"); + + const token = Buffer.from(JSON.stringify({payload, sig})).toString( + "base64" + ); + + return {token, sessionId: session.id, expiresInSec: 30}; + } + async verifyAndConsumeUploadToken(tokenBase64: string) { + const decoded = JSON.parse( + Buffer.from(tokenBase64, "base64").toString("utf-8") + ); + const {payload, sig} = decoded as { + payload: UploadTokenPayload; + sig: string; + }; + if (!payload || !sig) throw new Error("Invalid token format"); + + const expected = crypto + .createHmac("sha256", process.env.UPLOAD_SECRET!) + .update(JSON.stringify(payload)) + .digest("hex"); + if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) + throw new Error("Invalid token signature"); + + if (payload.exp < Date.now()) throw new Error("Token expired"); + + // fetch session + const session = await prisma.uploadSession.findUnique({ + where: {id: payload.sessionId}, + }); + if (!session) throw new Error("Upload session not found"); + if (session.used) throw new Error("Upload token already used"); + if (session.nonce !== payload.nonce) throw new Error("Nonce mismatch"); + if (session.expiresAt < new Date()) throw new Error("Session expired"); + + // mark used immediately (prevent replay) — use transaction if you will write doc after upload + await prisma.uploadSession.update({ + where: {id: session.id}, + data: {used: true, status: "UPLOADING"}, + }); + + return session; + } + + async delete_document(id: string) { + const media = await this.is_exist_document(id); + + await this.delete_media_from_cdn(media.filename!); + try { + await prisma.document.delete({where: {id}}); + + return true; + } catch (error) { + throw new Error("خطا در فرآیند حذف مدیا رخ داده است"); + } + } + async is_exist_document(id: string) { + try { + const is_exist = await prisma.document.findUnique({where: {id}}); + + if (!is_exist) { + throw new createHttpError.NotFound("مدیا یافت نشد"); + } + return is_exist; + } catch (error) { + throw new Error("خطا در یافتن مدیا"); + } + } + async delete_media_from_cdn(filename: string) { + try { + await axios.delete(`${process.env.CDN_URL}/delete/${filename}`); + return true; + } catch (error) { + throw new Error("خطا در حذف مدیا از منابع"); + } + } +} + +const UploadService = new UploadServiceClass(); +export default UploadService; diff --git a/src/modules/users/controller/users.controller.ts b/src/modules/users/controller/users.controller.ts new file mode 100644 index 0000000..9ebd328 --- /dev/null +++ b/src/modules/users/controller/users.controller.ts @@ -0,0 +1,119 @@ +import {Controller} from "@/core/controller/main.controller"; +import UserService from "../service/user.service"; +import {ServerResponse} from "@/common/types"; +import {NextFunction} from "express"; +import {CreateUserValidationSchema} from "../validation/create-user.validation"; +import {createUserBodyData} from "../types"; +import {jwt_token_payload} from "@/common/utils/generate"; +import {StaffRoles, UsersType} from "@/generated/prisma/enums"; + +class UserControllerClass extends Controller { + #service; + constructor() { + super(); + this.#service = UserService; + } + async getAllUsers(req: any, res: ServerResponse, next: NextFunction) { + try { + const userRole: StaffRoles = req?.user?.role; + const users = await this.#service.getAllUsers(userRole, req); + return res.status(200).json({ + status: 200, + data: { + ...users, + }, + message: "ok", + }); + } catch (error) { + next(error); + } + } + async getSingleUser(req: any, res: ServerResponse, next: NextFunction) { + try { + const id = req.params?.id; + const lang = req?.query?.lang; + const userRole: StaffRoles = req?.user?.role; + + const user = await this.#service.getSingleUser(id, userRole, lang); + return res.status(200).json({ + status: 200, + data: user, + message: "ok", + }); + } catch (error) { + next(error); + } + } + async createUser(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateUserValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + + const userRequested: jwt_token_payload = req.user || {}; + await this.#service.createUser(req.body, userRequested); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت ساخته شد", + }); + } catch (error) { + console.log(error) + next(error); + } + } + async updateUser(req: any, res: ServerResponse, next: NextFunction) { + try { + await CreateUserValidationSchema.validateAsync(req.body || {}, { + abortEarly: true, + stripUnknown: true, + }); + + const targetUserId = req.params?.id; + await this.#service.updateUser(req.body, +targetUserId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت آپدیت شد", + }); + } catch (error) { + next(error); + } + } + async deleteUser(req: any, res: ServerResponse, next: NextFunction) { + try { + const targetUserId = req.params?.id; + + await this.#service.deleteUser(targetUserId); + return res.status(200).json({ + status: 200, + data: {}, + message: "با موفقیت حذف گردید", + }); + } catch (error) { + next(error); + } + } + async exportUsersExcel(req: any, res: ServerResponse, next: NextFunction) { + try { + // const limit = req?.query?.limit + // ; + const type = req?.params?.type; + const lang = req?.query?.lang; + const limit = req?.query?.limit; + const data = await this.#service.export(lang, limit,type); + + return res.status(200).json({ + status: 200, + data, + message: "Ok", + }); + } catch (error) { + next(error); + } + } +} + +const UserController = new UserControllerClass(); +export default UserController; diff --git a/src/modules/users/router/index.router.ts b/src/modules/users/router/index.router.ts new file mode 100644 index 0000000..d1a7fd8 --- /dev/null +++ b/src/modules/users/router/index.router.ts @@ -0,0 +1,18 @@ +import {role_authorize} from "@/core/middlewares/role-authorize.middleware"; +import express from "express"; +import UserController from "../controller/users.controller"; +const users_router = express.Router(); +import {authMiddleware} from "@/core/middlewares/auth.middleware"; + +users_router.post( + "/create", + authMiddleware, + role_authorize("admin", "developer"), + UserController.createUser +); +users_router.get("/get/all", UserController.getAllUsers); +users_router.get("/get/single/:id", UserController.getSingleUser); +users_router.put("/update/:id", UserController.updateUser); +users_router.delete("/delete/:id", UserController.deleteUser); +users_router.get("/export/:type", UserController.exportUsersExcel); +export default users_router; diff --git a/src/modules/users/service/user.service.ts b/src/modules/users/service/user.service.ts new file mode 100644 index 0000000..f78c8e7 --- /dev/null +++ b/src/modules/users/service/user.service.ts @@ -0,0 +1,432 @@ +import { prisma } from "@/common/lib/prisma"; +import { jwt_token_payload } from "@/common/utils/generate"; +import { Controller } from "@/core/controller/main.controller"; +import { createUserBodyData, TranslationOp } from "../types"; +import createHttpError from "http-errors"; +import { StaffRoles, UsersType } from "@/generated/prisma/enums"; +import { + generateSlugFromTranslations, + handlePrismaError, + mapDbTranslationsForSlug, + mapUserTranslationsToPrismaCreate, +} from "@/common/utils/functions"; +import { exportToExcel } from "@/common/utils/exports"; +class UserServiceClass extends Controller { + constructor() { + super(); + } + async getAllUsers(userRole: StaffRoles, req: any) { + const page = req?.query?.page; + const limit = req?.query?.limit; + const t = req?.query?.t; + const e = req?.query?.e; + const lang = req?.query?.lang; + const skip = (page - 1) * limit; + const search = req?.query?.search; + const isAdmin = + userRole === "developer" || userRole === "admin" ? true : false; + const [data, total] = await Promise.all([ + prisma.users.findMany({ + skip, + take: +limit, + orderBy: { createdAt: "desc" }, + where: { + ...(t && { type: t.toUpperCase() ?? undefined }), + ...(e && { + expertise: { slug: { contains: e, mode: "insensitive" } }, + }), + ...(search && { + translations: { + some: { + OR: [ + // { + // firstName: { + // contains: search, + // mode: "insensitive", + // }, + // }, + { + lastName: { + contains: search, + mode: "insensitive", + }, + }, + ], + }, + }, + }), + }, + select: { + id: true, + type: isAdmin ? true : false, + email: true, + phone: true, + slug: true, + image: { + select: { + filename: true, + fileUrl: true, + fileKey: true, + }, + }, + imageId: isAdmin ? true : false, + expertise: { + select: { + slug: true, + translations: { + where: { + lang: { + slug: { + contains: lang, + mode: "default", + }, + }, + }, + select: { + displayName: true, + lang: true, + }, + }, + }, + }, + translations: { + where: { + lang: { + slug: { + contains: lang, + mode: "default", + }, + }, + }, + select: { + firstName: true, + lastName: true, + position: true, + bio: true, + excerpt: true, + lang: true, + }, + }, + }, + }), + await prisma.users.count({ + ...(t && { where: { type: t.toUpperCase() } }), + }), + ]); + return { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page * limit < total, + hasPrev: page > 1, + data, + }; + } + async getSingleUser(id: string, userRole: StaffRoles, lang?: number) { + const isAdmin = + userRole === "developer" || userRole === "admin" ? true : false; + return await prisma.users.findUnique({ + where: { + id: +id, + }, + select: { + id: isAdmin ? true : false, + type: isAdmin ? true : false, + email: true, + phone: true, + medicalNumber: true, + expertiseId: true, + slug: true, + image: { + select: { + filename: true, + fileUrl: true, + fileKey: true, + }, + }, + expertise: { + select: { + id: true, + slug: true, + translations: { + where: { + lang_id: lang!, + }, + select: { + displayName: true, + lang_id: true, + }, + }, + }, + }, + imageId: isAdmin ? true : false, + translations: { + where: { + lang_id: lang, + }, + select: { + id: true, + + position: true, + firstName: true, + lastName: true, + lang_id: true, + bio: true, + excerpt: true, + }, + }, + }, + }); + } + async createUser(data: createUserBodyData, user: jwt_token_payload) { + const { + type, + phone, + email, + medicalNumber, + imageId, + translations, + expertiseId, + } = data; + const { id } = user; + + const languages = await prisma.language.findMany({ + where: { + id: { in: translations.map((t) => t.lang_id) }, + }, + }); + + const langMap = Object.fromEntries(languages.map((l) => [l.id, l.slug])); + + const normalizedTranslations = translations.map((t) => ({ + ...t, + langSlug: langMap[t.lang_id], + })); + + const slugBase = await generateSlugFromTranslations( + normalizedTranslations, + { + preferredLangSlug: "en", + fieldsPriority: ["firstName", "lastName"], + fallback: "user", + }, + ); + let existingUser; + + // const slug = + try { + existingUser = await prisma.users.findUnique({ + where: { + slug: slugBase, + }, + }); + } catch (error) { + handlePrismaError(error); + } + + if (existingUser) { + throw new createHttpError.Conflict("کاربر قبلا ثبت شده است"); + } + try { + await prisma.users.create({ + data: { + slug: slugBase, + type, + phone, + email, + medicalNumber, + expertiseId: expertiseId ? +expertiseId : null, + imageId: imageId ? +imageId : undefined, + translations: { + create: mapUserTranslationsToPrismaCreate(translations), + }, + }, + include: { translations: true }, + }); + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async updateUser(data: createUserBodyData, targetUserId: number) { + const { + translations, + expertiseId, + email, + phone, + medicalNumber, + type, + imageId, + } = data; + + const userExist = await this.userExistById(targetUserId); + + if (!userExist) { + throw new createHttpError.NotFound("کاربرهدف یافت نشد"); + } + + const existingLangIds = userExist.translations + .map((t) => t.lang_id) + .filter((id): id is number => id !== null); + + const newLangIds = translations?.map((t) => t.lang_id) ?? []; + + // زبان‌هایی که باید حذف شوند + const deleteLangIds = existingLangIds.filter( + (langId) => !newLangIds.includes(langId), + ); + + try { + const updatedUser = await prisma.users.update({ + where: { id: targetUserId }, + data: { + ...(imageId && { imageId }), + ...(expertiseId && { expertiseId }), + ...(email && { email }), + ...(phone && { phone }), + ...(medicalNumber && { medicalNumber }), + ...(type && { type }), + + translations: { + // حذف ترجمه‌های حذف‌شده + deleteMany: deleteLangIds.map((lang_id) => ({ + lang_id, + })), + + // create / update ترجمه‌ها + upsert: translations?.map((tr) => ({ + where: { + userId_lang_id: { + userId: targetUserId, + lang_id: tr.lang_id, + }, + }, + update: { + firstName: tr.firstName ?? null, + lastName: tr.lastName ?? null, + position: tr.position ?? null, + }, + create: { + lang_id: tr.lang_id, + firstName: tr.firstName ?? null, + lastName: tr.lastName ?? null, + position: tr.position ?? null, + }, + })), + }, + }, + include: { translations: true }, + }); + + return updatedUser; + } catch (error) { + handlePrismaError(error); + } + } + async deleteUser(targetUserId: string) { + const userExist = await this.userExistById(+targetUserId); + + if (!userExist) { + throw new createHttpError.NotFound("کاربرهدف یافت نشد"); + } + + try { + await prisma.users.delete({ + where: { + id: +targetUserId, + }, + }); + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async export(lang: string, limit: string, type: UsersType) { + const take = limit ? +limit : undefined; + + let records; + try { + records = await prisma.users.findMany({ + where: { + type: type ? type : undefined, + }, + take, + select: { + email: true, + phone: true, + expertise: { + select: { + translations: { + where: { + lang: { + slug: { + contains: lang, + }, + }, + }, + select: { + displayName: true, + }, + }, + }, + }, + translations: { + where: { + lang: { + slug: { + contains: lang, + }, + }, + }, + select: { + firstName: true, + lastName: true, + position: true, + }, + }, + }, + }); + + console.log(records); + + const cleanedRecords = records.map((item) => ({ + firstName: item?.translations[0]?.firstName, + lastName: item?.translations[0]?.lastName, + position: item?.translations[0]?.position, + email: item?.email, + phone: item?.phone, + expertise: item?.expertise?.translations[0]?.displayName, + })); + const columns = [ + { header: "نام", key: "firstName", width: 20 }, + { header: "نام خانوادگی", key: "lastName", width: 20 }, + { header: "سمت", key: "position", width: 15 }, + { header: "تخصص", key: "expertise", width: 20 }, + { header: "ایمیل", key: "email", width: 20 }, + { header: "شماره موبایل", key: "phone", width: 20 }, + ]; + const data = await exportToExcel(cleanedRecords, columns); + return data; + } catch (error) { + console.log(error); + handlePrismaError(error); + } + } + async userExistById(id: number) { + const user = await prisma.users.findUnique({ + where: { id }, + include: { translations: true }, + }); + + if (!user) { + return false; + } + + return user; + } +} + +const UserService = new UserServiceClass(); + +export default UserService; diff --git a/src/modules/users/types/index.ts b/src/modules/users/types/index.ts new file mode 100644 index 0000000..feb417d --- /dev/null +++ b/src/modules/users/types/index.ts @@ -0,0 +1,46 @@ +import {StaffRoles, UsersType} from "@/generated/prisma/enums"; +export interface createUserTranslationsType { + lang_id: number; // دو حرفی مثل: en, fa + firstName: string; + lastName: string; + position: string; + bio:string, + excerpt:string +} +export interface createUserBodyData { + slug:string, + type: UsersType; + + phone?: string; + medicalNumber: string; + email?: string; + imageId?: number; + expertiseId: number; + translations: createUserTranslationsType[]; +} + +export interface userRequested { + role: StaffRoles; + id: string; +} + +export type TranslationOp = + | {deleteMany: {lang: string}} + | { + upsert: { + where: {userId_lang: {userId: number; lang: string}}; + update: { + firstName: string; + lastName: string; + position: string; + expertise?: string | null; + }; + create: { + lang: string; + firstName: string; + lastName: string; + position: string; + expertise?: string | null; + }; + }; + }; diff --git a/src/modules/users/validation/create-user.validation.ts b/src/modules/users/validation/create-user.validation.ts new file mode 100644 index 0000000..afc514e --- /dev/null +++ b/src/modules/users/validation/create-user.validation.ts @@ -0,0 +1,84 @@ +import { UsersType } from "@/generated/prisma/enums"; +import Joi from "joi"; +const translationSchema = Joi.object({ + lang_id: Joi.number().required().messages({ + "any.required": "Language is required", + }), + firstName: Joi.string().max(255).required().messages({ + "string.base": "First name must be a string", + "string.empty": "First name cannot be empty", + "any.required": "First name is required", + }), + lastName: Joi.string().max(255).required().messages({ + "string.base": "Last name must be a string", + "string.empty": "Last name cannot be empty", + "any.required": "Last name is required", + }), + bio: Joi.string().max(255).allow("").optional().messages({ + "string.base": "Bio must be a string", + "string.empty": "Bio cannot be empty", + }), + excerpt: Joi.string().max(255).allow("").optional().messages({ + "string.base": "Bio must be a string", + "string.empty": "Bio cannot be empty", + }), + position: Joi.string().max(255).optional().messages({ + "string.base": "Position must be a string", + "string.empty": "Position cannot be empty", + "any.required": "Position is required", + }), + expertise: Joi.string().max(500).allow(null, "").messages({ + "string.base": "Expertise must be a string", + }), +}); + +export const CreateUserValidationSchema = Joi.object({ + // slug:Joi.string().required().messages({ + // 'string.base':"فرمت اسلاگ نامعتبر است", + // "string.empty":"اسلاگ نمیتواند خالی باشد", + // "any.required":"اسلاگ وارد نشده است" + // }), + email: Joi.string().email().required().messages({ + "any.required": "ایمیل الزامیست", + "string.email": "فرمت ایمیل اشتباه است", + "string.empty": "نام کاربر نمیتواند خالی باشد", + }), + medicalNumber: Joi.string().optional().allow("").messages({ + "any.required": "شماره نظام پزشکی الزامیست", + "string.email": "فرمت شماره نظام پزشکی اشتباه است", + "string.empty": "شماره نظام پزشکی نمیتواند خالی باشد", + }), + phone: Joi.string() + .optional() + .pattern(/^(?:\+98|0)?9\d{9}$/) + .messages({ + "string.base": "فرمت موبایل اشتباه است", + "string.empty": "موبایل کاربر نمیتواند خالی باشد", + "string.pattern.base": "فرمت موبایل اشتباه است", + }), + type: Joi.valid( + UsersType.DOCTOR, + UsersType.TRANSFER_TEAM, + UsersType.DEPARTMENT, + ) + .required() + .messages({ + "any.only": "دسته بندی کاربر اشتباه است", + "any.required": "دسته بندی کاربر وارد نشده است", + }), + imageId: Joi.number().optional().allow(null), + expertiseId: Joi.number().optional().allow(null).messages({ + "number.base": "فرمت تخصص اشتباه است", + "number.empty": "تخصص نمیتواند خالی باشد", + "any.required": "تخصص وارد نشده است", + }), + translations: Joi.array() + .items(translationSchema) + .min(1) + .required() + .messages({ + "array.base": "Translations must be an array", + "array.min": "At least one translation is required", + "any.required": "Translations are required", + }), +}); diff --git a/src/seed/default-info.seed.ts b/src/seed/default-info.seed.ts new file mode 100644 index 0000000..adeb82a --- /dev/null +++ b/src/seed/default-info.seed.ts @@ -0,0 +1,17 @@ +// import {prisma} from "../common/lib/prisma"; + +// const generatePanelConfig = async () => { +// await prisma.default.create({ +// data:{ +// address:"مازندران ، آمل ، بلوار مدرس ، خیابان آفتاب 47 ، بیمارستان شمال آمل", +// email:"ipd@shomal.hospital", +// hospitalPhone:"011-4492", +// instagramLink:"https://instagram.com/shomalhospital", +// mapAddress:"asd", +// linkedinLink:"https://www.linkedin.com/in/shomal-amol-hospital-7a1699392/", +// logoUrl:"", + +// } +// }); +// }; +// generatePanelConfig(); diff --git a/src/seed/insert-developer-account.ts b/src/seed/insert-developer-account.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/seed/panel-configs.seed.js b/src/seed/panel-configs.seed.js new file mode 100644 index 0000000..6730d5a --- /dev/null +++ b/src/seed/panel-configs.seed.js @@ -0,0 +1,89 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var prisma_1 = require("../common/lib/prisma"); +var generatePanelConfig = function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, prisma_1.prisma.panelConfig.createMany({ + data: [ + { + key: "email.enabled", + value: "true", + type: "BOOLEAN", + description: "Enable/disable email sending module", + }, + { + key: "sms.enabled", + value: "false", + type: "BOOLEAN", + description: "Enable/disable SMS sending module", + }, + { + key: "upload.document.maxSize", + value: "5242880", + type: "NUMBER", + description: "Max upload Documents size in bytes (5MB)", + }, + { + key: "upload.document.formats", + value: "[application/pdf,application/zip,application/x-rar-compressed,application/dicom,text/plain]", + type: "STRING_ARRAY", + description: "Max upload size in bytes (5MB)", + }, + { + key: "upload.profile.maxSize", + value: "5242880", + type: "NUMBER", + description: "Max upload profile picture size in bytes (5MB)", + }, + { + key: "upload.profile.formats", + value: "[png,jpg,webp]", + type: "STRING_ARRAY", + description: "Max upload profile picture size in bytes (5MB)", + }, + ], + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); +}); }; +generatePanelConfig(); diff --git a/src/seed/panel-configs.seed.ts b/src/seed/panel-configs.seed.ts new file mode 100644 index 0000000..1778b73 --- /dev/null +++ b/src/seed/panel-configs.seed.ts @@ -0,0 +1,47 @@ +import {prisma} from "../common/lib/prisma"; + +const generatePanelConfig = async () => { + await prisma.panelConfig.createMany({ + data: [ + { + key: "email.enabled", + value: "true", + type: "BOOLEAN", + description: "Enable/disable email sending module", + }, + { + key: "sms.enabled", + value: "false", + type: "BOOLEAN", + description: "Enable/disable SMS sending module", + }, + { + key: "upload.document.maxSize", + value: "5242880", + type: "NUMBER", + description: "Max upload Documents size in bytes (5MB)", + }, + + { + key: "upload.document.formats", + value: + "[application/pdf,application/zip,application/x-rar-compressed,application/dicom,text/plain]", + type: "STRING_ARRAY", + description: "Max upload size in bytes (5MB)", + }, + { + key: "upload.profile.maxSize", + value: "5242880", + type: "NUMBER", + description: "Max upload profile picture size in bytes (5MB)", + }, + { + key: "upload.profile.formats", + value: "[png,jpg,webp]", + type: "STRING_ARRAY", + description: "Max upload profile picture size in bytes (5MB)", + }, + ], + }); +}; +generatePanelConfig(); diff --git a/src/seed/save-countries.seed.js b/src/seed/save-countries.seed.js new file mode 100644 index 0000000..85e219b --- /dev/null +++ b/src/seed/save-countries.seed.js @@ -0,0 +1,56 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var variables_1 = require("../common/constants/variables"); +var prisma_1 = require("../common/lib/prisma"); +var generateCountries = function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, prisma_1.prisma.countries.createMany({ + data: variables_1.allCountries.map(function (country) { return ({ + name: country.label, + callCode: country.code, + }); }), + })]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); +}); }; +generateCountries(); diff --git a/src/seed/save-countries.seed.ts b/src/seed/save-countries.seed.ts new file mode 100644 index 0000000..89fee9c --- /dev/null +++ b/src/seed/save-countries.seed.ts @@ -0,0 +1,12 @@ +import {allCountries} from "../common/constants/variables"; +import {prisma} from "../common/lib/prisma"; + +const generateCountries = async () => { + await prisma.countries.createMany({ + data: allCountries.map((country) => ({ + name: country.label, + callCode: country.code, + })), + }); +}; +generateCountries(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7a8b169 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,122 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + // "@/services/*":["services/*"], + // "@/infrastructure/*":["infrastructure/*"], + // "@/modules/*":["modules/*"], + // "@/common/*":["common/*"], + // "@/core/*":["core/*"], + // "@/shared/*":["shared/*"], + // "@/app/*":["app/*"], + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +}