diff --git a/Backend/jest.config.js b/Backend/jest.config.js deleted file mode 100644 index 89ba304b..00000000 --- a/Backend/jest.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @type {import('jest').Config} */ -const config = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/tests'], - testMatch: ['**/*.test.ts'], - moduleNameMapper: { - '^@/(.*)$': '/src/$1' - }, - clearMocks: true, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.d.ts', - '!src/generated/**', - '!src/server.ts' - ], - transform: { - '^.+\\.tsx?$': ['ts-jest', { tsconfig: './tsconfig.test.json' }] - } -}; - -module.exports = config; diff --git a/Backend/package-lock.json b/Backend/package-lock.json index 7891b301..87a6afa3 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -8,10 +8,10 @@ "name": "e-learning-backend", "version": "1.0.0", "dependencies": { - "@node-rs/bcrypt": "^1.10.7", "@pdf-lib/fontkit": "^1.1.1", "@prisma/client": "^5.22.0", "@tsoa/runtime": "^6.4.0", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.2", @@ -33,7 +33,6 @@ "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", - "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.10.5", @@ -1365,37 +1364,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -2382,16 +2350,6 @@ } } }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -2453,16 +2411,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", @@ -2479,30 +2427,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/pattern": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", - "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern/node_modules/jest-regex-util": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", - "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -2702,16 +2626,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "license": "MIT", - "optional": true, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" } }, "node_modules/@noble/hashes": { @@ -2727,259 +2659,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@node-rs/bcrypt": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.10.7.tgz", - "integrity": "sha512-1wk0gHsUQC/ap0j6SJa2K34qNhomxXRcEe3T8cI5s+g6fgHBgLTN7U9LzWTG/HE6G4+2tWWLeCabk1wiYGEQSA==", - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@node-rs/bcrypt-android-arm-eabi": "1.10.7", - "@node-rs/bcrypt-android-arm64": "1.10.7", - "@node-rs/bcrypt-darwin-arm64": "1.10.7", - "@node-rs/bcrypt-darwin-x64": "1.10.7", - "@node-rs/bcrypt-freebsd-x64": "1.10.7", - "@node-rs/bcrypt-linux-arm-gnueabihf": "1.10.7", - "@node-rs/bcrypt-linux-arm64-gnu": "1.10.7", - "@node-rs/bcrypt-linux-arm64-musl": "1.10.7", - "@node-rs/bcrypt-linux-x64-gnu": "1.10.7", - "@node-rs/bcrypt-linux-x64-musl": "1.10.7", - "@node-rs/bcrypt-wasm32-wasi": "1.10.7", - "@node-rs/bcrypt-win32-arm64-msvc": "1.10.7", - "@node-rs/bcrypt-win32-ia32-msvc": "1.10.7", - "@node-rs/bcrypt-win32-x64-msvc": "1.10.7" - } - }, - "node_modules/@node-rs/bcrypt-android-arm-eabi": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.10.7.tgz", - "integrity": "sha512-8dO6/PcbeMZXS3VXGEtct9pDYdShp2WBOWlDvSbcRwVqyB580aCBh0BEFmKYtXLzLvUK8Wf+CG3U6sCdILW1lA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-android-arm64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.10.7.tgz", - "integrity": "sha512-UASFBS/CucEMHiCtL/2YYsAY01ZqVR1N7vSb94EOvG5iwW7BQO06kXXCTgj+Xbek9azxixrCUmo3WJnkJZ0hTQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-darwin-arm64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.10.7.tgz", - "integrity": "sha512-DgzFdAt455KTuiJ/zYIyJcKFobjNDR/hnf9OS7pK5NRS13Nq4gLcSIIyzsgHwZHxsJWbLpHmFc1H23Y7IQoQBw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-darwin-x64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.10.7.tgz", - "integrity": "sha512-SPWVfQ6sxSokoUWAKWD0EJauvPHqOGQTd7CxmYatcsUgJ/bruvEHxZ4bIwX1iDceC3FkOtmeHO0cPwR480n/xA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-freebsd-x64": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.10.7.tgz", - "integrity": "sha512-gpa+Ixs6GwEx6U6ehBpsQetzUpuAGuAFbOiuLB2oo4N58yU4AZz1VIcWyWAHrSWRs92O0SHtmo2YPrMrwfBbSw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.10.7.tgz", - "integrity": "sha512-kYgJnTnpxrzl9sxYqzflobvMp90qoAlaX1oDL7nhNTj8OYJVDIk0jQgblj0bIkjmoPbBed53OJY/iu4uTS+wig==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.10.7.tgz", - "integrity": "sha512-7cEkK2RA+gBCj2tCVEI1rDSJV40oLbSq7bQ+PNMHNI6jCoXGmj9Uzo7mg7ZRbNZ7piIyNH5zlJqutjo8hh/tmA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-musl": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.10.7.tgz", - "integrity": "sha512-X7DRVjshhwxUqzdUKDlF55cwzh+wqWJ2E/tILvZPboO3xaNO07Um568Vf+8cmKcz+tiZCGP7CBmKbBqjvKN/Pw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-x64-gnu": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.10.7.tgz", - "integrity": "sha512-LXRZsvG65NggPD12hn6YxVgH0W3VR5fsE/o1/o2D5X0nxKcNQGeLWnRzs5cP8KpoFOuk1ilctXQJn8/wq+Gn/Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-x64-musl": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.10.7.tgz", - "integrity": "sha512-tCjHmct79OfcO3g5q21ME7CNzLzpw1MAsUXCLHLGWH+V6pp/xTvMbIcLwzkDj6TI3mxK6kehTn40SEjBkZ3Rog==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.10.7.tgz", - "integrity": "sha512-4qXSihIKeVXYglfXZEq/QPtYtBUvR8d3S85k15Lilv3z5B6NSGQ9mYiNleZ7QHVLN2gEc5gmi7jM353DMH9GkA==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.10.7.tgz", - "integrity": "sha512-FdfUQrqmDfvC5jFhntMBkk8EI+fCJTx/I1v7Rj+Ezlr9rez1j1FmuUnywbBj2Cg15/0BDhwYdbyZ5GCMFli2aQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.10.7.tgz", - "integrity": "sha512-lZLf4Cx+bShIhU071p5BZft4OvP4PGhyp542EEsb3zk34U5GLsGIyCjOafcF/2DGewZL6u8/aqoxbSuROkgFXg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-x64-msvc": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.10.7.tgz", - "integrity": "sha512-hdw7tGmN1DxVAMTzICLdaHpXjy+4rxaxnBMgI8seG1JL5e3VcRGsd1/1vVDogVp2cbsmgq+6d6yAY+D9lW/DCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -3920,16 +3599,6 @@ "yarn": ">=1.9.4" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", @@ -4120,230 +3789,6 @@ "@types/istanbul-lib-report": "*" } }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@types/jest/node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.2.0", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "jest-util": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.2.0", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@types/jest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4741,6 +4186,12 @@ "license": "(Unlicense OR Apache-2.0)", "optional": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4777,6 +4228,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4854,6 +4317,26 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5044,6 +4527,20 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5391,6 +4888,15 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -5507,6 +5013,15 @@ "node": ">=12.20" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/color/node_modules/color-convert": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", @@ -5555,7 +5070,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -5603,6 +5117,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -5712,7 +5232,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5794,6 +5313,12 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -5813,6 +5338,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -6669,11 +6203,40 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "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==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -6700,6 +6263,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -6802,7 +6386,6 @@ "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", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -6836,7 +6419,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6847,7 +6429,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6957,6 +6538,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7005,6 +6592,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7096,7 +6696,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.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -8311,6 +7910,30 @@ "yallist": "^3.0.2" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -8501,6 +8124,37 @@ "node": ">=8" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -8560,6 +8214,32 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8659,6 +8339,21 @@ "node": ">=4" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8682,6 +8377,19 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8719,7 +8427,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8877,7 +8584,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9390,6 +9096,22 @@ "node": ">=10" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9517,6 +9239,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -9637,7 +9365,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC" }, "node_modules/simple-update-notifier": { @@ -10010,6 +9737,41 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -10151,6 +9913,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -10277,7 +10045,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, + "dev": true, "license": "0BSD" }, "node_modules/tsoa": { @@ -10533,6 +10301,22 @@ "@zxing/text-encoding": "0.9.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10569,6 +10353,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -10660,7 +10453,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/Backend/package.json b/Backend/package.json index 8f71c83c..392d6b82 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -44,7 +44,6 @@ "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", - "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.7", "@types/multer": "^1.4.12", "@types/node": "^22.10.5", diff --git a/Backend/pnpm-lock.yaml b/Backend/pnpm-lock.yaml index a30e07ad..f7f8d01b 100644 --- a/Backend/pnpm-lock.yaml +++ b/Backend/pnpm-lock.yaml @@ -78,9 +78,6 @@ importers: '@types/express': specifier: ^5.0.0 version: 5.0.6 - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.10 @@ -122,7 +119,7 @@ importers: version: 7.2.2 ts-jest: specifier: ^29.2.5 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -618,10 +615,6 @@ packages: node-notifier: optional: true - '@jest/diff-sequences@30.0.1': - resolution: {integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/environment@29.7.0': resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -630,10 +623,6 @@ packages: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/expect-utils@30.2.0': - resolution: {integrity: sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/expect@29.7.0': resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -642,18 +631,10 @@ packages: resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/get-type@30.1.0': - resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/globals@29.7.0': resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/pattern@30.0.1': - resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/reporters@29.7.0': resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -667,10 +648,6 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/schemas@30.0.5': - resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jest/source-map@29.6.3': resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -691,10 +668,6 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/types@30.2.0': - resolution: {integrity: sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -889,9 +862,6 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.34.48': - resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} - '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -1145,9 +1115,6 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@30.0.0': - resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1466,10 +1433,6 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - ci-info@4.4.0: - resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} - engines: {node: '>=8'} - cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -1766,10 +1729,6 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - expect@30.2.0: - resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - express-rate-limit@7.5.1: resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} engines: {node: '>= 16'} @@ -2159,10 +2118,6 @@ packages: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-diff@30.2.0: - resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-docblock@29.7.0: resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2191,26 +2146,14 @@ packages: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-matcher-utils@30.2.0: - resolution: {integrity: sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-message-util@29.7.0: resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-message-util@30.2.0: - resolution: {integrity: sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-mock@29.7.0: resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-mock@30.2.0: - resolution: {integrity: sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-pnp-resolver@1.2.3: resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} engines: {node: '>=6'} @@ -2224,10 +2167,6 @@ packages: resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-regex-util@30.0.1: - resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-resolve-dependencies@29.7.0: resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2252,10 +2191,6 @@ packages: resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-util@30.2.0: - resolution: {integrity: sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - jest-validate@29.7.0: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2649,10 +2584,6 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@30.2.0: - resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} - engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - prisma@5.22.0: resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} engines: {node: '>=16.13'} @@ -4096,8 +4027,6 @@ snapshots: - supports-color - ts-node - '@jest/diff-sequences@30.0.1': {} - '@jest/environment@29.7.0': dependencies: '@jest/fake-timers': 29.7.0 @@ -4109,10 +4038,6 @@ snapshots: dependencies: jest-get-type: 29.6.3 - '@jest/expect-utils@30.2.0': - dependencies: - '@jest/get-type': 30.1.0 - '@jest/expect@29.7.0': dependencies: expect: 29.7.0 @@ -4129,8 +4054,6 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - '@jest/get-type@30.1.0': {} - '@jest/globals@29.7.0': dependencies: '@jest/environment': 29.7.0 @@ -4140,11 +4063,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@jest/pattern@30.0.1': - dependencies: - '@types/node': 22.19.5 - jest-regex-util: 30.0.1 - '@jest/reporters@29.7.0': dependencies: '@bcoe/v8-coverage': 0.2.3 @@ -4178,10 +4096,6 @@ snapshots: dependencies: '@sinclair/typebox': 0.27.8 - '@jest/schemas@30.0.5': - dependencies: - '@sinclair/typebox': 0.34.48 - '@jest/source-map@29.6.3': dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -4231,16 +4145,6 @@ snapshots: '@types/yargs': 17.0.35 chalk: 4.1.2 - '@jest/types@30.2.0': - dependencies: - '@jest/pattern': 30.0.1 - '@jest/schemas': 30.0.5 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.5 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4412,8 +4316,6 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sinclair/typebox@0.34.48': {} - '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -4819,11 +4721,6 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@30.0.0': - dependencies: - expect: 30.2.0 - pretty-format: 30.2.0 - '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.10': @@ -5219,8 +5116,6 @@ snapshots: ci-info@3.9.0: {} - ci-info@4.4.0: {} - cjs-module-lexer@1.4.3: {} cliui@8.0.1: @@ -5503,15 +5398,6 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - expect@30.2.0: - dependencies: - '@jest/expect-utils': 30.2.0 - '@jest/get-type': 30.1.0 - jest-matcher-utils: 30.2.0 - jest-message-util: 30.2.0 - jest-mock: 30.2.0 - jest-util: 30.2.0 - express-rate-limit@7.5.1(express@4.22.1): dependencies: express: 4.22.1 @@ -5986,13 +5872,6 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 - jest-diff@30.2.0: - dependencies: - '@jest/diff-sequences': 30.0.1 - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - pretty-format: 30.2.0 - jest-docblock@29.7.0: dependencies: detect-newline: 3.1.0 @@ -6044,13 +5923,6 @@ snapshots: jest-get-type: 29.6.3 pretty-format: 29.7.0 - jest-matcher-utils@30.2.0: - dependencies: - '@jest/get-type': 30.1.0 - chalk: 4.1.2 - jest-diff: 30.2.0 - pretty-format: 30.2.0 - jest-message-util@29.7.0: dependencies: '@babel/code-frame': 7.27.1 @@ -6063,38 +5935,18 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-message-util@30.2.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 30.2.0 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - stack-utils: 2.0.6 - jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 '@types/node': 22.19.5 jest-util: 29.7.0 - jest-mock@30.2.0: - dependencies: - '@jest/types': 30.2.0 - '@types/node': 22.19.5 - jest-util: 30.2.0 - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} - jest-regex-util@30.0.1: {} - jest-resolve-dependencies@29.7.0: dependencies: jest-regex-util: 29.6.3 @@ -6201,15 +6053,6 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.1 - jest-util@30.2.0: - dependencies: - '@jest/types': 30.2.0 - '@types/node': 22.19.5 - chalk: 4.1.2 - ci-info: 4.4.0 - graceful-fs: 4.2.11 - picomatch: 4.0.3 - jest-validate@29.7.0: dependencies: '@jest/types': 29.6.3 @@ -6601,12 +6444,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-format@30.2.0: - dependencies: - '@jest/schemas': 30.0.5 - ansi-styles: 5.2.0 - react-is: 18.3.1 - prisma@5.22.0: dependencies: '@prisma/engines': 5.22.0 @@ -6960,7 +6797,7 @@ snapshots: ts-deepmerge@7.0.3: {} - ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.5))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -6976,9 +6813,9 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 '@jest/transform': 29.7.0 - '@jest/types': 30.2.0 + '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.28.5) - jest-util: 30.2.0 + jest-util: 29.7.0 tsconfig-paths@4.2.0: dependencies: diff --git a/Backend/src/controllers/AdminCourseApprovalController.ts b/Backend/src/controllers/AdminCourseApprovalController.ts index 46cc1028..fc60b670 100644 --- a/Backend/src/controllers/AdminCourseApprovalController.ts +++ b/Backend/src/controllers/AdminCourseApprovalController.ts @@ -1,10 +1,11 @@ import { Body, Get, Path, Post, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'; import { ValidationError } from '../middleware/errorHandler'; import { AdminCourseApprovalService } from '../services/AdminCourseApproval.service'; -import { RejectCourseValidator } from '../validators/AdminCourseApproval.validator'; +import { ApproveCourseValidator, RejectCourseValidator } from '../validators/AdminCourseApproval.validator'; import { ListPendingCoursesResponse, GetCourseDetailForAdminResponse, + ApproveCourseBody, ApproveCourseResponse, RejectCourseBody, RejectCourseResponse, @@ -24,7 +25,9 @@ export class AdminCourseApprovalController { @Response('401', 'Unauthorized') @Response('403', 'Forbidden - Admin only') public async listPendingCourses(@Request() request: any): Promise { - return await AdminCourseApprovalService.listPendingCourses(request.user.id); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await AdminCourseApprovalService.listPendingCourses(token); } /** @@ -39,7 +42,9 @@ export class AdminCourseApprovalController { @Response('403', 'Forbidden - Admin only') @Response('404', 'Course not found') public async getCourseDetail(@Request() request: any, @Path() courseId: number): Promise { - return await AdminCourseApprovalService.getCourseDetail(request.user.id, courseId); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await AdminCourseApprovalService.getCourseDetail(token, courseId); } /** @@ -56,9 +61,19 @@ export class AdminCourseApprovalController { @Response('404', 'Course not found') public async approveCourse( @Request() request: any, - @Path() courseId: number + @Path() courseId: number, + @Body() body?: ApproveCourseBody ): Promise { - return await AdminCourseApprovalService.approveCourse(request.user.id, courseId, undefined); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + + // Validate body if provided + if (body) { + const { error } = ApproveCourseValidator.validate(body); + if (error) throw new ValidationError(error.details[0].message); + } + + return await AdminCourseApprovalService.approveCourse(token, courseId, body?.comment); } /** @@ -78,10 +93,13 @@ export class AdminCourseApprovalController { @Path() courseId: number, @Body() body: RejectCourseBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + // Validate body const { error } = RejectCourseValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); - return await AdminCourseApprovalService.rejectCourse(request.user.id, courseId, body.comment); + return await AdminCourseApprovalService.rejectCourse(token, courseId, body.comment); } } diff --git a/Backend/src/controllers/AuditController.ts b/Backend/src/controllers/AuditController.ts index 6f715061..5de912fc 100644 --- a/Backend/src/controllers/AuditController.ts +++ b/Backend/src/controllers/AuditController.ts @@ -40,6 +40,11 @@ export class AuditController { @Query() page?: number, @Query() limit?: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await auditService.getLogs({ userId, action, @@ -67,6 +72,11 @@ export class AuditController { @Request() request: any, @Path() logId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + const log = await auditService.getLogById(logId); if (!log) { throw new ValidationError('Audit log not found'); @@ -84,6 +94,11 @@ export class AuditController { @Response('401', 'Unauthorized') @Response('403', 'Forbidden - Admin only') public async getAuditStats(@Request() request: any): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await auditService.getStats(); } @@ -103,6 +118,11 @@ export class AuditController { @Path() entityType: string, @Path() entityId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await auditService.getEntityHistory(entityType, entityId); } @@ -122,6 +142,11 @@ export class AuditController { @Path() userId: number, @Query() limit?: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await auditService.getUserActivity(userId, limit || 50); } @@ -139,8 +164,13 @@ export class AuditController { @Request() request: any, @Query() days: number = 90 ): Promise<{ deleted: number; message: string }> { - if (days < 6) { - throw new ValidationError('Cannot delete logs newer than 6 days'); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + if (days < 30) { + throw new ValidationError('Cannot delete logs newer than 30 days'); } const deleted = await auditService.deleteOldLogs(days); diff --git a/Backend/src/controllers/AuthController.ts b/Backend/src/controllers/AuthController.ts index b3c8e280..96cb0673 100644 --- a/Backend/src/controllers/AuthController.ts +++ b/Backend/src/controllers/AuthController.ts @@ -33,6 +33,32 @@ export class AuthController { data: { token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + user: { + id: 1, + username: 'admin', + email: 'admin@elearning.local', + email_verified_at: new Date('2024-01-01T00:00:00Z'), + updated_at: new Date('2024-01-01T00:00:00Z'), + created_at: new Date('2024-01-01T00:00:00Z'), + role: { + code: 'ADMIN', + name: { + th: 'ผู้ดูแลระบบ', + en: 'Administrator' + } + }, + profile: { + prefix: { + th: 'นาย', + en: 'Mr.' + }, + first_name: 'Admin', + last_name: 'User', + phone: null, + avatar_url: null, + birth_date: null + } + } } }) public async login(@Body() body: LoginRequest): Promise { diff --git a/Backend/src/controllers/CategoriesController.ts b/Backend/src/controllers/CategoriesController.ts index fd4b0d20..81fd8b86 100644 --- a/Backend/src/controllers/CategoriesController.ts +++ b/Backend/src/controllers/CategoriesController.ts @@ -27,11 +27,13 @@ export class CategoriesAdminController { @SuccessResponse('200', 'Category created successfully') @Response('401', 'Invalid or expired token') public async createCategory(@Request() request: any, @Body() body: createCategory): Promise { + const token = request.headers.authorization?.replace('Bearer ', '') || ''; + // Validate body const { error } = CreateCategoryValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); - return await this.categoryService.createCategory(request.user.id, body); + return await this.categoryService.createCategory(token, body); } @Put('{id}') @@ -39,11 +41,13 @@ export class CategoriesAdminController { @SuccessResponse('200', 'Category updated successfully') @Response('401', 'Invalid or expired token') public async updateCategory(@Request() request: any, @Body() body: updateCategory): Promise { + const token = request.headers.authorization?.replace('Bearer ', '') || ''; + // Validate body const { error } = UpdateCategoryValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); - return await this.categoryService.updateCategory(request.user.id, body.id, body); + return await this.categoryService.updateCategory(token, body.id, body); } @Delete('{id}') @@ -51,6 +55,7 @@ export class CategoriesAdminController { @SuccessResponse('200', 'Category deleted successfully') @Response('401', 'Invalid or expired token') public async deleteCategory(@Request() request: any, @Path() id: number): Promise { - return await this.categoryService.deleteCategory(request.user.id, id); + const token = request.headers.authorization?.replace('Bearer ', '') || ''; + return await this.categoryService.deleteCategory(token, id); } } \ No newline at end of file diff --git a/Backend/src/controllers/CertificateController.ts b/Backend/src/controllers/CertificateController.ts index a6e3f5bf..13ec2075 100644 --- a/Backend/src/controllers/CertificateController.ts +++ b/Backend/src/controllers/CertificateController.ts @@ -1,4 +1,5 @@ import { Get, Post, Route, Tags, SuccessResponse, Response, Security, Path, Request } from 'tsoa'; +import { ValidationError } from '../middleware/errorHandler'; import { CertificateService } from '../services/certificate.service'; import { GenerateCertificateResponse, @@ -20,7 +21,9 @@ export class CertificateController { @SuccessResponse('200', 'Certificates retrieved successfully') @Response('401', 'Invalid or expired token') public async listMyCertificates(@Request() request: any): Promise { - return await this.certificateService.listMyCertificates({ userId: request.user.id }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await this.certificateService.listMyCertificates({ token }); } /** @@ -34,7 +37,9 @@ export class CertificateController { @Response('401', 'Invalid or expired token') @Response('404', 'Certificate not found') public async getCertificate(@Request() request: any, @Path() courseId: number): Promise { - return await this.certificateService.getCertificate({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await this.certificateService.getCertificate({ token, course_id: courseId }); } /** @@ -49,6 +54,8 @@ export class CertificateController { @Response('401', 'Invalid or expired token') @Response('404', 'Enrollment not found') public async generateCertificate(@Request() request: any, @Path() courseId: number): Promise { - return await this.certificateService.generateCertificate({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await this.certificateService.generateCertificate({ token, course_id: courseId }); } } diff --git a/Backend/src/controllers/ChaptersLessonInstructorController.ts b/Backend/src/controllers/ChaptersLessonInstructorController.ts index 213ae095..7ba48f5c 100644 --- a/Backend/src/controllers/ChaptersLessonInstructorController.ts +++ b/Backend/src/controllers/ChaptersLessonInstructorController.ts @@ -65,11 +65,14 @@ export class ChaptersLessonInstructorController { @Path() courseId: number, @Body() body: CreateChapterBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = CreateChapterValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.createChapter({ - userId: request.user.id, + token, course_id: courseId, title: body.title, description: body.description, @@ -93,11 +96,14 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: UpdateChapterBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = UpdateChapterValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateChapter({ - userId: request.user.id, + token, course_id: courseId, chapter_id: chapterId, ...body, @@ -119,7 +125,9 @@ export class ChaptersLessonInstructorController { @Path() courseId: number, @Path() chapterId: number ): Promise { - return await chaptersLessonService.deleteChapter({ userId: request.user.id, course_id: courseId, chapter_id: chapterId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await chaptersLessonService.deleteChapter({ token, course_id: courseId, chapter_id: chapterId }); } /** @@ -135,11 +143,14 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: ReorderChapterBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = ReorderChapterValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.reorderChapter({ - userId: request.user.id, + token, course_id: courseId, chapter_id: chapterId, sort_order: body.sort_order, @@ -163,7 +174,9 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Path() lessonId: number ): Promise { - return await chaptersLessonService.getLesson({ userId: request.user.id, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await chaptersLessonService.getLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); } /** @@ -179,11 +192,14 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: CreateLessonBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = CreateLessonValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.createLesson({ - userId: request.user.id, + token, course_id: courseId, chapter_id: chapterId, title: body.title, @@ -207,11 +223,14 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Body() body: UpdateLessonBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = UpdateLessonValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateLesson({ - userId: request.user.id, + token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId, @@ -239,7 +258,9 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Path() lessonId: number ): Promise { - return await chaptersLessonService.deleteLesson({ userId: request.user.id, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await chaptersLessonService.deleteLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId }); } /** @@ -255,11 +276,14 @@ export class ChaptersLessonInstructorController { @Path() chapterId: number, @Body() body: ReorderLessonsBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = ReorderLessonsValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.reorderLessons({ - userId: request.user.id, + token, course_id: courseId, chapter_id: chapterId, lesson_id: body.lesson_id, @@ -285,11 +309,14 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Body() body: AddQuestionBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = AddQuestionValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.addQuestion({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, ...body, @@ -311,11 +338,14 @@ export class ChaptersLessonInstructorController { @Path() questionId: number, @Body() body: UpdateQuestionBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = UpdateQuestionValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateQuestion({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, question_id: questionId, @@ -334,11 +364,14 @@ export class ChaptersLessonInstructorController { @Path() questionId: number, @Body() body: ReorderQuestionBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = ReorderQuestionValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.reorderQuestion({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, question_id: questionId, @@ -360,8 +393,10 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Path() questionId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await chaptersLessonService.deleteQuestion({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, question_id: questionId, @@ -382,11 +417,14 @@ export class ChaptersLessonInstructorController { @Path() lessonId: number, @Body() body: UpdateQuizBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = UpdateQuizValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.updateQuiz({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, ...body, diff --git a/Backend/src/controllers/CoursesInstructorController.ts b/Backend/src/controllers/CoursesInstructorController.ts index 4556281c..3657e928 100644 --- a/Backend/src/controllers/CoursesInstructorController.ts +++ b/Backend/src/controllers/CoursesInstructorController.ts @@ -22,10 +22,12 @@ import { GetCourseApprovalHistoryResponse, setCourseDraftResponse, CloneCourseResponse, - GetAllMyStudentsResponse, } from '../types/CoursesInstructor.types'; import { CreateCourseValidator, UpdateCourseValidator, CloneCourseValidator } from "../validators/CoursesInstructor.validator"; +import jwt from 'jsonwebtoken'; +import { config } from '../config'; + @Route('api/instructors/courses') @Tags('CoursesInstructor') export class CoursesInstructorController { @@ -43,7 +45,11 @@ export class CoursesInstructorController { @Request() request: any, @Query() status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED' ): Promise { - return await CoursesInstructorService.listMyCourses({ userId: request.user.id, status }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await CoursesInstructorService.listMyCourses({ token, status }); } /** @@ -61,23 +67,9 @@ export class CoursesInstructorController { @Path() courseId: number, @Query() query: string ): Promise { - return await CoursesInstructorService.searchInstructors({ userId: request.user.id, query, course_id: courseId }); - } - - /** - * ดึงผู้เรียนทั้งหมดของ instructor ทุกคอร์ส - * Get all students enrolled in all of instructor's courses - * - * @returns รายการผู้เรียนแยกตามคอร์ส พร้อม total_enrolled และ total_completed ต่อคอร์ส - */ - @Get('my-students') - @Security('jwt', ['instructor']) - @SuccessResponse('200', 'Students retrieved successfully') - @Response('401', 'Unauthorized') - public async getMyAllStudents( - @Request() request: any - ): Promise { - return await CoursesInstructorService.getMyAllStudents(request.user.id); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await CoursesInstructorService.searchInstructors({ token, query, course_id: courseId }); } /** @@ -91,7 +83,11 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async getMyCourse(@Request() request: any, @Path() courseId: number): Promise { - return await CoursesInstructorService.getmyCourse({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await CoursesInstructorService.getmyCourse({ token, course_id: courseId }); } /** @@ -105,10 +101,13 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async updateCourse(@Request() request: any, @Path() courseId: number, @Body() body: UpdateMyCourse): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = UpdateCourseValidator.validate(body.data); if (error) throw new ValidationError(error.details[0].message); - return await CoursesInstructorService.updateCourse(request.user.id, courseId, body.data); + return await CoursesInstructorService.updateCourse(token, courseId, body.data); } /** @@ -127,6 +126,10 @@ export class CoursesInstructorController { @FormField() data: string, @UploadedFile() thumbnail?: Express.Multer.File ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const parsed = JSON.parse(data); const { error, value } = CreateCourseValidator.validate(parsed); if (error) throw new ValidationError(error.details[0].message); @@ -134,7 +137,7 @@ export class CoursesInstructorController { // Validate thumbnail file type if provided if (thumbnail && !thumbnail.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed for thumbnail'); - return await CoursesInstructorService.createCourse(value, request.user.id, thumbnail); + return await CoursesInstructorService.createCourse(value, decoded.id, thumbnail); } /** @@ -153,9 +156,11 @@ export class CoursesInstructorController { @Path() courseId: number, @UploadedFile() file: Express.Multer.File ): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); if (!file.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed'); - return await CoursesInstructorService.uploadThumbnail(request.user.id, courseId, file); + return await CoursesInstructorService.uploadThumbnail(token, courseId, file); } /** @@ -169,7 +174,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async deleteCourse(@Request() request: any, @Path() courseId: number): Promise { - return await CoursesInstructorService.deleteCourse(request.user.id, courseId); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.deleteCourse(token, courseId); } /** @@ -189,11 +196,14 @@ export class CoursesInstructorController { @Path() courseId: number, @Body() body: { title: { th: string; en: string } } ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + const { error } = CloneCourseValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); const result = await CoursesInstructorService.cloneCourse({ - userId: request.user.id, + token, course_id: courseId, title: body.title }); @@ -210,7 +220,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async submitCourse(@Request() request: any, @Path() courseId: number): Promise { - return await CoursesInstructorService.sendCourseForReview({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.sendCourseForReview({ token, course_id: courseId }); } /** @@ -224,7 +236,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Course not found') public async setCourseDraft(@Request() request: any, @Path() courseId: number): Promise { - return await CoursesInstructorService.setCourseDraft({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.setCourseDraft({ token, course_id: courseId }); } /** @@ -239,7 +253,9 @@ export class CoursesInstructorController { @Response('403', 'You are not an instructor of this course') @Response('404', 'Course not found') public async getCourseApprovals(@Request() request: any, @Path() courseId: number): Promise { - return await CoursesInstructorService.getCourseApprovals(request.user.id, courseId); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.getCourseApprovals(token, courseId); } /** @@ -253,7 +269,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Instructors not found') public async listInstructorCourses(@Request() request: any, @Path() courseId: number): Promise { - return await CoursesInstructorService.listInstructorsOfCourse({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.listInstructorsOfCourse({ token, course_id: courseId }); } /** @@ -268,7 +286,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Instructor not found') public async addInstructor(@Request() request: any, @Path() courseId: number, @Path() emailOrUsername: string): Promise { - return await CoursesInstructorService.addInstructorToCourse({ userId: request.user.id, course_id: courseId, email_or_username: emailOrUsername }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.addInstructorToCourse({ token, course_id: courseId, email_or_username: emailOrUsername }); } /** @@ -283,7 +303,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Instructor not found') public async removeInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise { - return await CoursesInstructorService.removeInstructorFromCourse({ userId: request.user.id, course_id: courseId, user_id: userId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.removeInstructorFromCourse({ token, course_id: courseId, user_id: userId }); } /** @@ -298,7 +320,9 @@ export class CoursesInstructorController { @Response('401', 'Invalid or expired token') @Response('404', 'Primary instructor not found') public async setPrimaryInstructor(@Request() request: any, @Path() courseId: number, @Path() userId: number): Promise { - return await CoursesInstructorService.setPrimaryInstructor({ userId: request.user.id, course_id: courseId, user_id: userId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided') + return await CoursesInstructorService.setPrimaryInstructor({ token, course_id: courseId, user_id: userId }); } /** @@ -323,8 +347,10 @@ export class CoursesInstructorController { @Query() search?: string, @Query() status?: 'ENROLLED' | 'COMPLETED' ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getEnrolledStudents({ - userId: request.user.id, + token, course_id: courseId, page, limit, @@ -350,8 +376,10 @@ export class CoursesInstructorController { @Path() courseId: number, @Path() studentId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getEnrolledStudentDetail({ - userId: request.user.id, + token, course_id: courseId, student_id: studentId, }); @@ -382,8 +410,10 @@ export class CoursesInstructorController { @Query() search?: string, @Query() isPassed?: boolean ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getQuizScores({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, page, @@ -412,8 +442,10 @@ export class CoursesInstructorController { @Path() lessonId: number, @Path() studentId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await CoursesInstructorService.getQuizAttemptDetail({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, student_id: studentId, @@ -435,6 +467,8 @@ export class CoursesInstructorController { @Request() request: any, @Path() courseId: number ): Promise { - return await CoursesInstructorService.getCourseApprovalHistory(request.user.id, courseId); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await CoursesInstructorService.getCourseApprovalHistory(token, courseId); } } \ No newline at end of file diff --git a/Backend/src/controllers/CoursesStudentController.ts b/Backend/src/controllers/CoursesStudentController.ts index 62e01636..87a5a613 100644 --- a/Backend/src/controllers/CoursesStudentController.ts +++ b/Backend/src/controllers/CoursesStudentController.ts @@ -36,7 +36,11 @@ export class CoursesStudentController { @Response('404', 'Course not found') @Response('409', 'Already enrolled in this course') public async enrollCourse(@Request() request: any, @Path() courseId: number): Promise { - return await this.service.enrollCourse({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.enrollCourse({ token, course_id: courseId }); } /** @@ -56,7 +60,11 @@ export class CoursesStudentController { @Query() limit?: number, @Query() status?: EnrollmentStatus ): Promise { - return await this.service.GetEnrolledCourses({ userId: request.user.id, page, limit, status }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.GetEnrolledCourses({ token, page, limit, status }); } /** @@ -71,7 +79,11 @@ export class CoursesStudentController { @Response('403', 'Not enrolled in this course') @Response('404', 'Course not found') public async getCourseLearning(@Request() request: any, @Path() courseId: number): Promise { - return await this.service.getCourseLearning({ userId: request.user.id, course_id: courseId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.getCourseLearning({ token, course_id: courseId }); } /** @@ -91,7 +103,11 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - return await this.service.getlessonContent({ userId: request.user.id, course_id: courseId, lesson_id: lessonId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.getlessonContent({ token, course_id: courseId, lesson_id: lessonId }); } /** @@ -110,7 +126,11 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - return await this.service.checkAccessLesson({ userId: request.user.id, course_id: courseId, lesson_id: lessonId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.checkAccessLesson({ token, course_id: courseId, lesson_id: lessonId }); } /** @@ -129,12 +149,14 @@ export class CoursesStudentController { @Path() lessonId: number, @Body() body: SaveVideoProgressBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); const { error } = SaveVideoProgressValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await this.service.saveVideoProgress({ - userId: request.user.id, + token, lesson_id: lessonId, video_progress_seconds: body.video_progress_seconds, video_duration_seconds: body.video_duration_seconds, @@ -156,7 +178,11 @@ export class CoursesStudentController { @Request() request: any, @Path() lessonId: number ): Promise { - return await this.service.getVideoProgress({ userId: request.user.id, lesson_id: lessonId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.getVideoProgress({ token, lesson_id: lessonId }); } /** @@ -176,7 +202,11 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { - return await this.service.completeLesson({ userId: request.user.id, lesson_id: lessonId }); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.service.completeLesson({ token, lesson_id: lessonId }); } /** @@ -197,12 +227,14 @@ export class CoursesStudentController { @Path() lessonId: number, @Body() body: SubmitQuizBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); const { error } = SubmitQuizValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await this.service.submitQuiz({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, answers: body.answers, @@ -226,8 +258,12 @@ export class CoursesStudentController { @Path() courseId: number, @Path() lessonId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } return await this.service.getQuizAttempts({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, }); diff --git a/Backend/src/controllers/LessonsController.ts b/Backend/src/controllers/LessonsController.ts index 33cc1cef..0323f4ab 100644 --- a/Backend/src/controllers/LessonsController.ts +++ b/Backend/src/controllers/LessonsController.ts @@ -42,6 +42,8 @@ export class LessonsController { @Path() lessonId: number, @UploadedFile() video: Express.Multer.File ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); if (!video) { throw new ValidationError('Video file is required'); @@ -55,7 +57,7 @@ export class LessonsController { }; return await chaptersLessonService.uploadVideo({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, video: videoInfo, @@ -85,6 +87,8 @@ export class LessonsController { @Path() lessonId: number, @UploadedFile() video: Express.Multer.File ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); if (!video) { throw new ValidationError('Video file is required'); @@ -98,7 +102,7 @@ export class LessonsController { }; return await chaptersLessonService.updateVideo({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, video: videoInfo, @@ -128,6 +132,8 @@ export class LessonsController { @Path() lessonId: number, @UploadedFile() attachment: Express.Multer.File ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); if (!attachment) { throw new ValidationError('Attachment file is required'); @@ -141,7 +147,7 @@ export class LessonsController { }; return await chaptersLessonService.uploadAttachment({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, attachment: attachmentInfo, @@ -171,9 +177,11 @@ export class LessonsController { @Path() lessonId: number, @Path() attachmentId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await chaptersLessonService.deleteAttachment({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, attachment_id: attachmentId, @@ -203,12 +211,14 @@ export class LessonsController { @Path() lessonId: number, @Body() body: SetYouTubeVideoBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); const { error } = SetYouTubeVideoValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await chaptersLessonService.setYouTubeVideo({ - userId: request.user.id, + token, course_id: courseId, lesson_id: lessonId, youtube_video_id: body.youtube_video_id, diff --git a/Backend/src/controllers/RecommendedCoursesController.ts b/Backend/src/controllers/RecommendedCoursesController.ts index fa0b17c5..06bff36d 100644 --- a/Backend/src/controllers/RecommendedCoursesController.ts +++ b/Backend/src/controllers/RecommendedCoursesController.ts @@ -1,4 +1,5 @@ import { Get, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'; +import { ValidationError } from '../middleware/errorHandler'; import { RecommendedCoursesService } from '../services/RecommendedCourses.service'; import { ListApprovedCoursesResponse, @@ -19,12 +20,10 @@ export class RecommendedCoursesController { @SuccessResponse('200', 'Approved courses retrieved successfully') @Response('401', 'Unauthorized') @Response('403', 'Forbidden - Admin only') - public async listApprovedCourses( - @Request() request: any, - @Query() search?: string, - @Query() categoryId?: number - ): Promise { - return await RecommendedCoursesService.listApprovedCourses(request.user.id, { search, categoryId }); + public async listApprovedCourses(@Request() request: any): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await RecommendedCoursesService.listApprovedCourses(token); } /** @@ -40,7 +39,9 @@ export class RecommendedCoursesController { @Response('403', 'Forbidden - Admin only') @Response('404', 'Course not found') public async getCourseById(@Request() request: any, @Path() courseId: number): Promise { - return await RecommendedCoursesService.getCourseById(request.user.id, courseId); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await RecommendedCoursesService.getCourseById(token, courseId); } /** @@ -60,6 +61,8 @@ export class RecommendedCoursesController { @Path() courseId: number, @Query() is_recommended: boolean ): Promise { - return await RecommendedCoursesService.toggleRecommended(request.user.id, courseId, is_recommended); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await RecommendedCoursesService.toggleRecommended(token, courseId, is_recommended); } } diff --git a/Backend/src/controllers/UserController.ts b/Backend/src/controllers/UserController.ts index 72b9a33a..b8169827 100644 --- a/Backend/src/controllers/UserController.ts +++ b/Backend/src/controllers/UserController.ts @@ -1,15 +1,16 @@ -import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Security, Request, Put, UploadedFile } from 'tsoa'; +import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Example, Controller, Security, Request, Put, UploadedFile } from 'tsoa'; import { ValidationError } from '../middleware/errorHandler'; import { UserService } from '../services/user.service'; import { UserResponse, + ProfileResponse, ProfileUpdate, ProfileUpdateResponse, + ChangePasswordRequest, ChangePasswordResponse, updateAvatarResponse, SendVerifyEmailResponse, - VerifyEmailResponse, - rolesResponse + VerifyEmailResponse } from '../types/user.types'; import { ChangePassword } from '../types/auth.types'; import { profileUpdateSchema, changePasswordSchema } from "../validators/user.validator"; @@ -21,6 +22,8 @@ export class UserController { /** * Get current user profile + * @summary Retrieve authenticated user's profile information + * @param request Express request object with JWT token in Authorization header */ @Get('me') @SuccessResponse('200', 'User found') @@ -28,7 +31,12 @@ export class UserController { @Response('401', 'Invalid or expired token') @Security('jwt') public async getMe(@Request() request: any): Promise { - return await this.userService.getUserProfile(request.user.id); + // Extract token from Authorization header + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.userService.getUserProfile(token); } @Put('me') @@ -38,20 +46,22 @@ export class UserController { @Response('400', 'Validation error') public async updateProfile(@Request() request: any, @Body() body: ProfileUpdate): Promise { const { error } = profileUpdateSchema.validate(body); - if (error) throw new ValidationError(error.details[0].message); - return await this.userService.updateProfile(request.user.id, body); - } - - @Get('roles') - @Security('jwt') - @SuccessResponse('200', 'Roles retrieved successfully') - @Response('401', 'Invalid or expired token') - public async getRoles(): Promise { - return await this.userService.getRoles(); + if (error) { + throw new ValidationError(error.details[0].message); + } + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + return await this.userService.updateProfile(token, body); } /** * Change password + * @summary Change user password using old password + * @param request Express request object with JWT token in Authorization header + * @param body Old password and new password + * @returns Success message */ @Post('change-password') @Security('jwt') @@ -60,12 +70,22 @@ export class UserController { @Response('400', 'Validation error') public async changePassword(@Request() request: any, @Body() body: ChangePassword): Promise { const { error } = changePasswordSchema.validate(body); - if (error) throw new ValidationError(error.details[0].message); - return await this.userService.changePassword(request.user.id, body.oldPassword, body.newPassword); + if (error) { + throw new ValidationError(error.details[0].message); + } + + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) { + throw new ValidationError('No token provided'); + } + + return await this.userService.changePassword(token, body.oldPassword, body.newPassword); } /** * Upload user avatar picture + * @param request Express request object with JWT token in Authorization header + * @param file Avatar image file */ @Post('upload-avatar') @Security('jwt') @@ -76,6 +96,9 @@ export class UserController { @Request() request: any, @UploadedFile() file: Express.Multer.File ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + // Validate file type (images only) if (!file.mimetype?.startsWith('image/')) throw new ValidationError('Only image files are allowed'); @@ -83,11 +106,13 @@ export class UserController { const maxSize = 5 * 1024 * 1024; // 5MB if (file.size > maxSize) throw new ValidationError('File size must be less than 5MB'); - return await this.userService.uploadAvatarPicture(request.user.id, file); + return await this.userService.uploadAvatarPicture(token, file); } /** * Send verification email to user + * @summary Send email verification link to authenticated user's email + * @param request Express request object with JWT token in Authorization header */ @Post('send-verify-email') @Security('jwt') @@ -95,7 +120,9 @@ export class UserController { @Response('401', 'Invalid or expired token') @Response('400', 'Email already verified') public async sendVerifyEmail(@Request() request: any): Promise { - return await this.userService.sendVerifyEmail(request.user.id); + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + return await this.userService.sendVerifyEmail(token); } /** diff --git a/Backend/src/controllers/announcementsController.ts b/Backend/src/controllers/announcementsController.ts index aa38ca54..8ac03c70 100644 --- a/Backend/src/controllers/announcementsController.ts +++ b/Backend/src/controllers/announcementsController.ts @@ -37,8 +37,10 @@ export class AnnouncementsController { @Query() page?: number, @Query() limit?: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await announcementsService.listAnnouncement({ - userId: request.user.id, + token, course_id: courseId, page, limit, @@ -61,6 +63,9 @@ export class AnnouncementsController { @FormField() data: string, @UploadedFiles() files?: Express.Multer.File[] ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + // Parse JSON data field const parsed = JSON.parse(data) as CreateAnnouncementBody; @@ -69,7 +74,7 @@ export class AnnouncementsController { if (error) throw new ValidationError(error.details[0].message); return await announcementsService.createAnnouncement({ - userId: request.user.id, + token, course_id: courseId, title: parsed.title, content: parsed.content, @@ -98,12 +103,15 @@ export class AnnouncementsController { @Path() announcementId: number, @Body() body: UpdateAnnouncementBody ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); + // Validate body const { error } = UpdateAnnouncementValidator.validate(body); if (error) throw new ValidationError(error.details[0].message); return await announcementsService.updateAnnouncement({ - userId: request.user.id, + token, course_id: courseId, announcement_id: announcementId, title: body.title, @@ -131,8 +139,10 @@ export class AnnouncementsController { @Path() courseId: number, @Path() announcementId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await announcementsService.deleteAnnouncement({ - userId: request.user.id, + token, course_id: courseId, announcement_id: announcementId, }); @@ -156,8 +166,10 @@ export class AnnouncementsController { @Path() announcementId: number, @UploadedFile() file: Express.Multer.File ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await announcementsService.uploadAttachment({ - userId: request.user.id, + token, course_id: courseId, announcement_id: announcementId, file: file as any, @@ -183,8 +195,10 @@ export class AnnouncementsController { @Path() announcementId: number, @Path() attachmentId: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await announcementsService.deleteAttachment({ - userId: request.user.id, + token, course_id: courseId, announcement_id: announcementId, attachment_id: attachmentId, @@ -214,8 +228,10 @@ export class AnnouncementsStudentController { @Query() page?: number, @Query() limit?: number ): Promise { + const token = request.headers.authorization?.replace('Bearer ', ''); + if (!token) throw new ValidationError('No token provided'); return await announcementsService.listAnnouncement({ - userId: request.user.id, + token, course_id: courseId, page, limit, diff --git a/Backend/src/services/AdminCourseApproval.service.ts b/Backend/src/services/AdminCourseApproval.service.ts index 808f2747..f9446457 100644 --- a/Backend/src/services/AdminCourseApproval.service.ts +++ b/Backend/src/services/AdminCourseApproval.service.ts @@ -1,6 +1,8 @@ import { prisma } from '../config/database'; +import { config } from '../config'; import { logger } from '../config/logger'; -import { ValidationError, NotFoundError } from '../middleware/errorHandler'; +import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; +import jwt from 'jsonwebtoken'; import { getPresignedUrl } from '../config/minio'; import { ListPendingCoursesResponse, @@ -16,7 +18,7 @@ export class AdminCourseApprovalService { /** * Get all pending courses for admin review */ - static async listPendingCourses(userId: number): Promise { + static async listPendingCourses(token: string): Promise { try { const courses = await prisma.course.findMany({ where: { status: 'PENDING' }, @@ -94,8 +96,9 @@ export class AdminCourseApprovalService { }; } catch (error) { logger.error('Failed to list pending courses', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Course', entityId: 0, @@ -110,7 +113,7 @@ export class AdminCourseApprovalService { /** * Get course details for admin review */ - static async getCourseDetail(userId: number, courseId: number): Promise { + static async getCourseDetail(token: string,courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, @@ -130,11 +133,7 @@ export class AdminCourseApprovalService { }, chapters: { orderBy: { sort_order: 'asc' }, - select: { - id: true, - title: true, - sort_order: true, - is_published: true, + include: { lessons: { orderBy: { sort_order: 'asc' }, select: { @@ -225,8 +224,9 @@ export class AdminCourseApprovalService { }; } catch (error) { logger.error('Failed to get course detail', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -241,8 +241,9 @@ export class AdminCourseApprovalService { /** * Approve a course */ - static async approveCourse(userId: number, courseId: number, comment?: string): Promise { + static async approveCourse(token: string, courseId: number, comment?: string): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { @@ -259,7 +260,7 @@ export class AdminCourseApprovalService { where: { id: courseId }, data: { status: 'APPROVED', - approved_by: userId, + approved_by: decoded.id, approved_at: new Date() } }), @@ -268,7 +269,7 @@ export class AdminCourseApprovalService { data: { course_id: courseId, submitted_by: course.created_by, - reviewed_by: userId, + reviewed_by: decoded.id, action: 'APPROVED', previous_status: course.status, new_status: 'APPROVED', @@ -279,7 +280,7 @@ export class AdminCourseApprovalService { // Audit log - APPROVE_COURSE await auditService.logSync({ - userId, + userId: decoded.id, action: AuditAction.APPROVE_COURSE, entityType: 'Course', entityId: courseId, @@ -294,8 +295,9 @@ export class AdminCourseApprovalService { }; } catch (error) { logger.error('Failed to approve course', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -311,8 +313,9 @@ export class AdminCourseApprovalService { /** * Reject a course */ - static async rejectCourse(userId: number, courseId: number, comment: string): Promise { + static async rejectCourse(token: string, courseId: number, comment: string): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { @@ -343,7 +346,7 @@ export class AdminCourseApprovalService { data: { course_id: courseId, submitted_by: course.created_by, - reviewed_by: userId, + reviewed_by: decoded.id, action: 'REJECTED', previous_status: course.status, new_status: 'REJECTED', @@ -354,7 +357,7 @@ export class AdminCourseApprovalService { // Audit log - REJECT_COURSE await auditService.logSync({ - userId, + userId: decoded.id, action: AuditAction.REJECT_COURSE, entityType: 'Course', entityId: courseId, @@ -369,8 +372,9 @@ export class AdminCourseApprovalService { }; } catch (error) { logger.error('Failed to reject course', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, diff --git a/Backend/src/services/ChaptersLesson.service.ts b/Backend/src/services/ChaptersLesson.service.ts index 63a68356..003670b1 100644 --- a/Backend/src/services/ChaptersLesson.service.ts +++ b/Backend/src/services/ChaptersLesson.service.ts @@ -59,11 +59,14 @@ import { AuditAction } from '@prisma/client'; * ตรวจสอบสิทธิ์เข้าถึง Course (สำหรับทั้ง Instructor และ Student) * Returns: { hasAccess: boolean, role: 'INSTRUCTOR' | 'STUDENT' | null, userId: number } */ -async function validateCourseAccess(userId: number, course_id: number): Promise<{ +async function validateCourseAccess(token: string, course_id: number): Promise<{ hasAccess: boolean; role: 'INSTRUCTOR' | 'STUDENT' | null; userId: number; }> { + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const userId = decodedToken.id; + const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { throw new UnauthorizedError('Invalid token'); @@ -95,8 +98,9 @@ async function validateCourseAccess(userId: number, course_id: number): Promise< export class ChaptersLessonService { async listChapters(request: ChaptersRequest): Promise { try { - const { userId, course_id } = request; - const user = await prisma.user.findUnique({ where: { id: userId } }); + const { token, course_id } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } @@ -113,13 +117,14 @@ export class ChaptersLessonService { async createChapter(request: CreateChapterInput): Promise { try { - const { userId, course_id, title, description, sort_order } = request; + const { token, course_id, title, description, sort_order } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create chapter'); } @@ -127,7 +132,7 @@ export class ChaptersLessonService { // Audit log - CREATE Chapter auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.CREATE, entityType: 'Chapter', entityId: chapter.id, @@ -137,8 +142,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error creating chapter: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: 0, @@ -153,13 +159,14 @@ export class ChaptersLessonService { async updateChapter(request: UpdateChapterInput): Promise { try { - const { userId, course_id, chapter_id, title, description, sort_order } = request; + const { token, course_id, chapter_id, title, description, sort_order } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update chapter'); } @@ -167,8 +174,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData }; } catch (error) { logger.error(`Error updating chapter: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: request.chapter_id, @@ -183,13 +191,14 @@ export class ChaptersLessonService { async deleteChapter(request: DeleteChapterRequest): Promise { try { - const { userId, course_id, chapter_id } = request; + const { token, course_id, chapter_id } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to delete chapter'); } @@ -197,7 +206,7 @@ export class ChaptersLessonService { // Audit log - DELETE Chapter auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.DELETE, entityType: 'Chapter', entityId: chapter_id, @@ -210,8 +219,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter deleted successfully' }; } catch (error) { logger.error(`Error deleting chapter: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: request.chapter_id, @@ -226,13 +236,14 @@ export class ChaptersLessonService { async reorderChapter(request: ReorderChapterRequest): Promise { try { - const { userId, course_id, chapter_id, sort_order: newSortOrder } = request; + const { token, course_id, chapter_id, sort_order: newSortOrder } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to reorder chapter'); } @@ -302,8 +313,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Chapter reordered successfully', data: chapters as ChapterData[] }; } catch (error) { logger.error(`Error reordering chapter: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Chapter', entityId: request.chapter_id, @@ -323,13 +335,14 @@ export class ChaptersLessonService { */ async createLesson(request: CreateLessonInput): Promise { try { - const { userId, course_id, chapter_id, title, content, type, sort_order } = request; + const { token, course_id, chapter_id, title, content, type, sort_order } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to create lesson'); } @@ -341,6 +354,7 @@ export class ChaptersLessonService { // If QUIZ type, create empty Quiz shell if (type === 'QUIZ') { + const userId = decodedToken.id; await prisma.quiz.create({ data: { @@ -362,7 +376,7 @@ export class ChaptersLessonService { // Audit log - CREATE Lesson (QUIZ) auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.CREATE, entityType: 'Lesson', entityId: lesson.id, @@ -374,7 +388,7 @@ export class ChaptersLessonService { // Audit log - CREATE Lesson auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.CREATE, entityType: 'Lesson', entityId: lesson.id, @@ -384,8 +398,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson created successfully', data: lesson as LessonData }; } catch (error) { logger.error(`Error creating lesson: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: 0, @@ -404,10 +419,10 @@ export class ChaptersLessonService { */ async getLesson(request: GetLessonRequest): Promise { try { - const { userId, course_id, lesson_id } = request; + const { token, course_id, lesson_id } = request; // Check access for both instructor and enrolled student - const access = await validateCourseAccess(userId, course_id); + const access = await validateCourseAccess(token, course_id); if (!access.hasAccess) { throw new ForbiddenError('You do not have access to this course'); } @@ -534,8 +549,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson fetched successfully', data: lessonData as LessonData }; } catch (error) { logger.error(`Error fetching lesson: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -550,13 +566,14 @@ export class ChaptersLessonService { async updateLesson(request: UpdateLessonRequest): Promise { try { - const { userId, course_id, lesson_id, data } = request; + const { token, course_id, lesson_id, data } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) { throw new UnauthorizedError('Invalid token'); } - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not permitted to update lesson'); } @@ -564,8 +581,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson updated successfully', data: lesson as LessonData }; } catch (error) { logger.error(`Error updating lesson: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -584,13 +602,14 @@ export class ChaptersLessonService { */ async reorderLessons(request: ReorderLessonsRequest): Promise { try { - const { userId, course_id, chapter_id, lesson_id, sort_order: newSortOrder } = request; + const { token, course_id, chapter_id, lesson_id, sort_order: newSortOrder } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to reorder lessons'); // Verify chapter exists and belongs to the course @@ -663,8 +682,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Lessons reordered successfully', data: lessons as LessonData[] }; } catch (error) { logger.error(`Error reordering lessons: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -684,13 +704,14 @@ export class ChaptersLessonService { */ async deleteLesson(request: DeleteLessonRequest): Promise { try { - const { userId, course_id, lesson_id } = request; + const { token, course_id, lesson_id } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to delete this lesson'); // Fetch lesson with all related data @@ -730,7 +751,7 @@ export class ChaptersLessonService { // Audit log - DELETE Lesson auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.DELETE, entityType: 'Lesson', entityId: lesson_id, @@ -743,8 +764,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Lesson deleted successfully' }; } catch (error) { logger.error(`Error deleting lesson: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -767,13 +789,14 @@ export class ChaptersLessonService { */ async uploadVideo(request: UploadVideoInput): Promise { try { - const { userId, course_id, lesson_id, video } = request; + const { token, course_id, lesson_id, video } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is VIDEO type @@ -810,7 +833,7 @@ export class ChaptersLessonService { // Audit log - UPLOAD_FILE (Video) auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.UPLOAD_FILE, entityType: 'Lesson', entityId: lesson_id, @@ -830,8 +853,9 @@ export class ChaptersLessonService { }; } catch (error) { logger.error(`Error uploading video: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -850,13 +874,14 @@ export class ChaptersLessonService { */ async updateVideo(request: UpdateVideoInput): Promise { try { - const { userId, course_id, lesson_id, video } = request; + const { token, course_id, lesson_id, video } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is VIDEO type @@ -921,8 +946,9 @@ export class ChaptersLessonService { }; } catch (error) { logger.error(`Error updating video: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -941,13 +967,14 @@ export class ChaptersLessonService { */ async setYouTubeVideo(request: SetYouTubeVideoInput): Promise { try { - const { userId, course_id, lesson_id, youtube_video_id, video_title } = request; + const { token, course_id, lesson_id, youtube_video_id, video_title } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is VIDEO type @@ -1011,8 +1038,9 @@ export class ChaptersLessonService { }; } catch (error) { logger.error(`Error setting YouTube video: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Lesson', entityId: request.lesson_id, @@ -1031,13 +1059,14 @@ export class ChaptersLessonService { */ async uploadAttachment(request: UploadAttachmentInput): Promise { try { - const { userId, course_id, lesson_id, attachment } = request; + const { token, course_id, lesson_id, attachment } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists @@ -1072,7 +1101,7 @@ export class ChaptersLessonService { // Audit log - UPLOAD_FILE (Attachment) auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.UPLOAD_FILE, entityType: 'LessonAttachment', entityId: newAttachment.id, @@ -1096,8 +1125,9 @@ export class ChaptersLessonService { }; } catch (error) { logger.error(`Error uploading attachment: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'LessonAttachment', entityId: request.lesson_id, @@ -1116,13 +1146,14 @@ export class ChaptersLessonService { */ async deleteAttachment(request: DeleteAttachmentInput): Promise { try { - const { userId, course_id, lesson_id, attachment_id } = request; + const { token, course_id, lesson_id, attachment_id } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists @@ -1153,7 +1184,7 @@ export class ChaptersLessonService { // Audit log - DELETE_FILE (Attachment) auditService.log({ - userId: userId, + userId: decodedToken.id, action: AuditAction.DELETE_FILE, entityType: 'LessonAttachment', entityId: attachment_id, @@ -1163,8 +1194,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Attachment deleted successfully' }; } catch (error) { logger.error(`Error deleting attachment: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'LessonAttachment', entityId: request.attachment_id, @@ -1184,13 +1216,14 @@ export class ChaptersLessonService { */ async addQuestion(request: AddQuestionInput): Promise { try { - const { userId, course_id, lesson_id, question, explanation, question_type, sort_order, choices } = request; + const { token, course_id, lesson_id, question, explanation, question_type, sort_order, choices } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1248,8 +1281,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Question added successfully', data: completeQuestion as QuizQuestionData }; } catch (error) { logger.error(`Error adding question: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: 0, @@ -1269,13 +1303,14 @@ export class ChaptersLessonService { */ async updateQuestion(request: UpdateQuestionInput): Promise { try { - const { userId, course_id, lesson_id, question_id, question, explanation, question_type, sort_order, choices } = request; + const { token, course_id, lesson_id, question_id, question, explanation, question_type, sort_order, choices } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1332,8 +1367,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Question updated successfully', data: completeQuestion as QuizQuestionData }; } catch (error) { logger.error(`Error updating question: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: request.question_id, @@ -1348,13 +1384,14 @@ export class ChaptersLessonService { async reorderQuestion(request: ReorderQuestionInput): Promise { try { - const { userId, course_id, lesson_id, question_id, sort_order } = request; + const { token, course_id, lesson_id, question_id, sort_order } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1434,8 +1471,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] }; } catch (error) { logger.error(`Error reordering question: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: request.question_id, @@ -1455,13 +1493,14 @@ export class ChaptersLessonService { */ async deleteQuestion(request: DeleteQuestionInput): Promise { try { - const { userId, course_id, lesson_id, question_id } = request; + const { token, course_id, lesson_id, question_id } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type @@ -1491,8 +1530,9 @@ export class ChaptersLessonService { return { code: 200, message: 'Question deleted successfully' }; } catch (error) { logger.error(`Error deleting question: ${error}`); + const decodedToken = jwt.decode(request.token) as { id: number } | null; await auditService.logSync({ - userId: request.userId || 0, + userId: decodedToken?.id || 0, action: AuditAction.ERROR, entityType: 'Question', entityId: request.question_id, @@ -1640,13 +1680,14 @@ export class ChaptersLessonService { */ async updateQuiz(request: UpdateQuizInput): Promise { try { - const { userId, course_id, lesson_id, title, description, passing_score, time_limit, shuffle_questions, shuffle_choices, show_answers_after_completion, is_skippable, allow_multiple_attempts } = request; + const { token, course_id, lesson_id, title, description, passing_score, time_limit, shuffle_questions, shuffle_choices, show_answers_after_completion, is_skippable, allow_multiple_attempts } = request; + const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number }; await CoursesInstructorService.validateCourseStatus(course_id); - const user = await prisma.user.findUnique({ where: { id: userId } }); + const user = await prisma.user.findUnique({ where: { id: decodedToken.id } }); if (!user) throw new UnauthorizedError('Invalid token'); - const courseInstructor = await CoursesInstructorService.validateCourseInstructor(userId, course_id); + const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id); if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson'); // Verify lesson exists and is QUIZ type diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index 959ef80f..e1b40d0c 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -1,7 +1,9 @@ import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; +import { config } from '../config'; import { logger } from '../config/logger'; -import { ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; +import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; +import jwt from 'jsonwebtoken'; import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio'; import { CreateCourseInput, @@ -25,7 +27,6 @@ import { SearchInstructorResponse, GetEnrolledStudentsInput, GetEnrolledStudentsResponse, - EnrolledStudentData, GetQuizScoresInput, GetQuizScoresResponse, GetQuizAttemptDetailInput, @@ -37,7 +38,6 @@ import { CloneCourseResponse, setCourseDraft, setCourseDraftResponse, - GetAllMyStudentsResponse, } from "../types/CoursesInstructor.types"; import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; @@ -121,9 +121,10 @@ export class CoursesInstructorService { static async listMyCourses(input: ListMyCoursesInput): Promise { try { + const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number; type: string }; const courseInstructors = await prisma.courseInstructor.findMany({ where: { - user_id: input.userId, + user_id: decoded.id, course: input.status ? { status: input.status } : undefined }, include: { @@ -156,8 +157,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve courses', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: 0, @@ -172,10 +174,12 @@ export class CoursesInstructorService { static async getmyCourse(getmyCourse: getmyCourse): Promise { try { + const decoded = jwt.verify(getmyCourse.token, config.jwt.secret) as { id: number; type: string }; + // Check if user is instructor of this course const courseInstructor = await prisma.courseInstructor.findFirst({ where: { - user_id: getmyCourse.userId, + user_id: decoded.id, course_id: getmyCourse.course_id }, include: { @@ -221,8 +225,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve course', { error }); + const decoded = jwt.decode(getmyCourse.token) as { id: number } | null; await auditService.logSync({ - userId: getmyCourse.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: getmyCourse.course_id, @@ -235,9 +240,9 @@ export class CoursesInstructorService { } } - static async updateCourse(userId: number, courseId: number, courseData: UpdateCourseInput): Promise { + static async updateCourse(token: string, courseId: number, courseData: UpdateCourseInput): Promise { try { - await this.validateCourseInstructor(userId, courseId); + await this.validateCourseInstructor(token, courseId); const course = await prisma.course.update({ where: { @@ -253,8 +258,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to update course', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -267,9 +273,9 @@ export class CoursesInstructorService { } } - static async uploadThumbnail(userId: number, courseId: number, file: Express.Multer.File): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { + static async uploadThumbnail(token: string, courseId: number, file: Express.Multer.File): Promise<{ code: number; message: string; data: { course_id: number; thumbnail_url: string } }> { try { - await this.validateCourseInstructor(userId, courseId); + await this.validateCourseInstructor(token, courseId); // Get current course to check for existing thumbnail const currentCourse = await prisma.course.findUnique({ @@ -316,8 +322,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to upload thumbnail', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -330,9 +337,9 @@ export class CoursesInstructorService { } } - static async deleteCourse(userId: number, courseId: number): Promise { + static async deleteCourse(token: string, courseId: number): Promise { try { - const courseInstructorId = await this.validateCourseInstructor(userId, courseId); + const courseInstructorId = await this.validateCourseInstructor(token, courseId); if (!courseInstructorId.is_primary) { throw new ForbiddenError('You have no permission to delete this course'); } @@ -358,8 +365,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to delete course', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -374,10 +382,11 @@ export class CoursesInstructorService { static async sendCourseForReview(sendCourseForReview: sendCourseForReview): Promise { try { + const decoded = jwt.verify(sendCourseForReview.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseApproval.create({ data: { course_id: sendCourseForReview.course_id, - submitted_by: sendCourseForReview.userId, + submitted_by: decoded.id, } }); await prisma.course.update({ @@ -389,7 +398,7 @@ export class CoursesInstructorService { } }); await auditService.logSync({ - userId: sendCourseForReview.userId, + userId: decoded.id, action: AuditAction.UPDATE, entityType: 'Course', entityId: sendCourseForReview.course_id, @@ -403,8 +412,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to send course for review', { error }); + const decoded = jwt.decode(sendCourseForReview.token) as { id: number } | null; await auditService.logSync({ - userId: sendCourseForReview.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: sendCourseForReview.course_id, @@ -419,7 +429,7 @@ export class CoursesInstructorService { static async setCourseDraft(setCourseDraft: setCourseDraft): Promise { try { - await this.validateCourseInstructor(setCourseDraft.userId, setCourseDraft.course_id); + await this.validateCourseInstructor(setCourseDraft.token, setCourseDraft.course_id); await prisma.course.update({ where: { id: setCourseDraft.course_id, @@ -435,8 +445,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to set course to draft', { error }); + const decoded = jwt.decode(setCourseDraft.token) as { id: number } | null; await auditService.logSync({ - userId: setCourseDraft.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: setCourseDraft.course_id, @@ -449,7 +460,7 @@ export class CoursesInstructorService { } } - static async getCourseApprovals(userId: number, courseId: number): Promise<{ + static async getCourseApprovals(token: string, courseId: number): Promise<{ code: number; message: string; data: any[]; @@ -457,7 +468,7 @@ export class CoursesInstructorService { }> { try { // Validate instructor access - await this.validateCourseInstructor(userId, courseId); + await this.validateCourseInstructor(token, courseId); const approvals = await prisma.courseApproval.findMany({ where: { course_id: courseId }, @@ -480,8 +491,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve course approvals', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -498,6 +510,8 @@ export class CoursesInstructorService { static async searchInstructors(input: SearchInstructorInput): Promise { try { + const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number }; + // Get existing instructors in the course const existingInstructors = await prisma.courseInstructor.findMany({ where: { course_id: input.course_id }, @@ -514,7 +528,7 @@ export class CoursesInstructorService { ], role: { code: 'INSTRUCTOR' }, id: { - notIn: [input.userId, ...existingInstructorIds], + notIn: [decoded.id, ...existingInstructorIds], }, }, include: { @@ -549,8 +563,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to search instructors', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -566,7 +581,7 @@ export class CoursesInstructorService { static async addInstructorToCourse(addinstructorCourse: addinstructorCourse): Promise { try { // Validate user is instructor of this course - await this.validateCourseInstructor(addinstructorCourse.userId, addinstructorCourse.course_id); + await this.validateCourseInstructor(addinstructorCourse.token, addinstructorCourse.course_id); // Find user by email or username const user = await prisma.user.findFirst({ @@ -604,8 +619,9 @@ export class CoursesInstructorService { } }); + const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null; await auditService.logSync({ - userId: addinstructorCourse.userId, + userId: decoded?.id || 0, action: AuditAction.CREATE, entityType: 'Course', entityId: addinstructorCourse.course_id, @@ -621,8 +637,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to add instructor to course', { error }); + const decoded = jwt.decode(addinstructorCourse.token) as { id: number } | null; await auditService.logSync({ - userId: addinstructorCourse.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: addinstructorCourse.course_id, @@ -637,6 +654,7 @@ export class CoursesInstructorService { static async removeInstructorFromCourse(removeinstructorCourse: removeinstructorCourse): Promise { try { + const decoded = jwt.verify(removeinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.delete({ where: { course_id_user_id: { @@ -647,7 +665,7 @@ export class CoursesInstructorService { }); await auditService.logSync({ - userId: removeinstructorCourse.userId, + userId: decoded?.id || 0, action: AuditAction.DELETE, entityType: 'Course', entityId: removeinstructorCourse.course_id, @@ -664,8 +682,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to remove instructor from course', { error }); + const decoded = jwt.decode(removeinstructorCourse.token) as { id: number } | null; await auditService.logSync({ - userId: removeinstructorCourse.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: removeinstructorCourse.course_id, @@ -680,6 +699,7 @@ export class CoursesInstructorService { static async listInstructorsOfCourse(listinstructorCourse: listinstructorCourse): Promise { try { + const decoded = jwt.verify(listinstructorCourse.token, config.jwt.secret) as { id: number; type: string }; const courseInstructors = await prisma.courseInstructor.findMany({ where: { course_id: listinstructorCourse.course_id, @@ -723,8 +743,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to retrieve instructors of course', { error }); + const decoded = jwt.decode(listinstructorCourse.token) as { id: number } | null; await auditService.logSync({ - userId: listinstructorCourse.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: listinstructorCourse.course_id, @@ -739,6 +760,7 @@ export class CoursesInstructorService { static async setPrimaryInstructor(setprimaryCourseInstructor: setprimaryCourseInstructor): Promise { try { + const decoded = jwt.verify(setprimaryCourseInstructor.token, config.jwt.secret) as { id: number; type: string }; await prisma.courseInstructor.update({ where: { course_id_user_id: { @@ -752,7 +774,7 @@ export class CoursesInstructorService { }); await auditService.logSync({ - userId: setprimaryCourseInstructor.userId, + userId: decoded?.id || 0, action: AuditAction.UPDATE, entityType: 'Course', entityId: setprimaryCourseInstructor.course_id, @@ -769,8 +791,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error('Failed to set primary instructor', { error }); + const decoded = jwt.decode(setprimaryCourseInstructor.token) as { id: number } | null; await auditService.logSync({ - userId: setprimaryCourseInstructor.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: setprimaryCourseInstructor.course_id, @@ -783,10 +806,11 @@ export class CoursesInstructorService { } } - static async validateCourseInstructor(userId: number, courseId: number): Promise<{ user_id: number; is_primary: boolean }> { + static async validateCourseInstructor(token: string, courseId: number): Promise<{ user_id: number; is_primary: boolean }> { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; const courseInstructor = await prisma.courseInstructor.findFirst({ where: { - user_id: userId, + user_id: decoded.id, course_id: courseId } }); @@ -815,10 +839,10 @@ export class CoursesInstructorService { */ static async getEnrolledStudents(input: GetEnrolledStudentsInput): Promise { try { - const { userId, course_id, page = 1, limit = 20, search, status } = input; + const { token, course_id, page = 1, limit = 20, search, status } = input; // Validate instructor - await this.validateCourseInstructor(userId, course_id); + await this.validateCourseInstructor(token, course_id); // Build where clause const whereClause: any = { course_id }; @@ -893,8 +917,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting enrolled students: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -913,10 +938,11 @@ export class CoursesInstructorService { */ static async getQuizScores(input: GetQuizScoresInput): Promise { try { - const { userId, course_id, lesson_id, page = 1, limit = 20, search, is_passed } = input; + const { token, course_id, lesson_id, page = 1, limit = 20, search, is_passed } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor - await this.validateCourseInstructor(userId, course_id); + await this.validateCourseInstructor(token, course_id); // Get lesson and verify it's a QUIZ type const lesson = await prisma.lesson.findUnique({ @@ -1069,8 +1095,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting quiz scores: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1089,10 +1116,10 @@ export class CoursesInstructorService { */ static async getQuizAttemptDetail(input: GetQuizAttemptDetailInput): Promise { try { - const { userId, course_id, lesson_id, student_id } = input; + const { token, course_id, lesson_id, student_id } = input; // Validate instructor - await this.validateCourseInstructor(userId, course_id); + await this.validateCourseInstructor(token, course_id); // Get lesson and verify it's a QUIZ type const lesson = await prisma.lesson.findUnique({ @@ -1192,8 +1219,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting quiz attempt detail: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1212,10 +1240,10 @@ export class CoursesInstructorService { */ static async getEnrolledStudentDetail(input: GetEnrolledStudentDetailInput): Promise { try { - const { userId, course_id, student_id } = input; + const { token, course_id, student_id } = input; // Validate instructor - await this.validateCourseInstructor(userId, course_id); + await this.validateCourseInstructor(token, course_id); // Get student info const student = await prisma.user.findUnique({ @@ -1339,8 +1367,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting enrolled student detail: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1357,10 +1386,12 @@ export class CoursesInstructorService { * ดึงประวัติการขออนุมัติคอร์ส * Get course approval history for instructor to see rejection reasons */ - static async getCourseApprovalHistory(userId: number, courseId: number): Promise { + static async getCourseApprovalHistory(token: string, courseId: number): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + // Validate instructor access - await this.validateCourseInstructor(userId, courseId); + await this.validateCourseInstructor(token, courseId); // Get course with approval history const course = await prisma.course.findUnique({ @@ -1403,8 +1434,9 @@ export class CoursesInstructorService { }; } catch (error) { logger.error(`Error getting course approval history: ${error}`); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || undefined, action: AuditAction.ERROR, entityType: 'Course', entityId: courseId, @@ -1422,10 +1454,11 @@ export class CoursesInstructorService { */ static async cloneCourse(input: CloneCourseInput): Promise { try { - const { userId, course_id, title } = input; + const { token, course_id, title } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor - const courseInstructor = await this.validateCourseInstructor(userId, course_id); + const courseInstructor = await this.validateCourseInstructor(token, course_id); if (!courseInstructor) { throw new ForbiddenError('You are not an instructor of this course'); } @@ -1475,7 +1508,7 @@ export class CoursesInstructorService { is_free: originalCourse.is_free, have_certificate: originalCourse.have_certificate, status: 'DRAFT', // Reset status - created_by: userId + created_by: decoded.id } }); @@ -1483,7 +1516,7 @@ export class CoursesInstructorService { await tx.courseInstructor.create({ data: { course_id: createdCourse.id, - user_id: userId, + user_id: decoded.id, is_primary: true } }); @@ -1556,7 +1589,7 @@ export class CoursesInstructorService { shuffle_questions: lesson.quiz.shuffle_questions, shuffle_choices: lesson.quiz.shuffle_choices, show_answers_after_completion: lesson.quiz.show_answers_after_completion, - created_by: userId + created_by: decoded.id } }); @@ -1603,7 +1636,7 @@ export class CoursesInstructorService { }); await auditService.logSync({ - userId: input.userId, + userId: decoded.id, action: AuditAction.CREATE, entityType: 'Course', entityId: newCourse.id, @@ -1625,8 +1658,9 @@ export class CoursesInstructorService { } catch (error) { logger.error(`Error cloning course: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Course', entityId: input.course_id, @@ -1638,45 +1672,4 @@ export class CoursesInstructorService { throw error; } } - - /** - * ดึงผู้เรียนทั้งหมดในทุกคอร์สของ instructor - * Get all enrolled students across all courses the instructor owns/teaches - */ - static async getMyAllStudents(userId: number): Promise { - try { - // หา course IDs ทั้งหมดที่ instructor สอน - const instructorCourses = await prisma.courseInstructor.findMany({ - where: { user_id: userId }, - select: { course_id: true } - }); - - const courseIds = instructorCourses.map(ci => ci.course_id); - - if (courseIds.length === 0) { - return { code: 200, message: 'Students retrieved successfully', total_students: 0, total_completed: 0 }; - } - - // unique students ทั้งหมด - const uniqueStudents = await prisma.enrollment.groupBy({ - by: ['user_id'], - where: { course_id: { in: courseIds } }, - }); - - // จำนวน enrollment ที่ COMPLETED - const totalCompleted = await prisma.enrollment.count({ - where: { course_id: { in: courseIds }, status: 'COMPLETED' } - }); - - return { - code: 200, - message: 'Students retrieved successfully', - total_students: uniqueStudents.length, - total_completed: totalCompleted, - }; - } catch (error) { - logger.error(`Error getting all students: ${error}`); - throw error; - } - } } diff --git a/Backend/src/services/CoursesStudent.service.ts b/Backend/src/services/CoursesStudent.service.ts index 2ae599fc..0e9e5b86 100644 --- a/Backend/src/services/CoursesStudent.service.ts +++ b/Backend/src/services/CoursesStudent.service.ts @@ -133,7 +133,7 @@ export class CoursesStudentService { async enrollCourse(input: EnrollCourseInput): Promise { try { const { course_id } = input; - const userId = input.userId; + const decoded = jwt.verify(input.token, config.jwt.secret) as { id: number; type: string }; const course = await prisma.course.findUnique({ where: { id: course_id }, @@ -146,7 +146,7 @@ export class CoursesStudentService { const existingEnrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -159,7 +159,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.create({ data: { course_id, - user_id: userId, + user_id: decoded.id, status: 'ENROLLED', enrolled_at: new Date(), }, @@ -167,11 +167,11 @@ export class CoursesStudentService { // Audit log - ENROLL auditService.log({ - userId: userId, + userId: decoded.id, action: AuditAction.ENROLL, entityType: 'Enrollment', entityId: enrollment.id, - newValue: { course_id, user_id: userId, status: 'ENROLLED' } + newValue: { course_id, user_id: decoded.id, status: 'ENROLLED' } }); return { @@ -187,9 +187,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(`Error enrolling in course: ${error}`); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -206,13 +206,13 @@ export class CoursesStudentService { async GetEnrolledCourses(input: ListEnrolledCoursesInput): Promise { try { - // destructure input + const { token } = input; const page = input.page ?? 1; const limit = input.limit ?? 20; - const userId = input.userId; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; const enrollments = await prisma.enrollment.findMany({ where: { - user_id: userId, + user_id: decoded.id, }, include: { course: { @@ -230,7 +230,7 @@ export class CoursesStudentService { }); const total = await prisma.enrollment.count({ where: { - user_id: userId, + user_id: decoded.id, }, }); @@ -274,9 +274,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -290,8 +290,8 @@ export class CoursesStudentService { } async getCourseLearning(input: GetCourseLearningInput): Promise { try { - const { course_id } = input; - const userId = input.userId; + const { token, course_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Get course with chapters and lessons (basic info only) const course = await prisma.course.findUnique({ @@ -330,7 +330,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -340,24 +340,11 @@ export class CoursesStudentService { throw new ForbiddenError('You are not enrolled in this course'); } - // Update last_accessed_at (fire-and-forget — ไม่ block response) - if (enrollment.status === 'ENROLLED') { - prisma.enrollment.update({ - where: { - unique_enrollment: { - user_id: userId, - course_id, - }, - }, - data: { last_accessed_at: new Date() }, - }).catch(err => logger.warn(`Failed to update last_accessed_at: ${err}`)); - } - // Get all lesson progress for this user and course const lessonIds = course.chapters.flatMap(ch => ch.lessons.map(l => l.id)); const lessonProgress = await prisma.lessonProgress.findMany({ where: { - user_id: userId, + user_id: decoded.id, lesson_id: { in: lessonIds }, }, }); @@ -453,9 +440,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -470,8 +457,8 @@ export class CoursesStudentService { async getlessonContent(input: GetLessonContentInput): Promise { try { - const { course_id, lesson_id } = input; - const userId = input.userId; + const { token, course_id, lesson_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Import MinIO functions @@ -479,7 +466,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -528,7 +515,7 @@ export class CoursesStudentService { const lessonProgress = await prisma.lessonProgress.findUnique({ where: { user_id_lesson_id: { - user_id: userId, + user_id: decoded.id, lesson_id, }, }, @@ -639,7 +626,7 @@ export class CoursesStudentService { // Get latest quiz attempt for this user latestQuizAttempt = await prisma.quizAttempt.findFirst({ where: { - user_id: userId, + user_id: decoded.id, quiz_id: lesson.quiz.id, }, orderBy: { @@ -726,9 +713,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -744,14 +731,14 @@ export class CoursesStudentService { async checkAccessLesson(input: CheckLessonAccessInput): Promise { try { - const { course_id, lesson_id } = input; - const userId = input.userId; + const { token, course_id, lesson_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Check enrollment const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -845,7 +832,7 @@ export class CoursesStudentService { // Get user's progress for prerequisite lessons const prerequisiteProgress = await prisma.lessonProgress.findMany({ where: { - user_id: userId, + user_id: decoded.id, lesson_id: { in: prerequisiteIds }, }, }); @@ -879,7 +866,7 @@ export class CoursesStudentService { // Check if user passed the quiz const quizAttempt = await prisma.quizAttempt.findFirst({ where: { - user_id: userId, + user_id: decoded.id, quiz_id: prereqLesson.quiz.id, is_passed: true, }, @@ -925,9 +912,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -942,8 +929,8 @@ export class CoursesStudentService { async getVideoProgress(input: GetVideoProgressInput): Promise { try { - const { lesson_id } = input; - const userId = input.userId; + const { token, lesson_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Get lesson to find course_id const lesson = await prisma.lesson.findUnique({ @@ -966,7 +953,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -980,7 +967,7 @@ export class CoursesStudentService { const progress = await prisma.lessonProgress.findUnique({ where: { user_id_lesson_id: { - user_id: userId, + user_id: decoded.id, lesson_id, }, }, @@ -1010,9 +997,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -1027,8 +1014,8 @@ export class CoursesStudentService { async saveVideoProgress(input: SaveVideoProgressInput): Promise { try { - const { lesson_id, video_progress_seconds, video_duration_seconds } = input; - const userId = input.userId; + const { token, lesson_id, video_progress_seconds, video_duration_seconds } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Get lesson to find course_id const lesson = await prisma.lesson.findUnique({ @@ -1051,7 +1038,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -1074,12 +1061,12 @@ export class CoursesStudentService { const progress = await prisma.lessonProgress.upsert({ where: { user_id_lesson_id: { - user_id: userId, + user_id: decoded.id, lesson_id, }, }, create: { - user_id: userId, + user_id: decoded.id, lesson_id, video_progress_seconds, video_duration_seconds: video_duration_seconds ?? null, @@ -1098,7 +1085,7 @@ export class CoursesStudentService { // If video completed, mark lesson as complete and update enrollment progress let enrollmentProgress: { progress_percentage: number; is_course_completed: boolean } | undefined; if (isCompleted) { - const result = await this.markLessonComplete(userId, lesson_id, course_id); + const result = await this.markLessonComplete(decoded.id, lesson_id, course_id); enrollmentProgress = result.enrollmentProgress; } @@ -1118,9 +1105,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Enrollment', entityId: 0, @@ -1135,8 +1122,8 @@ export class CoursesStudentService { async completeLesson(input: CompleteLessonInput): Promise { try { - const { lesson_id } = input; - const userId = input.userId; + const { token, lesson_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Get lesson with chapter and course info const lesson = await prisma.lesson.findUnique({ @@ -1185,7 +1172,7 @@ export class CoursesStudentService { const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -1196,7 +1183,7 @@ export class CoursesStudentService { } // Mark lesson as complete and update enrollment progress - const { lessonProgress, enrollmentProgress } = await this.markLessonComplete(userId, lesson_id, course_id); + const { lessonProgress, enrollmentProgress } = await this.markLessonComplete(decoded.id, lesson_id, course_id); const { progress_percentage: course_progress_percentage, is_course_completed } = enrollmentProgress; // Find next lesson @@ -1225,7 +1212,7 @@ export class CoursesStudentService { // Check if certificate already exists const existingCertificate = await prisma.certificate.findFirst({ where: { - user_id: userId, + user_id: decoded.id, course_id, }, }); @@ -1233,10 +1220,10 @@ export class CoursesStudentService { if (!existingCertificate) { await prisma.certificate.create({ data: { - user_id: userId, + user_id: decoded.id, course_id, enrollment_id: enrollment.id, - file_path: `certificates/${course_id}/${userId}/${Date.now()}.pdf`, + file_path: `certificates/${course_id}/${decoded.id}/${Date.now()}.pdf`, issued_at: new Date(), }, }); @@ -1261,18 +1248,18 @@ export class CoursesStudentService { }; } catch (error) { logger.error(`Error completing lesson: ${error}`); - // userId from middleware - await auditService.logSync({ - userId: input.userId, - action: AuditAction.ERROR, - entityType: 'LessonProgress', - entityId: input.lesson_id, - metadata: { - operation: 'complete_lesson', - lesson_id: input.lesson_id, - error: error instanceof Error ? error.message : String(error) - } - }); + const decoded = jwt.decode(input.token) as { id: number } | null; + await auditService.logSync({ + userId: decoded?.id || 0, + action: AuditAction.ERROR, + entityType: 'LessonProgress', + entityId: input.lesson_id, + metadata: { + operation: 'complete_lesson', + lesson_id: input.lesson_id, + error: error instanceof Error ? error.message : String(error) + } + }); throw error; } } @@ -1283,14 +1270,14 @@ export class CoursesStudentService { */ async submitQuiz(input: SubmitQuizInput): Promise { try { - const { course_id, lesson_id, answers } = input; - const userId = input.userId; + const { token, course_id, lesson_id, answers } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Check enrollment const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -1331,7 +1318,7 @@ export class CoursesStudentService { // Get previous attempt count const previousAttempts = await prisma.quizAttempt.count({ where: { - user_id: userId, + user_id: decoded.id, quiz_id: quiz.id, }, }); @@ -1384,7 +1371,7 @@ export class CoursesStudentService { const now = new Date(); const quizAttempt = await prisma.quizAttempt.create({ data: { - user_id: userId, + user_id: decoded.id, quiz_id: quiz.id, score: earnedScore, total_questions: quiz.questions.length, @@ -1400,7 +1387,7 @@ export class CoursesStudentService { // If passed, mark lesson as complete and update enrollment progress let enrollmentProgress: { progress_percentage: number; is_course_completed: boolean } | undefined; if (isPassed) { - const result = await this.markLessonComplete(userId, lesson_id, course_id); + const result = await this.markLessonComplete(decoded.id, lesson_id, course_id); enrollmentProgress = result.enrollmentProgress; } @@ -1429,9 +1416,9 @@ export class CoursesStudentService { }; } catch (error) { logger.error(`Error submitting quiz: ${error}`); - // userId from middleware + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'QuizAttempt', entityId: 0, @@ -1452,14 +1439,14 @@ export class CoursesStudentService { */ async getQuizAttempts(input: GetQuizAttemptsInput): Promise { try { - const { course_id, lesson_id } = input; - const userId = input.userId; + const { token, course_id, lesson_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Check enrollment const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -1494,7 +1481,7 @@ export class CoursesStudentService { // Get all quiz attempts for this user const attempts = await prisma.quizAttempt.findMany({ where: { - user_id: userId, + user_id: decoded.id, quiz_id: lesson.quiz.id, }, orderBy: { attempt_number: 'desc' }, @@ -1539,20 +1526,22 @@ export class CoursesStudentService { }; } catch (error) { logger.error(error); - // userId from middleware - await auditService.logSync({ - userId: input.userId, - action: AuditAction.ERROR, - entityType: 'QuizAttempt', - entityId: 0, - metadata: { - operation: 'get_quiz_attempts', - course_id: input.course_id, - lesson_id: input.lesson_id, - error: error instanceof Error ? error.message : String(error) - } - }); + const decoded = jwt.decode(input.token) as { id: number } | null; + if (decoded?.id) { + await auditService.logSync({ + userId: decoded.id, + action: AuditAction.ERROR, + entityType: 'QuizAttempt', + entityId: 0, + metadata: { + operation: 'get_quiz_attempts', + course_id: input.course_id, + lesson_id: input.lesson_id, + error: error instanceof Error ? error.message : String(error) + } + }); + } throw error; } } -} +} \ No newline at end of file diff --git a/Backend/src/services/RecommendedCourses.service.ts b/Backend/src/services/RecommendedCourses.service.ts index 750131c6..9c185c8a 100644 --- a/Backend/src/services/RecommendedCourses.service.ts +++ b/Backend/src/services/RecommendedCourses.service.ts @@ -1,13 +1,14 @@ import { prisma } from '../config/database'; +import { config } from '../config'; import { logger } from '../config/logger'; import { NotFoundError, ValidationError } from '../middleware/errorHandler'; +import jwt from 'jsonwebtoken'; import { getPresignedUrl } from '../config/minio'; import { ListApprovedCoursesResponse, GetCourseByIdResponse, ToggleRecommendedResponse, - RecommendedCourseData, - RecommendedCourseDetailData + RecommendedCourseData } from '../types/RecommendedCourses.types'; import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; @@ -17,24 +18,10 @@ export class RecommendedCoursesService { /** * List all approved courses (for admin to manage recommendations) */ - static async listApprovedCourses( - userId: number, - filters?: { search?: string; categoryId?: number } - ): Promise { + static async listApprovedCourses(token: string): Promise { try { - const { search, categoryId } = filters ?? {}; - const courses = await prisma.course.findMany({ - where: { - status: 'APPROVED', - ...(categoryId ? { category_id: categoryId } : {}), - ...(search ? { - OR: [ - { title: { path: ['th'], string_contains: search } }, - { title: { path: ['en'], string_contains: search } } - ] - } : {}) - }, + where: { status: 'APPROVED' }, orderBy: [ { is_recommended: 'desc' }, { updated_at: 'desc' } @@ -53,9 +40,9 @@ export class RecommendedCoursesService { } } }, - _count: { - select: { - chapters: true + chapters: { + include: { + lessons: true } } } @@ -94,7 +81,8 @@ export class RecommendedCoursesService { is_primary: i.is_primary, user: i.user })), - chapters_count: course._count.chapters, + chapters_count: course.chapters.length, + lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0) } as RecommendedCourseData; })); @@ -106,16 +94,19 @@ export class RecommendedCoursesService { }; } catch (error) { logger.error('Failed to list approved courses', { error }); - await auditService.logSync({ - userId, - action: AuditAction.ERROR, - entityType: 'RecommendedCourses', - entityId: 0, - metadata: { - operation: 'list_approved_courses', - error: error instanceof Error ? error.message : String(error) - } - }); + const decoded = jwt.decode(token) as { id: number } | null; + if (decoded?.id) { + await auditService.logSync({ + userId: decoded.id, + action: AuditAction.ERROR, + entityType: 'RecommendedCourses', + entityId: 0, + metadata: { + operation: 'list_approved_courses', + error: error instanceof Error ? error.message : String(error) + } + }); + } throw error; } } @@ -123,7 +114,7 @@ export class RecommendedCoursesService { /** * Get course by ID (for admin to view details) */ - static async getCourseById(userId: number, courseId: number): Promise { + static async getCourseById(token: string, courseId: number): Promise { try { const course = await prisma.course.findUnique({ where: { id: courseId }, @@ -167,7 +158,7 @@ export class RecommendedCoursesService { } } - const data: RecommendedCourseDetailData = { + const data: RecommendedCourseData = { id: course.id, title: course.title as { th: string; en: string }, slug: course.slug, @@ -190,15 +181,8 @@ export class RecommendedCoursesService { is_primary: i.is_primary, user: i.user })), - chapters: course.chapters.map(ch => ({ - id: ch.id, - title: ch.title as { th: string; en: string }, - sort_order: ch.sort_order, - lessons: ch.lessons.map(l => ({ - id: l.id, - title: l.title as { th: string; en: string } - })) - })) + chapters_count: course.chapters.length, + lessons_count: course.chapters.reduce((sum, ch) => sum + ch.lessons.length, 0) }; return { @@ -208,16 +192,19 @@ export class RecommendedCoursesService { }; } catch (error) { logger.error('Failed to get course by ID', { error }); - await auditService.logSync({ - userId, - action: AuditAction.ERROR, - entityType: 'RecommendedCourses', - entityId: 0, - metadata: { - operation: 'get_course_by_id', - error: error instanceof Error ? error.message : String(error) - } - }); + const decoded = jwt.decode(token) as { id: number } | null; + if (decoded?.id) { + await auditService.logSync({ + userId: decoded.id, + action: AuditAction.ERROR, + entityType: 'RecommendedCourses', + entityId: 0, + metadata: { + operation: 'get_course_by_id', + error: error instanceof Error ? error.message : String(error) + } + }); + } throw error; } } @@ -226,11 +213,12 @@ export class RecommendedCoursesService { * Toggle course recommendation status */ static async toggleRecommended( - userId: number, + token: string, courseId: number, isRecommended: boolean ): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const course = await prisma.course.findUnique({ where: { id: courseId } }); if (!course) { @@ -248,7 +236,7 @@ export class RecommendedCoursesService { // Audit log await auditService.logSync({ - userId, + userId: decoded.id, action: AuditAction.UPDATE, entityType: 'Course', entityId: courseId, @@ -267,8 +255,9 @@ export class RecommendedCoursesService { }; } catch (error) { logger.error('Failed to toggle recommended status', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'RecommendedCourses', entityId: courseId, diff --git a/Backend/src/services/announcements.service.ts b/Backend/src/services/announcements.service.ts index 36adabca..7e8b2d3e 100644 --- a/Backend/src/services/announcements.service.ts +++ b/Backend/src/services/announcements.service.ts @@ -1,6 +1,8 @@ import { prisma } from '../config/database'; +import { config } from '../config'; import { logger } from '../config/logger'; -import { ForbiddenError, NotFoundError } from '../middleware/errorHandler'; +import { UnauthorizedError, ForbiddenError, NotFoundError } from '../middleware/errorHandler'; +import jwt from 'jsonwebtoken'; import { ListAnnouncementResponse, CreateAnnouncementInput, @@ -29,26 +31,27 @@ export class AnnouncementsService { */ async listAnnouncement(input: ListAnnouncementInput): Promise { try { - const { userId, course_id, page = 1, limit = 10 } = input; + const { token, course_id, page = 1, limit = 10 } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string }; // Check user access - instructor, admin, or enrolled student const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { id: decoded.id }, include: { role: true }, }); - if (!user) throw new ForbiddenError('User not found'); + if (!user) throw new UnauthorizedError('Invalid token'); // Admin can access all courses const isAdmin = user.role.code === 'ADMIN'; // Check if instructor of this course const isInstructor = await prisma.courseInstructor.findFirst({ - where: { course_id, user_id: userId }, + where: { course_id, user_id: decoded.id }, }); // Check if enrolled student const isEnrolled = await prisma.enrollment.findFirst({ - where: { course_id, user_id: userId }, + where: { course_id, user_id: decoded.id }, }); if (!isAdmin && !isInstructor && !isEnrolled) throw new ForbiddenError('You do not have access to this course announcements'); @@ -58,7 +61,7 @@ export class AnnouncementsService { // Students only see PUBLISHED announcements with published_at <= now const now = new Date(); const whereClause: any = { course_id }; - + if (!(isAdmin || isInstructor)) { // Students: only show PUBLISHED and published_at <= now whereClause.status = 'PUBLISHED'; @@ -127,8 +130,9 @@ export class AnnouncementsService { }; } catch (error) { logger.error(`Error listing announcements: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -146,10 +150,11 @@ export class AnnouncementsService { */ async createAnnouncement(input: CreateAnnouncementInput): Promise { try { - const { userId, course_id, title, content, status, is_pinned, published_at, files } = input; + const { token, course_id, title, content, status, is_pinned, published_at, files } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(userId, course_id); + await CoursesInstructorService.validateCourseInstructor(token, course_id); // Determine published_at: use provided value or default to now if status is PUBLISHED let finalPublishedAt: Date | null = null; @@ -166,7 +171,7 @@ export class AnnouncementsService { status: status as any, is_pinned, published_at: finalPublishedAt, - created_by: userId, + created_by: decoded.id, }, }); @@ -231,8 +236,9 @@ export class AnnouncementsService { }; } catch (error) { logger.error(`Error creating announcement: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -250,10 +256,11 @@ export class AnnouncementsService { */ async updateAnnouncement(input: UpdateAnnouncementInput): Promise { try { - const { userId, course_id, announcement_id, title, content, status, is_pinned, published_at } = input; + const { token, course_id, announcement_id, title, content, status, is_pinned, published_at } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(userId, course_id); + await CoursesInstructorService.validateCourseInstructor(token, course_id); // Check announcement exists and belongs to course const existing = await prisma.announcement.findFirst({ @@ -282,7 +289,7 @@ export class AnnouncementsService { status: status as any, is_pinned, published_at: finalPublishedAt, - updated_by: userId, + updated_by: decoded.id, }, include: { attachments: true, @@ -313,8 +320,9 @@ export class AnnouncementsService { }; } catch (error) { logger.error(`Error updating announcement: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -332,10 +340,11 @@ export class AnnouncementsService { */ async deleteAnnouncement(input: DeleteAnnouncementInput): Promise { try { - const { userId, course_id, announcement_id } = input; + const { token, course_id, announcement_id } = input; + jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(userId, course_id); + await CoursesInstructorService.validateCourseInstructor(token, course_id); // Check announcement exists and belongs to course const existing = await prisma.announcement.findFirst({ @@ -367,8 +376,9 @@ export class AnnouncementsService { }; } catch (error) { logger.error(`Error deleting announcement: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -386,10 +396,11 @@ export class AnnouncementsService { */ async uploadAttachment(input: UploadAnnouncementAttachmentInput): Promise { try { - const { userId, course_id, announcement_id, file } = input; + const { token, course_id, announcement_id, file } = input; + jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(userId, course_id); + await CoursesInstructorService.validateCourseInstructor(token, course_id); // Check announcement exists and belongs to course const existing = await prisma.announcement.findFirst({ @@ -440,8 +451,9 @@ export class AnnouncementsService { }; } catch (error) { logger.error(`Error uploading attachment: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, @@ -459,10 +471,11 @@ export class AnnouncementsService { */ async deleteAttachment(input: DeleteAnnouncementAttachmentInput): Promise { try { - const { userId, course_id, announcement_id, attachment_id } = input; + const { token, course_id, announcement_id, attachment_id } = input; + jwt.verify(token, config.jwt.secret) as { id: number }; // Validate instructor access - await CoursesInstructorService.validateCourseInstructor(userId, course_id); + await CoursesInstructorService.validateCourseInstructor(token, course_id); // Check attachment exists and belongs to announcement in this course const attachment = await prisma.announcementAttachment.findFirst({ @@ -495,8 +508,9 @@ export class AnnouncementsService { }; } catch (error) { logger.error(`Error deleting attachment: ${error}`); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'Announcement', entityId: 0, diff --git a/Backend/src/services/auth.service.ts b/Backend/src/services/auth.service.ts index 9d16d594..66899bed 100644 --- a/Backend/src/services/auth.service.ts +++ b/Backend/src/services/auth.service.ts @@ -74,6 +74,7 @@ export class AuthService { data: { token, refreshToken, + user: await this.formatUserResponse(user) } }; } diff --git a/Backend/src/services/categories.service.ts b/Backend/src/services/categories.service.ts index e18ec617..0e0defa2 100644 --- a/Backend/src/services/categories.service.ts +++ b/Backend/src/services/categories.service.ts @@ -1,7 +1,10 @@ import { prisma } from '../config/database'; import { Prisma } from '@prisma/client'; +import { config } from '../config'; import { logger } from '../config/logger'; +import jwt from 'jsonwebtoken'; import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, ListCategoriesResponse, Category } from '../types/categories.type'; +import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; @@ -23,13 +26,14 @@ export class CategoryService { } } - async createCategory(userId: number, category: createCategory): Promise { + async createCategory(token: string, category: createCategory): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; const newCategory = await prisma.category.create({ data: category }); auditService.log({ - userId, + userId: decoded.id, action: AuditAction.CREATE, entityType: 'Category', entityId: newCategory.id, @@ -43,13 +47,13 @@ export class CategoryService { name: newCategory.name as { th: string; en: string }, slug: newCategory.slug, description: newCategory.description as { th: string; en: string }, - created_by: userId, + created_by: decoded.id, } }; } catch (error) { logger.error('Failed to create category', { error }); await auditService.logSync({ - userId, + userId: 0, action: AuditAction.ERROR, entityType: 'Category', entityId: 0, @@ -62,14 +66,15 @@ export class CategoryService { } } - async updateCategory(userId: number, id: number, category: updateCategory): Promise { + async updateCategory(token: string, id: number, category: updateCategory): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; const updatedCategory = await prisma.category.update({ where: { id }, data: category }); auditService.log({ - userId, + userId: decoded.id, action: AuditAction.UPDATE, entityType: 'Category', entityId: id, @@ -83,13 +88,13 @@ export class CategoryService { name: updatedCategory.name as { th: string; en: string }, slug: updatedCategory.slug, description: updatedCategory.description as { th: string; en: string }, - updated_by: userId, + updated_by: decoded.id, } }; } catch (error) { logger.error('Failed to update category', { error }); await auditService.logSync({ - userId, + userId: 0, action: AuditAction.ERROR, entityType: 'Category', entityId: 0, @@ -102,13 +107,14 @@ export class CategoryService { } } - async deleteCategory(userId: number, id: number): Promise { + async deleteCategory(token: string, id: number): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; const deletedCategory = await prisma.category.delete({ where: { id } }); auditService.log({ - userId, + userId: decoded.id, action: AuditAction.DELETE, entityType: 'Category', entityId: id, @@ -121,7 +127,7 @@ export class CategoryService { } catch (error) { logger.error('Failed to delete category', { error }); await auditService.logSync({ - userId, + userId: 0, action: AuditAction.ERROR, entityType: 'Category', entityId: 0, diff --git a/Backend/src/services/certificate.service.ts b/Backend/src/services/certificate.service.ts index eafaffa0..4041ec41 100644 --- a/Backend/src/services/certificate.service.ts +++ b/Backend/src/services/certificate.service.ts @@ -1,6 +1,8 @@ import { prisma } from '../config/database'; +import { config } from '../config'; import { logger } from '../config/logger'; import { NotFoundError, ForbiddenError, ValidationError } from '../middleware/errorHandler'; +import jwt from 'jsonwebtoken'; import { PDFDocument, rgb } from 'pdf-lib'; import fontkit from '@pdf-lib/fontkit'; import * as fs from 'fs'; @@ -27,13 +29,14 @@ export class CertificateService { */ async generateCertificate(input: GenerateCertificateInput): Promise { try { - const { userId, course_id } = input; + const { token, course_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; // Check enrollment and completion const enrollment = await prisma.enrollment.findUnique({ where: { unique_enrollment: { - user_id: userId, + user_id: decoded.id, course_id, }, }, @@ -62,7 +65,7 @@ export class CertificateService { // Check if certificate already exists const existingCertificate = await prisma.certificate.findFirst({ where: { - user_id: userId, + user_id: decoded.id, course_id, }, }); @@ -100,13 +103,13 @@ export class CertificateService { // Upload to MinIO const timestamp = Date.now(); - const filePath = `certificates/${course_id}/${userId}/${timestamp}.pdf`; + const filePath = `certificates/${course_id}/${decoded.id}/${timestamp}.pdf`; await uploadFile(filePath, Buffer.from(pdfBytes), 'application/pdf'); // Save to database const certificate = await prisma.certificate.create({ data: { - user_id: userId, + user_id: decoded.id, course_id, enrollment_id: enrollment.id, file_path: filePath, @@ -115,7 +118,7 @@ export class CertificateService { }); auditService.log({ - userId, + userId: decoded.id, action: AuditAction.CREATE, entityType: 'Certificate', entityId: certificate.id, @@ -136,8 +139,9 @@ export class CertificateService { }; } catch (error) { logger.error('Failed to generate certificate', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id, action: AuditAction.ERROR, entityType: 'Certificate', entityId: 0, @@ -156,11 +160,12 @@ export class CertificateService { */ async getCertificate(input: GetCertificateInput): Promise { try { - const { userId, course_id } = input; + const { token, course_id } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const certificate = await prisma.certificate.findFirst({ where: { - user_id: userId, + user_id: decoded.id, course_id, }, include: { @@ -197,8 +202,9 @@ export class CertificateService { }; } catch (error) { logger.error('Failed to get certificate', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id, action: AuditAction.ERROR, entityType: 'Certificate', entityId: 0, @@ -217,11 +223,12 @@ export class CertificateService { */ async listMyCertificates(input: ListMyCertificatesInput): Promise { try { - const { userId } = input; + const { token } = input; + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; const certificates = await prisma.certificate.findMany({ where: { - user_id: userId, + user_id: decoded.id, }, include: { enrollment: { @@ -260,8 +267,9 @@ export class CertificateService { }; } catch (error) { logger.error('Failed to list certificates', { error }); + const decoded = jwt.decode(input.token) as { id: number } | null; await auditService.logSync({ - userId: input.userId, + userId: decoded?.id, action: AuditAction.ERROR, entityType: 'Certificate', entityId: 0, diff --git a/Backend/src/services/courses.service.ts b/Backend/src/services/courses.service.ts index 6b810ca9..a0d96a45 100644 --- a/Backend/src/services/courses.service.ts +++ b/Backend/src/services/courses.service.ts @@ -103,53 +103,18 @@ export class CoursesService { const course = await prisma.course.findFirst({ where: { id, - status: 'APPROVED' + status: 'APPROVED' // Only show approved courses to students }, include: { - creator: { - select: { - id: true, - username: true, - email: true, - profile: { - select: { - first_name: true, - last_name: true, - avatar_url: true - } - } - } - }, - instructors: { - include: { - user: { - select: { - id: true, - username: true, - email: true, - profile: { - select: { - first_name: true, - last_name: true, - avatar_url: true - } - } - } - } - } - }, - category: { - select: { id: true, name: true } - }, chapters: { - orderBy: { sort_order: 'asc' }, select: { id: true, title: true, - sort_order: true, lessons: { - orderBy: { sort_order: 'asc' }, - select: { id: true, title: true } + select: { + id: true, + title: true, + } } } } @@ -173,69 +138,12 @@ export class CoursesService { logger.warn(`Failed to generate presigned URL for thumbnail: ${err}`); } } - - // Generate presigned URL for creator avatar - let creator_avatar_url: string | null = null; - if (course.creator.profile?.avatar_url) { - try { - creator_avatar_url = await getPresignedUrl(course.creator.profile.avatar_url, 3600); - } catch (err) { - logger.warn(`Failed to generate presigned URL for creator avatar: ${err}`); - } - } - - // Generate presigned URLs for instructor avatars - const instructorsWithAvatar = await Promise.all(course.instructors.map(async (i) => { - let avatar_url: string | null = null; - if (i.user.profile?.avatar_url) { - try { - avatar_url = await getPresignedUrl(i.user.profile.avatar_url, 3600); - } catch (err) { - logger.warn(`Failed to generate presigned URL for instructor avatar: ${err}`); - } - } - return { - user_id: i.user_id, - is_primary: i.is_primary, - user: { - ...i.user, - profile: i.user.profile ? { - ...i.user.profile, - avatar_url - } : null - } - }; - })); - return { code: 200, message: 'Course fetched successfully', data: { ...course, - title: course.title as { th: string; en: string }, - description: course.description as { th: string; en: string }, thumbnail_url: thumbnail_presigned_url, - creator: { - ...course.creator, - profile: course.creator.profile ? { - ...course.creator.profile, - avatar_url: creator_avatar_url - } : null - }, - instructors: instructorsWithAvatar, - category: course.category ? { - id: course.category.id, - name: course.category.name as { th: string; en: string } - } : null, - chapters: course.chapters.map(ch => ({ - id: ch.id, - title: ch.title as { th: string; en: string }, - sort_order: ch.sort_order, - lessons: ch.lessons.map(l => ({ - id: l.id, - title: l.title as { th: string; en: string } - })) - })) }, }; } catch (error) { diff --git a/Backend/src/services/user.service.ts b/Backend/src/services/user.service.ts index 552c7665..918b12e3 100644 --- a/Backend/src/services/user.service.ts +++ b/Backend/src/services/user.service.ts @@ -14,8 +14,7 @@ import { updateAvatarRequest, updateAvatarResponse, SendVerifyEmailResponse, - VerifyEmailResponse, - rolesResponse + VerifyEmailResponse } from '../types/user.types'; import nodemailer from 'nodemailer'; import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler'; @@ -24,10 +23,15 @@ import { auditService } from './audit.service'; import { AuditAction } from '@prisma/client'; export class UserService { - async getUserProfile(userId: number): Promise { + async getUserProfile(token: string): Promise { try { + // Decode JWT token to get user ID + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; + const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { + id: decoded.id + }, include: { profile: true, role: true @@ -63,6 +67,14 @@ export class UserService { } : undefined }; } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + logger.error('Invalid JWT token:', error); + throw new UnauthorizedError('Invalid token'); + } + if (error instanceof jwt.TokenExpiredError) { + logger.error('JWT token expired:', error); + throw new UnauthorizedError('Token expired'); + } logger.error('Error fetching user profile:', error); throw error; } @@ -71,9 +83,12 @@ export class UserService { /** * Change user password */ - async changePassword(userId: number, oldPassword: string, newPassword: string): Promise { + async changePassword(token: string, oldPassword: string, newPassword: string): Promise { try { - const user = await prisma.user.findUnique({ where: { id: userId } }); + // Decode JWT token to get user ID + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; + + const user = await prisma.user.findUnique({ where: { id: decoded.id } }); if (!user) throw new UnauthorizedError('User not found'); // Check if account is deactivated @@ -111,12 +126,21 @@ export class UserService { message: 'Password changed successfully' }; } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + logger.error('Invalid JWT token:', error); + throw new UnauthorizedError('Invalid token'); + } + if (error instanceof jwt.TokenExpiredError) { + logger.error('JWT token expired:', error); + throw new UnauthorizedError('Token expired'); + } logger.error('Failed to change password', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'User', - entityId: userId, + entityId: decoded?.id || 0, metadata: { operation: 'change_password', error: error instanceof Error ? error.message : String(error) @@ -129,9 +153,12 @@ export class UserService { /** * Update user profile */ - async updateProfile(userId: number, profile: ProfileUpdate): Promise { + async updateProfile(token: string, profile: ProfileUpdate): Promise { try { - const user = await prisma.user.findUnique({ where: { id: userId } }); + // Decode JWT token to get user ID + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string }; + + const user = await prisma.user.findUnique({ where: { id: decoded.id } }); if (!user) throw new UnauthorizedError('User not found'); // Check if account is deactivated @@ -161,12 +188,21 @@ export class UserService { } }; } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + logger.error('Invalid JWT token:', error); + throw new UnauthorizedError('Invalid token'); + } + if (error instanceof jwt.TokenExpiredError) { + logger.error('JWT token expired:', error); + throw new UnauthorizedError('Token expired'); + } logger.error('Failed to update profile', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.UPDATE, entityType: 'UserProfile', - entityId: userId, + entityId: decoded?.id || 0, metadata: { operation: 'update_profile', error: error instanceof Error ? error.message : String(error) @@ -176,29 +212,16 @@ export class UserService { } } - async getRoles(): Promise { - try { - const roles = await prisma.role.findMany({ - select: { - id: true, - code: true - } - }); - return { roles }; - } catch (error) { - logger.error('Failed to get roles', { error }); - throw error; - } - } - /** * Upload avatar picture to MinIO */ - async uploadAvatarPicture(userId: number, file: Express.Multer.File): Promise { + async uploadAvatarPicture(token: string, file: Express.Multer.File): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number }; + // Check if user exists const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { id: decoded.id }, include: { profile: true } }); @@ -217,7 +240,7 @@ export class UserService { const fileName = file.originalname || 'avatar'; const extension = fileName.split('.').pop() || 'jpg'; const safeFilename = `${timestamp}-${uniqueId}.${extension}`; - const filePath = `avatars/${userId}/${safeFilename}`; + const filePath = `avatars/${decoded.id}/${safeFilename}`; // Delete old avatar if exists if (user.profile?.avatar_url) { @@ -237,13 +260,13 @@ export class UserService { // Update or create profile - store only file path if (user.profile) { await prisma.userProfile.update({ - where: { user_id: userId }, + where: { user_id: decoded.id }, data: { avatar_url: filePath } }); } else { await prisma.userProfile.create({ data: { - user_id: userId, + user_id: decoded.id, avatar_url: filePath, first_name: '', last_name: '' @@ -253,10 +276,10 @@ export class UserService { // Audit log - UPLOAD_AVATAR await auditService.logSync({ - userId, + userId: decoded.id, action: AuditAction.UPLOAD_FILE, entityType: 'User', - entityId: userId, + entityId: decoded.id, metadata: { operation: 'upload_avatar', filePath @@ -270,17 +293,26 @@ export class UserService { code: 200, message: 'Avatar uploaded successfully', data: { - id: userId, + id: decoded.id, avatar_url: presignedUrl } }; } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + logger.error('Invalid JWT token:', error); + throw new UnauthorizedError('Invalid token'); + } + if (error instanceof jwt.TokenExpiredError) { + logger.error('JWT token expired:', error); + throw new UnauthorizedError('Token expired'); + } logger.error('Failed to upload avatar', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.UPLOAD_FILE, entityType: 'UserProfile', - entityId: userId, + entityId: decoded?.id || 0, metadata: { operation: 'upload_avatar', error: error instanceof Error ? error.message : String(error) @@ -333,10 +365,12 @@ export class UserService { /** * Send verification email to user */ - async sendVerifyEmail(userId: number): Promise { + async sendVerifyEmail(token: string): Promise { try { + const decoded = jwt.verify(token, config.jwt.secret) as { id: number; email: string; roleCode: string }; + const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { id: decoded.id }, include: { role: true } }); @@ -394,12 +428,15 @@ export class UserService { message: 'Verification email sent successfully' }; } catch (error) { + if (error instanceof jwt.JsonWebTokenError) throw new UnauthorizedError('Invalid token'); + if (error instanceof jwt.TokenExpiredError) throw new UnauthorizedError('Token expired'); logger.error('Failed to send verification email', { error }); + const decoded = jwt.decode(token) as { id: number } | null; await auditService.logSync({ - userId, + userId: decoded?.id || 0, action: AuditAction.ERROR, entityType: 'UserProfile', - entityId: userId, + entityId: decoded?.id || 0, metadata: { operation: 'send_verification_email', error: error instanceof Error ? error.message : String(error) diff --git a/Backend/src/types/AdminCourseApproval.types.ts b/Backend/src/types/AdminCourseApproval.types.ts index d68c8c81..fa98ea6b 100644 --- a/Backend/src/types/AdminCourseApproval.types.ts +++ b/Backend/src/types/AdminCourseApproval.types.ts @@ -117,6 +117,10 @@ export interface GetCourseDetailForAdminResponse { data: CourseDetailForAdmin; } +export interface ApproveCourseBody { + comment?: string; +} + export interface ApproveCourseResponse { code: number; message: string; diff --git a/Backend/src/types/ChaptersLesson.typs.ts b/Backend/src/types/ChaptersLesson.typs.ts index a2dd9a37..51a26e4b 100644 --- a/Backend/src/types/ChaptersLesson.typs.ts +++ b/Backend/src/types/ChaptersLesson.typs.ts @@ -98,18 +98,18 @@ export interface ChapterData { // ============================================ export interface ChaptersRequest { - userId: number; + token: string; course_id: number; } export interface GetChapterRequest { - userId: number; + token: string; course_id: number; chapter_id: number; } export interface CreateChapterInput { - userId: number; + token: string; course_id: number; title: MultiLanguageText; description?: MultiLanguageText; @@ -118,13 +118,13 @@ export interface CreateChapterInput { } export interface CreateChapterRequest { - userId: number; + token: string; course_id: number; data: CreateChapterInput; } export interface UpdateChapterInput { - userId: number; + token: string; course_id: number; chapter_id: number; title?: MultiLanguageText; @@ -134,20 +134,20 @@ export interface UpdateChapterInput { } export interface UpdateChapterRequest { - userId: number; + token: string; course_id: number; chapter_id: number; data: UpdateChapterInput; } export interface DeleteChapterRequest { - userId: number; + token: string; course_id: number; chapter_id: number; } export interface ReorderChapterRequest { - userId: number; + token: string; course_id: number; chapter_id: number; sort_order: number; @@ -199,7 +199,7 @@ export interface ReorderChapterResponse { // ============================================ export interface GetLessonRequest { - userId: number; + token: string; course_id: number; chapter_id: number; lesson_id: number; @@ -216,7 +216,7 @@ export interface UploadedFileInfo { } export interface CreateLessonInput { - userId: number; + token: string; course_id: number; chapter_id: number; title: MultiLanguageText; @@ -293,7 +293,7 @@ export interface QuizChoiceData { } export interface CreateLessonRequest { - userId: number; + token: string; course_id: number; chapter_id: number; data: CreateLessonInput; @@ -311,7 +311,7 @@ export interface UpdateLessonInput { } export interface UpdateLessonRequest { - userId: number; + token: string; course_id: number; chapter_id: number; lesson_id: number; @@ -319,14 +319,14 @@ export interface UpdateLessonRequest { } export interface DeleteLessonRequest { - userId: number; + token: string; course_id: number; chapter_id: number; lesson_id: number; } export interface ReorderLessonsRequest { - userId: number; + token: string; course_id: number; chapter_id: number; lesson_id: number; @@ -365,7 +365,7 @@ export interface UpdateLessonResponse { * Input for uploading video to a lesson */ export interface UploadVideoInput { - userId: number; + token: string; course_id: number; lesson_id: number; video: UploadedFileInfo; @@ -375,7 +375,7 @@ export interface UploadVideoInput { * Input for updating (replacing) video in a lesson */ export interface UpdateVideoInput { - userId: number; + token: string; course_id: number; lesson_id: number; video: UploadedFileInfo; @@ -385,7 +385,7 @@ export interface UpdateVideoInput { * Input for setting YouTube video to a lesson */ export interface SetYouTubeVideoInput { - userId: number; + token: string; course_id: number; lesson_id: number; youtube_video_id: string; @@ -411,7 +411,7 @@ export interface YouTubeVideoResponse { * Input for uploading a single attachment to a lesson */ export interface UploadAttachmentInput { - userId: number; + token: string; course_id: number; lesson_id: number; attachment: UploadedFileInfo; @@ -421,7 +421,7 @@ export interface UploadAttachmentInput { * Input for deleting an attachment from a lesson */ export interface DeleteAttachmentInput { - userId: number; + token: string; course_id: number; lesson_id: number; attachment_id: number; @@ -490,7 +490,7 @@ export interface LessonWithDetailsResponse { * Input for adding quiz to an existing QUIZ lesson */ export interface AddQuizToLessonInput { - userId: number; + token: string; course_id: number; lesson_id: number; quiz_data: { @@ -509,7 +509,7 @@ export interface AddQuizToLessonInput { * Input for adding a single question to a quiz lesson */ export interface AddQuestionInput { - userId: number; + token: string; course_id: number; lesson_id: number; question: MultiLanguageText; @@ -532,7 +532,7 @@ export interface AddQuestionResponse { * Input for updating a question */ export interface UpdateQuestionInput { - userId: number; + token: string; course_id: number; lesson_id: number; question_id: number; @@ -556,14 +556,14 @@ export interface UpdateQuestionResponse { * Input for deleting a question */ export interface DeleteQuestionInput { - userId: number; + token: string; course_id: number; lesson_id: number; question_id: number; } export interface ReorderQuestionInput { - userId: number; + token: string; course_id: number; lesson_id: number; question_id: number; @@ -588,7 +588,7 @@ export interface DeleteQuestionResponse { * Input for updating quiz settings */ export interface UpdateQuizInput { - userId: number; + token: string; course_id: number; lesson_id: number; title?: MultiLanguageText; diff --git a/Backend/src/types/CoursesInstructor.types.ts b/Backend/src/types/CoursesInstructor.types.ts index 8291a1d5..cc4aa149 100644 --- a/Backend/src/types/CoursesInstructor.types.ts +++ b/Backend/src/types/CoursesInstructor.types.ts @@ -24,7 +24,7 @@ export interface createCourseResponse { } export interface ListMyCoursesInput { - userId: number; + token: string; status?: 'DRAFT' | 'PENDING' | 'APPROVED' | 'REJECTED' | 'ARCHIVED'; } @@ -42,7 +42,7 @@ export interface GetMyCourseResponse { } export interface getmyCourse { - userId: number; + token: string; course_id: number; } @@ -94,13 +94,13 @@ export interface listCourseinstructorResponse { } export interface addinstructorCourse { - userId: number; + token: string; email_or_username: string; course_id: number; } export interface SearchInstructorInput { - userId: number; + token: string; query: string; course_id: number; } @@ -145,12 +145,12 @@ export interface listinstructorCourseResponse { } export interface listinstructorCourse { - userId: number; + token: string; course_id: number; } export interface removeinstructorCourse { - userId: number; + token: string; user_id: number; course_id: number; } @@ -161,7 +161,7 @@ export interface removeinstructorCourseResponse { } export interface setprimaryCourseInstructor { - userId: number; + token: string; user_id: number; course_id: number; } @@ -172,12 +172,12 @@ export interface setprimaryCourseInstructorResponse { } export interface sendCourseForReview { - userId: number; + token: string; course_id: number; } export interface setCourseDraft { - userId: number; + token: string; course_id: number; } @@ -220,7 +220,7 @@ export interface GetCourseApprovalsResponse { // ============================================ export interface GetEnrolledStudentsInput { - userId: number; + token: string; course_id: number; page?: number; limit?: number; @@ -254,7 +254,7 @@ export interface GetEnrolledStudentsResponse { // ============================================ export interface GetQuizScoresInput { - userId: number; + token: string; course_id: number; lesson_id: number; page?: number; @@ -305,7 +305,7 @@ export interface GetQuizScoresResponse { // ============================================ export interface GetQuizAttemptDetailInput { - userId: number; + token: string; course_id: number; lesson_id: number; student_id: number; @@ -353,7 +353,7 @@ export interface GetQuizAttemptDetailResponse { // ============================================ export interface GetEnrolledStudentDetailInput { - userId: number; + token: string; course_id: number; student_id: number; } @@ -435,7 +435,7 @@ export interface GetCourseApprovalHistoryResponse { } export interface CloneCourseInput { - userId: number; + token: string; course_id: number; title: MultiLanguageText; } @@ -448,14 +448,3 @@ export interface CloneCourseResponse { title: MultiLanguageText; }; } - -// ============================================ -// Get All Students across all instructor courses -// ============================================ - -export interface GetAllMyStudentsResponse { - code: number; - message: string; - total_students: number; - total_completed: number; -} diff --git a/Backend/src/types/CoursesStudent.types.ts b/Backend/src/types/CoursesStudent.types.ts index 2c92850f..541787c1 100644 --- a/Backend/src/types/CoursesStudent.types.ts +++ b/Backend/src/types/CoursesStudent.types.ts @@ -9,7 +9,7 @@ export type MultiLangText = MultiLanguageText; // ============================================ export interface EnrollCourseInput { - userId: number; + token: string; course_id: number; } @@ -26,7 +26,7 @@ export interface EnrollCourseResponse { } export interface ListEnrolledCoursesInput { - userId: number; + token: string; page?: number; limit?: number; status?: EnrollmentStatus; @@ -64,7 +64,7 @@ export interface ListEnrolledCoursesResponse { // ============================================ export interface GetCourseLearningInput { - userId: number; + token: string; course_id: number; } @@ -126,7 +126,7 @@ export interface GetCourseLearningResponse { // ============================================ export interface GetLessonContentInput { - userId: number; + token: string; course_id: number; lesson_id: number; } @@ -204,7 +204,7 @@ export interface GetLessonContentResponse { // ============================================ export interface CheckLessonAccessInput { - userId: number; + token: string; course_id: number; lesson_id: number; } @@ -236,7 +236,7 @@ export interface CheckLessonAccessResponse { // ============================================ export interface SaveVideoProgressInput { - userId: number; + token: string; lesson_id: number; video_progress_seconds: number; video_duration_seconds?: number; @@ -258,7 +258,7 @@ export interface SaveVideoProgressResponse { } export interface GetVideoProgressInput { - userId: number; + token: string; lesson_id: number; } @@ -281,7 +281,7 @@ export interface GetVideoProgressResponse { // ============================================ export interface MarkLessonCompleteInput { - userId: number; + token: string; course_id: number; lesson_id: number; } @@ -314,7 +314,7 @@ export interface EnrollCourseBody { } export interface CompleteLessonInput { - userId: number; + token: string; lesson_id: number; } @@ -342,7 +342,7 @@ export interface QuizAnswerInput { } export interface SubmitQuizInput { - userId: number; + token: string; course_id: number; lesson_id: number; answers: QuizAnswerInput[]; @@ -384,7 +384,7 @@ export interface SubmitQuizResponse { // ============================================ export interface GetQuizAttemptsInput { - userId: number; + token: string; course_id: number; lesson_id: number; } diff --git a/Backend/src/types/RecommendedCourses.types.ts b/Backend/src/types/RecommendedCourses.types.ts index 48f495a7..c11c6c93 100644 --- a/Backend/src/types/RecommendedCourses.types.ts +++ b/Backend/src/types/RecommendedCourses.types.ts @@ -1,10 +1,14 @@ import { MultiLanguageText } from './index'; +// ============================================ +// Request Types +// ============================================ + + // ============================================ // Response Types // ============================================ -/** ใช้ใน listApprovedCourses — มีแค่ chapters_count */ export interface RecommendedCourseData { id: number; title: MultiLanguageText; @@ -37,19 +41,7 @@ export interface RecommendedCourseData { }; }>; chapters_count: number; -} - -/** ใช้ใน getCourseById — มี chapters + lessons พร้อมชื่อ */ -export interface RecommendedCourseDetailData extends Omit { - chapters: { - id: number; - title: MultiLanguageText; - sort_order: number; - lessons: { - id: number; - title: MultiLanguageText; - }[]; - }[]; + lessons_count: number; } export interface ListApprovedCoursesResponse { @@ -62,7 +54,7 @@ export interface ListApprovedCoursesResponse { export interface GetCourseByIdResponse { code: number; message: string; - data: RecommendedCourseDetailData; + data: RecommendedCourseData; } export interface ToggleRecommendedResponse { diff --git a/Backend/src/types/announcements.types.ts b/Backend/src/types/announcements.types.ts index b026809e..f960a961 100644 --- a/Backend/src/types/announcements.types.ts +++ b/Backend/src/types/announcements.types.ts @@ -22,7 +22,7 @@ export interface AnnouncementAttachment { updated_at: Date; } -export interface ListAnnouncementResponse { +export interface ListAnnouncementResponse{ code: number; message: string; data: Announcement[]; @@ -31,15 +31,15 @@ export interface ListAnnouncementResponse { limit: number; } -export interface ListAnnouncementInput { - userId: number; +export interface ListAnnouncementInput{ + token: string; course_id: number; page?: number; limit?: number; } -export interface CreateAnnouncementInput { - userId: number; +export interface CreateAnnouncementInput{ + token: string; course_id: number; title: MultiLanguageText; content: MultiLanguageText; @@ -49,39 +49,39 @@ export interface CreateAnnouncementInput { files?: Express.Multer.File[]; } -export interface UploadAnnouncementAttachmentInput { - userId: number; +export interface UploadAnnouncementAttachmentInput{ + token: string; course_id: number; announcement_id: number; file: File; } -export interface UploadAnnouncementAttachmentResponse { +export interface UploadAnnouncementAttachmentResponse{ code: number; message: string; data: AnnouncementAttachment; } -export interface DeleteAnnouncementAttachmentInput { - userId: number; +export interface DeleteAnnouncementAttachmentInput{ + token: string; course_id: number; announcement_id: number; attachment_id: number; } -export interface DeleteAnnouncementAttachmentResponse { +export interface DeleteAnnouncementAttachmentResponse{ code: number; message: string; } -export interface CreateAnnouncementResponse { +export interface CreateAnnouncementResponse{ code: number; message: string; data: Announcement; } -export interface UpdateAnnouncementInput { - userId: number; +export interface UpdateAnnouncementInput{ + token: string; course_id: number; announcement_id: number; title: MultiLanguageText; @@ -92,19 +92,19 @@ export interface UpdateAnnouncementInput { attachments?: AnnouncementAttachment[]; } -export interface UpdateAnnouncementResponse { +export interface UpdateAnnouncementResponse{ code: number; message: string; data: Announcement; } -export interface DeleteAnnouncementInput { - userId: number; +export interface DeleteAnnouncementInput{ + token: string; course_id: number; announcement_id: number; } -export interface DeleteAnnouncementResponse { +export interface DeleteAnnouncementResponse{ code: number; message: string; } diff --git a/Backend/src/types/auth.types.ts b/Backend/src/types/auth.types.ts index 1ec9235a..be1fe625 100644 --- a/Backend/src/types/auth.types.ts +++ b/Backend/src/types/auth.types.ts @@ -28,6 +28,7 @@ export interface LoginResponse { data: { token: string; refreshToken: string; + user: UserResponse; }; } diff --git a/Backend/src/types/certificate.types.ts b/Backend/src/types/certificate.types.ts index ac442f76..a35caa19 100644 --- a/Backend/src/types/certificate.types.ts +++ b/Backend/src/types/certificate.types.ts @@ -3,7 +3,7 @@ // ============================================ export interface GenerateCertificateInput { - userId: number; + token: string; course_id: number; } @@ -19,7 +19,7 @@ export interface GenerateCertificateResponse { } export interface GetCertificateInput { - userId: number; + token: string; course_id: number; } @@ -37,7 +37,7 @@ export interface GetCertificateResponse { } export interface ListMyCertificatesInput { - userId: number; + token: string; } export interface ListMyCertificatesResponse { diff --git a/Backend/src/types/courses.types.ts b/Backend/src/types/courses.types.ts index a294d7e2..42c83398 100644 --- a/Backend/src/types/courses.types.ts +++ b/Backend/src/types/courses.types.ts @@ -1,5 +1,4 @@ import { Course } from '@prisma/client'; -import { MultiLanguageText } from './index'; export interface ListCoursesInput { category_id?: number; @@ -19,47 +18,8 @@ export interface listCourseResponse { totalPages: number; } -export interface CourseDetail extends Omit { - title: MultiLanguageText; - description: MultiLanguageText; - creator: { - id: number; - username: string; - email: string; - profile: { - first_name: string; - last_name: string; - avatar_url: string | null; - } | null; - }; - instructors: { - user_id: number; - is_primary: boolean; - user: { - id: number; - username: string; - email: string; - profile: { - first_name: string; - last_name: string; - avatar_url: string | null; - } | null; - }; - }[]; - category: { id: number; name: MultiLanguageText } | null; - chapters: { - id: number; - title: MultiLanguageText; - sort_order: number; - lessons: { - id: number; - title: MultiLanguageText; - }[]; - }[]; -} - export interface getCourseResponse { code: number; message: string; - data: CourseDetail | null; + data: Course | null; } diff --git a/Backend/src/types/user.types.ts b/Backend/src/types/user.types.ts index 42ac8e75..413cb3f2 100644 --- a/Backend/src/types/user.types.ts +++ b/Backend/src/types/user.types.ts @@ -59,14 +59,6 @@ export interface ProfileUpdateResponse { }; }; -export interface role { - id: number; - code: string; -} - -export interface rolesResponse { - roles: role[]; -} export interface ChangePasswordRequest { old_password: string; diff --git a/Backend/src/validators/ChaptersLesson.validator.ts b/Backend/src/validators/ChaptersLesson.validator.ts index 45d7687a..933a3e1a 100644 --- a/Backend/src/validators/ChaptersLesson.validator.ts +++ b/Backend/src/validators/ChaptersLesson.validator.ts @@ -79,7 +79,7 @@ export const UpdateLessonValidator = Joi.object({ 'number.min': 'Duration must be at least 0' }), sort_order: Joi.number().integer().min(0).optional(), - prerequisite_lesson_ids: Joi.array().items(Joi.number().integer().positive()).allow(null).optional(), + prerequisite_lesson_ids: Joi.array().items(Joi.number().integer().positive()).optional(), is_published: Joi.boolean().optional() }); diff --git a/Backend/tests/k6/enroll-load-test.js b/Backend/tests/k6/enroll-load-test.js deleted file mode 100644 index d3e1032b..00000000 --- a/Backend/tests/k6/enroll-load-test.js +++ /dev/null @@ -1,160 +0,0 @@ -// Backend/tests/k6/enroll-load-test.js -// -// จำลองนักเรียนหลายคน login แล้ว enroll คอร์สพร้อมกัน -// -// Flow: -// 1. Login -// 2. Enroll คอร์ส -// 3. ตรวจสอบ enrolled courses -// -// Usage: -// k6 run -e APP_URL=http://192.168.1.137:4000 -e COURSE_ID=1 tests/k6/enroll-load-test.js - -import http from 'k6/http'; -import { check, sleep, group } from 'k6'; -import { Rate, Trend, Counter } from 'k6/metrics'; -import { SharedArray } from 'k6/data'; - -// ─── Custom Metrics ─────────────────────────────────────────────────────────── -const errorRate = new Rate('errors'); -const loginTime = new Trend('login_duration', true); -const enrollTime = new Trend('enroll_duration', true); -const enrolledCount = new Counter('successful_enrollments'); - -// ─── Load student credentials ───────────────────────────────────────────────── -const students = new SharedArray('students', function () { - return JSON.parse(open('./test-credentials.json')).students; -}); - -// ─── Config ─────────────────────────────────────────────────────────────────── -const BASE_URL = __ENV.APP_URL || 'http://192.168.1.137:4000'; -const COURSE_ID = __ENV.COURSE_ID || '1'; - -// ─── Test Options ───────────────────────────────────────────────────────────── -export const options = { - stages: [ - { duration: '20s', target: 10 }, // Ramp up - { duration: '1m', target: 30 }, // Increase - { duration: '30s', target: 50 }, // Peak: 50 คน enroll พร้อมกัน - { duration: '30s', target: 0 }, // Ramp down - ], - thresholds: { - 'login_duration': ['p(95)<2000'], // Login < 2s - 'enroll_duration': ['p(95)<1000'], // Enroll < 1s - 'errors': ['rate<0.05'], - 'http_req_failed': ['rate<0.05'], - }, -}; - -// ─── Helper ─────────────────────────────────────────────────────────────────── -function jsonHeaders(token) { - const h = { 'Content-Type': 'application/json' }; - if (token) h['Authorization'] = `Bearer ${token}`; - return h; -} - -// ─── Main ───────────────────────────────────────────────────────────────────── -export default function () { - const student = students[__VU % students.length]; - let token = null; - - // ── Step 1: Login ────────────────────────────────────────────────────────── - group('1. Login', () => { - const res = http.post( - `${BASE_URL}/api/auth/login`, - JSON.stringify({ email: student.email, password: student.password }), - { headers: jsonHeaders(null) } - ); - - loginTime.add(res.timings.duration); - errorRate.add(res.status !== 200); - - check(res, { - 'login: status 200': (r) => r.status === 200, - 'login: has token': (r) => { try { return !!r.json('data.token'); } catch { return false; } }, - }); - - if (res.status === 200) { - try { token = res.json('data.token'); } catch {} - } - }); - - if (!token) { - console.warn(`[VU ${__VU}] Login failed for ${student.email} — skipping`); - sleep(1); - return; - } - - sleep(0.5); - - // ── Step 2: Enroll ───────────────────────────────────────────────────────── - group('2. Enroll Course', () => { - const res = http.post( - `${BASE_URL}/api/students/courses/${COURSE_ID}/enroll`, - null, - { headers: jsonHeaders(token) } - ); - - enrollTime.add(res.timings.duration); - - // 200 = enrolled, 409 = already enrolled (ถือว่าโอเค) - const ok = res.status === 200 || res.status === 409; - errorRate.add(!ok); - - if (res.status === 200) enrolledCount.add(1); - - check(res, { - 'enroll: 200 or 409': (r) => r.status === 200 || r.status === 409, - 'enroll: fast response': (r) => r.timings.duration < 1000, - }); - }); - - sleep(0.5); - - // ── Step 3: Verify — ดึงรายการคอร์สที่ลงทะเบียน ───────────────────────── - group('3. Get Enrolled Courses', () => { - const res = http.get( - `${BASE_URL}/api/students/courses`, - { headers: jsonHeaders(token) } - ); - - errorRate.add(res.status !== 200); - - check(res, { - 'enrolled courses: status 200': (r) => r.status === 200, - }); - }); - - sleep(1); -} - -// ─── Summary ────────────────────────────────────────────────────────────────── -export function handleSummary(data) { - const m = data.metrics; - const avg = (k) => m[k]?.values?.avg?.toFixed(0) ?? 'N/A'; - const p95 = (k) => m[k]?.values?.['p(95)']?.toFixed(0) ?? 'N/A'; - const rate = (k) => ((m[k]?.values?.rate ?? 0) * 100).toFixed(2); - const cnt = (k) => m[k]?.values?.count ?? 0; - - return { - stdout: ` -╔══════════════════════════════════════════════════════════╗ -║ Course Enroll — Load Test ║ -╠══════════════════════════════════════════════════════════╣ -║ Course ID : ${String(COURSE_ID).padEnd(43)}║ -╠══════════════════════════════════════════════════════════╣ -║ RESPONSE TIMES (avg / p95) ║ -║ Login : ${avg('login_duration')}ms / ${p95('login_duration')}ms -║ Enroll : ${avg('enroll_duration')}ms / ${p95('enroll_duration')}ms -╠══════════════════════════════════════════════════════════╣ -║ COUNTS ║ -║ Total Requests : ${String(cnt('http_reqs')).padEnd(33)}║ -║ New Enrollments : ${String(cnt('successful_enrollments')).padEnd(33)}║ -╠══════════════════════════════════════════════════════════╣ -║ ERROR RATES ║ -║ HTTP Failed : ${(rate('http_req_failed') + '%').padEnd(39)}║ -║ Custom Errors : ${(rate('errors') + '%').padEnd(39)}║ -╚══════════════════════════════════════════════════════════╝ -`, - }; -} diff --git a/Backend/tests/k6/login-load-test.js b/Backend/tests/k6/login-load-test.js index aee4cb4a..2a0c375d 100644 --- a/Backend/tests/k6/login-load-test.js +++ b/Backend/tests/k6/login-load-test.js @@ -31,7 +31,7 @@ export const options = { thresholds: { http_req_duration: ['p(95)<2000'], // 95% of requests < 2s errors: ['rate<0.1'], // Error rate < 10% - login_duration: ['p(95)<2000'], // 95% pof logins < 2s + login_duration: ['p(95)<2000'], // 95% of logins < 2s }, }; diff --git a/Backend/tests/k6/video-watching-load-test.js b/Backend/tests/k6/video-watching-load-test.js deleted file mode 100644 index e3bb205c..00000000 --- a/Backend/tests/k6/video-watching-load-test.js +++ /dev/null @@ -1,269 +0,0 @@ -// Backend/tests/k6/video-watching-load-test.js -// -// จำลองนักเรียนหลายคนดูวีดีโอพร้อมกัน (Concurrent Video Watching) -// -// Flow จริงที่ simulate: -// 1. Login ด้วย account ของ student แต่ละคน -// 2. Load หน้าเรียนคอร์ส (getCourseLearning) -// 3. เปิดบทเรียนวีดีโอ (getLessonContent) -// 4. Save progress ทุก 5 วินาที (จำลองการ watch) -// 5. เมื่อดูครบ (≥90%) → mark lesson complete -// -// Usage: -// k6 run -e APP_URL=http://localhost:4000 -e COURSE_ID=1 -e LESSON_ID=1 tests/k6/video-watching-load-test.js -// -// ปรับจำนวน VUs และ duration ได้ด้วย: -// k6 run -e APP_URL=http://localhost:4000 -e COURSE_ID=1 -e LESSON_ID=1 --vus 30 --duration 2m tests/k6/video-watching-load-test.js - -import http from 'k6/http'; -import { check, sleep, group } from 'k6'; -import { Rate, Trend, Counter } from 'k6/metrics'; -import { SharedArray } from 'k6/data'; - -// ─── Custom Metrics ─────────────────────────────────────────────────────────── -const errorRate = new Rate('errors'); -const loginTime = new Trend('login_duration', true); -const courseLearningTime = new Trend('course_learning_duration', true); -const lessonLoadTime = new Trend('lesson_load_duration', true); -const progressSaveTime = new Trend('progress_save_duration', true); -const completeLessonTime = new Trend('complete_lesson_duration', true); -const completedCount = new Counter('completed_lessons'); -const progressSaveCount = new Counter('progress_saves'); -const videoLoadTime = new Trend('video_load_duration', true); - -// ─── Load student credentials ──────────────────────────────────────────────── -// อ่านจาก test-credentials.json (50 accounts) -const students = new SharedArray('students', function () { - return JSON.parse(open('./test-credentials.json')).students; -}); - -// ─── Config ─────────────────────────────────────────────────────────────────── -const BASE_URL = __ENV.APP_URL || 'http://192.168.1.137:4000'; -const COURSE_ID = __ENV.COURSE_ID || '1'; -const LESSON_ID = __ENV.LESSON_ID || '1'; - -// วีดีโอความยาว (วินาที) — ปรับตามจริง -const VIDEO_DURATION_SECONDS = parseInt(__ENV.VIDEO_DURATION || '98'); // default 5 นาที - -// save progress interval: ทุก 5 วินาที (เหมือน client จริง) -// แต่ในการ test เราจะ simulate เร็วขึ้นโดยใช้ sleep น้อยลง -const PROGRESS_INTERVAL_SECONDS = parseInt(__ENV.PROGRESS_INTERVAL || '15'); - -// ─── Test Options ───────────────────────────────────────────────────────────── -export const options = { - stages: [ - { duration: '30s', target: 10 }, // Ramp up: 10 คนเริ่มดูวีดีโอ - { duration: '1m', target: 30 }, // Ramp up: เพิ่มเป็น 30 คน - { duration: '2m', target: 30 }, // Steady: 30 คนดูพร้อมกัน - { duration: '30s', target: 50 }, // Peak: เพิ่มเป็น 50 คน - { duration: '1m', target: 50 }, // Steady Peak: 50 คนพร้อมกัน - { duration: '30s', target: 0 }, // Ramp down - ], - thresholds: { - // Response times - 'login_duration': ['p(95)<2000'], // Login < 2s - 'course_learning_duration': ['p(95)<1000'], // Load course page < 1s - 'lesson_load_duration': ['p(95)<1000'], // Load lesson < 1s - 'video_load_duration': ['p(95)<3000'], // Fetch video from MinIO < 3s - 'progress_save_duration': ['p(95)<500'], // Save progress < 500ms (critical — บ่อย) - 'complete_lesson_duration': ['p(95)<1000'], // Complete lesson < 1s - - // Error rate - 'errors': ['rate<0.05'], // Error < 5% - 'http_req_failed': ['rate<0.05'], // HTTP error < 5% - }, -}; - -// ─── Helper ─────────────────────────────────────────────────────────────────── -function jsonHeaders(token) { - const h = { 'Content-Type': 'application/json' }; - if (token) h['Authorization'] = `Bearer ${token}`; - return h; -} - -// ─── Per-VU persistent state (จำข้ามรอบ iteration) ────────────────────────── -// ตัวแปรนี้อยู่ระดับ module → k6 สร้างแยกต่างหากสำหรับแต่ละ VU -// ค่าจะถูกจำไว้ตลอดอายุของ VU (ข้ามหลายรอบ iteration) -let vuToken = null; // token ที่ login ไว้แล้ว -let vuSetupDone = false; // เคย load course+lesson แล้วหรือยัง -let vuProgress = 0; // ตำแหน่งวีดีโอปัจจุบัน (วินาที) -let vuCompleted = false; // lesson complete แล้วหรือยัง - -// ─── Main ───────────────────────────────────────────────────────────────────── -export default function () { - const student = students[__VU % students.length]; - - // ── Step 1: Login (ทำครั้งเดียวตอน VU เริ่มต้น หรือถ้า token หาย) ───────── - if (!vuToken) { - group('1. Login', () => { - const res = http.post( - `${BASE_URL}/api/auth/login`, - JSON.stringify({ email: student.email, password: student.password }), - { headers: jsonHeaders(null) } - ); - - loginTime.add(res.timings.duration); - const ok = res.status === 200; - errorRate.add(!ok); - - check(res, { - 'login: status 200': (r) => r.status === 200, - 'login: has token': (r) => { try { return !!r.json('data.token'); } catch { return false; } }, - }); - - if (ok) { - try { vuToken = res.json('data.token'); } catch {} - } - }); - - if (!vuToken) { - console.warn(`[VU ${__VU}] Login failed for ${student.email} — skipping`); - sleep(2); - return; - } - } - - // ── Step 2 (removed): Enroll ทำผ่าน enroll-load-test.js แยกต่างหาก ───────── - - // ── Step 3+4: Setup — Load course และ open lesson (ทำครั้งเดียวต่อ VU) ───── - if (!vuSetupDone) { - group('3. Load Course Learning Page', () => { - const res = http.get( - `${BASE_URL}/api/students/courses/${COURSE_ID}/learn`, - { headers: jsonHeaders(vuToken) } - ); - courseLearningTime.add(res.timings.duration); - errorRate.add(res.status !== 200); - check(res, { 'course learn: status 200': (r) => r.status === 200 }); - }); - - sleep(1); - - let videoUrl = null; - group('4. Open Lesson', () => { - const res = http.get( - `${BASE_URL}/api/students/courses/${COURSE_ID}/lessons/${LESSON_ID}`, - { headers: jsonHeaders(vuToken) } - ); - lessonLoadTime.add(res.timings.duration); - errorRate.add(res.status !== 200); - check(res, { 'lesson: status 200': (r) => r.status === 200 }); - if (res.status === 200) { - try { videoUrl = res.json('data.video_url'); } catch {} - } - }); - - // ── Step 4.5: Fetch video จาก MinIO ────────────────────────────────────── - if (videoUrl) { - group('4.5 Fetch Video from MinIO', () => { - const res = http.get(videoUrl, { - headers: { 'Range': 'bytes=0-1048575' }, // ขอแค่ 1MB แรก - timeout: '10s', - }); - videoLoadTime.add(res.timings.duration); - const ok = res.status === 200 || res.status === 206; - errorRate.add(!ok); - check(res, { - 'minio video: 200 or 206': (r) => r.status === 200 || r.status === 206, - 'minio video: fast': (r) => r.timings.duration < 3000, - }); - }); - } else { - console.log(`[VU ${__VU}] No video_url returned — skipping MinIO fetch`); - } - - sleep(2); // รอ buffer เริ่มต้น - vuSetupDone = true; - } - - // ── Step 5: Save Progress ทีละ tick (ต่อจากตำแหน่งเดิม) ──────────────────── - // แต่ละ iteration ของ VU = ส่ง progress 1 ครั้ง แล้ว sleep ตาม interval จริง - if (!vuCompleted) { - vuProgress += PROGRESS_INTERVAL_SECONDS; - - group('5. Watch Video (Save Progress)', () => { - const res = http.post( - `${BASE_URL}/api/students/lessons/${LESSON_ID}/progress`, - JSON.stringify({ - video_progress_seconds: vuProgress, - video_duration_seconds: VIDEO_DURATION_SECONDS, - }), - { headers: jsonHeaders(vuToken) } - ); - - progressSaveTime.add(res.timings.duration); - progressSaveCount.add(1); - - const ok = res.status === 200; - errorRate.add(!ok); - check(res, { - 'progress save: status 200': (r) => r.status === 200, - 'progress save: fast': (r) => r.timings.duration < 500, - }); - - console.log(`[VU ${__VU}] progress: ${vuProgress}s / ${VIDEO_DURATION_SECONDS}s (${Math.round(vuProgress / VIDEO_DURATION_SECONDS * 100)}%)`); - }); - - // ── Step 6: Mark complete เมื่อดูครบ ≥95% ────────────────────────────── - if (vuProgress >= VIDEO_DURATION_SECONDS * 0.95) { - group('6. Complete Lesson', () => { - const res = http.post( - `${BASE_URL}/api/students/courses/${COURSE_ID}/lessons/${LESSON_ID}/complete`, - null, - { headers: jsonHeaders(vuToken) } - ); - completeLessonTime.add(res.timings.duration); - errorRate.add(res.status !== 200 && res.status !== 409); - if (res.status === 200) completedCount.add(1); - check(res, { - 'complete: status 200 or 409': (r) => r.status === 200 || r.status === 409, - }); - }); - - vuCompleted = true; - console.log(`[VU ${__VU}] ✓ Lesson completed`); - } - } - - // sleep ตาม interval จริง — ทุก VU ส่ง progress ทุก PROGRESS_INTERVAL_SECONDS วินาที - sleep(PROGRESS_INTERVAL_SECONDS); -} - -// ─── Summary ────────────────────────────────────────────────────────────────── -export function handleSummary(data) { - const m = data.metrics; - - const avg = (k) => m[k]?.values?.avg?.toFixed(0) ?? 'N/A'; - const p95 = (k) => m[k]?.values?.['p(95)']?.toFixed(0) ?? 'N/A'; - const rate = (k) => ((m[k]?.values?.rate ?? 0) * 100).toFixed(2); - const count = (k) => m[k]?.values?.count ?? 0; - - return { - stdout: ` -╔══════════════════════════════════════════════════════════╗ -║ Concurrent Video Watching — Load Test ║ -╠══════════════════════════════════════════════════════════╣ -║ Course ID : ${COURSE_ID.padEnd(44)}║ -║ Lesson ID : ${LESSON_ID.padEnd(44)}║ -║ Video : ${String(VIDEO_DURATION_SECONDS + 's').padEnd(44)}║ -╠══════════════════════════════════════════════════════════╣ -║ RESPONSE TIMES (avg / p95) ║ -║ Login : ${avg('login_duration')}ms / ${p95('login_duration')}ms${' '.repeat(Math.max(0, 20 - avg('login_duration').length - p95('login_duration').length))}║ -║ Course Learning Page: ${avg('course_learning_duration')}ms / ${p95('course_learning_duration')}ms${' '.repeat(Math.max(0, 20 - avg('course_learning_duration').length - p95('course_learning_duration').length))}║ -║ Lesson Load : ${avg('lesson_load_duration')}ms / ${p95('lesson_load_duration')}ms${' '.repeat(Math.max(0, 20 - avg('lesson_load_duration').length - p95('lesson_load_duration').length))}║ -║ MinIO Video Fetch : ${avg('video_load_duration')}ms / ${p95('video_load_duration')}ms${' '.repeat(Math.max(0, 20 - avg('video_load_duration').length - p95('video_load_duration').length))}║ -║ Save Progress : ${avg('progress_save_duration')}ms / ${p95('progress_save_duration')}ms${' '.repeat(Math.max(0, 20 - avg('progress_save_duration').length - p95('progress_save_duration').length))}║ -║ Complete Lesson : ${avg('complete_lesson_duration')}ms / ${p95('complete_lesson_duration')}ms${' '.repeat(Math.max(0, 20 - avg('complete_lesson_duration').length - p95('complete_lesson_duration').length))}║ -╠══════════════════════════════════════════════════════════╣ -║ COUNTS ║ -║ Total Requests : ${String(count('http_reqs')).padEnd(33)}║ -║ Progress Saves : ${String(count('progress_saves')).padEnd(33)}║ -║ Lessons Completed : ${String(count('completed_lessons')).padEnd(33)}║ -╠══════════════════════════════════════════════════════════╣ -║ ERROR RATES ║ -║ HTTP Failed : ${(rate('http_req_failed') + '%').padEnd(33)}║ -║ Custom Errors : ${(rate('errors') + '%').padEnd(33)}║ -╚══════════════════════════════════════════════════════════╝ -`, - }; -} diff --git a/Backend/tests/tsconfig.json b/Backend/tests/tsconfig.json deleted file mode 100644 index 330cd8fd..00000000 --- a/Backend/tests/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../tsconfig.test.json", - "compilerOptions": { - "rootDir": "..", - "types": [ - "node", - "jest" - ] - }, - "include": [ - "../src/**/*", - "./**/*" - ] -} \ No newline at end of file diff --git a/Backend/tests/unit/validators/AdminCourseApproval.validator.test.ts b/Backend/tests/unit/validators/AdminCourseApproval.validator.test.ts deleted file mode 100644 index 9068ad48..00000000 --- a/Backend/tests/unit/validators/AdminCourseApproval.validator.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - ApproveCourseValidator, - RejectCourseValidator, -} from '@/validators/AdminCourseApproval.validator'; - -describe('ApproveCourseValidator', () => { - it('should pass with no body (comment optional)', () => { - const { error } = ApproveCourseValidator.validate({}); - expect(error).toBeUndefined(); - }); - - it('should pass with optional comment', () => { - const { error } = ApproveCourseValidator.validate({ - comment: 'Looks great!', - }); - expect(error).toBeUndefined(); - }); - - it('should fail when comment exceeds 1000 characters', () => { - const { error } = ApproveCourseValidator.validate({ - comment: 'a'.repeat(1001), - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/must not exceed 1000/i); - }); - - it('should pass with comment exactly 1000 characters', () => { - const { error } = ApproveCourseValidator.validate({ - comment: 'a'.repeat(1000), - }); - expect(error).toBeUndefined(); - }); -}); - -describe('RejectCourseValidator', () => { - it('should pass with valid rejection comment', () => { - const { error } = RejectCourseValidator.validate({ - comment: 'The content is incomplete and needs more details.', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without comment', () => { - const { error } = RejectCourseValidator.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Comment is required when rejecting/i); - }); - - it('should fail when comment is too short (< 10 chars)', () => { - const { error } = RejectCourseValidator.validate({ comment: 'Too short' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/at least 10 characters/i); - }); - - it('should pass with exactly 10 characters', () => { - const { error } = RejectCourseValidator.validate({ comment: '1234567890' }); - expect(error).toBeUndefined(); - }); - - it('should fail when comment exceeds 1000 characters', () => { - const { error } = RejectCourseValidator.validate({ - comment: 'a'.repeat(1001), - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/must not exceed 1000/i); - }); -}); diff --git a/Backend/tests/unit/validators/ChaptersLesson.validator.test.ts b/Backend/tests/unit/validators/ChaptersLesson.validator.test.ts deleted file mode 100644 index 7e80bc68..00000000 --- a/Backend/tests/unit/validators/ChaptersLesson.validator.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { - CreateChapterValidator, - UpdateChapterValidator, - ReorderChapterValidator, - CreateLessonValidator, - UpdateLessonValidator, - ReorderLessonsValidator, - AddQuestionValidator, - UpdateQuizValidator, -} from '@/validators/ChaptersLesson.validator'; - - -// ============================================================ -// Chapter Validators -// ============================================================ - -describe('CreateChapterValidator', () => { - it('should pass with valid data', () => { - const { error } = CreateChapterValidator.validate({ - title: { th: 'บทที่ 1', en: 'Chapter 1' }, - }); - expect(error).toBeUndefined(); - }); - - it('should pass with optional fields', () => { - const { error } = CreateChapterValidator.validate({ - title: { th: 'บทที่ 1', en: 'Chapter 1' }, - description: { th: 'คำอธิบาย', en: 'Description' }, - sort_order: 0, - }); - expect(error).toBeUndefined(); - }); - - it('should fail if title is missing', () => { - const { error } = CreateChapterValidator.validate({}); - expect(error).toBeDefined(); - }); - - it('should fail if title.th is missing', () => { - const { error } = CreateChapterValidator.validate({ - title: { en: 'Chapter 1' }, - }); - expect(error).toBeDefined(); - }); - - it('should fail if title.en is missing', () => { - const { error } = CreateChapterValidator.validate({ - title: { th: 'บทที่ 1' }, - }); - expect(error).toBeDefined(); - }); -}); - -describe('UpdateChapterValidator', () => { - it('should pass with empty object (all fields optional)', () => { - const { error } = UpdateChapterValidator.validate({}); - expect(error).toBeUndefined(); - }); - - it('should pass with partial fields', () => { - const { error } = UpdateChapterValidator.validate({ - is_published: true, - }); - expect(error).toBeUndefined(); - }); -}); - -describe('ReorderChapterValidator', () => { - it('should pass with valid sort_order', () => { - const { error } = ReorderChapterValidator.validate({ sort_order: 0 }); - expect(error).toBeUndefined(); - }); - - it('should fail without sort_order', () => { - const { error } = ReorderChapterValidator.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Sort order is required/i); - }); - - it('should fail with negative sort_order', () => { - const { error } = ReorderChapterValidator.validate({ sort_order: -1 }); - expect(error).toBeDefined(); - }); -}); - -// ============================================================ -// Lesson Validators -// ============================================================ - -describe('CreateLessonValidator', () => { - it('should pass with VIDEO type', () => { - const { error } = CreateLessonValidator.validate({ - title: { th: 'บทเรียน 1', en: 'Lesson 1' }, - type: 'VIDEO', - }); - expect(error).toBeUndefined(); - }); - - it('should pass with QUIZ type', () => { - const { error } = CreateLessonValidator.validate({ - title: { th: 'แบบทดสอบ', en: 'Quiz' }, - type: 'QUIZ', - }); - expect(error).toBeUndefined(); - }); - - it('should fail with invalid type', () => { - const { error } = CreateLessonValidator.validate({ - title: { th: 'บทเรียน', en: 'Lesson' }, - type: 'DOCUMENT', - }); - expect(error).toBeDefined(); - }); - - it('should fail without title', () => { - const { error } = CreateLessonValidator.validate({ type: 'VIDEO' }); - expect(error).toBeDefined(); - }); - - it('should fail without type', () => { - const { error } = CreateLessonValidator.validate({ - title: { th: 'บทเรียน', en: 'Lesson' }, - }); - expect(error).toBeDefined(); - }); -}); - -describe('UpdateLessonValidator — prerequisite_lesson_ids', () => { - it('should pass with valid array of ids', () => { - const { error } = UpdateLessonValidator.validate({ - prerequisite_lesson_ids: [1, 2, 3], - }); - expect(error).toBeUndefined(); - }); - - it('should pass with null (clear prerequisites)', () => { - const { error } = UpdateLessonValidator.validate({ - prerequisite_lesson_ids: null, - }); - expect(error).toBeUndefined(); - }); - - it('should pass with empty array (clear prerequisites)', () => { - const { error } = UpdateLessonValidator.validate({ - prerequisite_lesson_ids: [], - }); - expect(error).toBeUndefined(); - }); - - it('should pass without the field (no change)', () => { - const { error } = UpdateLessonValidator.validate({}); - expect(error).toBeUndefined(); - }); - - it('should fail with non-integer ids', () => { - const { error } = UpdateLessonValidator.validate({ - prerequisite_lesson_ids: [1.5], - }); - expect(error).toBeDefined(); - }); - - it('should fail with negative ids', () => { - const { error } = UpdateLessonValidator.validate({ - prerequisite_lesson_ids: [-1], - }); - expect(error).toBeDefined(); - }); - - it('should fail with string ids', () => { - const { error } = UpdateLessonValidator.validate({ - prerequisite_lesson_ids: ['abc'], - }); - expect(error).toBeDefined(); - }); -}); - -describe('ReorderLessonsValidator', () => { - it('should pass with valid data', () => { - const { error } = ReorderLessonsValidator.validate({ - lesson_id: 1, - sort_order: 0, - }); - expect(error).toBeUndefined(); - }); - - it('should fail without lesson_id', () => { - const { error } = ReorderLessonsValidator.validate({ sort_order: 0 }); - expect(error).toBeDefined(); - }); - - it('should fail without sort_order', () => { - const { error } = ReorderLessonsValidator.validate({ lesson_id: 1 }); - expect(error).toBeDefined(); - }); -}); - -// ============================================================ -// Quiz Validators -// ============================================================ - -describe('AddQuestionValidator', () => { - it('should pass with MULTIPLE_CHOICE type + choices', () => { - const { error } = AddQuestionValidator.validate({ - question: { th: 'ข้อที่ 1 คืออะไร?', en: 'What is question 1?' }, - question_type: 'MULTIPLE_CHOICE', - choices: [ - { text: { th: 'ก', en: 'A' }, is_correct: true }, - { text: { th: 'ข', en: 'B' }, is_correct: false }, - ], - }); - expect(error).toBeUndefined(); - }); - - it('should pass with TRUE_FALSE type without choices', () => { - const { error } = AddQuestionValidator.validate({ - question: { th: 'ถูกหรือผิด?', en: 'True or False?' }, - question_type: 'TRUE_FALSE', - }); - expect(error).toBeUndefined(); - }); - - it('should fail with invalid question_type', () => { - const { error } = AddQuestionValidator.validate({ - question: { th: 'คำถาม', en: 'Question' }, - question_type: 'ESSAY', - }); - expect(error).toBeDefined(); - }); - - it('should fail without question', () => { - const { error } = AddQuestionValidator.validate({ - question_type: 'TRUE_FALSE', - }); - expect(error).toBeDefined(); - }); -}); - -describe('UpdateQuizValidator', () => { - it('should pass with empty object (all optional)', () => { - const { error } = UpdateQuizValidator.validate({}); - expect(error).toBeUndefined(); - }); - - it('should pass with valid passing_score', () => { - const { error } = UpdateQuizValidator.validate({ passing_score: 70 }); - expect(error).toBeUndefined(); - }); - - it('should fail with passing_score > 100', () => { - const { error } = UpdateQuizValidator.validate({ passing_score: 101 }); - expect(error).toBeDefined(); - }); - - it('should fail with passing_score < 0', () => { - const { error } = UpdateQuizValidator.validate({ passing_score: -1 }); - expect(error).toBeDefined(); - }); - - it('should pass with time_limit 0 (no limit)', () => { - const { error } = UpdateQuizValidator.validate({ time_limit: 0 }); - expect(error).toBeUndefined(); - }); -}); diff --git a/Backend/tests/unit/validators/CoursesInstructor.validator.test.ts b/Backend/tests/unit/validators/CoursesInstructor.validator.test.ts deleted file mode 100644 index 95c13c07..00000000 --- a/Backend/tests/unit/validators/CoursesInstructor.validator.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - CreateCourseValidator, - UpdateCourseValidator, - CloneCourseValidator, - addInstructorCourseValidator, -} from '@/validators/CoursesInstructor.validator'; - -// ============================================================ -// addInstructorCourseValidator -// ============================================================ - -describe('addInstructorCourseValidator', () => { - it('should pass with valid user_id and course_id', () => { - const { error } = addInstructorCourseValidator.validate({ - user_id: 1, - course_id: 2, - }); - expect(error).toBeUndefined(); - }); - - it('should fail without user_id', () => { - const { error } = addInstructorCourseValidator.validate({ course_id: 2 }); - expect(error).toBeDefined(); - }); - - it('should fail without course_id', () => { - const { error } = addInstructorCourseValidator.validate({ user_id: 1 }); - expect(error).toBeDefined(); - }); -}); - -// ============================================================ -// CreateCourseValidator -// ============================================================ - -describe('CreateCourseValidator', () => { - const validPayload = { - category_id: 1, - title: { th: 'คอร์สทดสอบ', en: 'Test Course' }, - slug: 'test-course', - description: { th: 'คำอธิบาย', en: 'Description' }, - price: 500, - is_free: false, - have_certificate: true, - }; - - it('should pass with all required fields', () => { - const { error } = CreateCourseValidator.validate(validPayload); - expect(error).toBeUndefined(); - }); - - it('should fail if title.th is missing', () => { - const { error } = CreateCourseValidator.validate({ - ...validPayload, - title: { en: 'Test Course' }, - }); - expect(error).toBeDefined(); - }); - - it('should fail if slug is missing', () => { - const { error } = CreateCourseValidator.validate({ - ...validPayload, - slug: undefined, - }); - expect(error).toBeDefined(); - }); - - it('should fail if price is missing', () => { - const { error } = CreateCourseValidator.validate({ - ...validPayload, - price: undefined, - }); - expect(error).toBeDefined(); - }); - - it('should fail if is_free is missing', () => { - const { error } = CreateCourseValidator.validate({ - ...validPayload, - is_free: undefined, - }); - expect(error).toBeDefined(); - }); - - it('should allow price = 0 (free course)', () => { - const { error } = CreateCourseValidator.validate({ - ...validPayload, - price: 0, - is_free: true, - }); - expect(error).toBeUndefined(); - }); -}); - -// ============================================================ -// UpdateCourseValidator -// ============================================================ - -describe('UpdateCourseValidator', () => { - it('should pass with empty object (all optional)', () => { - const { error } = UpdateCourseValidator.validate({}); - expect(error).toBeUndefined(); - }); - - it('should pass with partial update', () => { - const { error } = UpdateCourseValidator.validate({ price: 999 }); - expect(error).toBeUndefined(); - }); - - it('should pass with title partial update (th only)', () => { - const { error } = UpdateCourseValidator.validate({ - title: { th: 'ชื่อใหม่' }, - }); - expect(error).toBeUndefined(); - }); -}); - -// ============================================================ -// CloneCourseValidator -// ============================================================ - -describe('CloneCourseValidator', () => { - it('should pass with valid title', () => { - const { error } = CloneCourseValidator.validate({ - title: { th: 'คอร์ส Copy', en: 'Course Copy' }, - }); - expect(error).toBeUndefined(); - }); - - it('should fail without title.th', () => { - const { error } = CloneCourseValidator.validate({ - title: { en: 'Course Copy' }, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Thai title is required/i); - }); - - it('should fail without title.en', () => { - const { error } = CloneCourseValidator.validate({ - title: { th: 'คอร์ส Copy' }, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/English title is required/i); - }); - - it('should fail without title entirely', () => { - const { error } = CloneCourseValidator.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Title is required/i); - }); -}); diff --git a/Backend/tests/unit/validators/CoursesStudent.validator.test.ts b/Backend/tests/unit/validators/CoursesStudent.validator.test.ts deleted file mode 100644 index ae9569c4..00000000 --- a/Backend/tests/unit/validators/CoursesStudent.validator.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - SaveVideoProgressValidator, - SubmitQuizValidator, -} from '@/validators/CoursesStudent.validator'; - -describe('SaveVideoProgressValidator', () => { - it('should pass with required field only', () => { - const { error } = SaveVideoProgressValidator.validate({ - video_progress_seconds: 60, - }); - expect(error).toBeUndefined(); - }); - - it('should pass with all fields', () => { - const { error } = SaveVideoProgressValidator.validate({ - video_progress_seconds: 120, - video_duration_seconds: 600, - }); - expect(error).toBeUndefined(); - }); - - it('should pass with progress = 0 (start of video)', () => { - const { error } = SaveVideoProgressValidator.validate({ - video_progress_seconds: 0, - }); - expect(error).toBeUndefined(); - }); - - it('should fail without video_progress_seconds', () => { - const { error } = SaveVideoProgressValidator.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Video progress seconds is required/i); - }); - - it('should fail with negative progress', () => { - const { error } = SaveVideoProgressValidator.validate({ - video_progress_seconds: -1, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/at least 0/i); - }); - - it('should fail with negative video duration', () => { - const { error } = SaveVideoProgressValidator.validate({ - video_progress_seconds: 10, - video_duration_seconds: -1, - }); - expect(error).toBeDefined(); - }); -}); - -describe('SubmitQuizValidator', () => { - const validAnswer = { question_id: 1, choice_id: 2 }; - - it('should pass with valid answers', () => { - const { error } = SubmitQuizValidator.validate({ - answers: [validAnswer, { question_id: 2, choice_id: 5 }], - }); - expect(error).toBeUndefined(); - }); - - it('should fail without answers', () => { - const { error } = SubmitQuizValidator.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Answers are required/i); - }); - - it('should fail with empty answers array', () => { - const { error } = SubmitQuizValidator.validate({ answers: [] }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/At least one answer/i); - }); - - it('should fail if question_id is missing in an answer', () => { - const { error } = SubmitQuizValidator.validate({ - answers: [{ choice_id: 2 }], - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Question ID is required/i); - }); - - it('should fail if choice_id is missing in an answer', () => { - const { error } = SubmitQuizValidator.validate({ - answers: [{ question_id: 1 }], - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Choice ID is required/i); - }); - - it('should fail if question_id is not a positive integer', () => { - const { error } = SubmitQuizValidator.validate({ - answers: [{ question_id: -1, choice_id: 1 }], - }); - expect(error).toBeDefined(); - }); -}); diff --git a/Backend/tests/unit/validators/Lessons.validator.test.ts b/Backend/tests/unit/validators/Lessons.validator.test.ts deleted file mode 100644 index 81ef5471..00000000 --- a/Backend/tests/unit/validators/Lessons.validator.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { SetYouTubeVideoValidator } from '@/validators/Lessons.validator'; - -describe('SetYouTubeVideoValidator', () => { - it('should pass with valid youtube_video_id and video_title', () => { - const { error } = SetYouTubeVideoValidator.validate({ - youtube_video_id: 'dQw4w9WgXcQ', - video_title: 'Introduction to TypeScript', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without youtube_video_id', () => { - const { error } = SetYouTubeVideoValidator.validate({ - video_title: 'Intro to TS', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/YouTube video ID is required/i); - }); - - it('should fail with empty youtube_video_id string', () => { - const { error } = SetYouTubeVideoValidator.validate({ - youtube_video_id: '', - video_title: 'Intro to TS', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/cannot be empty/i); - }); - - it('should fail without video_title', () => { - const { error } = SetYouTubeVideoValidator.validate({ - youtube_video_id: 'dQw4w9WgXcQ', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Video title is required/i); - }); - - it('should fail with empty video_title string', () => { - const { error } = SetYouTubeVideoValidator.validate({ - youtube_video_id: 'dQw4w9WgXcQ', - video_title: '', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/cannot be empty/i); - }); -}); diff --git a/Backend/tests/unit/validators/announcements.validator.test.ts b/Backend/tests/unit/validators/announcements.validator.test.ts deleted file mode 100644 index 9e186ca2..00000000 --- a/Backend/tests/unit/validators/announcements.validator.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - CreateAnnouncementValidator, - UpdateAnnouncementValidator, -} from '@/validators/announcements.validator'; - -describe('CreateAnnouncementValidator', () => { - const validPayload = { - title: { th: 'ประกาศใหม่', en: 'New Announcement' }, - content: { th: 'เนื้อหา', en: 'Content' }, - status: 'DRAFT', - is_pinned: false, - }; - - it('should pass with all required fields', () => { - const { error } = CreateAnnouncementValidator.validate(validPayload); - expect(error).toBeUndefined(); - }); - - it('should pass with optional published_at as ISO date', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - published_at: '2026-03-01T00:00:00.000Z', - }); - expect(error).toBeUndefined(); - }); - - it('should fail with invalid published_at format', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - published_at: '01-03-2026', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/ISO date/i); - }); - - it('should fail with invalid status', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - status: 'HIDDEN', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/DRAFT, PUBLISHED, ARCHIVED/i); - }); - - it('should pass with PUBLISHED status', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - status: 'PUBLISHED', - }); - expect(error).toBeUndefined(); - }); - - it('should pass with ARCHIVED status', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - status: 'ARCHIVED', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without title', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - title: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Title is required/i); - }); - - it('should fail without content', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - content: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Content is required/i); - }); - - it('should fail without is_pinned', () => { - const { error } = CreateAnnouncementValidator.validate({ - ...validPayload, - is_pinned: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/is_pinned is required/i); - }); -}); - -describe('UpdateAnnouncementValidator', () => { - it('should pass with empty object (all optional)', () => { - const { error } = UpdateAnnouncementValidator.validate({}); - expect(error).toBeUndefined(); - }); - - it('should pass with partial update', () => { - const { error } = UpdateAnnouncementValidator.validate({ - status: 'PUBLISHED', - is_pinned: true, - }); - expect(error).toBeUndefined(); - }); - - it('should fail with invalid status', () => { - const { error } = UpdateAnnouncementValidator.validate({ status: 'DELETED' }); - expect(error).toBeDefined(); - }); - - it('should fail with invalid published_at format', () => { - const { error } = UpdateAnnouncementValidator.validate({ - published_at: 'not-a-date', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/ISO date/i); - }); -}); diff --git a/Backend/tests/unit/validators/auth.validator.test.ts b/Backend/tests/unit/validators/auth.validator.test.ts deleted file mode 100644 index 2ef38652..00000000 --- a/Backend/tests/unit/validators/auth.validator.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { - loginSchema, - registerSchema, - refreshTokenSchema, - resetPasswordSchema, - changePasswordSchema, - resetRequestSchema, -} from '@/validators/auth.validator'; - -// ============================================================ -// loginSchema -// ============================================================ - -describe('loginSchema', () => { - it('should pass with valid email and password', () => { - const { error } = loginSchema.validate({ - email: 'user@example.com', - password: 'password123', - }); - expect(error).toBeUndefined(); - }); - - it('should fail with invalid email format', () => { - const { error } = loginSchema.validate({ - email: 'not-an-email', - password: 'password123', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/valid email/i); - }); - - it('should fail without email', () => { - const { error } = loginSchema.validate({ password: 'password123' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Email is required/i); - }); - - it('should fail without password', () => { - const { error } = loginSchema.validate({ email: 'user@example.com' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Password is required/i); - }); - - it('should fail with password shorter than 6 characters', () => { - const { error } = loginSchema.validate({ - email: 'user@example.com', - password: '12345', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/at least 6 characters/i); - }); -}); - -// ============================================================ -// registerSchema -// ============================================================ - -describe('registerSchema', () => { - const validPayload = { - username: 'john_doe', - email: 'john@example.com', - password: 'securepass', - first_name: 'John', - last_name: 'Doe', - phone: '0812345678', - }; - - it('should pass with all required fields', () => { - const { error } = registerSchema.validate(validPayload); - expect(error).toBeUndefined(); - }); - - it('should pass with optional prefix', () => { - const { error } = registerSchema.validate({ - ...validPayload, - prefix: { th: 'นาย', en: 'Mr.' }, - }); - expect(error).toBeUndefined(); - }); - - it('should fail if username has invalid characters', () => { - const { error } = registerSchema.validate({ - ...validPayload, - username: 'john doe!', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/letters, numbers, and underscores/i); - }); - - it('should fail if username is too short (< 3 chars)', () => { - const { error } = registerSchema.validate({ - ...validPayload, - username: 'ab', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/at least 3 characters/i); - }); - - it('should fail if username is too long (> 50 chars)', () => { - const { error } = registerSchema.validate({ - ...validPayload, - username: 'a'.repeat(51), - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/not exceed 50 characters/i); - }); - - it('should fail with invalid email', () => { - const { error } = registerSchema.validate({ - ...validPayload, - email: 'bad-email', - }); - expect(error).toBeDefined(); - }); - - it('should fail if phone is too short (< 10 chars)', () => { - const { error } = registerSchema.validate({ - ...validPayload, - phone: '081234', - }); - expect(error).toBeDefined(); - }); - - it('should fail without first_name', () => { - const { error } = registerSchema.validate({ - ...validPayload, - first_name: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/First name is required/i); - }); - - it('should fail without last_name', () => { - const { error } = registerSchema.validate({ - ...validPayload, - last_name: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Last name is required/i); - }); -}); - -// ============================================================ -// refreshTokenSchema -// ============================================================ - -describe('refreshTokenSchema', () => { - it('should pass with a valid refreshToken', () => { - const { error } = refreshTokenSchema.validate({ - refreshToken: 'some-refresh-token-string', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without refreshToken', () => { - const { error } = refreshTokenSchema.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Refresh token is required/i); - }); -}); - -// ============================================================ -// resetPasswordSchema -// ============================================================ - -describe('resetPasswordSchema', () => { - it('should pass with valid token and password', () => { - const { error } = resetPasswordSchema.validate({ - token: 'reset-token-abc', - password: 'newpassword', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without token', () => { - const { error } = resetPasswordSchema.validate({ password: 'newpassword' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Reset token is required/i); - }); - - it('should fail with password too short', () => { - const { error } = resetPasswordSchema.validate({ - token: 'abc', - password: '12345', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/at least 6 characters/i); - }); - - it('should fail with password too long (> 100 chars)', () => { - const { error } = resetPasswordSchema.validate({ - token: 'abc', - password: 'a'.repeat(101), - }); - expect(error).toBeDefined(); - }); -}); - -// ============================================================ -// changePasswordSchema (auth) -// ============================================================ - -describe('changePasswordSchema (auth.validator)', () => { - it('should pass with valid old and new passwords', () => { - const { error } = changePasswordSchema.validate({ - oldPassword: 'oldpass123', - newPassword: 'newpass456', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without oldPassword', () => { - const { error } = changePasswordSchema.validate({ newPassword: 'newpass456' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Password is required/i); - }); - - it('should fail without newPassword', () => { - const { error } = changePasswordSchema.validate({ oldPassword: 'oldpass123' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Password is required/i); - }); -}); - -// ============================================================ -// resetRequestSchema -// ============================================================ - -describe('resetRequestSchema', () => { - it('should pass with valid email', () => { - const { error } = resetRequestSchema.validate({ email: 'user@example.com' }); - expect(error).toBeUndefined(); - }); - - it('should fail without email', () => { - const { error } = resetRequestSchema.validate({}); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Email is required/i); - }); - - it('should fail with invalid email', () => { - const { error } = resetRequestSchema.validate({ email: 'not-email' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/valid email/i); - }); -}); diff --git a/Backend/tests/unit/validators/categories.validator.test.ts b/Backend/tests/unit/validators/categories.validator.test.ts deleted file mode 100644 index 64249c2a..00000000 --- a/Backend/tests/unit/validators/categories.validator.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { - CreateCategoryValidator, - UpdateCategoryValidator, -} from '@/validators/categories.validator'; - -describe('CreateCategoryValidator', () => { - const validPayload = { - name: { th: 'การพัฒนาเว็บ', en: 'Web Development' }, - slug: 'web-development', - description: { th: 'หมวดหมู่การพัฒนาเว็บ', en: 'Web development category' }, - }; - - it('should pass with all required fields', () => { - const { error } = CreateCategoryValidator.validate(validPayload); - expect(error).toBeUndefined(); - }); - - it('should pass with optional created_by', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - created_by: 1, - }); - expect(error).toBeUndefined(); - }); - - it('should fail without name', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - name: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Name is required/i); - }); - - it('should fail without name.th', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - name: { en: 'Web Development' }, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Thai name is required/i); - }); - - it('should fail without slug', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - slug: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Slug is required/i); - }); - - it('should fail with invalid slug format (uppercase)', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - slug: 'Web-Development', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/lowercase with hyphens/i); - }); - - it('should fail with invalid slug format (spaces)', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - slug: 'web development', - }); - expect(error).toBeDefined(); - }); - - it('should pass with valid slug formats', () => { - const slugs = ['web', 'web-dev', 'web-development-101']; - for (const slug of slugs) { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - slug, - }); - expect(error).toBeUndefined(); - } - }); - - it('should fail without description', () => { - const { error } = CreateCategoryValidator.validate({ - ...validPayload, - description: undefined, - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Description is required/i); - }); -}); - -describe('UpdateCategoryValidator', () => { - it('should pass with only required id', () => { - const { error } = UpdateCategoryValidator.validate({ id: 1 }); - expect(error).toBeUndefined(); - }); - - it('should pass with all optional fields', () => { - const { error } = UpdateCategoryValidator.validate({ - id: 1, - name: { th: 'ใหม่', en: 'New' }, - slug: 'new-category', - description: { th: 'คำอธิบาย', en: 'Description' }, - }); - expect(error).toBeUndefined(); - }); - - it('should fail without id', () => { - const { error } = UpdateCategoryValidator.validate({ name: { th: 'ใหม่', en: 'New' } }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Category ID is required/i); - }); - - it('should fail with invalid slug format', () => { - const { error } = UpdateCategoryValidator.validate({ - id: 1, - slug: 'Invalid Slug!', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/lowercase with hyphens/i); - }); -}); diff --git a/Backend/tests/unit/validators/user.validator.test.ts b/Backend/tests/unit/validators/user.validator.test.ts deleted file mode 100644 index 349ceaa4..00000000 --- a/Backend/tests/unit/validators/user.validator.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - profileUpdateSchema, - changePasswordSchema, -} from '@/validators/user.validator'; - -describe('profileUpdateSchema', () => { - it('should pass with empty object (all optional)', () => { - const { error } = profileUpdateSchema.validate({}); - expect(error).toBeUndefined(); - }); - - it('should pass with all fields', () => { - const { error } = profileUpdateSchema.validate({ - prefix: { th: 'นาย', en: 'Mr.' }, - first_name: 'John', - last_name: 'Doe', - phone: '0812345678', - avatar_url: 'https://example.com/avatar.jpg', - birth_date: new Date('1990-01-01'), - }); - expect(error).toBeUndefined(); - }); - - it('should pass with partial update (first_name only)', () => { - const { error } = profileUpdateSchema.validate({ first_name: 'Jane' }); - expect(error).toBeUndefined(); - }); - - it('should fail if first_name is empty string', () => { - const { error } = profileUpdateSchema.validate({ first_name: '' }); - expect(error).toBeDefined(); - }); - - it('should fail if first_name exceeds 100 characters', () => { - const { error } = profileUpdateSchema.validate({ first_name: 'a'.repeat(101) }); - expect(error).toBeDefined(); - }); - - it('should fail if phone is too short (< 10 chars)', () => { - const { error } = profileUpdateSchema.validate({ phone: '081234' }); - expect(error).toBeDefined(); - }); - - it('should fail if phone exceeds 15 characters', () => { - const { error } = profileUpdateSchema.validate({ phone: '1'.repeat(16) }); - expect(error).toBeDefined(); - }); - - it('should pass with valid birth_date', () => { - const { error } = profileUpdateSchema.validate({ birth_date: new Date('2000-06-15') }); - expect(error).toBeUndefined(); - }); -}); - -describe('changePasswordSchema (user.validator)', () => { - it('should pass with valid old and new passwords', () => { - const { error } = changePasswordSchema.validate({ - oldPassword: 'oldpass123', - newPassword: 'newpass456', - }); - expect(error).toBeUndefined(); - }); - - it('should fail without oldPassword', () => { - const { error } = changePasswordSchema.validate({ newPassword: 'newpass456' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Old password is required/i); - }); - - it('should fail without newPassword', () => { - const { error } = changePasswordSchema.validate({ oldPassword: 'oldpass123' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/New password is required/i); - }); - - it('should fail if oldPassword is shorter than 6 chars', () => { - const { error } = changePasswordSchema.validate({ - oldPassword: '12345', - newPassword: 'newpass456', - }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/at least 6 characters/i); - }); - - it('should fail if newPassword is shorter than 6 chars', () => { - const { error } = changePasswordSchema.validate({ - oldPassword: 'oldpass123', - newPassword: '123', - }); - expect(error).toBeDefined(); - }); - - it('should fail if oldPassword exceeds 100 characters', () => { - const { error } = changePasswordSchema.validate({ - oldPassword: 'a'.repeat(101), - newPassword: 'newpass456', - }); - expect(error).toBeDefined(); - }); -}); diff --git a/Backend/tests/unit/validators/usermanagement.validator.test.ts b/Backend/tests/unit/validators/usermanagement.validator.test.ts deleted file mode 100644 index 6d57de81..00000000 --- a/Backend/tests/unit/validators/usermanagement.validator.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - getUserByIdValidator, - updateUserRoleValidator, -} from '@/validators/usermanagement.validator'; - -describe('getUserByIdValidator', () => { - it('should pass with valid id', () => { - const { error } = getUserByIdValidator.validate({ id: 1 }); - expect(error).toBeUndefined(); - }); - - it('should fail without id', () => { - const { error } = getUserByIdValidator.validate({}); - expect(error).toBeDefined(); - }); - - it('should fail with non-numeric id', () => { - const { error } = getUserByIdValidator.validate({ id: 'abc' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/ID must be a number/i); - }); - - it('should pass with id = 0', () => { - // Joi number() allows 0 by default unless positive() is specified - const { error } = getUserByIdValidator.validate({ id: 0 }); - expect(error).toBeUndefined(); - }); -}); - -describe('updateUserRoleValidator', () => { - it('should pass with valid id and role_id', () => { - const { error } = updateUserRoleValidator.validate({ id: 1, role_id: 2 }); - expect(error).toBeUndefined(); - }); - - it('should fail without id', () => { - const { error } = updateUserRoleValidator.validate({ role_id: 2 }); - expect(error).toBeDefined(); - }); - - it('should fail without role_id', () => { - const { error } = updateUserRoleValidator.validate({ id: 1 }); - expect(error).toBeDefined(); - // Joi uses field name in message when custom messages don't match - expect(error?.details[0].message).toContain('role_id'); - }); - - it('should fail with non-numeric role_id', () => { - const { error } = updateUserRoleValidator.validate({ id: 1, role_id: 'admin' }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/Role ID must be a number/i); - }); - - it('should fail with non-numeric id', () => { - const { error } = updateUserRoleValidator.validate({ id: 'abc', role_id: 1 }); - expect(error).toBeDefined(); - expect(error?.details[0].message).toMatch(/ID must be a number/i); - }); -}); diff --git a/Backend/tsconfig.test.json b/Backend/tsconfig.test.json deleted file mode 100644 index 5eb9ef95..00000000 --- a/Backend/tsconfig.test.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": ".", - "baseUrl": ".", - "paths": { - "@/*": [ - "src/*" - ] - }, - "types": [ - "node", - "jest" - ] - }, - "include": [ - "src/**/*", - "tests/**/*" - ] -} \ No newline at end of file diff --git a/Frontend-Learner/.nuxtrc b/Frontend-Learner/.nuxtrc deleted file mode 100644 index 1e1fe833..00000000 --- a/Frontend-Learner/.nuxtrc +++ /dev/null @@ -1 +0,0 @@ -setups.@nuxt/test-utils="4.0.0" \ No newline at end of file diff --git a/Frontend-Learner/app.vue b/Frontend-Learner/app.vue index 1dfac678..a1ac35c7 100644 --- a/Frontend-Learner/app.vue +++ b/Frontend-Learner/app.vue @@ -1,34 +1,27 @@ - diff --git a/Frontend-Learner/nuxt.config.ts b/Frontend-Learner/nuxt.config.ts index e0617f89..b3079964 100644 --- a/Frontend-Learner/nuxt.config.ts +++ b/Frontend-Learner/nuxt.config.ts @@ -33,11 +33,8 @@ export default defineNuxtConfig({ // การตั้งค่า Quasar Framework quasar: { - iconSet: 'material-icons-outlined', extras: { - fontIcons: [ - "material-icons", - "material-icons-outlined"] // ใช้ไอคอน Material Icons, material-icons-outlined + fontIcons: ["material-icons"], }, plugins: ["Notify", "Dialog"], // เปิดใช้ Plugin Notify และ Dialog config: { @@ -69,11 +66,10 @@ export default defineNuxtConfig({ { name: "viewport", content: "width=device-width, initial-scale=1" }, ], link: [ - { rel: 'icon', type: 'image/png', href: '/img/logo.png' }, { rel: "stylesheet", // โหลด Font: Inter, Prompt, Sarabun - href: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Prompt:wght@300;400;500;600;700;800;900&family=Sarabun:wght@300;400;500;600;700;800&family=Poppins:wght@300;400;500;600;700;800;900&display=swap", + href: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Prompt:wght@300;400;500;600;700;800;900&family=Sarabun:wght@300;400;500;600;700;800&display=swap", }, ], }, diff --git a/Frontend-Learner/package-lock.json b/Frontend-Learner/package-lock.json index 4736d625..311492c7 100644 --- a/Frontend-Learner/package-lock.json +++ b/Frontend-Learner/package-lock.json @@ -17,24 +17,12 @@ }, "devDependencies": { "@nuxt/eslint-config": "^1.12.1", - "@nuxt/test-utils": "^4.0.0", "@nuxtjs/i18n": "^10.2.1", - "@playwright/test": "^1.58.2", "@types/node": "^22.19.8", - "@vue/test-utils": "^2.4.6", "eslint": "^9.39.2", - "jsdom": "^28.1.0", - "typescript": "^5.4.5", - "vitest": "^4.0.18" + "typescript": "^5.4.5" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -61,64 +49,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.1.1", - "@csstools/css-color-parser": "^4.0.2", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -409,12 +339,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -505,9 +435,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -542,19 +472,6 @@ } } }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, "node_modules/@clack/core": { "version": "1.0.0-alpha.7", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0-alpha.7.tgz", @@ -600,140 +517,6 @@ "node": ">=10.0.0" } }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", - "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.1.1" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0" - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/@csstools/selector-resolve-nested": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", @@ -1597,24 +1380,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/@heroicons/vue": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", @@ -3041,233 +2806,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/@nuxt/test-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nuxt/test-utils/-/test-utils-4.0.0.tgz", - "integrity": "sha512-QJfyCiqYxflUKA5xlEGuXdDApTBhJxoPXxYePIDtA90hkmKbhYs/mrMM+Bi9LiUrI/cCJOPRyIx9jOzhMvTIgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@clack/prompts": "1.0.0", - "@nuxt/devtools-kit": "^2.7.0", - "@nuxt/kit": "^3.21.0", - "c12": "^3.3.3", - "consola": "^3.4.2", - "defu": "^6.1.4", - "destr": "^2.0.5", - "estree-walker": "^3.0.3", - "exsolve": "^1.0.8", - "fake-indexeddb": "^6.2.5", - "get-port-please": "^3.2.0", - "h3": "^1.15.5", - "h3-next": "npm:h3@2.0.1-rc.11", - "local-pkg": "^1.1.2", - "magic-string": "^0.30.21", - "node-fetch-native": "^1.6.7", - "node-mock-http": "^1.0.4", - "nypm": "^0.6.4", - "ofetch": "^1.5.1", - "pathe": "^2.0.3", - "perfect-debounce": "^2.1.0", - "radix3": "^1.1.2", - "scule": "^1.3.0", - "std-env": "^3.10.0", - "tinyexec": "^1.0.2", - "ufo": "^1.6.3", - "unplugin": "^3.0.0", - "vitest-environment-nuxt": "^1.0.1", - "vue": "^3.5.27" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@cucumber/cucumber": ">=11.0.0", - "@jest/globals": ">=30.0.0", - "@playwright/test": "^1.43.1", - "@testing-library/vue": "^8.0.1", - "@vue/test-utils": "^2.4.2", - "happy-dom": ">=20.0.11", - "jsdom": ">=27.4.0", - "playwright-core": "^1.43.1", - "vitest": "^4.0.2" - }, - "peerDependenciesMeta": { - "@cucumber/cucumber": { - "optional": true - }, - "@jest/globals": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "@testing-library/vue": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "@vue/test-utils": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "playwright-core": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@nuxt/test-utils/node_modules/@clack/core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0.tgz", - "integrity": "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "sisteransi": "^1.0.5" - } - }, - "node_modules/@nuxt/test-utils/node_modules/@clack/prompts": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.0.0.tgz", - "integrity": "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@clack/core": "1.0.0", - "picocolors": "^1.0.0", - "sisteransi": "^1.0.5" - } - }, - "node_modules/@nuxt/test-utils/node_modules/@nuxt/devtools-kit": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-2.7.0.tgz", - "integrity": "sha512-MIJdah6CF6YOW2GhfKnb8Sivu6HpcQheqdjOlZqShBr+1DyjtKQbAKSCAyKPaoIzZP4QOo2SmTFV6aN8jBeEIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nuxt/kit": "^3.19.3", - "execa": "^8.0.1" - }, - "peerDependencies": { - "vite": ">=6.0" - } - }, - "node_modules/@nuxt/test-utils/node_modules/@nuxt/kit": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.1.tgz", - "integrity": "sha512-QORZRjcuTKgo++XP1Pc2c2gqwRydkaExrIRfRI9vFsPA3AzuHVn5Gfmbv1ic8y34e78mr5DMBvJlelUaeOuajg==", - "dev": true, - "license": "MIT", - "dependencies": { - "c12": "^3.3.3", - "consola": "^3.4.2", - "defu": "^6.1.4", - "destr": "^2.0.5", - "errx": "^0.1.0", - "exsolve": "^1.0.8", - "ignore": "^7.0.5", - "jiti": "^2.6.1", - "klona": "^2.0.6", - "knitwork": "^1.3.0", - "mlly": "^1.8.0", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "rc9": "^3.0.0", - "scule": "^1.3.0", - "semver": "^7.7.4", - "tinyglobby": "^0.2.15", - "ufo": "^1.6.3", - "unctx": "^2.5.0", - "untyped": "^2.0.0" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/@nuxt/test-utils/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@nuxt/test-utils/node_modules/h3-next": { - "name": "h3", - "version": "2.0.1-rc.11", - "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.11.tgz", - "integrity": "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "rou3": "^0.7.12", - "srvx": "^0.10.1" - }, - "engines": { - "node": ">=20.11.1" - }, - "peerDependencies": { - "crossws": "^0.4.1" - }, - "peerDependenciesMeta": { - "crossws": { - "optional": true - } - } - }, - "node_modules/@nuxt/test-utils/node_modules/rc9": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.0.tgz", - "integrity": "sha512-MGOue0VqscKWQ104udASX/3GYDcKyPI4j4F8gu/jHHzglpmy9a/anZK3PNe8ug6aZFl+9GxLtdhe3kVZuMaQbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.5" - } - }, - "node_modules/@nuxt/test-utils/node_modules/srvx": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.10.1.tgz", - "integrity": "sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "srvx": "bin/srvx.mjs" - }, - "engines": { - "node": ">=20.16.0" - } - }, - "node_modules/@nuxt/test-utils/node_modules/unplugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", - "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "picomatch": "^4.0.3", - "webpack-virtual-modules": "^0.6.2" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@nuxt/vite-builder": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.20.2.tgz", @@ -4066,13 +3604,6 @@ "unctx": "^2.4.1" } }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", - "dev": true, - "license": "MIT" - }, "node_modules/@oxc-minify/binding-android-arm64": { "version": "0.102.0", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm64/-/binding-android-arm64-0.102.0.tgz", @@ -5167,23 +4698,6 @@ "node": ">=14" } }, - "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "playwright": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -5780,13 +5294,6 @@ "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", "license": "CC0-1.0" }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, "node_modules/@stylistic/eslint-plugin": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.7.0.tgz", @@ -5819,24 +5326,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -6473,127 +5962,6 @@ "integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==", "license": "MIT" }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.0.18", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.0.18", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@volar/language-core": { "version": "2.4.27", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz", @@ -6703,40 +6071,40 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", - "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.29", - "entities": "^7.0.1", + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", - "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", - "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", "license": "MIT", "peer": true, "dependencies": { - "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.29", - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29", + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -6744,13 +6112,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", - "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" } }, "node_modules/@vue/devtools-api": { @@ -6816,67 +6184,55 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", - "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.29" + "@vue/shared": "3.5.26" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", - "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", - "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.29", - "@vue/runtime-core": "3.5.29", - "@vue/shared": "3.5.29", + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", - "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" }, "peerDependencies": { - "vue": "3.5.29" + "vue": "3.5.26" } }, "node_modules/@vue/shared": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", - "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", "license": "MIT" }, - "node_modules/@vue/test-utils": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", - "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "js-beautify": "^1.14.9", - "vue-component-type-helpers": "^2.0.0" - } - }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -7134,16 +6490,6 @@ "devOptional": true, "license": "Python-2.0" }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/ast-kit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", @@ -7296,16 +6642,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7616,16 +6952,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7916,24 +7242,6 @@ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -8284,90 +7592,12 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, - "node_modules/cssstyle": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", - "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cssstyle/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "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==", "license": "MIT" }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/db0": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz", @@ -8419,13 +7649,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, "node_modules/deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -8690,51 +7913,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/editorconfig": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", - "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8776,9 +7954,9 @@ } }, "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -9543,16 +8721,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -9577,16 +8745,6 @@ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "license": "MIT" }, - "node_modules/fake-indexeddb": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", - "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10083,9 +9241,9 @@ } }, "node_modules/h3": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz", - "integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz", + "integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==", "license": "MIT", "dependencies": { "cookie-es": "^1.2.2", @@ -10093,9 +9251,9 @@ "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.4", + "node-mock-http": "^1.0.2", "radix3": "^1.1.2", - "ufo": "^1.6.3", + "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, @@ -10153,19 +9311,6 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -10250,20 +9395,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http-shutdown": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", @@ -10639,13 +9770,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -10776,64 +9900,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-beautify": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", - "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.4.2", - "js-cookie": "^3.0.5", - "nopt": "^7.2.1" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-beautify/node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/js-beautify/node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10863,96 +9929,6 @@ "node": ">=20.0.0" } }, - "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", - "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/jsdom/node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/jsdom/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12720,28 +11696,24 @@ "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", - "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", "license": "MIT", "dependencies": { - "citty": "^0.2.0", + "citty": "^0.1.6", + "consola": "^3.4.2", "pathe": "^2.0.3", - "tinyexec": "^1.0.2" + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": ">=18" + "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", - "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13137,32 +12109,6 @@ "node": ">=14.13.0" } }, - "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13259,9 +12205,9 @@ "license": "MIT" }, "node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", "license": "MIT" }, "node_modules/picocolors": { @@ -13311,54 +12257,6 @@ "pathe": "^2.0.3" } }, - "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.58.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -14059,13 +12957,6 @@ "node": ">= 6" } }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true, - "license": "ISC" - }, "node_modules/protocols": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.2.tgz", @@ -14366,16 +13257,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/reserved-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", @@ -14574,13 +13455,6 @@ } } }, - "node_modules/rou3": { - "version": "0.7.12", - "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", - "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -14665,19 +13539,6 @@ "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", "license": "BlueOak-1.0.0" }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scslre": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", @@ -14700,9 +13561,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "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" @@ -14822,13 +13683,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -14987,13 +13841,6 @@ "node": ">=12.0.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -15310,13 +14157,6 @@ "node": ">=16" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -15660,13 +14500,6 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -15692,36 +14525,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.23" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", - "dev": true, - "license": "MIT" - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -15778,19 +14581,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -15916,9 +14706,9 @@ } }, "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, "node_modules/ultrahtml": { @@ -15954,16 +14744,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -16772,95 +15552,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^3.10.0", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest-environment-nuxt": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vitest-environment-nuxt/-/vitest-environment-nuxt-1.0.1.tgz", - "integrity": "sha512-eBCwtIQriXW5/M49FjqNKfnlJYlG2LWMSNFsRVKomc8CaMqmhQPBS5LZ9DlgYL9T8xIVsiA6RZn2lk7vxov3Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nuxt/test-utils": ">=3.13.1" - } - }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -16868,17 +15559,17 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", - "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.29", - "@vue/compiler-sfc": "3.5.29", - "@vue/runtime-dom": "3.5.29", - "@vue/server-renderer": "3.5.29", - "@vue/shared": "3.5.29" + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" }, "peerDependencies": { "typescript": "*" @@ -16898,13 +15589,6 @@ "ufo": "^1.6.1" } }, - "node_modules/vue-component-type-helpers": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", - "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", - "dev": true, - "license": "MIT" - }, "node_modules/vue-devtools-stub": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", @@ -17005,29 +15689,6 @@ "vue": "^3.5.0" } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -17040,16 +15701,6 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -17075,23 +15726,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -17242,13 +15876,6 @@ "node": ">=12" } }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/Frontend-Learner/package.json b/Frontend-Learner/package.json index ea5dc734..84040469 100644 --- a/Frontend-Learner/package.json +++ b/Frontend-Learner/package.json @@ -19,14 +19,9 @@ }, "devDependencies": { "@nuxt/eslint-config": "^1.12.1", - "@nuxt/test-utils": "^4.0.0", "@nuxtjs/i18n": "^10.2.1", - "@playwright/test": "^1.58.2", "@types/node": "^22.19.8", - "@vue/test-utils": "^2.4.6", "eslint": "^9.39.2", - "jsdom": "^28.1.0", - "typescript": "^5.4.5", - "vitest": "^4.0.18" + "typescript": "^5.4.5" } } diff --git a/Frontend-Learner/pages/auth/login.vue b/Frontend-Learner/pages/auth/login.vue index a3d6ed63..e30602df 100644 --- a/Frontend-Learner/pages/auth/login.vue +++ b/Frontend-Learner/pages/auth/login.vue @@ -23,7 +23,7 @@ const isLoading = ref(false) const rememberMe = ref(false) const showPassword = ref(false) -// ข้อมูลฟอร์มสำหรับเก็บค่าอีเมลและรหัสผ่าน +// Form data model const loginForm = reactive({ email: '', password: '' @@ -31,7 +31,7 @@ const loginForm = reactive({ type LoginField = keyof typeof loginForm -// การตั้งค่ากฎการตรวจสอบ (Validation Rules) +// Validation rules definition // กำหนดกฎการตรวจสอบข้อมูล (Validation Rules) const loginRules = { email: { @@ -108,12 +108,12 @@ const handleLogin = async () => { } - // แสดงข้อผิดพลาดตามช่องหรือแสดงข้อผิดพลาดรวม (เพื่อความปลอดภัย) - // กรณีเข้าสู่ระบบไม่สำเร็จ + // Show error on specific fields + // Show generic error for security (or specific if role mismatch) if (result.error === 'Email ไม่ถูกต้อง') { - errors.value.email = result.error // กรณี Role ไม่ตรงกัน + errors.value.email = result.error // Role mismatch case } else { - // ข้อผิดพลาดแบบเหมาสำหรับปัญหาการเข้าสู่ระบบทั่วไป (เช่น รหัสผิด, ไม่พบผู้ใช้) + // Generic login failure (401, 404, etc.) const msg = 'กรุณาเช็ค Email หรือ รหัสผ่านใหม่อีกครั้ง' errors.value.email = msg errors.value.password = msg @@ -147,7 +147,7 @@ onMounted(() => { ========================================== -->
- +
E @@ -158,10 +158,10 @@ onMounted(() => {
- +
- +
@@ -180,7 +180,7 @@ onMounted(() => { {{ errors.email }}
- +
@@ -206,7 +206,7 @@ onMounted(() => { {{ errors.password }}
- +