added keycloak structure
This commit is contained in:
parent
5b31cc0e0f
commit
ae7e86a3e0
|
|
@ -1,32 +1,37 @@
|
|||
{
|
||||
"name": "proj",
|
||||
"name": "saha",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "proj",
|
||||
"name": "saha",
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.3",
|
||||
"@nestjs/common": "^11.1.3",
|
||||
"@nestjs/common": "^11.1.5",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.3",
|
||||
"@nestjs/core": "^11.1.5",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.5",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"bullmq": "^5.56.7",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"glob": "^11.0.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^3.2.0",
|
||||
"libphonenumber-js": "^1.12.9",
|
||||
"mongodb": "^6.17.0",
|
||||
"mongoose": "^8.16.3",
|
||||
"mongoose-paginate-v2": "^1.9.1",
|
||||
"nestjs-paginate": "^12.5.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
|
|
@ -2691,9 +2696,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nestjs/common": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.3.tgz",
|
||||
"integrity": "sha512-ogEK+GriWodIwCw6buQ1rpcH4Kx+G7YQ9EwuPySI3rS05pSdtQ++UhucjusSI9apNidv+QURBztJkRecwwJQXg==",
|
||||
"version": "11.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.5.tgz",
|
||||
"integrity": "sha512-DQpWdr3ShO0BHWkHl3I4W/jR6R3pDtxyBlmrpTuZF+PXxQyBXNvsUne0Wyo6QHPEDi+pAz9XchBFoKbqOhcdTg==",
|
||||
"dependencies": {
|
||||
"file-type": "21.0.0",
|
||||
"iterare": "1.2.1",
|
||||
|
|
@ -2746,9 +2751,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nestjs/core": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz",
|
||||
"integrity": "sha512-5lTni0TCh8x7bXETRD57pQFnKnEg1T6M+VLE7wAmyQRIecKQU+2inRGZD+A4v2DC1I04eA0WffP0GKLxjOKlzw==",
|
||||
"version": "11.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.5.tgz",
|
||||
"integrity": "sha512-Qr25MEY9t8VsMETy7eXQ0cNXqu0lzuFrrTr+f+1G57ABCtV5Pogm7n9bF71OU2bnkDD32Bi4hQLeFR90cku3Tw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxt/opencollective": "0.4.1",
|
||||
|
|
@ -2815,6 +2820,15 @@
|
|||
"rxjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/passport": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
|
||||
"integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"passport": "^0.5.0 || ^0.6.0 || ^0.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "11.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz",
|
||||
|
|
@ -3539,7 +3553,6 @@
|
|||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
|
|
@ -3549,7 +3562,6 @@
|
|||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
|
|
@ -3618,8 +3630,7 @@
|
|||
"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
|
||||
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
|
|
@ -3665,7 +3676,6 @@
|
|||
"version": "9.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/ms": "*",
|
||||
"@types/node": "*"
|
||||
|
|
@ -3680,20 +3690,17 @@
|
|||
"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
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
||||
},
|
||||
"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
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz",
|
||||
"integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
|
|
@ -3701,20 +3708,17 @@
|
|||
"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
|
||||
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="
|
||||
},
|
||||
"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
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
|
|
@ -3724,7 +3728,6 @@
|
|||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
|
||||
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/node": "*",
|
||||
|
|
@ -5249,8 +5252,7 @@
|
|||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
|
|
@ -5286,6 +5288,16 @@
|
|||
"fastq": "^1.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||
|
|
@ -6000,7 +6012,6 @@
|
|||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
|
|
@ -6311,7 +6322,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
|
@ -6539,7 +6549,6 @@
|
|||
"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==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
|
|
@ -7349,6 +7358,25 @@
|
|||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
|
|
@ -7410,7 +7438,6 @@
|
|||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
|
|
@ -7435,7 +7462,6 @@
|
|||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
|
@ -7444,7 +7470,6 @@
|
|||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
|
|
@ -8960,6 +8985,14 @@
|
|||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.9",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -9093,6 +9126,44 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jwks-rsa": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz",
|
||||
"integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==",
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/jsonwebtoken": "^9.0.4",
|
||||
"debug": "^4.3.4",
|
||||
"jose": "^4.15.4",
|
||||
"limiter": "^1.1.5",
|
||||
"lru-memoizer": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/jwks-rsa/node_modules/@types/express": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz",
|
||||
"integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
|
|
@ -9201,6 +9272,11 @@
|
|||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/limiter": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
|
||||
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
|
|
@ -9254,6 +9330,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
|
|
@ -9348,6 +9429,31 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-memoizer": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
|
||||
"integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
|
||||
"dependencies": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lru-cache": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-memoizer/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-memoizer/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
|
||||
|
|
@ -10151,6 +10257,40 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
|
|
@ -10217,6 +10357,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/peek-readable": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz",
|
||||
|
|
@ -10597,6 +10742,11 @@
|
|||
"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=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -12589,8 +12739,7 @@
|
|||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
|
|
@ -12687,6 +12836,14 @@
|
|||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
|
|
|
|||
13
package.json
13
package.json
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "proj",
|
||||
"name": "saha",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
|
|
@ -21,23 +21,28 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.3",
|
||||
"@nestjs/common": "^11.1.3",
|
||||
"@nestjs/common": "^11.1.5",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.3",
|
||||
"@nestjs/core": "^11.1.5",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.5",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"bullmq": "^5.56.7",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"glob": "^11.0.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^3.2.0",
|
||||
"libphonenumber-js": "^1.12.9",
|
||||
"mongodb": "^6.17.0",
|
||||
"mongoose": "^8.16.3",
|
||||
"mongoose-paginate-v2": "^1.9.1",
|
||||
"nestjs-paginate": "^12.5.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { UpdateAnswerDto } from './dto/update-answer.dto';
|
|||
import { Answer } from './entity/answer.entity';
|
||||
import { AuthRequest } from '../middleware/jwtMiddleware';
|
||||
|
||||
@Controller('answers')
|
||||
@Controller('answer')
|
||||
export class AnswerController {
|
||||
constructor(private readonly answerService: AnswerService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateAnswerDto, @Request() req: AuthRequest): Promise<Answer> {
|
||||
return this.answerService.create(body, req.user);
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { AnswerService } from './answer.service';
|
||||
import { AnswerController } from './answer.controller';
|
||||
import { Answer } from './entity/answer.entity';
|
||||
import { Question } from '../question/entity/question.entity';
|
||||
import { Participant } from '../participant/entity/participant.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Answer]),
|
||||
TypeOrmModule.forFeature([Answer, Question, Participant]),
|
||||
],
|
||||
controllers: [AnswerController],
|
||||
providers: [AnswerService],
|
||||
|
|
|
|||
|
|
@ -5,22 +5,37 @@ import { Answer } from './entity/answer.entity';
|
|||
import { CreateAnswerDto } from './dto/create-answer.dto';
|
||||
import { UpdateAnswerDto } from './dto/update-answer.dto';
|
||||
import { AuthRequest } from '../middleware/jwtMiddleware';
|
||||
import { Question } from '../question/entity/question.entity';
|
||||
import { Participant } from '../participant/entity/participant.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AnswerService {
|
||||
constructor(
|
||||
@InjectRepository(Answer)
|
||||
private readonly answerRepository: Repository<Answer>,
|
||||
@InjectRepository(Question)
|
||||
private readonly questionRepository: Repository<Question>,
|
||||
@InjectRepository(Participant)
|
||||
private readonly participantRepository: Repository<Participant>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateAnswerDto, user: AuthRequest['user']): Promise<Answer> {
|
||||
const question = await this.questionRepository.findOne({ where: { id: data.questionId } });
|
||||
if (!question) {
|
||||
throw new NotFoundException('Question with this ID not found');
|
||||
}
|
||||
|
||||
const participant = await this.participantRepository.findOne({ where: { userId: user.sub } });
|
||||
if (!participant) {
|
||||
throw new NotFoundException('Participant with this ID not found');
|
||||
}
|
||||
|
||||
const answer = this.answerRepository.create({
|
||||
...data,
|
||||
participantId: user.sub,
|
||||
participantId: participant.id,
|
||||
});
|
||||
return await this.answerRepository.save(answer);
|
||||
}
|
||||
|
||||
async findAll(page = 1, limit = 10): Promise<{
|
||||
docs: Answer[];
|
||||
totalDocs: number;
|
||||
|
|
@ -67,17 +82,13 @@ export class AnswerService {
|
|||
}
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateAnswerDto): Promise<Answer | null> {
|
||||
try {
|
||||
await this.answerRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
const updatedAnswer = await this.answerRepository.findOne({ where: { id } });
|
||||
if (!updatedAnswer) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
return updatedAnswer;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update answer: ${error.message}`);
|
||||
async update(id: string, data: UpdateAnswerDto): Promise<Answer> {
|
||||
const result = await this.answerRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
const updatedAnswer = await this.answerRepository.findOne({ where: { id } });
|
||||
return updatedAnswer!;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { IsString, IsUUID } from 'class-validator';
|
|||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
|
||||
export class CreateAnswerDto extends BaseDto {
|
||||
@IsUUID('4')
|
||||
participantId: string;
|
||||
// @IsUUID('4')
|
||||
// participantId: string;
|
||||
|
||||
@IsUUID('4')
|
||||
questionId: string;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { typeOrmConfig } from './config/database.config';
|
||||
|
||||
import { ParticipantModule } from './participant/participant.module';
|
||||
import { QuestionModule } from './question/question.module';
|
||||
import { AnswerModule } from './answer/answer.module';
|
||||
|
|
@ -13,16 +17,15 @@ import { FormSectionModule } from './formSection/formSection.module';
|
|||
import { OptionModule } from './option/option.module';
|
||||
import { ParticipanGrouptModule } from './participantGroup/participantGroup.module';
|
||||
import { RealmModule } from './realm/realm.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { typeOrmConfig } from './config/database.config';
|
||||
import { FormResultWorkerModule } from './formResult/formResultWorker.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot(typeOrmConfig),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true, // Makes ConfigModule available globally
|
||||
isGlobal: true,
|
||||
}),
|
||||
TypeOrmModule.forRoot(typeOrmConfig),
|
||||
ParticipantModule,
|
||||
QuestionModule,
|
||||
AnswerModule,
|
||||
|
|
@ -35,8 +38,9 @@ import { FormResultWorkerModule } from './formResult/formResultWorker.module';
|
|||
ParticipanGrouptModule,
|
||||
RealmModule,
|
||||
FormResultWorkerModule,
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { UpdateAttachmentDto } from './dto/update_attachment.dto';
|
|||
import { Attachment } from './entity/attachment.entity';
|
||||
import { AuthRequest } from '../middleware/jwtMiddleware';
|
||||
|
||||
@Controller('attachments')
|
||||
@Controller('attachment')
|
||||
export class AttachmentController {
|
||||
constructor(private readonly attachmentService: AttachmentService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateAttachmentDto, @Request() req: AuthRequest): Promise<Attachment> {
|
||||
return this.attachmentService.create(body, req.user);
|
||||
|
|
|
|||
|
|
@ -70,19 +70,16 @@ export class AttachmentService {
|
|||
}
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateAttachmentDto): Promise<Attachment | null> {
|
||||
try {
|
||||
await this.attachmentRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
const updatedAttachment = await this.attachmentRepository.findOne({ where: { id } });
|
||||
if (!updatedAttachment) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
return updatedAttachment;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update attachment: ${error.message}`);
|
||||
async update(id: string, data: UpdateAttachmentDto): Promise<Attachment> {
|
||||
const result = await this.attachmentRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
const updatedAttachment = await this.attachmentRepository.findOne({ where: { id } });
|
||||
return updatedAttachment!;
|
||||
}
|
||||
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
try {
|
||||
const result = await this.attachmentRepository.delete({ id });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule],
|
||||
providers: [JwtStrategy],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import * as jwksRsa from 'jwks-rsa';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
// Extract the Bearer token from Authorization header
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKeyProvider: jwksRsa.passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 10,
|
||||
jwksUri: 'http://localhost:8080/realms/testRealm/protocol/openid-connect/certs',
|
||||
}),
|
||||
audience: 'saha', // Keycloak client ID
|
||||
issuer: 'http://localhost:8080/realms/testRealm',
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
return {
|
||||
userId: payload.sub,
|
||||
username: payload.preferred_username,
|
||||
email: payload.email,
|
||||
roles: payload.realm_access?.roles || [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ export class CreateFormDto extends BaseDto {
|
|||
description: string;
|
||||
|
||||
@IsBoolean()
|
||||
isEnded: boolean;
|
||||
@IsOptional()
|
||||
isEnded?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { CreateFormDto } from './dto/create-form.dto';
|
|||
import { UpdateFormDto } from './dto/update-form.dto';
|
||||
import { Form } from './entity/form.entity';
|
||||
|
||||
@Controller('forms')
|
||||
@Controller('form')
|
||||
export class FormController {
|
||||
constructor(private readonly formService: FormService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateFormDto): Promise<Form> {
|
||||
return this.formService.create(body);
|
||||
|
|
|
|||
|
|
@ -18,14 +18,16 @@ export class FormService {
|
|||
const form = this.formRepo.create({
|
||||
...data,
|
||||
});
|
||||
const savedForm = await this.formRepo.save(form);
|
||||
await this.formResultService.create({
|
||||
formId: savedForm.id,
|
||||
numParticipants: 0,
|
||||
numQuestions: 0,
|
||||
numAnswers: 0,
|
||||
numComplete: 0,
|
||||
opinions: [],
|
||||
}); // Create corresponding FormResult instance and schedule statistics
|
||||
return await this.formRepo.save(form);
|
||||
return savedForm;
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -101,6 +103,11 @@ export class FormService {
|
|||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Form with ID "${id}" not found`);
|
||||
}
|
||||
const updatedForm = await this.findById(id);
|
||||
if (updatedForm && data.isEnded !== undefined) {
|
||||
if (updatedForm.isEnded)
|
||||
await this.formResultService.scheduleFormStatistics(id); // Trigger scheduler update
|
||||
}
|
||||
return await this.findById(id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { CreateFormPageDto } from './dto/create-formPage.dto';
|
|||
import { UpdateFormPageDto } from './dto/update-formPage.dto';
|
||||
import { FormPage } from './entity/formPage.entity';
|
||||
|
||||
@Controller('formPages')
|
||||
@Controller('formPage')
|
||||
export class FormPageController {
|
||||
constructor(private readonly formPageService: FormPageService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateFormPageDto): Promise<FormPage> {
|
||||
return this.formPageService.create(body);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,6 @@ export class FormPageService {
|
|||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`FormPage with ID "${id}" not found`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { IsNumber, IsString, IsUUID, ValidateNested, IsArray } from 'class-validator';
|
||||
import { IsNumber, IsString, IsUUID, ValidateNested, IsArray, IsOptional } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
|
||||
|
|
@ -24,6 +24,9 @@ class CreateOpinionDto {
|
|||
|
||||
// Main DTO for creating a FormResult
|
||||
export class CreateFormResultDto extends BaseDto {
|
||||
@IsUUID('4')
|
||||
formId: string;
|
||||
|
||||
@IsNumber()
|
||||
numParticipants: number;
|
||||
|
||||
|
|
@ -36,8 +39,9 @@ export class CreateFormResultDto extends BaseDto {
|
|||
@IsNumber()
|
||||
numComplete: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateOpinionDto)
|
||||
opinions: CreateOpinionDto[];
|
||||
opinions?: CreateOpinionDto[];
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Entity, Column } from 'typeorm';
|
||||
import { Entity, Column, Unique } from 'typeorm';
|
||||
import { BaseEntity } from '../../_core/entity/_base.entity';
|
||||
|
||||
// Options class (embedded)
|
||||
|
|
@ -22,6 +22,7 @@ export class Opinion {
|
|||
}
|
||||
|
||||
@Entity()
|
||||
@Unique(['formId'])
|
||||
export class FormResult extends BaseEntity {
|
||||
@Column({ type: 'uuid' })
|
||||
formId: string;
|
||||
|
|
@ -38,6 +39,6 @@ export class FormResult extends BaseEntity {
|
|||
@Column({ type: 'int', default: 0 })
|
||||
numComplete: number;
|
||||
|
||||
@Column(() => Opinion)
|
||||
opinions: Opinion[];
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
opinions?: Opinion[];
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { FormResult } from './entity/formResult.entity';
|
|||
export class FormResultController {
|
||||
constructor(private readonly formResultService: FormResultService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateFormResultDto): Promise<FormResult> {
|
||||
return this.formResultService.create(body);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { CreateFormResultDto } from './dto/create-formResult.dto';
|
|||
import { UpdateFormResultDto } from './dto/update-formResult.dto';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Queue } from 'bullmq';
|
||||
import { Form } from '../form/entity/form.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FormResultService {
|
||||
|
|
@ -13,6 +14,8 @@ export class FormResultService {
|
|||
constructor(
|
||||
@InjectRepository(FormResult)
|
||||
private readonly formResultRepo: Repository<FormResult>,
|
||||
@InjectRepository(Form)
|
||||
private readonly formRepo: Repository<Form>,
|
||||
) {
|
||||
this.statisticsQueue = new Queue('form-statistics', {
|
||||
connection: {
|
||||
|
|
@ -83,7 +86,6 @@ export class FormResultService {
|
|||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`FormResult with ID "${id}" not found`);
|
||||
}
|
||||
|
|
@ -106,6 +108,14 @@ export class FormResultService {
|
|||
}
|
||||
|
||||
async scheduleFormStatistics(formId: string): Promise<void> {
|
||||
const form = await this.formRepo.findOne({ where: { id: formId } });
|
||||
if (!form) {
|
||||
throw new NotFoundException('Form with this ID not found');
|
||||
}
|
||||
if (form.isEnded) {
|
||||
await this.statisticsQueue.remove(`form-statistics-${formId}`);
|
||||
return; // Stop scheduler if form is ended
|
||||
}
|
||||
await this.statisticsQueue.upsertJobScheduler(
|
||||
`form-statistics-${formId}`,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -66,26 +66,40 @@ export class FormResultWorker {
|
|||
const questionIds = formSections.flatMap(section => section.questionIds || []);
|
||||
const numQuestions = questionIds.length;
|
||||
|
||||
// Skip remaining calculations if no question IDs are found
|
||||
if (!questionIds.length) {
|
||||
await this.formResultRepo.update({ formId }, {
|
||||
numQuestions: 0,
|
||||
numParticipants: 0,
|
||||
numAnswers: 0,
|
||||
numComplete: 0,
|
||||
opinions: [],
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Compute numParticipants
|
||||
const numParticipantsResult = await this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('COUNT(DISTINCT answer.participantId)', 'count')
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds: questionIds.length ? questionIds : ['none'] })
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds })
|
||||
.getRawOne();
|
||||
const numParticipants = numParticipantsResult ? Number(numParticipantsResult.count) : 0;
|
||||
|
||||
// 4. Compute numAnswers
|
||||
const numAnswers = await this.answerRepository.count({
|
||||
where: { questionId: In(questionIds.length ? questionIds : ['none']) },
|
||||
where: { questionId: In(questionIds) },
|
||||
});
|
||||
|
||||
// 5. Compute numComplete
|
||||
const subQuery = this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('answer.participantId')
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds: questionIds.length ? questionIds : ['none'] })
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds })
|
||||
.groupBy('answer.participantId')
|
||||
.having('COUNT(DISTINCT answer.questionId) = :numQuestions', { numQuestions: numQuestions || 1 });
|
||||
.having('COUNT(DISTINCT answer.questionId) = :numQuestions', { numQuestions });
|
||||
|
||||
const numCompleteParticipantsResult = await this.answerRepository.manager
|
||||
.createQueryBuilder()
|
||||
.select('COUNT(*)', 'count')
|
||||
|
|
@ -96,7 +110,7 @@ export class FormResultWorker {
|
|||
|
||||
// 6. Compute opinions with question options and their vote counts
|
||||
const questions = await this.questionRepository.find({
|
||||
where: { id: In(questionIds.length ? questionIds : ['none']) },
|
||||
where: { id: In(questionIds) },
|
||||
relations: ['options'],
|
||||
});
|
||||
|
||||
|
|
@ -108,7 +122,7 @@ export class FormResultWorker {
|
|||
.select('answer.value', 'value')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('answer.questionId = :questionId', { questionId: question.id })
|
||||
.andWhere('answer.value IN (:...optionIds)', { optionIds: optionIds.length ? optionIds : ['none'] })
|
||||
.andWhere('answer.value IN (:...optionIds)', { optionIds })
|
||||
.groupBy('answer.value')
|
||||
.getRawMany();
|
||||
|
||||
|
|
@ -127,26 +141,15 @@ export class FormResultWorker {
|
|||
}),
|
||||
);
|
||||
|
||||
// 7. Find or create FormResult
|
||||
let formResult = await this.formResultRepo.findOne({ where: { formId } });
|
||||
if (!formResult) {
|
||||
formResult = this.formResultRepo.create({
|
||||
formId,
|
||||
numQuestions,
|
||||
numParticipants,
|
||||
numAnswers,
|
||||
numComplete,
|
||||
opinions,
|
||||
});
|
||||
} else {
|
||||
formResult.numQuestions = numQuestions;
|
||||
formResult.numParticipants = numParticipants;
|
||||
formResult.numAnswers = numAnswers;
|
||||
formResult.numComplete = numComplete;
|
||||
formResult.opinions = opinions;
|
||||
}
|
||||
|
||||
// 8. Save
|
||||
await this.formResultRepo.save(formResult);
|
||||
// 7. Update existing FormResult tuple
|
||||
await this.formResultRepo.update({ formId }, {
|
||||
numQuestions,
|
||||
numParticipants,
|
||||
numAnswers,
|
||||
numComplete,
|
||||
opinions,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,6 @@ export class FormSection extends BaseEntity {
|
|||
@Column('simple-array', { nullable: true })
|
||||
questionIds: string[];
|
||||
|
||||
@Column(() => DisplayCondition) // needs to be checked
|
||||
displayCondition: DisplayCondition[];
|
||||
// @Column(() => DisplayCondition) // needs to be checked
|
||||
// displayCondition: DisplayCondition[];
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ import { CreateFormSectionDto } from './dto/create-formSection.dto';
|
|||
import { UpdateFormSectionDto } from './dto/update-formSection.dto';
|
||||
import { FormSection } from './entity/formSection.entity';
|
||||
|
||||
@Controller('formSections')
|
||||
@Controller('formSection')
|
||||
export class FormSectionController {
|
||||
constructor(private readonly formSectionService: FormSectionService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateFormSectionDto): Promise<FormSection> {
|
||||
return this.formSectionService.create(body);
|
||||
|
|
|
|||
|
|
@ -68,17 +68,13 @@ export class FormSectionService {
|
|||
id: string,
|
||||
data: UpdateFormSectionDto,
|
||||
): Promise<FormSection | null> {
|
||||
// TypeORM's update method returns UpdateResult, not the entity itself.
|
||||
// We update and then fetch the updated entity.
|
||||
const result = await this.formSectionRepo.update(
|
||||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`FormSection with ID "${id}" not found`);
|
||||
}
|
||||
// Fetch and return the updated entity to ensure all fields are fresh.
|
||||
return await this.findById(id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { CreateOptionDto } from './dto/create-option.dto';
|
|||
import { UpdateOptionDto } from './dto/update-option.dto';
|
||||
import { Option } from './entity/option.entity';
|
||||
|
||||
@Controller('options')
|
||||
@Controller('option')
|
||||
export class OptionController {
|
||||
constructor(private readonly optionService: OptionService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateOptionDto): Promise<Option> {
|
||||
return this.optionService.create(body);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { IsString, IsEnum, IsOptional, IsUUID } from 'class-validator';
|
|||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
|
||||
export class CreateParticipantDto extends BaseDto {
|
||||
@IsUUID('4')
|
||||
userId: string;
|
||||
// @IsUUID('4')
|
||||
// userId: string;
|
||||
|
||||
@IsString()
|
||||
displayName: string;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, Request } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { ParticipantService } from './participant.service';
|
||||
import { CreateParticipantDto } from './dto/create-participant.dto';
|
||||
import { UpdateParticipantDto } from './dto/update-participant.dto';
|
||||
import { Participant } from './entity/participant.entity';
|
||||
import { AuthRequest } from '../middleware/jwtMiddleware';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
|
||||
@Controller('participants')
|
||||
export class ParticipantController {
|
||||
constructor(private readonly participantService: ParticipantService) {}
|
||||
|
||||
@Post()
|
||||
|
||||
// @UseGuards(JwtAuthGuard)
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async create(@Body() body: CreateParticipantDto, @Request() req: AuthRequest): Promise<Participant> {
|
||||
return this.participantService.create(body, req.user);
|
||||
|
|
@ -45,7 +61,7 @@ export class ParticipantController {
|
|||
}
|
||||
|
||||
@Delete(':userId')
|
||||
async remove(@Param('userId') userId: string): Promise<void> {
|
||||
async remove(@Param('userId') userId: string): Promise<string> {
|
||||
return this.participantService.remove(userId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, InternalServerErrorException, ConflictException } from '@nestjs/common';
|
||||
import { Injectable, InternalServerErrorException, ConflictException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateParticipantDto } from './dto/create-participant.dto';
|
||||
|
|
@ -19,14 +19,12 @@ export class ParticipantService {
|
|||
if (existingParticipant) {
|
||||
throw new ConflictException(`Participant with userId ${user.sub} already exists`);
|
||||
}
|
||||
|
||||
const participant = this.participantRepository.create({
|
||||
...data,
|
||||
userId: user.sub,
|
||||
role: data.role || 'user',
|
||||
});
|
||||
|
||||
return await this.participantRepository.save(participant);
|
||||
return this.participantRepository.save(participant);
|
||||
} catch (error) {
|
||||
if (error instanceof ConflictException) {
|
||||
throw error;
|
||||
|
|
@ -77,18 +75,20 @@ export class ParticipantService {
|
|||
}
|
||||
}
|
||||
|
||||
async update(userId: string, data: UpdateParticipantDto): Promise<Participant | null> {
|
||||
try {
|
||||
await this.participantRepository.update({ userId }, { ...data, updatedAt: new Date() });
|
||||
return await this.participantRepository.findOne({ where: { userId } });
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to update participant: ${error.message}`);
|
||||
async update(userId: string, data: UpdateParticipantDto): Promise<Participant> {
|
||||
const result = await this.participantRepository.update({ userId }, { ...data, updatedAt: new Date() });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Participant with userId "${userId}" not found`);
|
||||
}
|
||||
const updatedParticipant = await this.participantRepository.findOne({ where: { userId } });
|
||||
return updatedParticipant!;
|
||||
}
|
||||
|
||||
async remove(userId: string): Promise<void> {
|
||||
|
||||
async remove(userId: string): Promise<string> {
|
||||
try {
|
||||
await this.participantRepository.delete({ userId });
|
||||
return "success"
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to delete participant: ${error.message}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { CreateParticipantGroupDto } from './dto/create-participantGroup.dto';
|
|||
import { UpdateParticipantGroupDto } from './dto/update-participantGroup.dto';
|
||||
import { ParticipantGroup } from './entity/participantGroup.entity';
|
||||
|
||||
@Controller('participantGroupGroups')
|
||||
@Controller('participantGroup')
|
||||
export class ParticipantGroupController {
|
||||
constructor(private readonly participantGroupService: ParticipantGroupService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async create(@Body() body: CreateParticipantGroupDto): Promise<ParticipantGroup> {
|
||||
return this.participantGroupService.create(body);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { CreateQuestionDto } from './dto/create-question.dto';
|
|||
import { UpdateQuestionDto } from './dto/update-question.dto';
|
||||
import { Question } from './entity/question.entity';
|
||||
|
||||
@Controller('questions')
|
||||
@Controller('question')
|
||||
export class QuestionController {
|
||||
constructor(private readonly questionService: QuestionService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateQuestionDto): Promise<Question> {
|
||||
return this.questionService.create(body);
|
||||
|
|
|
|||
|
|
@ -45,11 +45,16 @@ export class QuestionService {
|
|||
return question;
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateQuestionDto): Promise<Question | null> {
|
||||
await this.questionRepo.update(id, data);
|
||||
return this.findById(id);
|
||||
async update(id: string, data: UpdateQuestionDto): Promise<Question> {
|
||||
const result = await this.questionRepo.update(id, { ...data, updatedAt: new Date() });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Question with ID "${id}" not found`);
|
||||
}
|
||||
const updatedQuestion = await this.findById(id);
|
||||
return updatedQuestion!;
|
||||
}
|
||||
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.questionRepo.delete(id);
|
||||
if (result.affected === 0) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ export class CreateRealmDto extends BaseDto {
|
|||
@IsUUID('4', { each: true }) // I'd say it's not needed, cause form has participants
|
||||
participantIds?: string[];
|
||||
|
||||
@IsUUID('4')
|
||||
ownerId: string;
|
||||
// @IsUUID('4')
|
||||
// ownerId: string;
|
||||
}
|
||||
|
|
@ -1,17 +1,18 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus, Request} from '@nestjs/common';
|
||||
import { RealmService } from './realm.service';
|
||||
import { CreateRealmDto } from './dto/create-realm.dto';
|
||||
import { UpdateRealmDto } from './dto/update-realm.dto';
|
||||
import { Realm } from './entity/realm.entity';
|
||||
import { AuthRequest } from '../middleware/jwtMiddleware';
|
||||
|
||||
@Controller('realms')
|
||||
@Controller('realm')
|
||||
export class RealmController {
|
||||
constructor(private readonly realmService: RealmService) {}
|
||||
|
||||
@Post()
|
||||
@Post('create')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async create(@Body() body: CreateRealmDto): Promise<Realm> {
|
||||
return this.realmService.create(body);
|
||||
async create(@Body() body: CreateRealmDto, @Request() req: AuthRequest): Promise<Realm> {
|
||||
return this.realmService.create(body, req.user);
|
||||
}
|
||||
|
||||
@Get('findAll')
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { UpdateRealmDto } from './dto/update-realm.dto';
|
|||
import { Participant } from '../participant/entity/participant.entity';
|
||||
import axios from 'axios';
|
||||
import { decode } from 'jsonwebtoken';
|
||||
import { AuthRequest } from '../middleware/jwtMiddleware';
|
||||
|
||||
@Injectable()
|
||||
export class RealmService {
|
||||
|
|
@ -17,57 +18,16 @@ export class RealmService {
|
|||
private readonly participantRepo: Repository<Participant>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateRealmDto): Promise<Realm> {
|
||||
try {
|
||||
// Here we send a sample request, in final implementation, there'll be no need for that, we'll get the decoded token
|
||||
const authResponse = await axios.post(
|
||||
'https://auth.didvan.com/realms/didvan/protocol/openid-connect/token',
|
||||
new URLSearchParams({
|
||||
client_id: 'didvan-app',
|
||||
username: 'bob',
|
||||
password: 'developer_password',
|
||||
grant_type: 'password',
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Decode the access token to extract the 'sub' claim
|
||||
const token = authResponse.data.access_token;
|
||||
const decodedToken: any = decode(token);
|
||||
if (!decodedToken || !decodedToken.sub) {
|
||||
throw new InternalServerErrorException('Failed to decode token or extract sub');
|
||||
}
|
||||
|
||||
// Find or create a participant with userId set to sub
|
||||
let participant = await this.participantRepo.findOne({ where: { userId: decodedToken.sub } });
|
||||
if (!participant) {
|
||||
participant = this.participantRepo.create({
|
||||
displayName: data.title + ' Owner', // Default displayName
|
||||
role: 'admin', // Default role for owner
|
||||
userId: decodedToken.sub,
|
||||
metadata: data.metadata ,
|
||||
// status: 'active',
|
||||
});
|
||||
participant = await this.participantRepo.save(participant);
|
||||
}
|
||||
|
||||
// Create the realm with the participant as the owner
|
||||
const realm = this.realmRepo.create({
|
||||
...data,
|
||||
ownerId: participant.userId, // Set owner to the participant with userId = sub
|
||||
});
|
||||
|
||||
return await this.realmRepo.save(realm);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to create realm: ${error.message}`);
|
||||
}
|
||||
// service
|
||||
async create(data: CreateRealmDto, user: AuthRequest['user']): Promise<Realm> {
|
||||
const realm = this.realmRepo.create({
|
||||
...data,
|
||||
ownerId: user.sub, // we save userId as ownerId (not participantId)
|
||||
});
|
||||
return await this.realmRepo.save(realm);
|
||||
}
|
||||
|
||||
// Other methods (findAll, findById, update, remove) remain unchanged
|
||||
|
||||
async findAll(
|
||||
page = 1,
|
||||
limit = 10,
|
||||
|
|
@ -138,7 +98,6 @@ export class RealmService {
|
|||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Realm with ID "${id}" not found`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue