From 7b18ea021b72f0b870723633604c447beb9a1ca0 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 18:44:11 +0700 Subject: [PATCH 01/51] chore: update server-side for set setup --- server/.env.example | 0 server/.gitignore | 2 + server/app.js | 0 server/bin/www | 0 server/config/config.json | 23 + server/models/index.js | 43 + server/package-lock.json | 6559 +++++++++++++++++++++++++++++++++++++ server/package.json | 28 + 8 files changed, 6655 insertions(+) create mode 100644 server/.env.example create mode 100644 server/.gitignore create mode 100644 server/app.js create mode 100644 server/bin/www create mode 100644 server/config/config.json create mode 100644 server/models/index.js create mode 100644 server/package-lock.json create mode 100644 server/package.json diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 00000000..e69de29b diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..3ec544c7 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env \ No newline at end of file diff --git a/server/app.js b/server/app.js new file mode 100644 index 00000000..e69de29b diff --git a/server/bin/www b/server/bin/www new file mode 100644 index 00000000..e69de29b diff --git a/server/config/config.json b/server/config/config.json new file mode 100644 index 00000000..f816f428 --- /dev/null +++ b/server/config/config.json @@ -0,0 +1,23 @@ +{ + "development": { + "username": "postgres", + "password": "postgres", + "database": "SNS_DB", + "host": "127.0.0.1", + "dialect": "postgres" + }, + "test": { + "username": "postgres", + "password": "postgres", + "database": "SNS_DB_test", + "host": "127.0.0.1", + "dialect": "postgres" + }, + "production": { + "username": "root", + "password": null, + "database": "database_production", + "host": "127.0.0.1", + "dialect": "mysql" + } +} diff --git a/server/models/index.js b/server/models/index.js new file mode 100644 index 00000000..024200ec --- /dev/null +++ b/server/models/index.js @@ -0,0 +1,43 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const process = require('process'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require(__dirname + '/../config/config.json')[env]; +const db = {}; + +let sequelize; +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return ( + file.indexOf('.') !== 0 && + file !== basename && + file.slice(-3) === '.js' && + file.indexOf('.test.js') === -1 + ); + }) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..2f4ec0a5 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,6559 @@ +{ + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcryptjs": "^3.0.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "pg": "^8.15.6", + "sequelize": "^6.37.7" + }, + "devDependencies": { + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "sequelize-cli": "^6.6.2", + "supertest": "^7.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-nh7nrWhLr6CBq9ldtw0wx+z9wKnnv/uTVLA9g/3/TcOYxbpOSZE+MhKPmWqU+K0NvThjhv12uD8MuqijB0WzEA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "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/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.143", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz", + "integrity": "sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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-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", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/memoizee/node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/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/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pg": { + "version": "8.15.6", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.15.6.tgz", + "integrity": "sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.8.5", + "pg-pool": "^3.9.6", + "pg-protocol": "^1.9.5", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.5" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.8.5.tgz", + "integrity": "sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.9.6.tgz", + "integrity": "sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.9.5.tgz", + "integrity": "sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/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/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "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/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-cli": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.2.tgz", + "integrity": "sha512-V8Oh+XMz2+uquLZltZES6MVAD+yEnmMfwfn+gpXcDiwE3jyQygLt4xoI0zG8gKt6cRcs84hsKnXAKDQjG/JAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-color": "^2.0.3", + "fs-extra": "^9.1.0", + "js-beautify": "^1.14.5", + "lodash": "^4.17.21", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz", + "integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", + "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000..d1e2746b --- /dev/null +++ b/server/package.json @@ -0,0 +1,28 @@ +{ + "name": "server", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "npx jest --verbose --detectOpenHandles --forceExit --runInBand", + "dev": "npx nodemon app" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "bcryptjs": "^3.0.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", + "pg": "^8.15.6", + "sequelize": "^6.37.7" + }, + "devDependencies": { + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "sequelize-cli": "^6.6.2", + "supertest": "^7.1.0" + } +} From 63a59b4818d54806e7f2ca2afa5ccc059de03790 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 20:02:13 +0700 Subject: [PATCH 02/51] chore: config project to database --- server/data/carts.json | 22 +++ server/data/categories.json | 42 +++++ server/data/lectures.json | 145 ++++++++++++++++++ server/data/users.json | 20 +++ server/helpers/bcrypt.js | 11 ++ .../migrations/20250428121908-create-user.js | 77 ++++++++++ .../20250428122918-create-category.js | 46 ++++++ .../20250428123127-create-lecture.js | 100 ++++++++++++ .../migrations/20250428123222-create-cart.js | 39 +++++ server/models/cart.js | 29 ++++ server/models/category.js | 40 +++++ server/models/lecture.js | 100 ++++++++++++ server/models/user.js | 80 ++++++++++ server/schema.drawio.png | Bin 0 -> 77979 bytes server/seeders/20250428124524-seeding-data.js | 48 ++++++ 15 files changed, 799 insertions(+) create mode 100644 server/data/carts.json create mode 100644 server/data/categories.json create mode 100644 server/data/lectures.json create mode 100644 server/data/users.json create mode 100644 server/helpers/bcrypt.js create mode 100644 server/migrations/20250428121908-create-user.js create mode 100644 server/migrations/20250428122918-create-category.js create mode 100644 server/migrations/20250428123127-create-lecture.js create mode 100644 server/migrations/20250428123222-create-cart.js create mode 100644 server/models/cart.js create mode 100644 server/models/category.js create mode 100644 server/models/lecture.js create mode 100644 server/models/user.js create mode 100644 server/schema.drawio.png create mode 100644 server/seeders/20250428124524-seeding-data.js diff --git a/server/data/carts.json b/server/data/carts.json new file mode 100644 index 00000000..673f8c73 --- /dev/null +++ b/server/data/carts.json @@ -0,0 +1,22 @@ +[ + { + "id": 1, + "UserId": 2, + "LectureId": 1 + }, + { + "id": 2, + "UserId": 2, + "LectureId": 5 + }, + { + "id": 3, + "UserId": 2, + "LectureId": 3 + }, + { + "id": 4, + "UserId": 2, + "LectureId": 7 + } +] diff --git a/server/data/categories.json b/server/data/categories.json new file mode 100644 index 00000000..74cdc6dd --- /dev/null +++ b/server/data/categories.json @@ -0,0 +1,42 @@ +[ + { + "id": 1, + "name": "Advanced NDT", + "description": "Advanced non-destructive testing methods like Ultrasonic Testing, designed for complex inspections in oil, gas, and petrochemical industries.", + "techniques": [ + "Ultrasonic Testing (UT)", + "Phased Array Ultrasonic Testing (PAUT)" + ] + }, + { + "id": 2, + "name": "Conventional NDT", + "description": "Traditional NDT methods such as Radiographic Testing, widely used for structural and pipeline integrity assessments.", + "techniques": ["Radiographic Testing (RT)", "Eddy Current Testing (ECT)"] + }, + { + "id": 3, + "name": "Surface NDT", + "description": "Surface inspection techniques like Magnetic Particle Testing, Penetrant Testing, and Visual Testing for detecting surface discontinuities.", + "techniques": [ + "Magnetic Particle Testing (MT)", + "Penetrant Testing (PT)", + "Visual Testing (VT)" + ] + }, + { + "id": 4, + "name": "Specialized NDT", + "description": "Specialized NDT techniques for specific applications in aerospace and automotive industries.", + "techniques": [ + "Acoustic Emission Testing (AET)", + "Infrared Thermography (IRT)" + ] + }, + { + "id": 5, + "name": "Digital NDT", + "description": "Modern NDT methods using digital technologies for enhanced accuracy and reporting.", + "techniques": ["Digital Radiography (DR)", "Computed Tomography (CT)"] + } +] diff --git a/server/data/lectures.json b/server/data/lectures.json new file mode 100644 index 00000000..692108a6 --- /dev/null +++ b/server/data/lectures.json @@ -0,0 +1,145 @@ +[ + { + "id": 1, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Ultrasonic Testing (UT)", + "CategoryId": 1, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 510"], + "description": "Expert in ultrasonic testing for weld imperfections and material thickness evaluation.", + "price": 7990000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_ut.jpg" + }, + { + "id": 2, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Phased Array Ultrasonic Testing (PAUT)", + "CategoryId": 1, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 510"], + "description": "Specializes in phased array ultrasonic testing for advanced inspections.", + "price": 8500000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_paut.jpg" + }, + { + "id": 3, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Radiographic Testing (RT)", + "CategoryId": 2, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 653"], + "description": "Provides comprehensive radiographic testing training for pipelines.", + "price": 8990000, + "availability": "Limited", + "image": "https://example.com/saenal_aladin_rapi_rt.jpg" + }, + { + "id": 4, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Eddy Current Testing (ECT)", + "CategoryId": 2, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 653"], + "description": "Expert in eddy current testing for conductive materials.", + "price": 7500000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_ect.jpg" + }, + { + "id": 5, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Magnetic Particle Testing (MT)", + "CategoryId": 3, + "experience_years": 20, + "certifications": ["ASNT Level II", "API 570"], + "description": "Specializes in magnetic particle testing for surface defect detection.", + "price": 5990000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_mt.jpg" + }, + { + "id": 6, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Penetrant Testing (PT)", + "CategoryId": 3, + "experience_years": 20, + "certifications": ["ASNT Level II", "AWS CWI"], + "description": "Experienced in penetrant testing for non-porous materials.", + "price": 4990000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_pt.jpg" + }, + { + "id": 7, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Visual Testing (VT)", + "CategoryId": 3, + "experience_years": 20, + "certifications": ["ASNT Level II", "API 1169"], + "description": "Delivers visual testing courses for surface inspections.", + "price": 5490000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_vt.jpg" + }, + { + "id": 8, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Acoustic Emission Testing (AET)", + "CategoryId": 4, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 510"], + "description": "Expert in acoustic emission testing for structural health monitoring.", + "price": 8200000, + "availability": "Limited", + "image": "https://example.com/saenal_aladin_rapi_aet.jpg" + }, + { + "id": 9, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Infrared Thermography (IRT)", + "CategoryId": 4, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 510"], + "description": "Specializes in infrared thermography for heat leak detection.", + "price": 7800000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_irt.jpg" + }, + { + "id": 10, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Digital Radiography (DR)", + "CategoryId": 5, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 653"], + "description": "Provides digital radiography training for enhanced imaging.", + "price": 9200000, + "availability": "Available", + "image": "https://example.com/saenal_aladin_rapi_dr.jpg" + }, + { + "id": 11, + "name": "Saenal Aladin Rapi", + "title": "Lead NDT Instructor", + "technique": "Computed Tomography (CT)", + "CategoryId": 5, + "experience_years": 20, + "certifications": ["ASNT Level III", "API 653"], + "description": "Expert in computed tomography for 3D internal inspections.", + "price": 9500000, + "availability": "Limited", + "image": "https://example.com/saenal_aladin_rapi_ct.jpg" + } +] diff --git a/server/data/users.json b/server/data/users.json new file mode 100644 index 00000000..a46b9e20 --- /dev/null +++ b/server/data/users.json @@ -0,0 +1,20 @@ +[ + { + "id": 1, + "username": "Admin", + "email": "admin@gmail.com", + "password": "Admin123", + "role": "Admin", + "phoneNumber": "081234567890", + "address": "Jl. Raya No. 1, Jakarta" + }, + { + "id": 2, + "username": "User", + "email": "user@gmail.com", + "password": "User123", + "role": "User", + "phoneNumber": "081234567891", + "address": "Jl. Raya No. 2, Jakarta" + } +] diff --git a/server/helpers/bcrypt.js b/server/helpers/bcrypt.js new file mode 100644 index 00000000..72a1394b --- /dev/null +++ b/server/helpers/bcrypt.js @@ -0,0 +1,11 @@ +const bcrypt = require("bcryptjs"); +const hashPassword = (password) => { + const salt = bcrypt.genSaltSync(10); + return bcrypt.hashSync(password, salt); +}; + +const comparePassword = (password, hashedPassword) => { + return bcrypt.compareSync(password, hashedPassword); +}; + +module.exports = { hashPassword, comparePassword }; diff --git a/server/migrations/20250428121908-create-user.js b/server/migrations/20250428121908-create-user.js new file mode 100644 index 00000000..05c23965 --- /dev/null +++ b/server/migrations/20250428121908-create-user.js @@ -0,0 +1,77 @@ +"use strict"; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("Users", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + username: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Username cannot be empty" }, + }, + }, + email: { + type: Sequelize.STRING, + allowNull: false, + unique: { msg: "Email already exists" }, + validate: { + notEmpty: { msg: "Email cannot be empty" }, + isEmail: { msg: "Invalid email format" }, + }, + }, + password: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Password cannot be empty" }, + len: { + args: [6, 128], + msg: "Password must be between 6 and 128 characters", + }, + }, + }, + role: { + type: Sequelize.ENUM("Admin", "User"), + allowNull: false, + validate: { + notEmpty: { msg: "Role cannot be empty" }, + isIn: { + args: [["Admin", "User"]], + msg: "Role must be either Admin or User", + }, + }, + }, + phoneNumber: { + type: Sequelize.STRING, // Ubah dari INTEGER ke STRING + allowNull: false, + validate: { + notEmpty: { msg: "Phone number cannot be empty" }, + }, + }, + address: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Address cannot be empty" }, + }, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("Users"); + }, +}; diff --git a/server/migrations/20250428122918-create-category.js b/server/migrations/20250428122918-create-category.js new file mode 100644 index 00000000..347dfea9 --- /dev/null +++ b/server/migrations/20250428122918-create-category.js @@ -0,0 +1,46 @@ +"use strict"; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("Categories", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Category name cannot be empty" }, + }, + }, + description: { + type: Sequelize.TEXT, // Ubah dari STRING ke TEXT + allowNull: false, + validate: { + notEmpty: { msg: "Category description cannot be empty" }, + }, + }, + techniques: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: false, + validate: { + notEmpty: { msg: "Techniques cannot be empty" }, + }, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("Categories"); + }, +}; diff --git a/server/migrations/20250428123127-create-lecture.js b/server/migrations/20250428123127-create-lecture.js new file mode 100644 index 00000000..3dd40c98 --- /dev/null +++ b/server/migrations/20250428123127-create-lecture.js @@ -0,0 +1,100 @@ +"use strict"; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("Lectures", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + name: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Instructor name cannot be empty" }, + }, + }, + title: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Title cannot be empty" }, + }, + }, + technique: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Technique cannot be empty" }, + }, + }, + CategoryId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: "Categories", key: "id" }, + onDelete: "CASCADE", + onUpdate: "CASCADE", + }, + experience_years: { + type: Sequelize.INTEGER, + allowNull: false, + validate: { + min: { args: [0], msg: "Experience years cannot be negative" }, + }, + }, + certifications: { + type: Sequelize.ARRAY(Sequelize.STRING), + allowNull: false, + validate: { + notEmpty: { msg: "Certifications cannot be empty" }, + }, + }, + description: { + type: Sequelize.TEXT, // Ubah dari STRING ke TEXT + allowNull: false, + validate: { + notEmpty: { msg: "Description cannot be empty" }, + }, + }, + price: { + type: Sequelize.INTEGER, + allowNull: false, + validate: { + min: { args: [0], msg: "Price cannot be negative" }, + }, + }, + availability: { + type: Sequelize.ENUM("Available", "Limited", "Unavailable"), + allowNull: false, + validate: { + notEmpty: { msg: "Availability cannot be empty" }, + isIn: { + args: [["Available", "Limited", "Unavailable"]], + msg: "Availability must be Available, Limited, or Unavailable", + }, + }, + }, + image: { + type: Sequelize.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Image URL cannot be empty" }, + isUrl: { msg: "Invalid image URL format" }, + }, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("Lectures"); + }, +}; diff --git a/server/migrations/20250428123222-create-cart.js b/server/migrations/20250428123222-create-cart.js new file mode 100644 index 00000000..c64239f4 --- /dev/null +++ b/server/migrations/20250428123222-create-cart.js @@ -0,0 +1,39 @@ +"use strict"; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable("Carts", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + UserId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: "Users", key: "id" }, + onDelete: "CASCADE", + onUpdate: "CASCADE", + }, + LectureId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { model: "Lectures", key: "id" }, + onDelete: "CASCADE", + onUpdate: "CASCADE", + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable("Carts"); + }, +}; diff --git a/server/models/cart.js b/server/models/cart.js new file mode 100644 index 00000000..3bbeb83e --- /dev/null +++ b/server/models/cart.js @@ -0,0 +1,29 @@ +"use strict"; +const { Model } = require("sequelize"); +module.exports = (sequelize, DataTypes) => { + class Cart extends Model { + static associate(models) { + // Relasi Cart → User (Many-to-One) + Cart.belongsTo(models.User); + // Relasi Cart → Lecture (Many-to-One) + Cart.belongsTo(models.Lecture); + } + } + Cart.init( + { + UserId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + LectureId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + sequelize, + modelName: "Cart", + } + ); + return Cart; +}; diff --git a/server/models/category.js b/server/models/category.js new file mode 100644 index 00000000..19459c51 --- /dev/null +++ b/server/models/category.js @@ -0,0 +1,40 @@ +"use strict"; +const { Model } = require("sequelize"); +module.exports = (sequelize, DataTypes) => { + class Category extends Model { + static associate(models) { + // Relasi Category → Lecture (One-to-Many) + Category.hasMany(models.Lecture); + } + } + Category.init( + { + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Category name cannot be empty" }, + }, + }, + description: { + type: DataTypes.TEXT, + allowNull: false, + validate: { + notEmpty: { msg: "Category description cannot be empty" }, + }, + }, + techniques: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: false, + validate: { + notEmpty: { msg: "Techniques cannot be empty" }, + }, + }, + }, + { + sequelize, + modelName: "Category", + } + ); + return Category; +}; diff --git a/server/models/lecture.js b/server/models/lecture.js new file mode 100644 index 00000000..cd5c5326 --- /dev/null +++ b/server/models/lecture.js @@ -0,0 +1,100 @@ +"use strict"; +const { Model } = require("sequelize"); +module.exports = (sequelize, DataTypes) => { + class Lecture extends Model { + static associate(models) { + // Relasi Lecture → Category (Many-to-One) + Lecture.belongsTo(models.Category, { + foreignKey: "CategoryId", + as: "category", + }); + // Relasi Lecture → User (Many-to-One, untuk pengelola lecture) + Lecture.belongsTo(models.User); + // Relasi Lecture ↔ User (Many-to-Many melalui Cart) + Lecture.belongsToMany(models.User, { + through: models.Cart, + }); + } + } + Lecture.init( + { + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Instructor name cannot be empty" }, + }, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Title cannot be empty" }, + }, + }, + technique: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Technique cannot be empty" }, + }, + }, + CategoryId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + experience_years: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: { args: [0], msg: "Experience years cannot be negative" }, + }, + }, + certifications: { + type: DataTypes.ARRAY(DataTypes.STRING), + allowNull: false, + validate: { + notEmpty: { msg: "Certifications cannot be empty" }, + }, + }, + description: { + type: DataTypes.TEXT, + allowNull: false, + validate: { + notEmpty: { msg: "Description cannot be empty" }, + }, + }, + price: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: { args: [0], msg: "Price cannot be negative" }, + }, + }, + availability: { + type: DataTypes.ENUM("Available", "Limited", "Unavailable"), + allowNull: false, + validate: { + notEmpty: { msg: "Availability cannot be empty" }, + isIn: { + args: [["Available", "Limited", "Unavailable"]], + msg: "Availability must be Available, Limited, or Unavailable", + }, + }, + }, + image: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Image URL cannot be empty" }, + isUrl: { msg: "Invalid image URL format" }, + }, + }, + }, + { + sequelize, + modelName: "Lecture", + } + ); + return Lecture; +}; diff --git a/server/models/user.js b/server/models/user.js new file mode 100644 index 00000000..da075143 --- /dev/null +++ b/server/models/user.js @@ -0,0 +1,80 @@ +"use strict"; +const { Model } = require("sequelize"); +const { hashPassword } = require("../helpers/bcrypt"); +module.exports = (sequelize, DataTypes) => { + class User extends Model { + static associate(models) { + // Relasi User → Lecture (One-to-Many, untuk pengelola lecture) + User.hasMany(models.Lecture); + // Relasi User ↔ Lecture (Many-to-Many melalui Cart) + User.belongsToMany(models.Lecture, { + through: models.Cart, + }); + } + } + User.init( + { + username: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Username cannot be empty" }, + }, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: { msg: "Email already exists" }, + validate: { + notEmpty: { msg: "Email cannot be empty" }, + isEmail: { msg: "Invalid email format" }, + }, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Password cannot be empty" }, + len: { + args: [6, 128], + msg: "Password must be between 6 and 128 characters", + }, + }, + }, + role: { + type: DataTypes.ENUM("Admin", "User"), + allowNull: false, + validate: { + notEmpty: { msg: "Role cannot be empty" }, + isIn: { + args: [["Admin", "User"]], + msg: "Role must be either Admin or User", + }, + }, + }, + phoneNumber: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Phone number cannot be empty" }, + }, + }, + address: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: { msg: "Address cannot be empty" }, + }, + }, + }, + { + sequelize, + modelName: "User", + } + ); + User.beforeCreate((user) => { + // Hash password before creating user + user.password = hashPassword(user.password); + }); + return User; +}; diff --git a/server/schema.drawio.png b/server/schema.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..a23618ab7c95e3b292a06ab452d44b8bb8a1d2cc GIT binary patch literal 77979 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 lFmInYBq#tkrYhtg{NhJ%E;g^f-?0XM7A7{v { + delete el.id; + el.createdAt = el.updatedAt = new Date(); + el.password = hashPassword(el.password); + return el; + } + ); + const category = JSON.parse( + await fs.readFile("./data/categories.json") + ).map((el) => { + delete el.id; + el.createdAt = el.updatedAt = new Date(); + return el; + }); + const lecture = JSON.parse(await fs.readFile("./data/lectures.json")).map( + (el) => { + delete el.id; + el.createdAt = el.updatedAt = new Date(); + return el; + } + ); + const cart = JSON.parse(await fs.readFile("./data/carts.json")).map( + (el) => { + delete el.id; + el.createdAt = el.updatedAt = new Date(); + return el; + } + ); + await queryInterface.bulkInsert("Users", user, {}); + await queryInterface.bulkInsert("Categories", category, {}); + await queryInterface.bulkInsert("Lectures", lecture, {}); + await queryInterface.bulkInsert("Carts", cart, {}); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.bulkDelete("Carts", null, {}); + await queryInterface.bulkDelete("Lectures", null, {}); + await queryInterface.bulkDelete("Categories", null, {}); + await queryInterface.bulkDelete("Users", null, {}); + }, +}; From d2868340dd17523bd28fbc574f0dc9cd0ee4e572 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 20:20:04 +0700 Subject: [PATCH 03/51] chore: add helpers/jwt --- server/app.js | 19 +++++++++++++++++++ server/helpers/jwt.js | 13 +++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 server/helpers/jwt.js diff --git a/server/app.js b/server/app.js index e69de29b..d4734892 100644 --- a/server/app.js +++ b/server/app.js @@ -0,0 +1,19 @@ +require('dotenv').config(); +const express = require("express"); +const app = express(); +const port = 3000; +const cors = require("cors"); + +app.use(cors()); +//middleware body-parser +app.use(express.urlencoded({ extended: false })); +app.use(express.json()); + + +app.get("/", (req, res) => { + res.send("Welcome to SAR NDT SERVICES"); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/server/helpers/jwt.js b/server/helpers/jwt.js new file mode 100644 index 00000000..4ad3aa26 --- /dev/null +++ b/server/helpers/jwt.js @@ -0,0 +1,13 @@ +const jwt = require("jsonwebtoken"); + +const JWT_SECRET = process.env.JWT_SECRET || "sns-secret-key"; + +const generateToken = (payload) => { + return jwt.sign(payload, JWT_SECRET); +}; + +const verifyToken = (token) => { + return jwt.verify(token, JWT_SECRET); +}; + +module.exports = { generateToken, verifyToken }; From 43aeed536e6b1c64cf93937ff08b9ed22404d520 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 20:22:49 +0700 Subject: [PATCH 04/51] chore: update middlewares --- server/middlewares/authenticantion.js | 38 +++++++++++++++++++++++++++ server/middlewares/authorization.js | 12 +++++++++ 2 files changed, 50 insertions(+) create mode 100644 server/middlewares/authenticantion.js create mode 100644 server/middlewares/authorization.js diff --git a/server/middlewares/authenticantion.js b/server/middlewares/authenticantion.js new file mode 100644 index 00000000..7fe0cac9 --- /dev/null +++ b/server/middlewares/authenticantion.js @@ -0,0 +1,38 @@ +const { verifyToken } = require("../helpers/jwt"); +const { User } = require("../models"); + +const authentication = async (req, res, next) => { + try { + // Check if token exists + const { access_token } = req.headers; + if (!access_token) { + throw { name: "Unauthorized", message: "Please login first" }; + } + + // Verify token + const payload = verifyToken(access_token); + if (!payload) { + throw { name: "JsonWebTokenError", message: "Invalid token" }; + } + + // Find user by id + const user = await User.findByPk(payload.id); + if (!user) { + throw { name: "Unauthorized", message: "User not found" }; + } + + // Add user to request + req.user = { + id: user.id, + email: user.email, + role: user.role, + username: user.username, + }; + + next(); + } catch (err) { + next(err); + } +}; + +module.exports = authentication; diff --git a/server/middlewares/authorization.js b/server/middlewares/authorization.js new file mode 100644 index 00000000..99262582 --- /dev/null +++ b/server/middlewares/authorization.js @@ -0,0 +1,12 @@ +const adminAuthorization = async (req, res, next) => { + try { + if (req.user.role !== "Admin") { + throw { name: "Forbidden", message: "You are not authorized" }; + } + next(); + } catch (err) { + next(err); + } +}; + +module.exports = { adminAuthorization }; From e1800cb76495bca34a2e635af3f624586fff4c23 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 20:27:06 +0700 Subject: [PATCH 05/51] chore: update errorHandler, make routes, and controller --- server/controllers/cartController.js | 77 ++++++++++++ server/controllers/category | 89 ++++++++++++++ server/controllers/lectureController.js | 149 ++++++++++++++++++++++++ server/controllers/userController.js | 83 +++++++++++++ server/middlewares/errorHandler.js | 41 +++++++ server/routes/cartRoutes.js | 12 ++ server/routes/categoryRoutes.js | 18 +++ server/routes/index.js | 13 +++ server/routes/lectureRoutes.js | 18 +++ server/routes/userRoutes.js | 13 +++ 10 files changed, 513 insertions(+) create mode 100644 server/controllers/cartController.js create mode 100644 server/controllers/category create mode 100644 server/controllers/lectureController.js create mode 100644 server/controllers/userController.js create mode 100644 server/middlewares/errorHandler.js create mode 100644 server/routes/cartRoutes.js create mode 100644 server/routes/categoryRoutes.js create mode 100644 server/routes/index.js create mode 100644 server/routes/lectureRoutes.js create mode 100644 server/routes/userRoutes.js diff --git a/server/controllers/cartController.js b/server/controllers/cartController.js new file mode 100644 index 00000000..370f734b --- /dev/null +++ b/server/controllers/cartController.js @@ -0,0 +1,77 @@ +const { Cart, Lecture, User } = require("../models"); + +class CartController { + static async getUserCart(req, res, next) { + try { + const userId = req.user.id; + + const carts = await Cart.findAll({ + where: { UserId: userId }, + include: [ + { + model: Lecture, + include: ["category"] + } + ] + }); + + res.status(200).json(carts); + } catch (err) { + next(err); + } + } + + static async addToCart(req, res, next) { + try { + const UserId = req.user.id; + const { LectureId } = req.body; + + // Check if lecture exists + const lecture = await Lecture.findByPk(LectureId); + if (!lecture) { + throw { name: "NotFound", message: "Lecture not found" }; + } + + // Check if already in cart + const existingCart = await Cart.findOne({ + where: { UserId, LectureId } + }); + + if (existingCart) { + throw { name: "BadRequest", message: "Lecture already in cart" }; + } + + const newCart = await Cart.create({ + UserId, + LectureId + }); + + res.status(201).json(newCart); + } catch (err) { + next(err); + } + } + + static async removeFromCart(req, res, next) { + try { + const UserId = req.user.id; + const { id } = req.params; + + const cart = await Cart.findOne({ + where: { id, UserId } + }); + + if (!cart) { + throw { name: "NotFound", message: "Cart item not found" }; + } + + await cart.destroy(); + + res.status(200).json({ message: "Lecture removed from cart successfully" }); + } catch (err) { + next(err); + } + } +} + +module.exports = CartController; \ No newline at end of file diff --git a/server/controllers/category b/server/controllers/category new file mode 100644 index 00000000..6e63134a --- /dev/null +++ b/server/controllers/category @@ -0,0 +1,89 @@ +Controller.js +const { Category } = require("../models"); + +class CategoryController { + static async getAllCategories(req, res, next) { + try { + const categories = await Category.findAll({ + order: [["id", "ASC"]] + }); + + res.status(200).json(categories); + } catch (err) { + next(err); + } + } + + static async getCategoryById(req, res, next) { + try { + const { id } = req.params; + const category = await Category.findByPk(id); + + if (!category) { + throw { name: "NotFound", message: "Category not found" }; + } + + res.status(200).json(category); + } catch (err) { + next(err); + } + } + + static async createCategory(req, res, next) { + try { + const { name, description, techniques } = req.body; + + const newCategory = await Category.create({ + name, + description, + techniques + }); + + res.status(201).json(newCategory); + } catch (err) { + next(err); + } + } + + static async updateCategory(req, res, next) { + try { + const { id } = req.params; + const { name, description, techniques } = req.body; + + const category = await Category.findByPk(id); + + if (!category) { + throw { name: "NotFound", message: "Category not found" }; + } + + await category.update({ + name, + description, + techniques + }); + + res.status(200).json(category); + } catch (err) { + next(err); + } + } + + static async deleteCategory(req, res, next) { + try { + const { id } = req.params; + const category = await Category.findByPk(id); + + if (!category) { + throw { name: "NotFound", message: "Category not found" }; + } + + await category.destroy(); + + res.status(200).json({ message: "Category deleted successfully" }); + } catch (err) { + next(err); + } + } +} + +module.exports = CategoryController; \ No newline at end of file diff --git a/server/controllers/lectureController.js b/server/controllers/lectureController.js new file mode 100644 index 00000000..f0080abb --- /dev/null +++ b/server/controllers/lectureController.js @@ -0,0 +1,149 @@ +const { Lecture, Category, User } = require("../models"); + +class LectureController { + static async getAllLectures(req, res, next) { + try { + const lectures = await Lecture.findAll({ + include: [ + { + model: Category, + as: "category" + }, + { + model: User, + attributes: ["username", "email"] + } + ], + order: [["id", "ASC"]] + }); + + res.status(200).json(lectures); + } catch (err) { + next(err); + } + } + + static async getLectureById(req, res, next) { + try { + const { id } = req.params; + const lecture = await Lecture.findByPk(id, { + include: [ + { + model: Category, + as: "category" + }, + { + model: User, + attributes: ["username", "email"] + } + ] + }); + + if (!lecture) { + throw { name: "NotFound", message: "Lecture not found" }; + } + + res.status(200).json(lecture); + } catch (err) { + next(err); + } + } + + static async createLecture(req, res, next) { + try { + const { + name, + title, + technique, + CategoryId, + experience_years, + certifications, + description, + price, + availability, + image + } = req.body; + + // Set the current admin as the lecture creator + const UserId = req.user.id; + + const newLecture = await Lecture.create({ + name, + title, + technique, + CategoryId, + experience_years, + certifications, + description, + price, + availability, + image, + UserId + }); + + res.status(201).json(newLecture); + } catch (err) { + next(err); + } + } + + static async updateLecture(req, res, next) { + try { + const { id } = req.params; + const { + name, + title, + technique, + CategoryId, + experience_years, + certifications, + description, + price, + availability, + image + } = req.body; + + const lecture = await Lecture.findByPk(id); + + if (!lecture) { + throw { name: "NotFound", message: "Lecture not found" }; + } + + await lecture.update({ + name, + title, + technique, + CategoryId, + experience_years, + certifications, + description, + price, + availability, + image + }); + + res.status(200).json(lecture); + } catch (err) { + next(err); + } + } + + static async deleteLecture(req, res, next) { + try { + const { id } = req.params; + const lecture = await Lecture.findByPk(id); + + if (!lecture) { + throw { name: "NotFound", message: "Lecture not found" }; + } + + await lecture.destroy(); + + res.status(200).json({ message: "Lecture deleted successfully" }); + } catch (err) { + next(err); + } + } +} + +module.exports = LectureController; \ No newline at end of file diff --git a/server/controllers/userController.js b/server/controllers/userController.js new file mode 100644 index 00000000..5fd0b9ab --- /dev/null +++ b/server/controllers/userController.js @@ -0,0 +1,83 @@ +const { User } = require("../models"); +const { comparePassword } = require("../helpers/bcrypt"); +const { generateToken } = require("../helpers/jwt"); + +class UserController { + static async register(req, res, next) { + try { + const { username, email, password, phoneNumber, address } = req.body; + + // Default role for new registrations is "User" + const newUser = await User.create({ + username, + email, + password, // Will be hashed by beforeCreate hook in model + role: "User", + phoneNumber, + address + }); + + res.status(201).json({ + id: newUser.id, + username: newUser.username, + email: newUser.email, + role: newUser.role + }); + } catch (err) { + next(err); + } + } + + static async login(req, res, next) { + try { + const { email, password } = req.body; + + if (!email || !password) { + throw { name: "BadRequest", message: "Email and password are required" }; + } + + const user = await User.findOne({ where: { email } }); + if (!user) { + throw { name: "Unauthorized", message: "Invalid email or password" }; + } + + const isPasswordValid = comparePassword(password, user.password); + if (!isPasswordValid) { + throw { name: "Unauthorized", message: "Invalid email or password" }; + } + + const payload = { + id: user.id, + email: user.email, + role: user.role + }; + + const access_token = generateToken(payload); + + res.status(200).json({ + access_token, + id: user.id, + username: user.username, + email: user.email, + role: user.role + }); + } catch (err) { + next(err); + } + } + + static async getUserProfile(req, res, next) { + try { + const userId = req.user.id; + const user = await User.findByPk(userId, { + attributes: { exclude: ['password'] } + }); + + res.status(200).json(user); + } catch (err) { + next(err); + } + } +} + +module.exports = UserController; \ No newline at end of file diff --git a/server/middlewares/errorHandler.js b/server/middlewares/errorHandler.js new file mode 100644 index 00000000..08231c63 --- /dev/null +++ b/server/middlewares/errorHandler.js @@ -0,0 +1,41 @@ +const errorHandler = (err, req, res, next) => { + console.log(err); + + let statusCode = 500; + let message = "Internal Server Error"; + + switch (err.name) { + case "SequelizeValidationError": + statusCode = 400; + message = err.errors.map(e => e.message).join(", "); + break; + case "SequelizeUniqueConstraintError": + statusCode = 400; + message = err.errors.map(e => e.message).join(", "); + break; + case "BadRequest": + statusCode = 400; + message = err.message; + break; + case "Unauthorized": + statusCode = 401; + message = err.message; + break; + case "Forbidden": + statusCode = 403; + message = err.message; + break; + case "NotFound": + statusCode = 404; + message = err.message; + break; + case "JsonWebTokenError": + statusCode = 401; + message = "Invalid token"; + break; + } + + res.status(statusCode).json({ message }); +}; + +module.exports = errorHandler; \ No newline at end of file diff --git a/server/routes/cartRoutes.js b/server/routes/cartRoutes.js new file mode 100644 index 00000000..822a7058 --- /dev/null +++ b/server/routes/cartRoutes.js @@ -0,0 +1,12 @@ +const express = require("express"); +const router = express.Router(); +const CartController = require("../controllers/cartController"); +const authentication = require("../middlewares/authentication"); + +// All cart routes require authentication +router.use(authentication); +router.get("/", CartController.getUserCart); +router.post("/", CartController.addToCart); +router.delete("/:id", CartController.removeFromCart); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/categoryRoutes.js b/server/routes/categoryRoutes.js new file mode 100644 index 00000000..139ab45e --- /dev/null +++ b/server/routes/categoryRoutes.js @@ -0,0 +1,18 @@ +const express = require("express"); +const router = express.Router(); +const CategoryController = require("../controllers/categoryController"); +const authentication = require("../middlewares/authentication"); +const { adminAuthorization } = require("../middlewares/authorization"); + +// Public routes +router.get("/", CategoryController.getAllCategories); +router.get("/:id", CategoryController.getCategoryById); + +// Protected routes (Admin only) +router.use(authentication); +router.use(adminAuthorization); +router.post("/", CategoryController.createCategory); +router.put("/:id", CategoryController.updateCategory); +router.delete("/:id", CategoryController.deleteCategory); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/index.js b/server/routes/index.js new file mode 100644 index 00000000..acc5693f --- /dev/null +++ b/server/routes/index.js @@ -0,0 +1,13 @@ +const express = require("express"); +const router = express.Router(); +const userRoutes = require("./userRoutes"); +const categoryRoutes = require("./categoryRoutes"); +const lectureRoutes = require("./lectureRoutes"); +const cartRoutes = require("./cartRoutes"); + +router.use("/users", userRoutes); +router.use("/categories", categoryRoutes); +router.use("/lectures", lectureRoutes); +router.use("/carts", cartRoutes); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/lectureRoutes.js b/server/routes/lectureRoutes.js new file mode 100644 index 00000000..a76807a6 --- /dev/null +++ b/server/routes/lectureRoutes.js @@ -0,0 +1,18 @@ +const express = require("express"); +const router = express.Router(); +const LectureController = require("../controllers/lectureController"); +const authentication = require("../middlewares/authentication"); +const { adminAuthorization } = require("../middlewares/authorization"); + +// Public routes +router.get("/", LectureController.getAllLectures); +router.get("/:id", LectureController.getLectureById); + +// Protected routes (Admin only) +router.use(authentication); +router.use(adminAuthorization); +router.post("/", LectureController.createLecture); +router.put("/:id", LectureController.updateLecture); +router.delete("/:id", LectureController.deleteLecture); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js new file mode 100644 index 00000000..8d71d6e6 --- /dev/null +++ b/server/routes/userRoutes.js @@ -0,0 +1,13 @@ +const express = require("express"); +const router = express.Router(); +const UserController = require("../controllers/userController"); +const authentication = require("../middlewares/authentication"); + +router.post("/register", UserController.register); +router.post("/login", UserController.login); + +// Protected routes +router.use(authentication); +router.get("/profile", UserController.getUserProfile); + +module.exports = router; \ No newline at end of file From 39d1a16057133b52664be07fe690803e61fe5515 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 20:52:41 +0700 Subject: [PATCH 06/51] chore: update API for this project --- server/app.js | 26 +++++++++++---- .../{category => categoryController.js} | 33 +++++++++---------- server/helpers/jwt.js | 2 +- .../{authenticantion.js => authentication.js} | 0 4 files changed, 36 insertions(+), 25 deletions(-) rename server/controllers/{category => categoryController.js} (89%) rename server/middlewares/{authenticantion.js => authentication.js} (100%) diff --git a/server/app.js b/server/app.js index d4734892..49d747a3 100644 --- a/server/app.js +++ b/server/app.js @@ -1,19 +1,31 @@ -require('dotenv').config(); +require("dotenv").config(); const express = require("express"); const app = express(); -const port = 3000; +const port = process.env.PORT || 3000; const cors = require("cors"); +const routes = require("./routes"); +const errorHandler = require("./middlewares/errorHandler"); app.use(cors()); //middleware body-parser app.use(express.urlencoded({ extended: false })); app.use(express.json()); - app.get("/", (req, res) => { - res.send("Welcome to SAR NDT SERVICES"); + res.send("SNS NDT Learning Platform API"); }); -app.listen(port, () => { - console.log(`Example app listening on port ${port}`); -}); +// API routes +app.use("/api", routes); + +// Error handler middleware +app.use(errorHandler); + +// Only listen if directly running this file (not in test environment) +if (process.env.NODE_ENV !== "test") { + app.listen(port, () => { + console.log(`SNS NDT Learning Platform API listening on port ${port}`); + }); +} + +module.exports = app; diff --git a/server/controllers/category b/server/controllers/categoryController.js similarity index 89% rename from server/controllers/category rename to server/controllers/categoryController.js index 6e63134a..a50cb919 100644 --- a/server/controllers/category +++ b/server/controllers/categoryController.js @@ -1,13 +1,12 @@ -Controller.js const { Category } = require("../models"); class CategoryController { static async getAllCategories(req, res, next) { try { const categories = await Category.findAll({ - order: [["id", "ASC"]] + order: [["id", "ASC"]], }); - + res.status(200).json(categories); } catch (err) { next(err); @@ -18,11 +17,11 @@ class CategoryController { try { const { id } = req.params; const category = await Category.findByPk(id); - + if (!category) { throw { name: "NotFound", message: "Category not found" }; } - + res.status(200).json(category); } catch (err) { next(err); @@ -32,13 +31,13 @@ class CategoryController { static async createCategory(req, res, next) { try { const { name, description, techniques } = req.body; - + const newCategory = await Category.create({ name, description, - techniques + techniques, }); - + res.status(201).json(newCategory); } catch (err) { next(err); @@ -49,19 +48,19 @@ class CategoryController { try { const { id } = req.params; const { name, description, techniques } = req.body; - + const category = await Category.findByPk(id); - + if (!category) { throw { name: "NotFound", message: "Category not found" }; } - + await category.update({ name, description, - techniques + techniques, }); - + res.status(200).json(category); } catch (err) { next(err); @@ -72,13 +71,13 @@ class CategoryController { try { const { id } = req.params; const category = await Category.findByPk(id); - + if (!category) { throw { name: "NotFound", message: "Category not found" }; } - + await category.destroy(); - + res.status(200).json({ message: "Category deleted successfully" }); } catch (err) { next(err); @@ -86,4 +85,4 @@ class CategoryController { } } -module.exports = CategoryController; \ No newline at end of file +module.exports = CategoryController; diff --git a/server/helpers/jwt.js b/server/helpers/jwt.js index 4ad3aa26..b7bc0b6e 100644 --- a/server/helpers/jwt.js +++ b/server/helpers/jwt.js @@ -1,6 +1,6 @@ const jwt = require("jsonwebtoken"); -const JWT_SECRET = process.env.JWT_SECRET || "sns-secret-key"; +const JWT_SECRET = process.env.JWT_SECRET; const generateToken = (payload) => { return jwt.sign(payload, JWT_SECRET); diff --git a/server/middlewares/authenticantion.js b/server/middlewares/authentication.js similarity index 100% rename from server/middlewares/authenticantion.js rename to server/middlewares/authentication.js From 2e1be56a8999b3b9ba416c1ec20245e6e1ec5872 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Mon, 28 Apr 2025 20:58:22 +0700 Subject: [PATCH 07/51] chore: just start client side --- client/.gitignore | 24 + client/README.md | 12 + client/eslint.config.js | 33 + client/index.html | 13 + client/package-lock.json | 3105 +++++++++++++++++++++++++++++++++++ client/package.json | 29 + client/public/vite.svg | 1 + client/src/App.css | 42 + client/src/App.jsx | 35 + client/src/assets/react.svg | 1 + client/src/index.css | 68 + client/src/main.jsx | 10 + client/vite.config.js | 7 + 13 files changed, 3380 insertions(+) create mode 100644 client/.gitignore create mode 100644 client/README.md create mode 100644 client/eslint.config.js create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/public/vite.svg create mode 100644 client/src/App.css create mode 100644 client/src/App.jsx create mode 100644 client/src/assets/react.svg create mode 100644 client/src/index.css create mode 100644 client/src/main.jsx create mode 100644 client/vite.config.js diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..7059a962 --- /dev/null +++ b/client/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 00000000..ec2b712d --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/client/index.html b/client/index.html new file mode 100644 index 00000000..0c589ecc --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 00000000..cd47b445 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,3105 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "axios": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.5.2" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.22.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "vite": "^6.3.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz", + "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", + "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", + "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", + "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", + "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", + "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", + "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", + "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", + "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", + "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", + "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", + "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", + "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", + "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", + "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", + "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", + "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", + "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", + "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", + "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", + "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", + "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001715", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", + "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.143", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz", + "integrity": "sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.25.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz", + "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.25.1", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz", + "integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", + "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.1", + "@rollup/rollup-android-arm64": "4.40.1", + "@rollup/rollup-darwin-arm64": "4.40.1", + "@rollup/rollup-darwin-x64": "4.40.1", + "@rollup/rollup-freebsd-arm64": "4.40.1", + "@rollup/rollup-freebsd-x64": "4.40.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", + "@rollup/rollup-linux-arm64-gnu": "4.40.1", + "@rollup/rollup-linux-arm64-musl": "4.40.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", + "@rollup/rollup-linux-riscv64-musl": "4.40.1", + "@rollup/rollup-linux-s390x-gnu": "4.40.1", + "@rollup/rollup-linux-x64-gnu": "4.40.1", + "@rollup/rollup-linux-x64-musl": "4.40.1", + "@rollup/rollup-win32-arm64-msvc": "4.40.1", + "@rollup/rollup-win32-ia32-msvc": "4.40.1", + "@rollup/rollup-win32-x64-msvc": "4.40.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz", + "integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 00000000..1a242b8f --- /dev/null +++ b/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.5.2" + }, + "devDependencies": { + "@eslint/js": "^9.22.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.22.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "vite": "^6.3.1" + } +} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 00000000..f67355ae --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 00000000..08a3ac9e --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 00000000..b9a1a6de --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 00000000..8b0f57b9 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From c1f72769f5fdfa7073aebae3181cf882d9f0eb9f Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 11:25:47 +0700 Subject: [PATCH 08/51] chore: update server side api/public --- client/index.html | 13 +- client/src/App.css | 42 ------ client/src/App.jsx | 49 +++---- client/src/index.css | 68 ---------- client/src/main.jsx | 1 - client/src/pages/Home.jsx | 16 +++ server/routes/index.js | 5 + server/routes/publicRoutes.js | 244 ++++++++++++++++++++++++++++++++++ 8 files changed, 298 insertions(+), 140 deletions(-) delete mode 100644 client/src/App.css delete mode 100644 client/src/index.css create mode 100644 client/src/pages/Home.jsx create mode 100644 server/routes/publicRoutes.js diff --git a/client/index.html b/client/index.html index 0c589ecc..7c148166 100644 --- a/client/index.html +++ b/client/index.html @@ -1,10 +1,21 @@ - + Vite + React + +
diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index b9d355df..00000000 --- a/client/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/client/src/App.jsx b/client/src/App.jsx index f67355ae..b89517ca 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,35 +1,28 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { BrowserRouter, Routes, Route } from "react-router"; +import Home from "./pages/Home"; function App() { - const [count, setCount] = useState(0) - return ( <> - -

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

+ + + } /> + {/* } /> + + }> + } /> + } /> + + + + } /> + } /> + } /> + */} + + - ) + ); } -export default App +export default App; diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index 08a3ac9e..00000000 --- a/client/src/index.css +++ /dev/null @@ -1,68 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/client/src/main.jsx b/client/src/main.jsx index b9a1a6de..3d9da8ac 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -1,6 +1,5 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx new file mode 100644 index 00000000..8cc0a340 --- /dev/null +++ b/client/src/pages/Home.jsx @@ -0,0 +1,16 @@ +export default function Home() { + return ( +
+
+
+
+ Ini adalah alert Bootstrap +
+
+
+ +
+
+
+ ); +} diff --git a/server/routes/index.js b/server/routes/index.js index acc5693f..3e1d7c9a 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -4,7 +4,12 @@ const userRoutes = require("./userRoutes"); const categoryRoutes = require("./categoryRoutes"); const lectureRoutes = require("./lectureRoutes"); const cartRoutes = require("./cartRoutes"); +const publicRoutes = require("./publicRoutes"); +// Public routes - tidak memerlukan authentication +router.use("/public", publicRoutes); + +// Protected routes - memerlukan authentication router.use("/users", userRoutes); router.use("/categories", categoryRoutes); router.use("/lectures", lectureRoutes); diff --git a/server/routes/publicRoutes.js b/server/routes/publicRoutes.js new file mode 100644 index 00000000..e37d8663 --- /dev/null +++ b/server/routes/publicRoutes.js @@ -0,0 +1,244 @@ +const express = require("express"); +const router = express.Router(); +const { Lecture, Category, User, sequelize } = require("../models"); +const { Op } = require("sequelize"); + +// Get landing page bundle (all homepage data in one request) +router.get("/homepage-bundle", async (req, res, next) => { + try { + // Get featured lectures, latest lectures, statistics, and popular categories in parallel + const [featuredLectures, latestLectures, statistics, popularCategories] = await Promise.all([ + Lecture.findAll({ + limit: 3, + include: [{ model: Category, as: "category" }], + order: [["price", "DESC"]], + attributes: { exclude: ["UserId", "createdAt", "updatedAt"] } + }), + + Lecture.findAll({ + limit: 6, + include: [{ model: Category, as: "category" }], + order: [["createdAt", "DESC"]], + attributes: { exclude: ["UserId", "createdAt", "updatedAt"] } + }), + + Promise.all([ + Lecture.count(), + Category.count(), + User.count(), + Lecture.findOne({ + attributes: [[sequelize.fn('AVG', sequelize.col('price')), 'averagePrice']], + raw: true + }) + ]), + + Category.findAll({ + limit: 5, + attributes: ['id', 'name', 'description'], + order: [["id", "ASC"]] + }) + ]); + + // Format statistics data + const [lectureCount, categoryCount, userCount, avgPrice] = statistics; + + res.status(200).json({ + featuredLectures, + latestLectures, + statistics: { + totalLectures: lectureCount, + totalCategories: categoryCount, + totalUsers: userCount, + averagePrice: avgPrice ? parseFloat(avgPrice.averagePrice).toFixed(2) : 0 + }, + popularCategories + }); + } catch (err) { + next(err); + } +}); + +// Advanced lecture search with filters, sorting and pagination +router.get("/lectures", async (req, res, next) => { + try { + // Pagination + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; + + // Search query + const searchQuery = req.query.search || ''; + + // Filters + const filters = {}; + if (req.query.categoryId) { + filters.CategoryId = req.query.categoryId; + } + + if (req.query.minPrice || req.query.maxPrice) { + filters.price = {}; + if (req.query.minPrice) { + filters.price[Op.gte] = parseFloat(req.query.minPrice); + } + if (req.query.maxPrice) { + filters.price[Op.lte] = parseFloat(req.query.maxPrice); + } + } + + if (req.query.minExperience) { + filters.experience_years = { + [Op.gte]: parseInt(req.query.minExperience) + }; + } + + // Search condition + if (searchQuery) { + filters[Op.or] = [ + { name: { [Op.iLike]: `%${searchQuery}%` } }, + { description: { [Op.iLike]: `%${searchQuery}%` } }, + { technique: { [Op.iLike]: `%${searchQuery}%` } } + ]; + } + + // Sorting + let order = [['id', 'ASC']]; // default sorting + if (req.query.sortBy) { + const validColumns = ['id', 'name', 'price', 'createdAt', 'experience_years']; + const validOrders = ['ASC', 'DESC']; + + if (validColumns.includes(req.query.sortBy)) { + const direction = req.query.sortDirection && + validOrders.includes(req.query.sortDirection.toUpperCase()) ? + req.query.sortDirection.toUpperCase() : 'ASC'; + order = [[req.query.sortBy, direction]]; + } + } + + // Execute query with all parameters + const { count, rows: lectures } = await Lecture.findAndCountAll({ + where: filters, + limit, + offset, + include: [{ model: Category, as: "category" }], + order, + distinct: true + }); + + res.status(200).json({ + totalItems: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + lectures + }); + } catch (err) { + next(err); + } +}); + +// Get lectures by category with pagination, filters, search +router.get("/categories/:categoryId/lectures", async (req, res, next) => { + try { + const { categoryId } = req.params; + + // Verify category exists + const category = await Category.findByPk(categoryId); + if (!category) { + return res.status(404).json({ message: "Category not found" }); + } + + // Pagination + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; + + // Search query + const searchQuery = req.query.search || ''; + + // Filters + const filters = { CategoryId: categoryId }; + + if (req.query.minPrice || req.query.maxPrice) { + filters.price = {}; + if (req.query.minPrice) filters.price[Op.gte] = parseFloat(req.query.minPrice); + if (req.query.maxPrice) filters.price[Op.lte] = parseFloat(req.query.maxPrice); + } + + // Search condition + if (searchQuery) { + filters[Op.and] = [ + { CategoryId: categoryId }, + { + [Op.or]: [ + { name: { [Op.iLike]: `%${searchQuery}%` } }, + { description: { [Op.iLike]: `%${searchQuery}%` } }, + { technique: { [Op.iLike]: `%${searchQuery}%` } } + ] + } + ]; + delete filters.CategoryId; // Remove duplicate condition + } + + // Sorting + let order = [['id', 'ASC']]; // default sorting + if (req.query.sortBy) { + const validColumns = ['id', 'name', 'price', 'createdAt', 'experience_years']; + const validOrders = ['ASC', 'DESC']; + + if (validColumns.includes(req.query.sortBy)) { + const direction = req.query.sortDirection && + validOrders.includes(req.query.sortDirection.toUpperCase()) ? + req.query.sortDirection.toUpperCase() : 'ASC'; + order = [[req.query.sortBy, direction]]; + } + } + + // Execute query + const { count, rows: lectures } = await Lecture.findAndCountAll({ + where: filters, + limit, + offset, + order, + distinct: true + }); + + res.status(200).json({ + category: { + id: category.id, + name: category.name, + description: category.description + }, + totalItems: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + lectures + }); + } catch (err) { + next(err); + } +}); + +// Get all categories with lecture counts +router.get("/categories", async (req, res, next) => { + try { + const categories = await Category.findAll({ + attributes: [ + 'id', + 'name', + 'description', + [sequelize.fn('COUNT', sequelize.col('Lectures.id')), 'lectureCount'] + ], + include: [{ + model: Lecture, + attributes: [] + }], + group: ['Category.id'], + order: [['id', 'ASC']] + }); + + res.status(200).json(categories); + } catch (err) { + next(err); + } +}); + +module.exports = router; \ No newline at end of file From ec7111db14861cd5d865be53c4237d1f24899e93 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 15:09:40 +0700 Subject: [PATCH 09/51] chore: update homePage and try to fetching data --- client/src/pages/Home.jsx | 234 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 226 insertions(+), 8 deletions(-) diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx index 8cc0a340..3eff3860 100644 --- a/client/src/pages/Home.jsx +++ b/client/src/pages/Home.jsx @@ -1,16 +1,234 @@ +import { useState, useEffect } from "react"; +import axios from "axios"; +import { Link } from "react-router"; // Perbaikan import + export default function Home() { + // Helper function untuk format IDR + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const [homeData, setHomeData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchHomeData = async () => { + try { + const { data } = await axios.get("http://localhost:3000/api/public/homepage-bundle"); + setHomeData(data); + } catch (error) { + console.error("Error fetching homepage data:", error); + } finally { + setLoading(false); + } + }; + + fetchHomeData(); + }, []); + + if (loading) return ( +
+
+ Loading... +
+
+ ); + return ( -
-
-
-
- Ini adalah alert Bootstrap +
+ {/* Hero Section */} +
+
+
+
+

SNS NDT Learning Platform

+

Expand your knowledge in Non-Destructive Testing with our expert instructors and comprehensive courses.

+ Explore Courses + Sign Up +
+
+ NDT Learning +
+
+
+
+ + {/* Statistics Section */} +
+
+
+
+
+
+
+ {homeData.statistics.totalLectures} +
+

Total Courses

+

Expert-led NDT training courses

+
+
+
+
+
+
+
+ {homeData.statistics.totalCategories} +
+

Categories

+

Specialized NDT disciplines

+
+
+
+
+
+
+
+ {homeData.statistics.totalUsers} +
+

Users

+

Professional community members

+
+
+
-
- +
+ + {/* Featured Courses Section */} +
+
+
+
+

Featured Courses

+

Our most popular NDT training options

+
+
+ View All Courses +
+
+ +
+ {homeData.featuredLectures.map((lecture) => ( +
+
+ {lecture.name} +
+
+ {lecture.category?.name || "General"} + {formatToIDR(lecture.price)} +
+
{lecture.name}
+

{lecture.technique}

+
+ View Details +
+
+
+
+ ))} +
-
+ + + {/* Latest Courses */} +
+
+
+
+

Latest Courses

+

Recently added to our platform

+
+
+ +
+ {homeData.latestLectures && homeData.latestLectures.slice(0, 3).map((lecture) => ( +
+
+
+ {lecture.name} +
+ New +
+
+
+
+ {lecture.category?.name || "General"} + {formatToIDR(lecture.price)} +
+
{lecture.name}
+

{lecture.technique}

+
+ View Details +
+
+
+
+ ))} +
+
+
+ + {/* Categories */} +
+
+
+
+

Explore Categories

+

Browse our comprehensive NDT training disciplines

+
+
+ +
+ {homeData.popularCategories.map((category) => ( +
+
+
+

{category.name}

+

{category.description || "Explore our comprehensive courses in this category."}

+ + Browse Courses + +
+
+
+ ))} +
+ +
+ View All Categories +
+
+
+ + {/* Call to Action */} +
+
+

Ready to Start Your NDT Learning Journey?

+

Join our community of professionals and enhance your skills with expert-led training.

+ Sign Up Now + Browse Courses +
+
); } From 393f247f4058677c7a3683e1d8afea9557cd1d64 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 15:24:44 +0700 Subject: [PATCH 10/51] chore: update RegisterPage and LoginPage --- client/src/App.jsx | 17 +--- client/src/pages/Login.jsx | 131 ++++++++++++++++++++++++++ client/src/pages/Register.jsx | 171 ++++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 12 deletions(-) create mode 100644 client/src/pages/Login.jsx create mode 100644 client/src/pages/Register.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index b89517ca..d2961f95 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,5 +1,7 @@ import { BrowserRouter, Routes, Route } from "react-router"; import Home from "./pages/Home"; +import Login from "./pages/Login"; +import Register from "./pages/Register"; function App() { return ( @@ -7,18 +9,9 @@ function App() { } /> - {/* } /> - - }> - } /> - } /> - - - - } /> - } /> - } /> - */} + } /> + } /> + {/* Rute lainnya */} diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx new file mode 100644 index 00000000..74d0c7a2 --- /dev/null +++ b/client/src/pages/Login.jsx @@ -0,0 +1,131 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router"; +import axios from "axios"; + +export default function Login() { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + email: "", + password: "", + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + const response = await axios.post("http://localhost:3000/api/users/login", formData); + + // Simpan token dan data user ke localStorage + localStorage.setItem("access_token", response.data.access_token); + localStorage.setItem("user", JSON.stringify({ + id: response.data.id, + username: response.data.username, + email: response.data.email, + role: response.data.role + })); + + // Redirect berdasarkan role + if (response.data.role === "Admin") { + navigate("/admin/dashboard"); + } else { + navigate("/"); + } + + } catch (error) { + console.error("Login error:", error); + setError(error.response?.data?.message || "Invalid email or password"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+
+

Login

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

+ Don't have an account?{" "} + + Register + +

+
+
+
+ +
+ + Back to Home + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Register.jsx b/client/src/pages/Register.jsx new file mode 100644 index 00000000..8fd969fc --- /dev/null +++ b/client/src/pages/Register.jsx @@ -0,0 +1,171 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router"; +import axios from "axios"; + +export default function Register() { + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + phoneNumber: "", + address: "" + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + await axios.post("http://localhost:3000/api/users/register", formData); + + // Redirect ke halaman login setelah berhasil register + navigate("/login", { + state: { + message: "Registration successful! Please login with your new account." + } + }); + + } catch (error) { + console.error("Registration error:", error); + setError( + error.response?.data?.message || + "Registration failed. Please check your information and try again." + ); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+
+

Create an Account

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
We'll never share your email with anyone else.
+
+ +
+ + +
Password must be at least 6 characters
+
+ +
+ + +
+ +
+ + +
+ + +
+ +
+

+ Already have an account?{" "} + + Log in + +

+
+
+
+ +
+ + Back to Home + +
+
+
+
+ ); +} \ No newline at end of file From a70df8372ce40ceecb960c33353d49c6a41bc6e4 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 15:34:30 +0700 Subject: [PATCH 11/51] chore: update navbar, authentication & authorization for Admin and User --- client/src/App.jsx | 21 +++--- client/src/components/Navbar.jsx | 92 ++++++++++++++++++++++++ client/src/components/ProtectedRoute.jsx | 38 ++++++++++ client/src/context/AuthContext.jsx | 63 ++++++++++++++++ 4 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 client/src/components/Navbar.jsx create mode 100644 client/src/components/ProtectedRoute.jsx create mode 100644 client/src/context/AuthContext.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index d2961f95..da774c03 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,20 +1,25 @@ import { BrowserRouter, Routes, Route } from "react-router"; +import { AuthProvider } from "./context/AuthContext"; +import Navbar from "./components/Navbar"; import Home from "./pages/Home"; import Login from "./pages/Login"; import Register from "./pages/Register"; function App() { return ( - <> + - - } /> - } /> - } /> - {/* Rute lainnya */} - + +
+ + } /> + } /> + } /> + {/* Add more routes as needed */} + +
- +
); } diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx new file mode 100644 index 00000000..fd2481df --- /dev/null +++ b/client/src/components/Navbar.jsx @@ -0,0 +1,92 @@ +import { Link } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +export default function Navbar() { + const { user, isAuthenticated, isAdmin, logout } = useAuth(); + + const handleLogout = () => { + logout(); + // Optional: redirect to home page + window.location.href = "/"; + }; + + return ( + + ); +} \ No newline at end of file diff --git a/client/src/components/ProtectedRoute.jsx b/client/src/components/ProtectedRoute.jsx new file mode 100644 index 00000000..4f2b88ae --- /dev/null +++ b/client/src/components/ProtectedRoute.jsx @@ -0,0 +1,38 @@ +import { Navigate, Outlet } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +export function ProtectedRoute() { + const { isAuthenticated, loading } = useAuth(); + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + return isAuthenticated ? : ; +} + +export function AdminRoute() { + const { isAuthenticated, isAdmin, loading } = useAuth(); + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + return isAuthenticated && isAdmin ? ( + + ) : ( + + ); +} \ No newline at end of file diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx new file mode 100644 index 00000000..d2cbaa6d --- /dev/null +++ b/client/src/context/AuthContext.jsx @@ -0,0 +1,63 @@ +import { createContext, useState, useEffect, useContext } from "react"; +import axios from "axios"; + +const AuthContext = createContext(); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check if user is logged in from localStorage + const token = localStorage.getItem("access_token"); + const userData = localStorage.getItem("user"); + + if (token && userData) { + const parsedUser = JSON.parse(userData); + setUser(parsedUser); + + // Set authorization header for all future requests + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + } + + setLoading(false); + }, []); + + const login = (userData, token) => { + localStorage.setItem("access_token", token); + localStorage.setItem("user", JSON.stringify(userData)); + setUser(userData); + + // Set authorization header + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + }; + + const logout = () => { + localStorage.removeItem("access_token"); + localStorage.removeItem("user"); + setUser(null); + + // Remove authorization header + delete axios.defaults.headers.common["Authorization"]; + }; + + const isAuthenticated = !!user; + const isAdmin = user?.role === "Admin"; + + return ( + + {children} + + ); +} + +export const useAuth = () => useContext(AuthContext); \ No newline at end of file From 45590d13c6bd29367d93fb4ce18c2c74f9fee36b Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 16:46:07 +0700 Subject: [PATCH 12/51] chore: update google login in server & client side --- client/.gitignore | 1 + client/index.html | 11 +- client/src/pages/Login.jsx | 126 ++++++++++++--- server/controllers/userController.js | 65 ++++++-- server/package-lock.json | 234 ++++++++++++++++++++++++++- server/package.json | 1 + server/routes/userRoutes.js | 1 + 7 files changed, 395 insertions(+), 44 deletions(-) diff --git a/client/.gitignore b/client/.gitignore index a547bf36..a120b79f 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -8,6 +8,7 @@ pnpm-debug.log* lerna-debug.log* node_modules +.env dist dist-ssr *.local diff --git a/client/index.html b/client/index.html index 7c148166..c2fbac1d 100644 --- a/client/index.html +++ b/client/index.html @@ -4,21 +4,22 @@ - Vite + React + SNS + + + +
+ - - -
- diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index 74d0c7a2..efc074fa 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { Link, useNavigate } from "react-router"; +import { useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router"; // PERBAIKAN: react-router-dom import axios from "axios"; export default function Login() { @@ -10,7 +10,7 @@ export default function Login() { }); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - + const handleChange = (e) => { const { name, value } = e.target; setFormData({ @@ -18,31 +18,36 @@ export default function Login() { [name]: value, }); }; - + const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setError(""); - + try { - const response = await axios.post("http://localhost:3000/api/users/login", formData); - + const response = await axios.post( + "http://localhost:3000/api/users/login", + formData + ); + // Simpan token dan data user ke localStorage localStorage.setItem("access_token", response.data.access_token); - localStorage.setItem("user", JSON.stringify({ - id: response.data.id, - username: response.data.username, - email: response.data.email, - role: response.data.role - })); - + localStorage.setItem( + "user", + JSON.stringify({ + id: response.data.id, + username: response.data.username, + email: response.data.email, + role: response.data.role, + }) + ); + // Redirect berdasarkan role if (response.data.role === "Admin") { navigate("/admin/dashboard"); } else { navigate("/"); } - } catch (error) { console.error("Login error:", error); setError(error.response?.data?.message || "Invalid email or password"); @@ -50,7 +55,62 @@ export default function Login() { setLoading(false); } }; - + + async function handleCredentialResponse(response) { + try { + console.log("Encoded JWT ID token: " + response.credential); + + const { data } = await axios.post( + "http://localhost:3000/api/users/login/google", + { id_token: response.credential } + ); + + // Simpan token dan data user ke localStorage (PERBAIKAN) + localStorage.setItem("access_token", data.access_token); + localStorage.setItem( + "user", + JSON.stringify({ + id: data.id, + username: data.username, + email: data.email, + role: data.role, + }) + ); + + // Redirect berdasarkan role + if (data.role === "Admin") { + navigate("/admin/dashboard"); + } else { + navigate("/"); + } + } catch (error) { + console.error("Google login error:", error); + setError(error.response?.data?.message || "Google login failed"); + } + } + + useEffect(() => { + // Pastikan window.google tersedia + if (window.google && window.google.accounts) { + window.google.accounts.id.initialize({ + client_id: import.meta.env.VITE_CLIENT_ID, + callback: handleCredentialResponse, + }); + + // Pastikan elemen buttonDiv sudah ada di DOM + const buttonDiv = document.getElementById("buttonDiv"); + if (buttonDiv) { + window.google.accounts.id.renderButton( + buttonDiv, + { theme: "outline", size: "large" } + ); + window.google.accounts.id.prompt(); + } + } else { + console.error("Google API belum tersedia"); + } + }, []); + return (
@@ -58,16 +118,18 @@ export default function Login() {

Login

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

Don't have an account?{" "} @@ -118,7 +192,7 @@ export default function Login() {

- +
Back to Home @@ -128,4 +202,4 @@ export default function Login() {
); -} \ No newline at end of file +} diff --git a/server/controllers/userController.js b/server/controllers/userController.js index 5fd0b9ab..ea6a4571 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -1,27 +1,65 @@ const { User } = require("../models"); const { comparePassword } = require("../helpers/bcrypt"); const { generateToken } = require("../helpers/jwt"); - +const { OAuth2Client } = require("google-auth-library"); +const client = new OAuth2Client(); class UserController { + static async googleLogin(req, res, next) { + try { + const { id_token } = req.body; + const ticket = await client.verifyIdToken({ + idToken: id_token, + audience: process.env.GOOGLE_CLIENT_ID, + }); + const payload = ticket.getPayload(); + + console.log(payload); + + let user = await User.findOne({ where: { email: payload.email } }); + if (!user) { + user = await User.create({ + username: payload.name, + email: payload.email, + password: Math.random().toString(), + role: "User", + phoneNumber: payload.phoneNumber || "0808080808", + address: payload.address || "unknown", + }); + } + const access_token = generateToken({ id: user.id }); + res.status(200).json({ + access_token, + id: user.id, + username: user.username, + email: user.email, + role: user.role, + phoneNumber: user.phoneNumber, + address: user.address, + }); + } catch (err) { + next(err); + } + } + static async register(req, res, next) { try { const { username, email, password, phoneNumber, address } = req.body; - + // Default role for new registrations is "User" const newUser = await User.create({ username, email, password, // Will be hashed by beforeCreate hook in model - role: "User", + role: "User", phoneNumber, - address + address, }); res.status(201).json({ id: newUser.id, username: newUser.username, email: newUser.email, - role: newUser.role + role: newUser.role, }); } catch (err) { next(err); @@ -31,9 +69,12 @@ class UserController { static async login(req, res, next) { try { const { email, password } = req.body; - + if (!email || !password) { - throw { name: "BadRequest", message: "Email and password are required" }; + throw { + name: "BadRequest", + message: "Email and password are required", + }; } const user = await User.findOne({ where: { email } }); @@ -49,7 +90,7 @@ class UserController { const payload = { id: user.id, email: user.email, - role: user.role + role: user.role, }; const access_token = generateToken(payload); @@ -59,7 +100,7 @@ class UserController { id: user.id, username: user.username, email: user.email, - role: user.role + role: user.role, }); } catch (err) { next(err); @@ -70,9 +111,9 @@ class UserController { try { const userId = req.user.id; const user = await User.findByPk(userId, { - attributes: { exclude: ['password'] } + attributes: { exclude: ["password"] }, }); - + res.status(200).json(user); } catch (err) { next(err); @@ -80,4 +121,4 @@ class UserController { } } -module.exports = UserController; \ No newline at end of file +module.exports = UserController; diff --git a/server/package-lock.json b/server/package-lock.json index 2f4ec0a5..7b36853f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "google-auth-library": "^9.15.1", "jsonwebtoken": "^9.0.2", "pg": "^8.15.6", "sequelize": "^6.37.7" @@ -1263,6 +1264,15 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1486,6 +1496,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcryptjs": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", @@ -1495,6 +1525,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2619,6 +2658,12 @@ "type": "^2.7.2" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2826,6 +2871,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2966,6 +3054,53 @@ "node": ">=4" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2985,6 +3120,40 @@ "dev": true, "license": "ISC" }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -3058,6 +3227,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3259,7 +3441,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4259,6 +4440,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4674,6 +4864,26 @@ "dev": true, "license": "ISC" }, + "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", @@ -6120,6 +6330,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/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", @@ -6292,6 +6508,22 @@ "makeerror": "1.0.12" } }, + "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", diff --git a/server/package.json b/server/package.json index d1e2746b..4585fa5f 100644 --- a/server/package.json +++ b/server/package.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^5.1.0", + "google-auth-library": "^9.15.1", "jsonwebtoken": "^9.0.2", "pg": "^8.15.6", "sequelize": "^6.37.7" diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js index 8d71d6e6..8125e93f 100644 --- a/server/routes/userRoutes.js +++ b/server/routes/userRoutes.js @@ -5,6 +5,7 @@ const authentication = require("../middlewares/authentication"); router.post("/register", UserController.register); router.post("/login", UserController.login); +router.post("/login/google", UserController.googleLogin); // Protected routes router.use(authentication); From b487d4a296478af6bced05b46d7dc9388fd985d5 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 17:07:54 +0700 Subject: [PATCH 13/51] fix: update authenication and guard for user --- client/src/App.jsx | 113 ++++++++++++++++++++++++++--- client/src/components/Navbar.jsx | 8 +- client/src/context/AuthContext.jsx | 52 +++++++++++-- client/src/pages/Login.jsx | 56 +++++++------- client/src/pages/Register.jsx | 17 ++++- 5 files changed, 197 insertions(+), 49 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index da774c03..836a29bb 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,23 +1,116 @@ -import { BrowserRouter, Routes, Route } from "react-router"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router"; import { AuthProvider } from "./context/AuthContext"; import Navbar from "./components/Navbar"; import Home from "./pages/Home"; import Login from "./pages/Login"; import Register from "./pages/Register"; +import { useAuth } from "./context/AuthContext"; +import { useEffect } from "react"; + +// Layout dengan Navbar +function MainLayout({ children }) { + return ( + <> + +
{children}
+ + ); +} + +// Layout tanpa Navbar untuk halaman auth +function AuthLayout({ children }) { + return
{children}
; +} + +// Protected Route component untuk mengalihkan pengguna yang sudah login +function GuestRoute({ children }) { + const { isAuthenticated } = useAuth(); + + if (isAuthenticated) { + return ; + } + + return children; +} + +function AppRoutes() { + const { isAuthenticated } = useAuth(); + + // Debug untuk melihat status autentikasi + useEffect(() => { + console.log("Authentication status:", isAuthenticated); + }, [isAuthenticated]); + + return ( + + {/* Halaman dengan Navbar */} + + + + } + /> + + {/* Halaman auth tanpa Navbar dan dengan pengalihan untuk pengguna yang sudah login */} + + + + + + } + /> + + + + + + } + /> + + {/* Rute lain (dengan Navbar) */} + +

Courses Page

+ + } + /> + +

Categories Page

+ + } + /> + + {/* Rute 404 */} + +

Page Not Found

+ + } + /> +
+ ); +} function App() { return ( - -
- - } /> - } /> - } /> - {/* Add more routes as needed */} - -
+
); diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index fd2481df..3de6e6fe 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -1,4 +1,4 @@ -import { Link } from "react-router"; +import { Link } from "react-router"; import { useAuth } from "../context/AuthContext"; export default function Navbar() { @@ -7,13 +7,13 @@ export default function Navbar() { const handleLogout = () => { logout(); // Optional: redirect to home page - window.location.href = "/"; + window.location.href = "/login"; }; return (
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + {/* Advanced Filters */} +
+
+
+ +
+
+ Rp + +
+
+ Rp + +
+
+
+ +
+ + +
+
+
+
+
+ + {/* Course Results */} + {loading ? ( +
+
+ Loading... +
+

Loading courses...

+
+ ) : courses.length === 0 ? ( +
+
+

No courses found

+

Try adjusting your filters or search criteria

+
+
+ ) : ( + <> +

+ Showing {courses.length} of {pagination.totalItems} courses +

+ +
+ {courses.map((course) => ( +
+
+ {course.name} +
+
+ {course.category?.name || "General"} + {formatToIDR(course.price)} +
+
{course.name}
+

{course.technique}

+
+ View Details +
+
+
+
+ ))} +
+ + {/* Pagination */} + {pagination.totalPages > 1 && ( + + )} + + )} + + ); +} \ No newline at end of file From 0348ecaf044d8d8a5a465433a2fdc42dced19a90 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 20:49:14 +0700 Subject: [PATCH 16/51] chore: update dashboard for admin, and add others page for more attractive --- client/package-lock.json | 30 ++ client/package.json | 2 + client/src/App.jsx | 106 ++--- client/src/components/AdminSidebar.jsx | 91 ++++ client/src/components/Footer.jsx | 98 ++++ client/src/main.jsx | 13 +- client/src/pages/Admin/Categories.jsx | 277 +++++++++++ client/src/pages/Admin/Courses.jsx | 635 +++++++++++++++++++++++++ client/src/pages/Admin/Dashboard.jsx | 336 +++++++++++++ client/src/pages/Admin/Users.jsx | 400 ++++++++++++++++ client/src/pages/Cart.jsx | 237 +++++++++ client/src/pages/Categories.jsx | 254 ++++++++++ client/src/pages/CategoryDetail.jsx | 247 ++++++++++ client/src/pages/Checkout.jsx | 326 +++++++++++++ client/src/pages/CourseDetail.jsx | 256 ++++++++++ client/src/pages/NotFound.jsx | 26 + client/src/pages/UserProfile.jsx | 505 ++++++++++++++++++++ client/src/styles/global.css | 34 ++ 18 files changed, 3807 insertions(+), 66 deletions(-) create mode 100644 client/src/components/AdminSidebar.jsx create mode 100644 client/src/components/Footer.jsx create mode 100644 client/src/pages/Admin/Categories.jsx create mode 100644 client/src/pages/Admin/Courses.jsx create mode 100644 client/src/pages/Admin/Dashboard.jsx create mode 100644 client/src/pages/Admin/Users.jsx create mode 100644 client/src/pages/Cart.jsx create mode 100644 client/src/pages/Categories.jsx create mode 100644 client/src/pages/CategoryDetail.jsx create mode 100644 client/src/pages/Checkout.jsx create mode 100644 client/src/pages/CourseDetail.jsx create mode 100644 client/src/pages/NotFound.jsx create mode 100644 client/src/pages/UserProfile.jsx create mode 100644 client/src/styles/global.css diff --git a/client/package-lock.json b/client/package-lock.json index cd47b445..625ace2b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.0", "dependencies": { "axios": "^1.9.0", + "chart.js": "^4.4.9", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router": "^7.5.2" }, @@ -1016,6 +1018,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", @@ -1587,6 +1595,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2727,6 +2747,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/client/package.json b/client/package.json index 1a242b8f..1883fd4b 100644 --- a/client/package.json +++ b/client/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "axios": "^1.9.0", + "chart.js": "^4.4.9", "react": "^19.0.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router": "^7.5.2" }, diff --git a/client/src/App.jsx b/client/src/App.jsx index d21f4b83..9985b630 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,6 +1,5 @@ import { BrowserRouter, Routes, Route, Navigate, Outlet } from "react-router"; import { AuthProvider, useAuth } from "./context/AuthContext"; -import { Suspense, lazy } from "react"; // Layouts import MainLayout from "./layouts/MainLayout"; @@ -10,23 +9,21 @@ import AdminLayout from "./layouts/AdminLayout"; // Regular Pages import Home from "./pages/Home"; import NotFound from "./pages/NotFound"; - -// Lazy-loaded pages -const Courses = lazy(() => import("./pages/Courses")); -const CourseDetail = lazy(() => import("./pages/CourseDetail")); -const Categories = lazy(() => import("./pages/Categories")); -const CategoryDetail = lazy(() => import("./pages/CategoryDetail")); -const Cart = lazy(() => import("./pages/Cart")); -const Checkout = lazy(() => import("./pages/Checkout")); -const UserProfile = lazy(() => import("./pages/UserProfile")); -const Login = lazy(() => import("./pages/Login")); -const Register = lazy(() => import("./pages/Register")); +import Courses from "./pages/Courses"; +import CourseDetail from "./pages/CourseDetail"; +import Categories from "./pages/Categories"; +import CategoryDetail from "./pages/CategoryDetail"; +import Cart from "./pages/Cart"; +import Checkout from "./pages/Checkout"; +import UserProfile from "./pages/UserProfile"; +import Login from "./pages/Login"; +import Register from "./pages/Register"; // Admin Pages -const AdminDashboard = lazy(() => import("./pages/Admin/Dashboard")); -const AdminUsers = lazy(() => import("./pages/Admin/Users")); -const AdminCourses = lazy(() => import("./pages/Admin/Courses")); -const AdminCategories = lazy(() => import("./pages/Admin/Categories")); +import AdminDashboard from "./pages/Admin/Dashboard"; +import AdminUsers from "./pages/Admin/Users"; +import AdminCourses from "./pages/Admin/Courses"; +import AdminCategories from "./pages/Admin/Categories"; // Route Guards const GuestRoute = () => { @@ -44,59 +41,48 @@ const AdminRoute = () => { return isAuthenticated && isAdmin ? : ; }; -// Loading Fallback -const LoadingFallback = () => ( -
-
- Loading... -
-
-); - function AppRoutes() { return ( - }> - - {/* Public Routes with MainLayout */} - }> - } /> - } /> - } /> - } /> - } /> - + + {/* Public Routes with MainLayout */} + }> + } /> + } /> + } /> + } /> + } /> + - {/* Auth Routes (Login/Register) */} - }> - }> - } /> - } /> - + {/* Auth Routes (Login/Register) */} + }> + }> + } /> + } /> + - {/* Protected Routes for logged-in users */} - }> - }> - } /> - } /> - } /> - + {/* Protected Routes for logged-in users */} + }> + }> + } /> + } /> + } /> + - {/* Admin Routes */} - }> - }> - } /> - } /> - } /> - } /> - + {/* Admin Routes */} + }> + }> + } /> + } /> + } /> + } /> + - {/* 404 Not Found */} - } /> - - + {/* 404 Not Found */} + } /> + ); } diff --git a/client/src/components/AdminSidebar.jsx b/client/src/components/AdminSidebar.jsx new file mode 100644 index 00000000..3dc81350 --- /dev/null +++ b/client/src/components/AdminSidebar.jsx @@ -0,0 +1,91 @@ +import { Link, useLocation } from "react-router"; +import { useAuth } from "../context/AuthContext"; + +export default function AdminSidebar() { + const location = useLocation(); + const { user, logout } = useAuth(); + + const isActive = (path) => { + return location.pathname === path ? "active" : ""; + }; + + const handleLogout = () => { + logout(); + window.location.href = "/"; + }; + + return ( +
+
+
+

Admin Panel

+
+
+ +
+
+
+
+ {user?.username?.charAt(0).toUpperCase() || "A"} +
+
+
+

{user?.username || "Admin"}

+ {user?.email || "admin@example.com"} +
+
+
+ +
+
    +
  • + + + Dashboard + +
  • +
  • + + + Users + +
  • +
  • + + + Courses + +
  • +
  • + + + Categories + +
  • +
+
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/Footer.jsx b/client/src/components/Footer.jsx new file mode 100644 index 00000000..16f3b5c2 --- /dev/null +++ b/client/src/components/Footer.jsx @@ -0,0 +1,98 @@ +import { Link } from "react-router"; + +export default function Footer() { + const currentYear = new Date().getFullYear(); + + return ( +
+
+
+
+
SNS NDT Learning
+

+ Leading provider of Non-Destructive Testing training and certification. + Empowering professionals with expertise in industry-standard testing methods. +

+
+ + + + +
+
+ +
+
Quick Links
+
    +
  • Home
  • +
  • Courses
  • +
  • Categories
  • +
  • About Us
  • +
  • Contact
  • +
+
+ +
+
Courses
+
    +
  • Radiography Testing
  • +
  • Ultrasonic Testing
  • +
  • Magnetic Testing
  • +
  • Penetrant Testing
  • +
  • All Courses
  • +
+
+ +
+
Contact Information
+
    +
  • + + Jl. Pemuda No. 123, Jakarta Timur, Indonesia +
  • +
  • + + +62 21 1234 5678 +
  • +
  • + + info@snsndt.com +
  • +
+ +
Subscribe to Newsletter
+
+ + +
+
+
+ +
+ +
+
+

© {currentYear} SNS NDT Learning. All rights reserved.

+
+
+
    +
  • + Privacy Policy +
  • +
  • + Terms of Service +
  • +
  • + FAQ +
  • +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/main.jsx b/client/src/main.jsx index 3d9da8ac..0ccf5ca1 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -1,9 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import App from './App.jsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; +import "./styles/global.css"; -createRoot(document.getElementById('root')).render( +createRoot(document.getElementById("root")).render( - , -) + +); diff --git a/client/src/pages/Admin/Categories.jsx b/client/src/pages/Admin/Categories.jsx new file mode 100644 index 00000000..93749de1 --- /dev/null +++ b/client/src/pages/Admin/Categories.jsx @@ -0,0 +1,277 @@ +import { useState, useEffect } from "react"; +import api from "../../utils/api"; + +export default function Categories() { + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [categoryToDelete, setCategoryToDelete] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(null); + const [formData, setFormData] = useState({ + name: "", + description: "" + }); + + // Fetch categories + const fetchCategories = async () => { + setLoading(true); + try { + const response = await api.get("/admin/categories"); + setCategories(response.data); + } catch (error) { + console.error("Error fetching categories:", error); + setError("Failed to load categories. Please try again."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchCategories(); + }, []); + + const handleEditClick = (category) => { + setSelectedCategory(category); + setFormData({ + name: category.name, + description: category.description || "" + }); + }; + + const handleDeleteClick = (category) => { + setCategoryToDelete(category); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + if (selectedCategory) { + await api.put(`/admin/categories/${selectedCategory.id}`, formData); + } else { + await api.post("/admin/categories", formData); + } + + // Refresh the category list + fetchCategories(); + + // Close modal and reset form + document.getElementById('categoryFormModal').querySelector('[data-bs-dismiss="modal"]').click(); + setSelectedCategory(null); + resetForm(); + } catch (error) { + console.error("Error saving category:", error); + alert(error.response?.data?.message || "Failed to save category"); + } + }; + + const confirmDelete = async () => { + try { + await api.delete(`/admin/categories/${categoryToDelete.id}`); + + // Remove category from list + setCategories(categories.filter(category => category.id !== categoryToDelete.id)); + + // Close modal + document.getElementById('deleteConfirmModal').querySelector('[data-bs-dismiss="modal"]').click(); + setCategoryToDelete(null); + } catch (error) { + console.error("Error deleting category:", error); + alert(error.response?.data?.message || "Failed to delete category. Make sure no courses are using this category."); + } + }; + + const openNewCategoryModal = () => { + setSelectedCategory(null); + resetForm(); + }; + + const resetForm = () => { + setFormData({ + name: "", + description: "" + }); + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + return ( +
+

Category Management

+ +
+
+
+
+ + Categories +
+ +
+
+
+ {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+ Loading... +
+
+ ) : ( +
+ + + + + + + + + + + + + {categories.map(category => ( + + + + + + + + + ))} + +
IDNameDescriptionCourse CountCreated AtActions
{category.id}{category.name}{category.description || No description}{category.lectureCount || 0}{formatDate(category.createdAt)} + + +
+
+ )} +
+
+ + {/* Category Form Modal */} + + + {/* Delete Confirmation Modal */} + +
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Admin/Courses.jsx b/client/src/pages/Admin/Courses.jsx new file mode 100644 index 00000000..0c67eaf9 --- /dev/null +++ b/client/src/pages/Admin/Courses.jsx @@ -0,0 +1,635 @@ +import { useState, useEffect } from "react"; +import api from "../../utils/api"; + +export default function Courses() { + const [courses, setCourses] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const [filter, setFilter] = useState({ + categoryId: "", + status: "" + }); + const [courseToDelete, setCourseToDelete] = useState(null); + const [selectedCourse, setSelectedCourse] = useState(null); + const [formData, setFormData] = useState({ + name: "", + description: "", + price: 0, + technique: "", + image: "", + experience_years: 0, + level: "Beginner", + language: "English", + instructor: "", + CategoryId: "", + status: "Active" + }); + + // Fetch courses with pagination and filters + const fetchCourses = async (page = 1, search = "", filters = {}) => { + setLoading(true); + try { + const params = new URLSearchParams(); + params.append("page", page); + params.append("limit", 10); + + if (search) params.append("search", search); + if (filters.categoryId) params.append("categoryId", filters.categoryId); + if (filters.status) params.append("status", filters.status); + + const response = await api.get(`/admin/lectures?${params.toString()}`); + + setCourses(response.data.lectures); + setTotalPages(response.data.totalPages); + setCurrentPage(response.data.currentPage); + } catch (error) { + console.error("Error fetching courses:", error); + setError("Failed to load courses. Please try again."); + } finally { + setLoading(false); + } + }; + + // Fetch categories for dropdown + const fetchCategories = async () => { + try { + const response = await api.get("/public/categories"); + setCategories(response.data); + } catch (error) { + console.error("Error fetching categories:", error); + } + }; + + useEffect(() => { + fetchCourses(currentPage, searchTerm, filter); + fetchCategories(); + }, [currentPage]); + + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const handleSearch = (e) => { + e.preventDefault(); + setCurrentPage(1); + fetchCourses(1, searchTerm, filter); + }; + + const handleFilterChange = (e) => { + const { name, value } = e.target; + setFilter({ + ...filter, + [name]: value + }); + }; + + const applyFilters = () => { + setCurrentPage(1); + fetchCourses(1, searchTerm, filter); + }; + + const resetFilters = () => { + setFilter({ + categoryId: "", + status: "" + }); + setCurrentPage(1); + fetchCourses(1, searchTerm, { categoryId: "", status: "" }); + }; + + const handleEditClick = (course) => { + setSelectedCourse(course); + setFormData({ + name: course.name, + description: course.description || "", + price: course.price, + technique: course.technique || "", + image: course.image || "", + experience_years: course.experience_years || 0, + level: course.level || "Beginner", + language: course.language || "English", + instructor: course.instructor || "", + CategoryId: course.CategoryId || "", + status: course.status || "Active" + }); + }; + + const handleDeleteClick = (course) => { + setCourseToDelete(course); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: name === "price" || name === "experience_years" ? Number(value) : value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + if (selectedCourse) { + await api.put(`/admin/lectures/${selectedCourse.id}`, formData); + } else { + await api.post("/admin/lectures", formData); + } + + // Refresh the course list + fetchCourses(currentPage, searchTerm, filter); + + // Close modal and reset form + document.getElementById('courseFormModal').querySelector('[data-bs-dismiss="modal"]').click(); + setSelectedCourse(null); + resetForm(); + } catch (error) { + console.error("Error saving course:", error); + alert(error.response?.data?.message || "Failed to save course"); + } + }; + + const confirmDelete = async () => { + try { + await api.delete(`/admin/lectures/${courseToDelete.id}`); + + // Remove course from list + setCourses(courses.filter(course => course.id !== courseToDelete.id)); + + // Close modal + document.getElementById('deleteConfirmModal').querySelector('[data-bs-dismiss="modal"]').click(); + setCourseToDelete(null); + } catch (error) { + console.error("Error deleting course:", error); + alert(error.response?.data?.message || "Failed to delete course"); + } + }; + + const openNewCourseModal = () => { + setSelectedCourse(null); + resetForm(); + }; + + const resetForm = () => { + setFormData({ + name: "", + description: "", + price: 0, + technique: "", + image: "", + experience_years: 0, + level: "Beginner", + language: "English", + instructor: "", + CategoryId: "", + status: "Active" + }); + }; + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + return ( +
+

Course Management

+ +
+
+
+
+ + Courses +
+ +
+
+
+ {/* Search and Filter Row */} +
+
+
+ setSearchTerm(e.target.value)} + /> + +
+
+
+
+ +
+
+
+ + {/* Collapsible Filter Section */} +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+ Loading... +
+
+ ) : ( + <> +
+ + + + + + + + + + + + + + + + {courses.map(course => ( + + + + + + + + + + + + ))} + +
IDImageCourse NameCategoryTechniquePriceStatusCreated AtActions
{course.id} + {course.name} + {course.name}{course.Category?.name || "Uncategorized"}{course.technique || "N/A"}{formatToIDR(course.price)} + + {course.status} + + {formatDate(course.createdAt)} + + +
+
+ + {/* Pagination */} + {totalPages > 1 && ( + + )} + + )} +
+
+ + {/* Course Form Modal */} + + + {/* Delete Confirmation Modal */} + +
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Admin/Dashboard.jsx b/client/src/pages/Admin/Dashboard.jsx new file mode 100644 index 00000000..5c62c208 --- /dev/null +++ b/client/src/pages/Admin/Dashboard.jsx @@ -0,0 +1,336 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router"; +import api from "../../utils/api"; +import { Chart as ChartJS, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; +import { Pie, Bar } from 'react-chartjs-2'; + +// Register ChartJS components +ChartJS.register(ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + +export default function Dashboard() { + const [stats, setStats] = useState({ + totalUsers: 0, + totalCourses: 0, + totalCategories: 0, + totalOrders: 0, + revenue: 0, + }); + const [recentUsers, setRecentUsers] = useState([]); + const [recentOrders, setRecentOrders] = useState([]); + const [coursesPerCategory, setCoursesPerCategory] = useState([]); + const [monthlySales, setMonthlySales] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchDashboardData = async () => { + try { + // In a real app, this might be a single endpoint that returns all dashboard data + const [statsResponse, usersResponse, ordersResponse, categoriesResponse, salesResponse] = await Promise.all([ + api.get("/admin/statistics"), + api.get("/admin/users?limit=5"), + api.get("/admin/orders?limit=5"), + api.get("/admin/categories/stats"), + api.get("/admin/orders/monthly") + ]); + + setStats(statsResponse.data); + setRecentUsers(usersResponse.data); + setRecentOrders(ordersResponse.data); + setCoursesPerCategory(categoriesResponse.data); + setMonthlySales(salesResponse.data); + } catch (error) { + console.error("Error fetching dashboard data:", error); + } finally { + setLoading(false); + } + }; + + fetchDashboardData(); + }, []); + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + // Prepare data for pie chart + const categoryChartData = { + labels: coursesPerCategory.map(item => item.name), + datasets: [ + { + data: coursesPerCategory.map(item => item.courseCount), + backgroundColor: [ + '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b', + '#5a5c69', '#858796', '#6f42c1', '#20c9a6', '#f8f9fc' + ], + borderWidth: 1, + }, + ], + }; + + // Prepare data for bar chart + const salesChartData = { + labels: monthlySales.map(item => item.month), + datasets: [ + { + label: 'Revenue', + data: monthlySales.map(item => item.revenue), + backgroundColor: '#4e73df', + }, + { + label: 'Orders', + data: monthlySales.map(item => item.orderCount), + backgroundColor: '#1cc88a', + }, + ], + }; + + const salesChartOptions = { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Monthly Sales Performance', + }, + }, + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + return ( +
+

Dashboard

+ + {/* Stats Cards */} +
+
+
+
+
+
+
Total Users
+
{stats.totalUsers}
+
+
+ +
+
+
+
+ + View Details + +
+
+
+
+ +
+
+
+
+
+
Total Courses
+
{stats.totalCourses}
+
+
+ +
+
+
+
+ + View Details + +
+
+
+
+ +
+
+
+
+
+
Total Orders
+
{stats.totalOrders}
+
+
+ +
+
+
+
+ + View Details + +
+
+
+
+ +
+
+
+
+
+
Total Revenue
+
{formatToIDR(stats.revenue)}
+
+
+ +
+
+
+
+ + View Reports + +
+
+
+
+
+ + {/* Charts Row */} +
+
+
+
+ + Monthly Sales +
+
+ +
+
+
+
+
+
+ + Courses by Category +
+
+ +
+
+
+
+ + {/* Tables Row */} +
+
+
+
+ + Recent Users +
+
+
+ + + + + + + + + + + {recentUsers.map(user => ( + + + + + + + ))} + +
NameEmailJoinedRole
{user.username}{user.email}{formatDate(user.createdAt)} + + {user.role} + +
+
+
+ + View All Users + +
+
+
+
+
+
+
+ + Recent Orders +
+
+
+ + + + + + + + + + + + {recentOrders.map(order => ( + + + + + + + + ))} + +
Order IDUserDateAmountStatus
#{order.id}{order.User.username}{formatDate(order.createdAt)}{formatToIDR(order.totalAmount)} + + {order.status} + +
+
+
+ + View All Orders + +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Admin/Users.jsx b/client/src/pages/Admin/Users.jsx new file mode 100644 index 00000000..ed51c5f3 --- /dev/null +++ b/client/src/pages/Admin/Users.jsx @@ -0,0 +1,400 @@ +import { useState, useEffect } from "react"; +import api from "../../utils/api"; + +export default function Users() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const [userToDelete, setUserToDelete] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); + const [formData, setFormData] = useState({ + username: "", + email: "", + role: "User", + phoneNumber: "", + address: "" + }); + + // Fetch users with pagination and optional search + const fetchUsers = async (page = 1, search = "") => { + setLoading(true); + try { + const params = new URLSearchParams(); + params.append("page", page); + params.append("limit", 10); + if (search) params.append("search", search); + + const response = await api.get(`/admin/users?${params.toString()}`); + + setUsers(response.data.users); + setTotalPages(response.data.totalPages); + setCurrentPage(response.data.currentPage); + } catch (error) { + console.error("Error fetching users:", error); + setError("Failed to load users. Please try again."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchUsers(currentPage, searchTerm); + }, [currentPage]); + + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const handleSearch = (e) => { + e.preventDefault(); + setCurrentPage(1); + fetchUsers(1, searchTerm); + }; + + const handleEditClick = (user) => { + setSelectedUser(user); + setFormData({ + username: user.username, + email: user.email, + role: user.role, + phoneNumber: user.phoneNumber || "", + address: user.address || "" + }); + }; + + const handleDeleteClick = (user) => { + setUserToDelete(user); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + try { + if (selectedUser) { + await api.put(`/admin/users/${selectedUser.id}`, formData); + } else { + await api.post("/admin/users", formData); + } + + // Refresh the user list + fetchUsers(currentPage, searchTerm); + + // Close modal and reset form + document.getElementById('userFormModal').querySelector('[data-bs-dismiss="modal"]').click(); + setSelectedUser(null); + setFormData({ + username: "", + email: "", + role: "User", + phoneNumber: "", + address: "" + }); + } catch (error) { + console.error("Error saving user:", error); + alert(error.response?.data?.message || "Failed to save user"); + } + }; + + const confirmDelete = async () => { + try { + await api.delete(`/admin/users/${userToDelete.id}`); + + // Remove user from list + setUsers(users.filter(user => user.id !== userToDelete.id)); + + // Close modal + document.getElementById('deleteConfirmModal').querySelector('[data-bs-dismiss="modal"]').click(); + setUserToDelete(null); + } catch (error) { + console.error("Error deleting user:", error); + alert(error.response?.data?.message || "Failed to delete user"); + } + }; + + const openNewUserModal = () => { + setSelectedUser(null); + setFormData({ + username: "", + email: "", + role: "User", + phoneNumber: "", + address: "" + }); + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + return ( +
+

User Management

+ +
+
+
+
+ + Users +
+ +
+
+
+ {/* Search Form */} +
+
+
+ setSearchTerm(e.target.value)} + /> + +
+
+
+ + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+ Loading... +
+
+ ) : ( + <> +
+ + + + + + + + + + + + + {users.map(user => ( + + + + + + + + + ))} + +
IDUsernameEmailRoleJoined DateActions
{user.id}{user.username}{user.email} + + {user.role} + + {formatDate(user.createdAt)} + + +
+
+ + {/* Pagination */} + {totalPages > 1 && ( + + )} + + )} +
+
+ + {/* User Form Modal */} + + + {/* Delete Confirmation Modal */} + +
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Cart.jsx b/client/src/pages/Cart.jsx new file mode 100644 index 00000000..0cba4bf1 --- /dev/null +++ b/client/src/pages/Cart.jsx @@ -0,0 +1,237 @@ +import { useState, useEffect } from "react"; +import { Link, useNavigate } from "react-router"; +import api from "../utils/api"; + +export default function Cart() { + const navigate = useNavigate(); + const [cartItems, setCartItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [processing, setProcessing] = useState(false); + + useEffect(() => { + const fetchCartItems = async () => { + try { + const { data } = await api.get("/carts"); + setCartItems(data); + } catch (error) { + console.error("Error fetching cart:", error); + setError("Failed to load your cart. Please try again later."); + } finally { + setLoading(false); + } + }; + + fetchCartItems(); + }, []); + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const handleRemoveItem = async (id) => { + try { + await api.delete(`/carts/${id}`); + setCartItems(cartItems.filter(item => item.id !== id)); + } catch (error) { + console.error("Error removing item:", error); + alert("Failed to remove item from cart"); + } + }; + + const handleCheckout = async () => { + setProcessing(true); + try { + // Placeholder for checkout logic + await new Promise(resolve => setTimeout(resolve, 1000)); + alert("Checkout successful! Redirecting to payment page."); + navigate("/checkout"); + } catch (error) { + console.error("Error checking out:", error); + alert("Failed to process checkout"); + } finally { + setProcessing(false); + } + }; + + const calculateTotal = () => { + return cartItems.reduce((sum, item) => sum + item.Lecture.price, 0); + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + if (error) { + return ( +
+
+ {error} +
+ + Back to Home + +
+ ); + } + + if (cartItems.length === 0) { + return ( +
+
+ +

Your cart is empty

+

Looks like you haven't added any courses yet.

+ + Explore Courses + +
+
+ ); + } + + return ( +
+

Shopping Cart

+ +
+
+
+
+
{cartItems.length} Courses in Cart
+ + {cartItems.map((item) => ( +
+ {item.Lecture.name} + +
+
+
{item.Lecture.name}
+
{formatToIDR(item.Lecture.price)}
+
+ +

{item.Lecture.technique}

+ +
+ + {item.Lecture.category?.name || "General"} + + + + {item.Lecture.instructor || "Expert Instructor"} + +
+ +
+ + + + View Details + +
+
+
+ ))} +
+
+
+ +
+
+
+
Order Summary
+ +
+ Subtotal: + {formatToIDR(calculateTotal())} +
+ +
+ Discount: + {formatToIDR(0)} +
+ +
+ +
+ Total: + {formatToIDR(calculateTotal())} +
+ + + +
+ +
+ +
+ + +
+
+
+
+ +
+
+
Need Help?
+

+ If you have questions about your order, please contact our support team. +

+ +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Categories.jsx b/client/src/pages/Categories.jsx new file mode 100644 index 00000000..af495123 --- /dev/null +++ b/client/src/pages/Categories.jsx @@ -0,0 +1,254 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router"; +import api from "../utils/api"; + +export default function Categories() { + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + + useEffect(() => { + const fetchCategories = async () => { + try { + const { data } = await api.get("/public/categories"); + setCategories(data); + } catch (error) { + console.error("Error fetching categories:", error); + setError("Failed to load categories. Please try again later."); + } finally { + setLoading(false); + } + }; + + fetchCategories(); + }, []); + + // Filter categories based on search term + const filteredCategories = categories.filter(category => + category.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + if (error) { + return ( +
+
+ {error} +
+ + Back to Home + +
+ ); + } + + return ( +
+
+
+

Course Categories

+

+ Browse our comprehensive NDT training disciplines +

+
+
+
+ setSearchTerm(e.target.value)} + /> + +
+
+
+ + {filteredCategories.length === 0 ? ( +
+
+

No categories found

+

Try adjusting your search criteria

+
+
+ ) : ( +
+ {filteredCategories.map((category) => ( +
+
+
+
+
+ +
+

{category.name}

+
+ +

+ {category.description || + `Explore comprehensive courses in ${category.name} and enhance your NDT skills with expert-led training.`} +

+ +
+ + {category.lectureCount || 0} Courses + + + Browse Courses + +
+
+
+
+ ))} +
+ )} + + {/* Featured NDT Methods Section */} +
+
+

Featured NDT Methods

+
+
+
+
+
+ Radiography Testing +
+

Radiography Testing

+

+ Utilizes X-rays or gamma rays to detect internal defects in materials + without damaging the specimen. +

+
+
+
+ +
+
+
+
+ Ultrasonic Testing +
+

Ultrasonic Testing

+

+ Uses high-frequency sound waves to detect surface and subsurface + defects in materials. +

+
+
+
+ +
+
+
+
+ Magnetic Testing +
+

Magnetic Testing

+

+ Detects surface and near-surface discontinuities in ferromagnetic materials. +

+
+
+
+
+
+
+ + {/* Why Choose NDT Training Section */} +
+
+
+
+

Why Choose NDT Training?

+

+ Non-Destructive Testing professionals are in high demand across multiple industries. +

+
    +
  • + +
    +
    Competitive Salary
    +

    + NDT technicians and inspectors enjoy above-average compensation packages. +

    +
    +
  • +
  • + +
    +
    Global Opportunities
    +

    + NDT certifications are recognized worldwide, opening doors to international careers. +

    +
    +
  • +
  • + +
    +
    Industry Growth
    +

    + The NDT market is projected to grow steadily over the next decade. +

    +
    +
  • +
+
+
+ NDT Certification +
+
+
+
+ + {/* CTA Section */} +
+
+

Ready to Start Your NDT Learning Journey?

+

Join our community of professionals and enhance your skills with expert-led training.

+ Browse Courses + Sign Up Now +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/CategoryDetail.jsx b/client/src/pages/CategoryDetail.jsx new file mode 100644 index 00000000..b43ee736 --- /dev/null +++ b/client/src/pages/CategoryDetail.jsx @@ -0,0 +1,247 @@ +import { useState, useEffect } from "react"; +import { Link, useParams } from "react-router"; +import api from "../utils/api"; + +export default function CategoryDetail() { + const { id } = useParams(); + const [category, setCategory] = useState(null); + const [courses, setCourses] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [sort, setSort] = useState("newest"); + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + useEffect(() => { + const fetchCategoryDetails = async () => { + try { + // Get category details + const categoryResponse = await api.get(`/public/categories/${id}`); + setCategory(categoryResponse.data); + + // Get courses in this category + const coursesResponse = await api.get(`/public/lectures?categoryId=${id}`); + setCourses(coursesResponse.data.lectures || []); + } catch (error) { + console.error("Error fetching category details:", error); + setError("Failed to load category details. Please try again later."); + } finally { + setLoading(false); + } + }; + + fetchCategoryDetails(); + }, [id]); + + const handleSortChange = (e) => { + setSort(e.target.value); + + // Sort courses based on selected option + let sortedCourses = [...courses]; + + if (e.target.value === "price_asc") { + sortedCourses.sort((a, b) => a.price - b.price); + } else if (e.target.value === "price_desc") { + sortedCourses.sort((a, b) => b.price - a.price); + } else if (e.target.value === "newest") { + sortedCourses.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + } + + setCourses(sortedCourses); + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + if (error) { + return ( +
+
+ {error} +
+ + Back to Categories + +
+ ); + } + + if (!category) { + return ( +
+
+ Category not found +
+ + Back to Categories + +
+ ); + } + + return ( +
+ + +
+
+

{category.name}

+

+ {category.description || + `Explore our comprehensive collection of courses in ${category.name}. + Gain expertise and advance your career with industry-recognized certifications.`} +

+
+
+
+ + +
+
+
+ + {courses.length === 0 ? ( +
+ No courses available in this category yet. Check back soon! +
+ ) : ( +
+ {courses.map((course) => ( +
+
+ {course.name} +
+
+ {category.name} + {formatToIDR(course.price)} +
+
{course.name}
+

{course.technique}

+
+ View Details +
+
+
+
+ ))} +
+ )} + + {/* Related Categories Section */} +
+

Explore Other Categories

+
+
+
+
+
Ultrasonic Testing
+

Detect flaws using high-frequency sound waves

+ View Category +
+
+
+
+
+
+
Magnetic Testing
+

Locate surface defects in ferromagnetic materials

+ View Category +
+
+
+
+
+
+
Penetrant Testing
+

Identify surface-breaking defects in materials

+ View Category +
+
+
+
+
+ + {/* Career Paths Section */} +
+
+

Career Paths in {category.name}

+
+
+
+
+
Entry Level Technician
+

Start your career with Level I certification and gain practical experience in testing procedures.

+
    +
  • Basic testing operations
  • +
  • Equipment calibration
  • +
  • Data collection
  • +
+
+
+
+
+
+
+
Advanced Inspector
+

With Level II certification, perform advanced inspections and interpret results.

+
    +
  • Advanced testing techniques
  • +
  • Result interpretation
  • +
  • Procedure selection
  • +
+
+
+
+
+
+
+
NDT Specialist
+

As a Level III specialist, oversee testing operations and develop procedures.

+
    +
  • Method development
  • +
  • Training supervision
  • +
  • Quality assurance
  • +
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Checkout.jsx b/client/src/pages/Checkout.jsx new file mode 100644 index 00000000..063dd39a --- /dev/null +++ b/client/src/pages/Checkout.jsx @@ -0,0 +1,326 @@ +import { useState, useEffect } from "react"; +import { Link, useNavigate } from "react-router"; +import api from "../utils/api"; +import { useAuth } from "../context/AuthContext"; + +export default function Checkout() { + const navigate = useNavigate(); + const { user } = useAuth(); + const [cartItems, setCartItems] = useState([]); + const [loading, setLoading] = useState(true); + const [processing, setProcessing] = useState(false); + const [error, setError] = useState(null); + const [formData, setFormData] = useState({ + fullName: user?.username || "", + email: user?.email || "", + phone: user?.phoneNumber || "", + address: user?.address || "", + paymentMethod: "bank_transfer" + }); + + useEffect(() => { + const fetchCartItems = async () => { + try { + const { data } = await api.get("/carts"); + setCartItems(data); + } catch (error) { + console.error("Error fetching cart:", error); + setError("Failed to load your cart items. Please try again."); + } finally { + setLoading(false); + } + }; + + fetchCartItems(); + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prevData => ({ + ...prevData, + [name]: value + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setProcessing(true); + + try { + // Create order from cart + const response = await api.post("/orders", { + ...formData, + items: cartItems.map(item => ({ + lectureId: item.LectureId, + price: item.Lecture.price + })) + }); + + // Redirect to success page with order ID + navigate(`/checkout/success?orderId=${response.data.id}`); + + } catch (error) { + console.error("Checkout error:", error); + setError(error.response?.data?.message || "Failed to process your order. Please try again."); + } finally { + setProcessing(false); + } + }; + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const calculateTotal = () => { + return cartItems.reduce((sum, item) => sum + item.Lecture.price, 0); + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + if (cartItems.length === 0) { + return ( +
+
+ +

Your cart is empty

+

You need to add courses to your cart before checkout.

+ Explore Courses +
+
+ ); + } + + return ( +
+

Checkout

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

Billing Details

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +

Payment Method

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {formData.paymentMethod === "credit_card" && ( +
+
+ + + Full name as displayed on card +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ )} + +
+ + +
+
+ +
+
+
+

Order Summary

+
+
+
    + {cartItems.map((item) => ( +
  • +
    +
    {item.Lecture.name}
    + {item.Lecture.technique} +
    + {formatToIDR(item.Lecture.price)} +
  • + ))} + +
  • + Total + {formatToIDR(calculateTotal())} +
  • +
+ +
+ + Return to Cart + +
+
+
+ +
+
+
Why Choose Our Courses
+
    +
  • + + Industry-recognized certifications +
  • +
  • + + Expert instructors with real-world experience +
  • +
  • + + Lifetime access to course materials +
  • +
  • + + 30-day money-back guarantee +
  • +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/CourseDetail.jsx b/client/src/pages/CourseDetail.jsx new file mode 100644 index 00000000..7e005b43 --- /dev/null +++ b/client/src/pages/CourseDetail.jsx @@ -0,0 +1,256 @@ +import { useState, useEffect } from "react"; +import { useParams, Link, useNavigate } from "react-router"; +import api from "../utils/api"; +import { useAuth } from "../context/AuthContext"; + +export default function CourseDetail() { + const { id } = useParams(); + const navigate = useNavigate(); + const { isAuthenticated} = useAuth(); + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [addingToCart, setAddingToCart] = useState(false); + + useEffect(() => { + const fetchCourseDetails = async () => { + try { + const { data } = await api.get(`/public/lectures/${id}`); + setCourse(data); + } catch (error) { + console.error("Error fetching course details:", error); + setError("Failed to load course details. Please try again later."); + } finally { + setLoading(false); + } + }; + + fetchCourseDetails(); + }, [id]); + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const handleAddToCart = async () => { + if (!isAuthenticated) { + navigate("/login"); + return; + } + + setAddingToCart(true); + try { + await api.post("/carts", { LectureId: id }); + // Show success message + alert("Course added to cart successfully!"); + } catch (error) { + console.error("Error adding to cart:", error); + alert(error.response?.data?.message || "Failed to add course to cart"); + } finally { + setAddingToCart(false); + } + }; + + if (loading) { + return ( +
+
+ Loading... +
+
+ ); + } + + if (error) { + return ( +
+
+ {error} +
+ + Back to Courses + +
+ ); + } + + if (!course) { + return ( +
+
+ Course not found +
+ + Back to Courses + +
+ ); + } + + return ( +
+
+
+ +
+
+ +
+
+ {course.name} + +

{course.name}

+ +
+ {course.category?.name} + + {course.instructor || "Expert Instructor"} + + + {course.duration || "Self-paced"} + +
+ +
+

Description

+

{course.description || "No description available for this course."}

+
+ +
+

What You'll Learn

+
+
+
    +
  • + + Understanding of {course.technique} principles +
  • +
  • + + Practical application skills +
  • +
  • + + Industry standard procedures +
  • +
+
+
+
    +
  • + + Safety protocols and best practices +
  • +
  • + + Equipment handling and maintenance +
  • +
  • + + Certification preparation +
  • +
+
+
+
+ +
+

Course Details

+
+
+

Technique: {course.technique}

+

Experience Required: {course.experience_years} years

+
+
+

Level: {course.level || "All Levels"}

+

Language: {course.language || "English"}

+
+
+
+
+ +
+
+
+

{formatToIDR(course.price)}

+ + + +

or

+ + + +
+ This course includes: +
+
    +
  • + On-demand video +
  • +
  • + Downloadable resources +
  • +
  • + Certificate of completion +
  • +
  • + Full lifetime access +
  • +
  • + Access on mobile and TV +
  • +
+ +
+ +
+

Have questions?

+ Contact instructor +
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/NotFound.jsx b/client/src/pages/NotFound.jsx new file mode 100644 index 00000000..031affc6 --- /dev/null +++ b/client/src/pages/NotFound.jsx @@ -0,0 +1,26 @@ +import { Link } from "react-router"; + +export default function NotFound() { + return ( +
+
+
+
+

404

+

Page Not Found

+

+ Oops! The page you are looking for might have been removed, had its name changed, + or is temporarily unavailable. +

+ + Back to Home + + + Browse Courses + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/UserProfile.jsx b/client/src/pages/UserProfile.jsx new file mode 100644 index 00000000..032442d9 --- /dev/null +++ b/client/src/pages/UserProfile.jsx @@ -0,0 +1,505 @@ +import { useState, useEffect } from "react"; +import { useAuth } from "../context/AuthContext"; +import api from "../utils/api"; + +export default function UserProfile() { + const { user, logout } = useAuth(); + const [activeTab, setActiveTab] = useState("profile"); + const [profileData, setProfileData] = useState({ + username: user?.username || "", + email: user?.email || "", + phoneNumber: user?.phoneNumber || "", + address: user?.address || "" + }); + const [passwordData, setPasswordData] = useState({ + currentPassword: "", + newPassword: "", + confirmPassword: "" + }); + const [orders, setOrders] = useState([]); + const [enrolledCourses, setEnrolledCourses] = useState([]); + const [loading, setLoading] = useState({ + profile: false, + password: false, + orders: true, + courses: true + }); + const [error, setError] = useState({ + profile: "", + password: "", + orders: "", + courses: "" + }); + const [success, setSuccess] = useState({ + profile: "", + password: "" + }); + + useEffect(() => { + // Fetch user's orders + const fetchOrders = async () => { + try { + const { data } = await api.get("/orders"); + setOrders(data); + } catch (err) { + setError({...err, orders: "Failed to fetch order history"}); + } finally { + setLoading({...loading, orders: false}); + } + }; + + // Fetch user's enrolled courses + const fetchEnrolledCourses = async () => { + try { + const { data } = await api.get("/enrollments"); + setEnrolledCourses(data); + } catch (err) { + setError({...err, courses: "Failed to fetch enrolled courses"}); + } finally { + setLoading({...loading, courses: false}); + } + }; + + fetchOrders(); + fetchEnrolledCourses(); + }, []); + + const handleProfileChange = (e) => { + const { name, value } = e.target; + setProfileData({...profileData, [name]: value}); + }; + + const handlePasswordChange = (e) => { + const { name, value } = e.target; + setPasswordData({...passwordData, [name]: value}); + }; + + const handleProfileSubmit = async (e) => { + e.preventDefault(); + setLoading({...loading, profile: true}); + setError({...error, profile: ""}); + setSuccess({...success, profile: ""}); + + try { + await api.put("/users/profile", profileData); + setSuccess({...success, profile: "Profile updated successfully!"}); + + // Update local storage user data + const userData = JSON.parse(localStorage.getItem("user")) || {}; + localStorage.setItem("user", JSON.stringify({ + ...userData, + username: profileData.username, + phoneNumber: profileData.phoneNumber, + address: profileData.address + })); + + } catch (err) { + setError({...error, profile: err.response?.data?.message || "Failed to update profile"}); + } finally { + setLoading({...loading, profile: false}); + } + }; + + const handlePasswordSubmit = async (e) => { + e.preventDefault(); + + // Password validation + if (passwordData.newPassword !== passwordData.confirmPassword) { + setError({...error, password: "Passwords don't match"}); + return; + } + + setLoading({...loading, password: true}); + setError({...error, password: ""}); + setSuccess({...success, password: ""}); + + try { + await api.put("/users/password", { + currentPassword: passwordData.currentPassword, + newPassword: passwordData.newPassword + }); + + setSuccess({...success, password: "Password updated successfully!"}); + setPasswordData({ + currentPassword: "", + newPassword: "", + confirmPassword: "" + }); + + } catch (err) { + setError({...error, password: err.response?.data?.message || "Failed to update password"}); + } finally { + setLoading({...loading, password: false}); + } + }; + + const formatDate = (dateString) => { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric" + }); + }; + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + return ( +
+

My Account

+ +
+
+
+
+
+
+ {user?.username?.charAt(0).toUpperCase() || "U"} +
+
+
{user?.username}
+ {user?.email} +
+
+ +
+ + + + + +
+
+
+
+ +
+ {/* Profile Tab */} + {activeTab === "profile" && ( +
+
+
Profile Information
+
+
+ {success.profile && ( +
+ {success.profile} +
+ )} + + {error.profile && ( +
+ {error.profile} +
+ )} + +
+
+ + +
+ +
+ + +
Email cannot be changed.
+
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ )} + + {/* Password Tab */} + {activeTab === "password" && ( +
+
+
Change Password
+
+
+ {success.password && ( +
+ {success.password} +
+ )} + + {error.password && ( +
+ {error.password} +
+ )} + +
+
+ + +
+ +
+ + +
Password must be at least 6 characters long.
+
+ +
+ + +
+ + +
+
+
+ )} + + {/* Orders Tab */} + {activeTab === "orders" && ( +
+
+
Order History
+
+
+ {loading.orders ? ( +
+
+ Loading... +
+

Loading orders...

+
+ ) : error.orders ? ( +
+ {error.orders} +
+ ) : orders.length === 0 ? ( +
+ +
No Orders Yet
+

You haven't placed any orders yet.

+
+ ) : ( +
+ + + + + + + + + + + + {orders.map(order => ( + + + + + + + + ))} + +
Order IDDateItemsTotalStatus
#{order.id}{formatDate(order.createdAt)}{order.items?.length || 0}{formatToIDR(order.totalAmount)} + + {order.status} + +
+
+ )} +
+
+ )} + + {/* Courses Tab */} + {activeTab === "courses" && ( +
+
+
My Courses
+
+
+ {loading.courses ? ( +
+
+ Loading... +
+

Loading courses...

+
+ ) : error.courses ? ( +
+ {error.courses} +
+ ) : enrolledCourses.length === 0 ? ( +
+ +
No Enrolled Courses
+

You haven't enrolled in any courses yet.

+
+ ) : ( +
+ {enrolledCourses.map(enrollment => ( +
+
+
+
+ Enrolled + Enrolled on {formatDate(enrollment.createdAt)} +
+
{enrollment.Lecture.name}
+

+ {enrollment.Lecture.technique} +

+

+ + Progress: {enrollment.progress || 0}% complete + +

+
+
+

+ +
+
+
+ ))} +
+ )} +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/styles/global.css b/client/src/styles/global.css new file mode 100644 index 00000000..5b6051bd --- /dev/null +++ b/client/src/styles/global.css @@ -0,0 +1,34 @@ +/* Hover effects for cards */ +.hover-shadow { + transition: all 0.3s ease; +} + +.hover-shadow:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important; +} + +/* Auth layout styling */ +.auth-layout { + background-color: #f8f9fa; +} + +/* Admin layout */ +.admin-main { + background-color: #f8f9fa; +} + +/* Active nav items */ +.nav-link.active { + font-weight: 600; +} + +/* Breadcrumb styling */ +.breadcrumb a { + color: #0d6efd; + text-decoration: none; +} + +.breadcrumb a:hover { + text-decoration: underline; +} \ No newline at end of file From ddcd1f42b5ba02a21cc77481b73335f8ed70facc Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Tue, 29 Apr 2025 22:39:43 +0700 Subject: [PATCH 17/51] fix: revision all cause add new migration for column UserId --- client/src/pages/Cart.jsx | 17 +- client/src/pages/CourseDetail.jsx | 4 +- client/src/pages/Courses.jsx | 27 +- client/src/pages/Home.jsx | 24 +- client/src/utils/api.js | 1 - server/app.js | 9 + server/controllers/cartController.js | 20 +- server/controllers/publicController.js | 205 +++++++++++++++ server/data/lectures.json | 33 ++- server/middlewares/authentication.js | 40 +-- server/middlewares/authorization.js | 8 +- server/middlewares/errorHandler.js | 25 +- .../20250429151455-addColumn-to-Lectures.js | 19 ++ server/models/category.js | 4 +- server/models/lecture.js | 11 +- server/routes/adminRoutes.js | 24 ++ server/routes/cartRoutes.js | 2 +- server/routes/index.js | 2 + server/routes/publicRoutes.js | 246 +----------------- 19 files changed, 406 insertions(+), 315 deletions(-) create mode 100644 server/controllers/publicController.js create mode 100644 server/migrations/20250429151455-addColumn-to-Lectures.js create mode 100644 server/routes/adminRoutes.js diff --git a/client/src/pages/Cart.jsx b/client/src/pages/Cart.jsx index 0cba4bf1..92c28606 100644 --- a/client/src/pages/Cart.jsx +++ b/client/src/pages/Cart.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Link, useNavigate } from "react-router"; +import { Link, useNavigate } from "react-router"; import api from "../utils/api"; export default function Cart() { @@ -43,19 +43,8 @@ export default function Cart() { } }; - const handleCheckout = async () => { - setProcessing(true); - try { - // Placeholder for checkout logic - await new Promise(resolve => setTimeout(resolve, 1000)); - alert("Checkout successful! Redirecting to payment page."); - navigate("/checkout"); - } catch (error) { - console.error("Error checking out:", error); - alert("Failed to process checkout"); - } finally { - setProcessing(false); - } + const handleCheckout = () => { + navigate("/checkout"); }; const calculateTotal = () => { diff --git a/client/src/pages/CourseDetail.jsx b/client/src/pages/CourseDetail.jsx index 7e005b43..cc12df19 100644 --- a/client/src/pages/CourseDetail.jsx +++ b/client/src/pages/CourseDetail.jsx @@ -6,7 +6,7 @@ import { useAuth } from "../context/AuthContext"; export default function CourseDetail() { const { id } = useParams(); const navigate = useNavigate(); - const { isAuthenticated} = useAuth(); + const { isAuthenticated } = useAuth(); const [course, setCourse] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -44,7 +44,7 @@ export default function CourseDetail() { setAddingToCart(true); try { - await api.post("/carts", { LectureId: id }); + await api.post("/carts/add", { lectureId: id }); // Perbaikan API endpoint untuk cart // Show success message alert("Course added to cart successfully!"); } catch (error) { diff --git a/client/src/pages/Courses.jsx b/client/src/pages/Courses.jsx index 1f3cfe6a..73fecccd 100644 --- a/client/src/pages/Courses.jsx +++ b/client/src/pages/Courses.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { Link } from "react-router"; +import { Link } from "react-router"; import api from "../utils/api"; export default function Courses() { @@ -27,6 +27,17 @@ export default function Courses() { }).format(price); }; + const addToCart = async (courseId) => { + try { + await api.post("/carts/add", { lectureId: courseId }); + // Tampilkan notifikasi sukses (bisa menggunakan toast notification atau alert) + alert("Kursus berhasil ditambahkan ke keranjang"); + } catch (error) { + console.error("Error adding course to cart:", error); + alert("Gagal menambahkan kursus ke keranjang"); + } + }; + useEffect(() => { // Fetch categories const fetchCategories = async () => { @@ -54,7 +65,7 @@ export default function Courses() { if (filters.search) queryParams.append("search", filters.search); if (filters.categoryId) queryParams.append("categoryId", filters.categoryId); if (filters.minPrice) queryParams.append("minPrice", filters.minPrice); - if (filters.maxPrice) queryParams.append("maxPrice", filters.maxPrice); + if (filters.maxPrice) queryParams.append("maxPrice", filters.maxPrice); // Perbaikan kondisi yang tidak lengkap // Handle sorting if (sort === "price_asc") { @@ -259,7 +270,17 @@ export default function Courses() {
{course.name}

{course.technique}

- View Details +
+ + Detail + + +
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx index 6754350d..ac12b2c1 100644 --- a/client/src/pages/Home.jsx +++ b/client/src/pages/Home.jsx @@ -14,14 +14,28 @@ export default function Home() { const [homeData, setHomeData] = useState(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { const fetchHomeData = async () => { try { const { data } = await api.get("/public/homepage-bundle"); setHomeData(data); + setError(null); } catch (error) { console.error("Error fetching homepage data:", error); + setError("Failed to load homepage data. Please try again later."); + // Set default data structure to prevent rendering errors + setHomeData({ + featuredLectures: [], + latestLectures: [], + popularCategories: [], + statistics: { + totalLectures: 0, + totalCategories: 0, + totalUsers: 0 + } + }); } finally { setLoading(false); } @@ -30,6 +44,12 @@ export default function Home() { fetchHomeData(); }, []); + if (error) return ( +
+ {error} +
+ ); + if (loading) return (
@@ -134,7 +154,7 @@ export default function Home() {
{lecture.name}

{lecture.technique}

- View Details + View Details
@@ -178,7 +198,7 @@ export default function Home() {
{lecture.name}

{lecture.technique}

- View Details + View Details
diff --git a/client/src/utils/api.js b/client/src/utils/api.js index 42a4ff00..df5f60ac 100644 --- a/client/src/utils/api.js +++ b/client/src/utils/api.js @@ -3,7 +3,6 @@ import axios from 'axios'; // Create axios instance with base URL const api = axios.create({ baseURL: import.meta.env.VITE_BASE_URL, - timeout: 10000, headers: { 'Content-Type': 'application/json', } diff --git a/server/app.js b/server/app.js index 49d747a3..d093d138 100644 --- a/server/app.js +++ b/server/app.js @@ -21,6 +21,12 @@ app.use("/api", routes); // Error handler middleware app.use(errorHandler); +// Add more detailed error logging +app.use((err, req, res, next) => { + console.error("Error details:", err); + next(err); +}); + // Only listen if directly running this file (not in test environment) if (process.env.NODE_ENV !== "test") { app.listen(port, () => { @@ -28,4 +34,7 @@ if (process.env.NODE_ENV !== "test") { }); } +// Di bagian akhir sebelum module.exports +app.use(require("./middlewares/errorHandler")); + module.exports = app; diff --git a/server/controllers/cartController.js b/server/controllers/cartController.js index 370f734b..310481a4 100644 --- a/server/controllers/cartController.js +++ b/server/controllers/cartController.js @@ -1,4 +1,4 @@ -const { Cart, Lecture, User } = require("../models"); +const { Cart, Lecture, Category, User } = require("../models"); class CartController { static async getUserCart(req, res, next) { @@ -10,7 +10,12 @@ class CartController { include: [ { model: Lecture, - include: ["category"] + include: [ + { + model: Category, + as: 'category' + } + ] } ] }); @@ -24,17 +29,20 @@ class CartController { static async addToCart(req, res, next) { try { const UserId = req.user.id; - const { LectureId } = req.body; + const { lectureId } = req.body; // Check if lecture exists - const lecture = await Lecture.findByPk(LectureId); + const lecture = await Lecture.findByPk(lectureId); if (!lecture) { throw { name: "NotFound", message: "Lecture not found" }; } // Check if already in cart const existingCart = await Cart.findOne({ - where: { UserId, LectureId } + where: { + UserId, + LectureId: lectureId + } }); if (existingCart) { @@ -43,7 +51,7 @@ class CartController { const newCart = await Cart.create({ UserId, - LectureId + LectureId: lectureId }); res.status(201).json(newCart); diff --git a/server/controllers/publicController.js b/server/controllers/publicController.js new file mode 100644 index 00000000..c65fdbd7 --- /dev/null +++ b/server/controllers/publicController.js @@ -0,0 +1,205 @@ +const { Category, Lecture, User, sequelize } = require("../models"); +const { Op } = require("sequelize"); + +class PublicController { + static async getCategories(req, res, next) { + try { + const categories = await Category.findAll({ + attributes: { + include: [ + [sequelize.fn("COUNT", sequelize.col("Lectures.id")), "lectureCount"] + ] + }, + include: [ + { + model: Lecture, + attributes: [] + } + ], + group: ["Category.id"] + }); + + res.json(categories); + } catch (err) { + next(err); + } + } + + static async getCategoryById(req, res, next) { + try { + const { id } = req.params; + + const category = await Category.findByPk(id, { + attributes: { + include: [ + [sequelize.fn("COUNT", sequelize.col("Lectures.id")), "lectureCount"] + ] + }, + include: [ + { + model: Lecture, + attributes: [] + } + ], + group: ["Category.id"] + }); + + if (!category) { + throw { name: "NotFound", message: "Category not found" }; + } + + res.json(category); + } catch (err) { + next(err); + } + } + + static async getLectures(req, res, next) { + try { + const { + page = 1, + limit = 9, + search = "", + categoryId, + minPrice, + maxPrice, + sortBy = "createdAt", + sortDirection = "DESC" + } = req.query; + + const offset = (page - 1) * limit; + + // Build where clause for filtering + const whereClause = {}; + if (search) { + whereClause.name = { [Op.iLike]: `%${search}%` }; + } + + if (categoryId) { + whereClause.CategoryId = categoryId; + } + + // Price filtering + if (minPrice || maxPrice) { + whereClause.price = {}; + if (minPrice) whereClause.price[Op.gte] = minPrice; + if (maxPrice) whereClause.price[Op.lte] = maxPrice; + } + + const { count, rows } = await Lecture.findAndCountAll({ + where: whereClause, + include: [ + { + model: Category, + as: 'category' + }, + { + model: User, + attributes: ['username'], + required: false + } + ], + order: [[sortBy, sortDirection]], + limit: parseInt(limit), + offset: parseInt(offset) + }); + + // Calculate pagination info + const totalItems = count; + const totalPages = Math.ceil(totalItems / limit); + + res.json({ + lectures: rows, + currentPage: parseInt(page), + totalPages, + totalItems + }); + } catch (err) { + console.error("Error in getLectures:", err); + next(err); + } + } + + static async getLectureById(req, res, next) { + try { + const { id } = req.params; + + const lecture = await Lecture.findByPk(id, { + include: [ + { + model: Category, + as: 'category' + } + ] + }); + + if (!lecture) { + throw { name: "NotFound", message: "Lecture not found" }; + } + + res.json(lecture); + } catch (err) { + next(err); + } + } + + static async getHomepageBundle(req, res, next) { + try { + // Get featured lectures (explicitly selecting only existing columns) + const featuredLectures = await Lecture.findAll({ + attributes: [ + 'id', 'name', 'title', 'technique', 'CategoryId', + 'experience_years', 'certifications', 'description', + 'price', 'availability', 'image', 'createdAt', 'updatedAt' + ], + include: [{ + model: Category, + as: 'category' + }], + limit: 3, + order: [['createdAt', 'DESC']] + }); + + // Get latest lectures (same as featured for now, could be different criteria) + const latestLectures = await Lecture.findAll({ + attributes: [ + 'id', 'name', 'title', 'technique', 'CategoryId', + 'experience_years', 'certifications', 'description', + 'price', 'availability', 'image', 'createdAt', 'updatedAt' + ], + include: [{ + model: Category, + as: 'category' + }], + limit: 3, + order: [['createdAt', 'DESC']] + }); + + // Get popular categories + const popularCategories = await Category.findAll({ + limit: 3 + }); + + // Get statistics + const totalLectures = await Lecture.count(); + const totalCategories = await Category.count(); + const totalUsers = await User.count(); + + res.json({ + featuredLectures, + latestLectures, + popularCategories, + statistics: { + totalLectures, + totalCategories, + totalUsers + } + }); + } catch (err) { + console.error("Error in getHomepageBundle:", err); + next(err); + } + } +} + +module.exports = PublicController; \ No newline at end of file diff --git a/server/data/lectures.json b/server/data/lectures.json index 692108a6..d5e4aad0 100644 --- a/server/data/lectures.json +++ b/server/data/lectures.json @@ -10,7 +10,8 @@ "description": "Expert in ultrasonic testing for weld imperfections and material thickness evaluation.", "price": 7990000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_ut.jpg" + "image": "https://example.com/saenal_aladin_rapi_ut.jpg", + "UserId": 1 }, { "id": 2, @@ -23,7 +24,8 @@ "description": "Specializes in phased array ultrasonic testing for advanced inspections.", "price": 8500000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_paut.jpg" + "image": "https://example.com/saenal_aladin_rapi_paut.jpg", + "UserId": 1 }, { "id": 3, @@ -36,7 +38,8 @@ "description": "Provides comprehensive radiographic testing training for pipelines.", "price": 8990000, "availability": "Limited", - "image": "https://example.com/saenal_aladin_rapi_rt.jpg" + "image": "https://example.com/saenal_aladin_rapi_rt.jpg", + "UserId": 1 }, { "id": 4, @@ -49,7 +52,8 @@ "description": "Expert in eddy current testing for conductive materials.", "price": 7500000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_ect.jpg" + "image": "https://example.com/saenal_aladin_rapi_ect.jpg", + "UserId": 1 }, { "id": 5, @@ -62,7 +66,8 @@ "description": "Specializes in magnetic particle testing for surface defect detection.", "price": 5990000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_mt.jpg" + "image": "https://example.com/saenal_aladin_rapi_mt.jpg", + "UserId": 1 }, { "id": 6, @@ -75,7 +80,8 @@ "description": "Experienced in penetrant testing for non-porous materials.", "price": 4990000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_pt.jpg" + "image": "https://example.com/saenal_aladin_rapi_pt.jpg", + "UserId": 1 }, { "id": 7, @@ -88,7 +94,8 @@ "description": "Delivers visual testing courses for surface inspections.", "price": 5490000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_vt.jpg" + "image": "https://example.com/saenal_aladin_rapi_vt.jpg", + "UserId": 1 }, { "id": 8, @@ -101,7 +108,8 @@ "description": "Expert in acoustic emission testing for structural health monitoring.", "price": 8200000, "availability": "Limited", - "image": "https://example.com/saenal_aladin_rapi_aet.jpg" + "image": "https://example.com/saenal_aladin_rapi_aet.jpg", + "UserId": 1 }, { "id": 9, @@ -114,7 +122,8 @@ "description": "Specializes in infrared thermography for heat leak detection.", "price": 7800000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_irt.jpg" + "image": "https://example.com/saenal_aladin_rapi_irt.jpg", + "UserId": 1 }, { "id": 10, @@ -127,7 +136,8 @@ "description": "Provides digital radiography training for enhanced imaging.", "price": 9200000, "availability": "Available", - "image": "https://example.com/saenal_aladin_rapi_dr.jpg" + "image": "https://example.com/saenal_aladin_rapi_dr.jpg", + "UserId": 1 }, { "id": 11, @@ -140,6 +150,7 @@ "description": "Expert in computed tomography for 3D internal inspections.", "price": 9500000, "availability": "Limited", - "image": "https://example.com/saenal_aladin_rapi_ct.jpg" + "image": "https://example.com/saenal_aladin_rapi_ct.jpg", + "UserId": 1 } ] diff --git a/server/middlewares/authentication.js b/server/middlewares/authentication.js index 7fe0cac9..98c6a660 100644 --- a/server/middlewares/authentication.js +++ b/server/middlewares/authentication.js @@ -1,38 +1,44 @@ -const { verifyToken } = require("../helpers/jwt"); const { User } = require("../models"); +const { verifyToken } = require("../helpers/jwt"); -const authentication = async (req, res, next) => { +async function authentication(req, res, next) { try { - // Check if token exists - const { access_token } = req.headers; - if (!access_token) { - throw { name: "Unauthorized", message: "Please login first" }; + // Get token from request header + const { authorization } = req.headers; + + if (!authorization) { + throw { name: "Unauthorized", message: "Authentication token required" }; } - + + // Format: "Bearer token" + const token = authorization.split(" ")[1]; + if (!token) { + throw { name: "Unauthorized", message: "Invalid token format" }; + } + // Verify token - const payload = verifyToken(access_token); + const payload = verifyToken(token); if (!payload) { - throw { name: "JsonWebTokenError", message: "Invalid token" }; + throw { name: "Unauthorized", message: "Invalid token" }; } - - // Find user by id + + // Find user based on token const user = await User.findByPk(payload.id); if (!user) { throw { name: "Unauthorized", message: "User not found" }; } - - // Add user to request + + // Set req.user to be used in controller req.user = { id: user.id, email: user.email, - role: user.role, - username: user.username, + role: user.role }; - + next(); } catch (err) { next(err); } -}; +} module.exports = authentication; diff --git a/server/middlewares/authorization.js b/server/middlewares/authorization.js index 99262582..6419c5ad 100644 --- a/server/middlewares/authorization.js +++ b/server/middlewares/authorization.js @@ -1,12 +1,12 @@ -const adminAuthorization = async (req, res, next) => { +function adminAuthorization(req, res, next) { try { - if (req.user.role !== "Admin") { - throw { name: "Forbidden", message: "You are not authorized" }; + if (req.user.role !== "admin") { + throw { name: "Forbidden", message: "Admin access required" }; } next(); } catch (err) { next(err); } -}; +} module.exports = { adminAuthorization }; diff --git a/server/middlewares/errorHandler.js b/server/middlewares/errorHandler.js index 08231c63..8962423a 100644 --- a/server/middlewares/errorHandler.js +++ b/server/middlewares/errorHandler.js @@ -1,8 +1,8 @@ -const errorHandler = (err, req, res, next) => { - console.log(err); - +function errorHandler(err, req, res, next) { + console.error("Error:", err); + let statusCode = 500; - let message = "Internal Server Error"; + let message = "Internal server error"; switch (err.name) { case "SequelizeValidationError": @@ -11,31 +11,30 @@ const errorHandler = (err, req, res, next) => { break; case "SequelizeUniqueConstraintError": statusCode = 400; - message = err.errors.map(e => e.message).join(", "); + message = "Data already exists"; break; case "BadRequest": statusCode = 400; - message = err.message; + message = err.message || "Bad request"; break; case "Unauthorized": statusCode = 401; - message = err.message; + message = err.message || "Authentication required"; break; case "Forbidden": statusCode = 403; - message = err.message; + message = err.message || "Access forbidden"; break; case "NotFound": statusCode = 404; - message = err.message; + message = err.message || "Resource not found"; break; - case "JsonWebTokenError": - statusCode = 401; - message = "Invalid token"; + default: + // Use default values break; } res.status(statusCode).json({ message }); -}; +} module.exports = errorHandler; \ No newline at end of file diff --git a/server/migrations/20250429151455-addColumn-to-Lectures.js b/server/migrations/20250429151455-addColumn-to-Lectures.js new file mode 100644 index 00000000..f2ddfdf7 --- /dev/null +++ b/server/migrations/20250429151455-addColumn-to-Lectures.js @@ -0,0 +1,19 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn("Lectures", "UserId", { + type: Sequelize.INTEGER, + allowNull: true, + references: { + model: "Users", + key: "id", + }, + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn("Lectures", "UserId"); + }, +}; diff --git a/server/models/category.js b/server/models/category.js index 19459c51..34011701 100644 --- a/server/models/category.js +++ b/server/models/category.js @@ -4,7 +4,9 @@ module.exports = (sequelize, DataTypes) => { class Category extends Model { static associate(models) { // Relasi Category → Lecture (One-to-Many) - Category.hasMany(models.Lecture); + Category.hasMany(models.Lecture, { + foreignKey: "CategoryId", + }); } } Category.init( diff --git a/server/models/lecture.js b/server/models/lecture.js index cd5c5326..048e09d5 100644 --- a/server/models/lecture.js +++ b/server/models/lecture.js @@ -3,14 +3,11 @@ const { Model } = require("sequelize"); module.exports = (sequelize, DataTypes) => { class Lecture extends Model { static associate(models) { - // Relasi Lecture → Category (Many-to-One) Lecture.belongsTo(models.Category, { foreignKey: "CategoryId", as: "category", }); - // Relasi Lecture → User (Many-to-One, untuk pengelola lecture) Lecture.belongsTo(models.User); - // Relasi Lecture ↔ User (Many-to-Many melalui Cart) Lecture.belongsToMany(models.User, { through: models.Cart, }); @@ -90,6 +87,14 @@ module.exports = (sequelize, DataTypes) => { isUrl: { msg: "Invalid image URL format" }, }, }, + UserId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: "Users", + key: "id", + }, + } }, { sequelize, diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js new file mode 100644 index 00000000..d3d14136 --- /dev/null +++ b/server/routes/adminRoutes.js @@ -0,0 +1,24 @@ +const express = require("express"); +const router = express.Router(); +const CategoryController = require("../controllers/categoryController"); +const LectureController = require("../controllers/lectureController"); +const authentication = require("../middlewares/authentication"); +const { adminAuthorization } = require("../middlewares/authorization"); + +// Add authentication and admin authorization to all admin routes +router.use(authentication); +router.use(adminAuthorization); + +// Categories routes +router.get("/categories", CategoryController.getAllCategories); +router.post("/categories", CategoryController.createCategory); +router.put("/categories/:id", CategoryController.updateCategory); +router.delete("/categories/:id", CategoryController.deleteCategory); + +// Lectures routes +router.get("/lectures", LectureController.getAllLectures); +router.post("/lectures", LectureController.createLecture); +router.put("/lectures/:id", LectureController.updateLecture); +router.delete("/lectures/:id", LectureController.deleteLecture); + +module.exports = router; \ No newline at end of file diff --git a/server/routes/cartRoutes.js b/server/routes/cartRoutes.js index 822a7058..a92e0629 100644 --- a/server/routes/cartRoutes.js +++ b/server/routes/cartRoutes.js @@ -6,7 +6,7 @@ const authentication = require("../middlewares/authentication"); // All cart routes require authentication router.use(authentication); router.get("/", CartController.getUserCart); -router.post("/", CartController.addToCart); +router.post("/add", CartController.addToCart); router.delete("/:id", CartController.removeFromCart); module.exports = router; \ No newline at end of file diff --git a/server/routes/index.js b/server/routes/index.js index 3e1d7c9a..41b2986a 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -5,6 +5,7 @@ const categoryRoutes = require("./categoryRoutes"); const lectureRoutes = require("./lectureRoutes"); const cartRoutes = require("./cartRoutes"); const publicRoutes = require("./publicRoutes"); +const adminRoutes = require("./adminRoutes"); // Tambahkan ini // Public routes - tidak memerlukan authentication router.use("/public", publicRoutes); @@ -14,5 +15,6 @@ router.use("/users", userRoutes); router.use("/categories", categoryRoutes); router.use("/lectures", lectureRoutes); router.use("/carts", cartRoutes); +router.use("/admin", adminRoutes); // Tambahkan ini module.exports = router; \ No newline at end of file diff --git a/server/routes/publicRoutes.js b/server/routes/publicRoutes.js index e37d8663..a2c5f921 100644 --- a/server/routes/publicRoutes.js +++ b/server/routes/publicRoutes.js @@ -1,244 +1,16 @@ const express = require("express"); const router = express.Router(); -const { Lecture, Category, User, sequelize } = require("../models"); -const { Op } = require("sequelize"); +const PublicController = require("../controllers/publicController"); -// Get landing page bundle (all homepage data in one request) -router.get("/homepage-bundle", async (req, res, next) => { - try { - // Get featured lectures, latest lectures, statistics, and popular categories in parallel - const [featuredLectures, latestLectures, statistics, popularCategories] = await Promise.all([ - Lecture.findAll({ - limit: 3, - include: [{ model: Category, as: "category" }], - order: [["price", "DESC"]], - attributes: { exclude: ["UserId", "createdAt", "updatedAt"] } - }), - - Lecture.findAll({ - limit: 6, - include: [{ model: Category, as: "category" }], - order: [["createdAt", "DESC"]], - attributes: { exclude: ["UserId", "createdAt", "updatedAt"] } - }), - - Promise.all([ - Lecture.count(), - Category.count(), - User.count(), - Lecture.findOne({ - attributes: [[sequelize.fn('AVG', sequelize.col('price')), 'averagePrice']], - raw: true - }) - ]), - - Category.findAll({ - limit: 5, - attributes: ['id', 'name', 'description'], - order: [["id", "ASC"]] - }) - ]); - - // Format statistics data - const [lectureCount, categoryCount, userCount, avgPrice] = statistics; - - res.status(200).json({ - featuredLectures, - latestLectures, - statistics: { - totalLectures: lectureCount, - totalCategories: categoryCount, - totalUsers: userCount, - averagePrice: avgPrice ? parseFloat(avgPrice.averagePrice).toFixed(2) : 0 - }, - popularCategories - }); - } catch (err) { - next(err); - } -}); +// Public routes for categories +router.get("/categories", PublicController.getCategories); +router.get("/categories/:id", PublicController.getCategoryById); -// Advanced lecture search with filters, sorting and pagination -router.get("/lectures", async (req, res, next) => { - try { - // Pagination - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const offset = (page - 1) * limit; - - // Search query - const searchQuery = req.query.search || ''; - - // Filters - const filters = {}; - if (req.query.categoryId) { - filters.CategoryId = req.query.categoryId; - } - - if (req.query.minPrice || req.query.maxPrice) { - filters.price = {}; - if (req.query.minPrice) { - filters.price[Op.gte] = parseFloat(req.query.minPrice); - } - if (req.query.maxPrice) { - filters.price[Op.lte] = parseFloat(req.query.maxPrice); - } - } - - if (req.query.minExperience) { - filters.experience_years = { - [Op.gte]: parseInt(req.query.minExperience) - }; - } - - // Search condition - if (searchQuery) { - filters[Op.or] = [ - { name: { [Op.iLike]: `%${searchQuery}%` } }, - { description: { [Op.iLike]: `%${searchQuery}%` } }, - { technique: { [Op.iLike]: `%${searchQuery}%` } } - ]; - } - - // Sorting - let order = [['id', 'ASC']]; // default sorting - if (req.query.sortBy) { - const validColumns = ['id', 'name', 'price', 'createdAt', 'experience_years']; - const validOrders = ['ASC', 'DESC']; - - if (validColumns.includes(req.query.sortBy)) { - const direction = req.query.sortDirection && - validOrders.includes(req.query.sortDirection.toUpperCase()) ? - req.query.sortDirection.toUpperCase() : 'ASC'; - order = [[req.query.sortBy, direction]]; - } - } - - // Execute query with all parameters - const { count, rows: lectures } = await Lecture.findAndCountAll({ - where: filters, - limit, - offset, - include: [{ model: Category, as: "category" }], - order, - distinct: true - }); - - res.status(200).json({ - totalItems: count, - totalPages: Math.ceil(count / limit), - currentPage: page, - lectures - }); - } catch (err) { - next(err); - } -}); +// Public routes for lectures +router.get("/lectures", PublicController.getLectures); +router.get("/lectures/:id", PublicController.getLectureById); -// Get lectures by category with pagination, filters, search -router.get("/categories/:categoryId/lectures", async (req, res, next) => { - try { - const { categoryId } = req.params; - - // Verify category exists - const category = await Category.findByPk(categoryId); - if (!category) { - return res.status(404).json({ message: "Category not found" }); - } - - // Pagination - const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 10; - const offset = (page - 1) * limit; - - // Search query - const searchQuery = req.query.search || ''; - - // Filters - const filters = { CategoryId: categoryId }; - - if (req.query.minPrice || req.query.maxPrice) { - filters.price = {}; - if (req.query.minPrice) filters.price[Op.gte] = parseFloat(req.query.minPrice); - if (req.query.maxPrice) filters.price[Op.lte] = parseFloat(req.query.maxPrice); - } - - // Search condition - if (searchQuery) { - filters[Op.and] = [ - { CategoryId: categoryId }, - { - [Op.or]: [ - { name: { [Op.iLike]: `%${searchQuery}%` } }, - { description: { [Op.iLike]: `%${searchQuery}%` } }, - { technique: { [Op.iLike]: `%${searchQuery}%` } } - ] - } - ]; - delete filters.CategoryId; // Remove duplicate condition - } - - // Sorting - let order = [['id', 'ASC']]; // default sorting - if (req.query.sortBy) { - const validColumns = ['id', 'name', 'price', 'createdAt', 'experience_years']; - const validOrders = ['ASC', 'DESC']; - - if (validColumns.includes(req.query.sortBy)) { - const direction = req.query.sortDirection && - validOrders.includes(req.query.sortDirection.toUpperCase()) ? - req.query.sortDirection.toUpperCase() : 'ASC'; - order = [[req.query.sortBy, direction]]; - } - } - - // Execute query - const { count, rows: lectures } = await Lecture.findAndCountAll({ - where: filters, - limit, - offset, - order, - distinct: true - }); - - res.status(200).json({ - category: { - id: category.id, - name: category.name, - description: category.description - }, - totalItems: count, - totalPages: Math.ceil(count / limit), - currentPage: page, - lectures - }); - } catch (err) { - next(err); - } -}); - -// Get all categories with lecture counts -router.get("/categories", async (req, res, next) => { - try { - const categories = await Category.findAll({ - attributes: [ - 'id', - 'name', - 'description', - [sequelize.fn('COUNT', sequelize.col('Lectures.id')), 'lectureCount'] - ], - include: [{ - model: Lecture, - attributes: [] - }], - group: ['Category.id'], - order: [['id', 'ASC']] - }); - - res.status(200).json(categories); - } catch (err) { - next(err); - } -}); +// Homepage bundle +router.get("/homepage-bundle", PublicController.getHomepageBundle); module.exports = router; \ No newline at end of file From 3bb1b08be169c982bc432bb135995808b49405bc Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Wed, 30 Apr 2025 08:05:41 +0700 Subject: [PATCH 18/51] chore: update new migration fot Transaction and TransactionDetail and adjust the others --- client/src/components/DashboardSkeleton.jsx | 94 +++++++ client/src/pages/Admin/Categories.jsx | 7 +- client/src/pages/Admin/Dashboard.jsx | 205 ++++++++++----- client/src/pages/Admin/Transactions.jsx | 235 ++++++++++++++++++ client/src/pages/Admin/Users.jsx | 12 +- client/src/pages/Cart.jsx | 2 +- server/app.js | 1 - server/controllers/adminController.js | 165 ++++++++++++ server/controllers/transactionController.js | 188 ++++++++++++++ server/controllers/userController.js | 126 +++++++++- server/middlewares/authorization.js | 2 +- .../20250429224300-create-transaction.js | 53 ++++ ...0250429224319-create-transaction-detail.js | 54 ++++ server/models/lecture.js | 8 +- server/models/transaction.js | 83 +++++++ server/models/transactiondetail.js | 51 ++++ server/models/user.js | 3 +- server/routes/adminRoutes.js | 15 ++ server/routes/index.js | 2 + server/routes/transactionRoutes.js | 19 ++ 20 files changed, 1252 insertions(+), 73 deletions(-) create mode 100644 client/src/components/DashboardSkeleton.jsx create mode 100644 client/src/pages/Admin/Transactions.jsx create mode 100644 server/controllers/adminController.js create mode 100644 server/controllers/transactionController.js create mode 100644 server/migrations/20250429224300-create-transaction.js create mode 100644 server/migrations/20250429224319-create-transaction-detail.js create mode 100644 server/models/transaction.js create mode 100644 server/models/transactiondetail.js create mode 100644 server/routes/transactionRoutes.js diff --git a/client/src/components/DashboardSkeleton.jsx b/client/src/components/DashboardSkeleton.jsx new file mode 100644 index 00000000..90a92850 --- /dev/null +++ b/client/src/components/DashboardSkeleton.jsx @@ -0,0 +1,94 @@ +export default function DashboardSkeleton() { + return ( +
+

Dashboard

+ + {/* Stat Cards Skeleton */} +
+ {[1, 2, 3, 4].map(i => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ + {/* Charts Skeleton */} +
+
+
+
+
+
+
+
+ Loading... +
+
+
+
+
+
+
+
+
+
+
+ Loading... +
+
+
+
+
+ + {/* Tables Skeleton */} +
+ {[1, 2].map(i => ( +
+
+
+
+
+
+ + + + {[1, 2, 3, 4].map(j => ( + + ))} + + + + {[1, 2, 3, 4, 5].map(row => ( + + {[1, 2, 3, 4].map(col => ( + + ))} + + ))} + +
+
+
+
+
+
+
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Admin/Categories.jsx b/client/src/pages/Admin/Categories.jsx index 93749de1..5653a35b 100644 --- a/client/src/pages/Admin/Categories.jsx +++ b/client/src/pages/Admin/Categories.jsx @@ -65,7 +65,10 @@ export default function Categories() { // Close modal and reset form document.getElementById('categoryFormModal').querySelector('[data-bs-dismiss="modal"]').click(); setSelectedCategory(null); - resetForm(); + setFormData({ + name: "", + description: "" + }); } catch (error) { console.error("Error saving category:", error); alert(error.response?.data?.message || "Failed to save category"); @@ -84,7 +87,7 @@ export default function Categories() { setCategoryToDelete(null); } catch (error) { console.error("Error deleting category:", error); - alert(error.response?.data?.message || "Failed to delete category. Make sure no courses are using this category."); + alert(error.response?.data?.message || "Failed to delete category"); } }; diff --git a/client/src/pages/Admin/Dashboard.jsx b/client/src/pages/Admin/Dashboard.jsx index 5c62c208..11d12c10 100644 --- a/client/src/pages/Admin/Dashboard.jsx +++ b/client/src/pages/Admin/Dashboard.jsx @@ -1,11 +1,28 @@ import { useState, useEffect } from "react"; import { Link } from "react-router"; import api from "../../utils/api"; -import { Chart as ChartJS, ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js'; -import { Pie, Bar } from 'react-chartjs-2'; +import { + Chart as ChartJS, + ArcElement, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, +} from "chart.js"; +import { Pie, Bar } from "react-chartjs-2"; // Register ChartJS components -ChartJS.register(ArcElement, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); +ChartJS.register( + ArcElement, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); export default function Dashboard() { const [stats, setStats] = useState({ @@ -20,17 +37,26 @@ export default function Dashboard() { const [coursesPerCategory, setCoursesPerCategory] = useState([]); const [monthlySales, setMonthlySales] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { const fetchDashboardData = async () => { try { - // In a real app, this might be a single endpoint that returns all dashboard data - const [statsResponse, usersResponse, ordersResponse, categoriesResponse, salesResponse] = await Promise.all([ + setLoading(true); + setError(null); + + const [ + statsResponse, + usersResponse, + ordersResponse, + categoriesResponse, + salesResponse, + ] = await Promise.all([ api.get("/admin/statistics"), - api.get("/admin/users?limit=5"), + api.get("/admin/recent-users?limit=5"), // Sesuaikan dengan route baru api.get("/admin/orders?limit=5"), api.get("/admin/categories/stats"), - api.get("/admin/orders/monthly") + api.get("/admin/orders/monthly"), ]); setStats(statsResponse.data); @@ -40,6 +66,9 @@ export default function Dashboard() { setMonthlySales(salesResponse.data); } catch (error) { console.error("Error fetching dashboard data:", error); + setError( + "Failed to load dashboard data. Please check the server connection." + ); } finally { setLoading(false); } @@ -49,49 +78,55 @@ export default function Dashboard() { }, []); const formatToIDR = (price) => { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0 + return new Intl.NumberFormat("id-ID", { + style: "currency", + currency: "IDR", + minimumFractionDigits: 0, }).format(price); }; const formatDate = (dateString) => { - return new Date(dateString).toLocaleDateString('id-ID', { - year: 'numeric', - month: 'short', - day: 'numeric', + return new Date(dateString).toLocaleDateString("id-ID", { + year: "numeric", + month: "short", + day: "numeric", }); }; - // Prepare data for pie chart const categoryChartData = { - labels: coursesPerCategory.map(item => item.name), + labels: coursesPerCategory.map((item) => item.name), datasets: [ { - data: coursesPerCategory.map(item => item.courseCount), + data: coursesPerCategory.map((item) => item.courseCount), backgroundColor: [ - '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b', - '#5a5c69', '#858796', '#6f42c1', '#20c9a6', '#f8f9fc' + "#4e73df", + "#1cc88a", + "#36b9cc", + "#f6c23e", + "#e74a3b", + "#5a5c69", + "#858796", + "#6f42c1", + "#20c9a6", + "#f8f9fc", ], borderWidth: 1, }, ], }; - // Prepare data for bar chart const salesChartData = { - labels: monthlySales.map(item => item.month), + labels: monthlySales.map((item) => item.month), datasets: [ { - label: 'Revenue', - data: monthlySales.map(item => item.revenue), - backgroundColor: '#4e73df', + label: "Revenue", + data: monthlySales.map((item) => item.revenue), + backgroundColor: "#4e73df", }, { - label: 'Orders', - data: monthlySales.map(item => item.orderCount), - backgroundColor: '#1cc88a', + label: "Orders", + data: monthlySales.map((item) => item.orderCount), + backgroundColor: "#1cc88a", }, ], }; @@ -100,18 +135,43 @@ export default function Dashboard() { responsive: true, plugins: { legend: { - position: 'top', + position: "top", }, title: { display: true, - text: 'Monthly Sales Performance', + text: "Monthly Sales Performance", }, }, }; + if (error) { + return ( +
+

Dashboard

+
+

Error Loading Dashboard!

+

{error}

+
+

+ Please check server connection or contact the administrator. +

+ +
+
+ ); + } + if (loading) { return ( -
+
Loading...
@@ -122,8 +182,7 @@ export default function Dashboard() { return (

Dashboard

- - {/* Stats Cards */} +
@@ -139,14 +198,19 @@ export default function Dashboard() {
- + View Details -
+
+ +
- +
@@ -161,14 +225,19 @@ export default function Dashboard() {
- + View Details -
+
+ +
- +
@@ -183,21 +252,28 @@ export default function Dashboard() {
- + View Details -
+
+ +
- +
Total Revenue
-
{formatToIDR(stats.revenue)}
+
+ {formatToIDR(stats.revenue)} +
@@ -205,16 +281,20 @@ export default function Dashboard() {
- + View Reports -
+
+ +
- {/* Charts Row */}
@@ -223,7 +303,11 @@ export default function Dashboard() { Monthly Sales
- +
@@ -240,7 +324,6 @@ export default function Dashboard() { - {/* Tables Row */}
@@ -260,13 +343,17 @@ export default function Dashboard() { - {recentUsers.map(user => ( + {recentUsers.map((user) => ( {user.username} {user.email} {formatDate(user.createdAt)} - + {user.role} @@ -302,18 +389,22 @@ export default function Dashboard() { - {recentOrders.map(order => ( + {recentOrders.map((order) => ( #{order.id} {order.User.username} {formatDate(order.createdAt)} {formatToIDR(order.totalAmount)} - + {order.status} @@ -333,4 +424,4 @@ export default function Dashboard() {
); -} \ No newline at end of file +} diff --git a/client/src/pages/Admin/Transactions.jsx b/client/src/pages/Admin/Transactions.jsx new file mode 100644 index 00000000..a5e07320 --- /dev/null +++ b/client/src/pages/Admin/Transactions.jsx @@ -0,0 +1,235 @@ +import { useState, useEffect } from "react"; +import { Link } from "react-router"; +import api from "../../utils/api"; + +export default function Transactions() { + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [statusFilter, setStatusFilter] = useState(""); + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + useEffect(() => { + fetchTransactions(); + }, [currentPage, statusFilter]); + + const fetchTransactions = async () => { + try { + setLoading(true); + const params = new URLSearchParams(); + params.append("page", currentPage); + if (statusFilter) params.append("status", statusFilter); + + const { data } = await api.get(`/admin/transactions?${params}`); + setTransactions(data.transactions || []); + setTotalPages(data.totalPages || 1); + } catch (error) { + console.error("Error fetching transactions:", error); + setError("Failed to load transactions"); + } finally { + setLoading(false); + } + }; + + const handleStatusChange = async (id, newStatus) => { + try { + await api.put(`/admin/transactions/${id}/status`, { status: newStatus }); + // Update local state without refetching + setTransactions(transactions.map(t => + t.id === id ? { ...t, status: newStatus } : t + )); + } catch (error) { + console.error("Error updating transaction status:", error); + alert("Failed to update transaction status"); + } + }; + + const getStatusBadgeClass = (status) => { + switch (status) { + case 'Completed': + return 'bg-success'; + case 'Processing': + return 'bg-info'; + case 'Cancelled': + return 'bg-danger'; + default: + return 'bg-warning'; + } + }; + + if (loading) { + return ( +
+

Transactions

+
+
+ Loading... +
+
+
+ ); + } + + if (error) { + return ( +
+

Transactions

+
{error}
+
+ ); + } + + return ( +
+

Transactions

+ +
+
+
+ + Transaction List +
+
+ +
+
+
+
+ + + + + + + + + + + + + + {transactions.length === 0 ? ( + + + + ) : ( + transactions.map((transaction) => ( + + + + + + + + + + )) + )} + +
IDInvoiceCustomerDateAmountStatusActions
No transactions found
{transaction.id}{transaction.invoice_number}{transaction.User?.username}{new Date(transaction.createdAt).toLocaleDateString()}{formatToIDR(transaction.total_amount)} + + {transaction.status} + + +
+ + + + +
+ +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ + {/* Pagination */} + {totalPages > 1 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/Admin/Users.jsx b/client/src/pages/Admin/Users.jsx index ed51c5f3..ba258d14 100644 --- a/client/src/pages/Admin/Users.jsx +++ b/client/src/pages/Admin/Users.jsx @@ -20,18 +20,16 @@ export default function Users() { // Fetch users with pagination and optional search const fetchUsers = async (page = 1, search = "") => { - setLoading(true); try { + setLoading(true); const params = new URLSearchParams(); params.append("page", page); - params.append("limit", 10); if (search) params.append("search", search); - - const response = await api.get(`/admin/users?${params.toString()}`); - setUsers(response.data.users); - setTotalPages(response.data.totalPages); - setCurrentPage(response.data.currentPage); + const response = await api.get(`/admin/users?${params}`); + setUsers(response.data.users || response.data); + setTotalPages(response.data.totalPages || 1); + setCurrentPage(response.data.currentPage || 1); } catch (error) { console.error("Error fetching users:", error); setError("Failed to load users. Please try again."); diff --git a/client/src/pages/Cart.jsx b/client/src/pages/Cart.jsx index 92c28606..aece82ba 100644 --- a/client/src/pages/Cart.jsx +++ b/client/src/pages/Cart.jsx @@ -7,7 +7,7 @@ export default function Cart() { const [cartItems, setCartItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [processing, setProcessing] = useState(false); + const [processing] = useState(false); useEffect(() => { const fetchCartItems = async () => { diff --git a/server/app.js b/server/app.js index d093d138..8660bd64 100644 --- a/server/app.js +++ b/server/app.js @@ -34,7 +34,6 @@ if (process.env.NODE_ENV !== "test") { }); } -// Di bagian akhir sebelum module.exports app.use(require("./middlewares/errorHandler")); module.exports = app; diff --git a/server/controllers/adminController.js b/server/controllers/adminController.js new file mode 100644 index 00000000..3bb912a6 --- /dev/null +++ b/server/controllers/adminController.js @@ -0,0 +1,165 @@ +const { User, Category, Lecture, Transaction, sequelize } = require("../models"); +const { Op } = require("sequelize"); + +class AdminController { + static async getStatistics(req, res, next) { + try { + const totalUsers = await User.count(); + const totalCourses = await Lecture.count(); + const totalCategories = await Category.count(); + + // Jika model Transaction belum ada, gunakan data dummy + let totalOrders = 0; + let revenue = 0; + + try { + totalOrders = await Transaction.count(); + const revenueResult = await Transaction.sum('total_amount', { + where: { status: 'Completed' } + }); + revenue = revenueResult || 0; + } catch (err) { + console.log("Transaction model not available, using dummy data"); + totalOrders = 15; + revenue = 25000000; + } + + res.json({ + totalUsers, + totalCourses, + totalCategories, + totalOrders, + revenue + }); + } catch (err) { + next(err); + } + } + + static async getRecentUsers(req, res, next) { + try { + const limit = parseInt(req.query.limit) || 5; + + const users = await User.findAll({ + attributes: ['id', 'username', 'email', 'role', 'createdAt'], + order: [['createdAt', 'DESC']], + limit + }); + + res.json(users); + } catch (err) { + next(err); + } + } + + static async getRecentOrders(req, res, next) { + try { + const limit = parseInt(req.query.limit) || 5; + + // Jika model Transaction belum ada, gunakan data dummy + try { + const orders = await Transaction.findAll({ + include: [{ + model: User, + attributes: ['id', 'username', 'email'] + }], + order: [['createdAt', 'DESC']], + limit + }); + + res.json(orders); + } catch (err) { + // Data dummy + const dummyOrders = Array.from({ length: limit }).map((_, index) => ({ + id: index + 1, + User: { + id: index + 1, + username: `user${index + 1}` + }, + createdAt: new Date(Date.now() - index * 86400000), + totalAmount: Math.floor(Math.random() * 5000000) + 1000000, + status: ["Completed", "Processing", "Pending"][Math.floor(Math.random() * 3)] + })); + + res.json(dummyOrders); + } + } catch (err) { + next(err); + } + } + + static async getCategoriesStats(req, res, next) { + try { + const categories = await Category.findAll({ + attributes: [ + 'id', + 'name', + [sequelize.fn('COUNT', sequelize.col('Lectures.id')), 'courseCount'] + ], + include: [{ + model: Lecture, + attributes: [] + }], + group: ['Category.id'], + order: [[sequelize.fn('COUNT', sequelize.col('Lectures.id')), 'DESC']] + }); + + res.json(categories); + } catch (err) { + next(err); + } + } + + static async getMonthlySales(req, res, next) { + try { + // Jika Transaction model belum siap, gunakan data dummy + try { + const currentYear = new Date().getFullYear(); + + const result = await Transaction.findAll({ + attributes: [ + [sequelize.fn('date_trunc', 'month', sequelize.col('createdAt')), 'month'], + [sequelize.fn('COUNT', sequelize.col('id')), 'orderCount'], + [sequelize.fn('SUM', sequelize.col('total_amount')), 'revenue'] + ], + where: { + createdAt: { + [Op.gte]: new Date(`${currentYear}-01-01`), + [Op.lt]: new Date(`${currentYear+1}-01-01`) + }, + status: 'Completed' + }, + group: [sequelize.fn('date_trunc', 'month', sequelize.col('createdAt'))], + order: [[sequelize.fn('date_trunc', 'month', sequelize.col('createdAt')), 'ASC']] + }); + + // Format data + const monthlySales = result.map(item => { + const data = item.toJSON(); + return { + month: new Date(data.month).toLocaleString('en-US', { month: 'short' }), + orderCount: parseInt(data.orderCount), + revenue: parseInt(data.revenue) + }; + }); + + res.json(monthlySales); + } catch (err) { + // Data dummy untuk 6 bulan terakhir + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']; + + const dummyData = months.map((month, index) => ({ + month, + orderCount: Math.floor(Math.random() * 20) + 5, + revenue: (Math.floor(Math.random() * 50) + 10) * 1000000 + })); + + res.json(dummyData); + } + } catch (err) { + next(err); + } + } +} + +module.exports = AdminController; \ No newline at end of file diff --git a/server/controllers/transactionController.js b/server/controllers/transactionController.js new file mode 100644 index 00000000..9dc6db6c --- /dev/null +++ b/server/controllers/transactionController.js @@ -0,0 +1,188 @@ +const { Transaction, TransactionDetail, Lecture, User, sequelize } = require("../models"); +const { v4: uuidv4 } = require('uuid'); + +class TransactionController { + // Mendapatkan semua transaksi untuk admin + static async getAllTransactions(req, res, next) { + try { + const { page = 1, limit = 10, status } = req.query; + const offset = (page - 1) * limit; + + const whereClause = {}; + if (status) { + whereClause.status = status; + } + + const { count, rows } = await Transaction.findAndCountAll({ + where: whereClause, + include: [{ + model: User, + attributes: ['id', 'username', 'email'] + }], + order: [['createdAt', 'DESC']], + limit: parseInt(limit), + offset: parseInt(offset) + }); + + const totalPages = Math.ceil(count / limit); + + res.status(200).json({ + transactions: rows, + currentPage: parseInt(page), + totalPages, + totalItems: count + }); + } catch (err) { + next(err); + } + } + + // Mendapatkan transaksi milik user yang sedang login + static async getUserTransactions(req, res, next) { + try { + const UserId = req.user.id; + + const transactions = await Transaction.findAll({ + where: { UserId }, + include: [{ + model: TransactionDetail, + include: [{ + model: Lecture, + include: ['category'] + }] + }], + order: [['createdAt', 'DESC']] + }); + + res.status(200).json(transactions); + } catch (err) { + next(err); + } + } + + // Mendapatkan detail transaksi + static async getTransactionById(req, res, next) { + try { + const { id } = req.params; + + const transaction = await Transaction.findByPk(id, { + include: [{ + model: TransactionDetail, + include: [{ + model: Lecture, + include: ['category'] + }] + }, { + model: User, + attributes: ['id', 'username', 'email', 'phoneNumber'] + }] + }); + + if (!transaction) { + throw { name: "NotFound", message: "Transaction not found" }; + } + + // Cek apakah transaksi milik user yang sedang login atau admin + if (transaction.UserId !== req.user.id && req.user.role !== 'Admin') { + throw { name: "Forbidden", message: "You don't have permission to access this transaction" }; + } + + res.status(200).json(transaction); + } catch (err) { + next(err); + } + } + + // Membuat transaksi baru dari cart + static async createTransaction(req, res, next) { + const t = await sequelize.transaction(); + + try { + const UserId = req.user.id; + const { payment_method } = req.body; + + // Get cart items + const cart = await Cart.findAll({ + where: { UserId }, + include: [{ + model: Lecture + }] + }); + + if (cart.length === 0) { + throw { name: "BadRequest", message: "Cart is empty" }; + } + + // Calculate total amount + const total_amount = cart.reduce((sum, item) => sum + item.Lecture.price, 0); + + // Create transaction + const transaction = await Transaction.create({ + UserId, + total_amount, + payment_method, + status: 'Pending' + }, { transaction: t }); + + // Create transaction details + const transactionDetails = cart.map(item => ({ + TransactionId: transaction.id, + LectureId: item.LectureId, + price: item.Lecture.price + })); + + await TransactionDetail.bulkCreate(transactionDetails, { transaction: t }); + + // Clear cart + await Cart.destroy({ + where: { UserId }, + transaction: t + }); + + await t.commit(); + + // Get complete transaction with details + const newTransaction = await Transaction.findByPk(transaction.id, { + include: [{ + model: TransactionDetail, + include: [{ + model: Lecture + }] + }] + }); + + res.status(201).json(newTransaction); + } catch (err) { + await t.rollback(); + next(err); + } + } + + // Update status transaksi (admin only) + static async updateTransactionStatus(req, res, next) { + try { + const { id } = req.params; + const { status } = req.body; + + const transaction = await Transaction.findByPk(id); + + if (!transaction) { + throw { name: "NotFound", message: "Transaction not found" }; + } + + // Validasi status + const validStatus = ['Pending', 'Processing', 'Completed', 'Cancelled']; + if (!validStatus.includes(status)) { + throw { name: "BadRequest", message: "Invalid status" }; + } + + await transaction.update({ status }); + + res.status(200).json(transaction); + } catch (err) { + next(err); + } + } +} + +module.exports = TransactionController; \ No newline at end of file diff --git a/server/controllers/userController.js b/server/controllers/userController.js index ea6a4571..f25848c0 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -1,7 +1,8 @@ const { User } = require("../models"); -const { comparePassword } = require("../helpers/bcrypt"); +const { comparePassword, hashPassword } = require("../helpers/bcrypt"); const { generateToken } = require("../helpers/jwt"); const { OAuth2Client } = require("google-auth-library"); +const { Op } = require("sequelize"); const client = new OAuth2Client(); class UserController { static async googleLogin(req, res, next) { @@ -119,6 +120,129 @@ class UserController { next(err); } } + + // Untuk mendapatkan semua pengguna dengan pagination + static async getAllUsers(req, res, next) { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const search = req.query.search || ""; + const offset = (page - 1) * limit; + + const whereClause = {}; + if (search) { + whereClause[Op.or] = [ + { username: { [Op.iLike]: `%${search}%` } }, + { email: { [Op.iLike]: `%${search}%` } } + ]; + } + + const { count, rows } = await User.findAndCountAll({ + where: whereClause, + attributes: { exclude: ['password'] }, + limit, + offset, + order: [['createdAt', 'DESC']] + }); + + const totalPages = Math.ceil(count / limit); + + res.status(200).json({ + users: rows, + currentPage: page, + totalPages, + totalItems: count + }); + } catch (err) { + next(err); + } + } + + // Untuk membuat pengguna baru + static async createUser(req, res, next) { + try { + const { username, email, role, phoneNumber, address } = req.body; + + // Generate random password + const randomPassword = Math.random().toString(36).slice(-8); + const hashedPassword = hashPassword(randomPassword); + + const user = await User.create({ + username, + email, + password: hashedPassword, + role: role || 'User', + phoneNumber, + address + }); + + // Remove password from response + const userResponse = user.toJSON(); + delete userResponse.password; + + // In a real app, you would send an email with the temporary password + // For demo, just return it (not secure for production) + res.status(201).json({ + ...userResponse, + temporaryPassword: randomPassword + }); + } catch (err) { + next(err); + } + } + + // Untuk update pengguna + static async updateUser(req, res, next) { + try { + const { id } = req.params; + const { username, email, role, phoneNumber, address } = req.body; + + const user = await User.findByPk(id); + if (!user) { + throw { name: "NotFound", message: "User not found" }; + } + + // Update user data + await user.update({ + username, + email, + role, + phoneNumber, + address + }); + + // Remove password from response + const userResponse = user.toJSON(); + delete userResponse.password; + + res.status(200).json(userResponse); + } catch (err) { + next(err); + } + } + + // Untuk menghapus pengguna + static async deleteUser(req, res, next) { + try { + const { id } = req.params; + + const user = await User.findByPk(id); + if (!user) { + throw { name: "NotFound", message: "User not found" }; + } + + // Don't allow deleting admin users + if (user.role === "Admin") { + throw { name: "Forbidden", message: "Cannot delete admin users" }; + } + + await user.destroy(); + + res.status(200).json({ message: "User deleted successfully" }); + } catch (err) { + next(err); + } + } } module.exports = UserController; diff --git a/server/middlewares/authorization.js b/server/middlewares/authorization.js index 6419c5ad..8a20bac9 100644 --- a/server/middlewares/authorization.js +++ b/server/middlewares/authorization.js @@ -1,6 +1,6 @@ function adminAuthorization(req, res, next) { try { - if (req.user.role !== "admin") { + if (req.user.role !== "Admin") { throw { name: "Forbidden", message: "Admin access required" }; } next(); diff --git a/server/migrations/20250429224300-create-transaction.js b/server/migrations/20250429224300-create-transaction.js new file mode 100644 index 00000000..bea9707c --- /dev/null +++ b/server/migrations/20250429224300-create-transaction.js @@ -0,0 +1,53 @@ +'use strict'; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('Transactions', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + UserId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Users', + key: 'id' + }, + onDelete: 'CASCADE' + }, + total_amount: { + type: Sequelize.INTEGER, + allowNull: false + }, + payment_method: { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'Bank Transfer' + }, + status: { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'Pending' + }, + invoice_number: { + type: Sequelize.STRING, + allowNull: false, + unique: true + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('Transactions'); + } +}; \ No newline at end of file diff --git a/server/migrations/20250429224319-create-transaction-detail.js b/server/migrations/20250429224319-create-transaction-detail.js new file mode 100644 index 00000000..dac29f38 --- /dev/null +++ b/server/migrations/20250429224319-create-transaction-detail.js @@ -0,0 +1,54 @@ +'use strict'; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('TransactionDetails', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + TransactionId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Transactions', + key: 'id' + }, + onDelete: 'CASCADE' + }, + LectureId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Lectures', + key: 'id' + }, + onDelete: 'CASCADE' + }, + price: { + type: Sequelize.INTEGER, + allowNull: false + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + + // Menambahkan unique constraint untuk mencegah duplikasi item dalam satu transaksi + await queryInterface.addConstraint('TransactionDetails', { + fields: ['TransactionId', 'LectureId'], + type: 'unique', + name: 'unique_transaction_lecture' + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('TransactionDetails'); + } +}; \ No newline at end of file diff --git a/server/models/lecture.js b/server/models/lecture.js index 048e09d5..af348aec 100644 --- a/server/models/lecture.js +++ b/server/models/lecture.js @@ -11,6 +11,12 @@ module.exports = (sequelize, DataTypes) => { Lecture.belongsToMany(models.User, { through: models.Cart, }); + Lecture.hasMany(models.TransactionDetail, { foreignKey: "LectureId" }); + Lecture.belongsToMany(models.Transaction, { + through: models.TransactionDetail, + foreignKey: "LectureId", + otherKey: "TransactionId", + }); } } Lecture.init( @@ -94,7 +100,7 @@ module.exports = (sequelize, DataTypes) => { model: "Users", key: "id", }, - } + }, }, { sequelize, diff --git a/server/models/transaction.js b/server/models/transaction.js new file mode 100644 index 00000000..59a8ab89 --- /dev/null +++ b/server/models/transaction.js @@ -0,0 +1,83 @@ +"use strict"; +const { Model } = require("sequelize"); +module.exports = (sequelize, DataTypes) => { + class Transaction extends Model { + static associate(models) { + Transaction.belongsTo(models.User, { foreignKey: "UserId" }); + Transaction.hasMany(models.TransactionDetail, { + foreignKey: "TransactionId", + }); + + // Transaction juga bisa memiliki asosiasi many-to-many dengan Lecture melalui TransactionDetail + Transaction.belongsToMany(models.Lecture, { + through: models.TransactionDetail, + foreignKey: "TransactionId", + otherKey: "LectureId", + }); + } + } + Transaction.init( + { + UserId: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { msg: "User ID is required" }, + notEmpty: { msg: "User ID cannot be empty" }, + }, + }, + total_amount: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { msg: "Total amount is required" }, + min: { + args: [0], + msg: "Total amount cannot be negative", + }, + }, + }, + payment_method: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "Bank Transfer", + validate: { + notEmpty: { msg: "Payment method cannot be empty" }, + }, + }, + status: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "Pending", + validate: { + isIn: { + args: [["Pending", "Processing", "Completed", "Cancelled"]], + msg: "Status must be one of: Pending, Processing, Completed, Cancelled", + }, + }, + }, + invoice_number: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + validate: { + notEmpty: { msg: "Invoice number cannot be empty" }, + }, + }, + }, + { + sequelize, + modelName: "Transaction", + } + ); + Transaction.beforeValidate((transaction) => { + if (!transaction.invoice_number) { + // Format: INV-YYYYMMDD-XXXXX (X = random) + const date = new Date(); + const dateStr = date.toISOString().slice(0, 10).replace(/-/g, ""); + const random = Math.floor(10000 + Math.random() * 90000); // 5 digit random + transaction.invoice_number = `INV-${dateStr}-${random}`; + } + }); + return Transaction; +}; diff --git a/server/models/transactiondetail.js b/server/models/transactiondetail.js new file mode 100644 index 00000000..6bae7bc4 --- /dev/null +++ b/server/models/transactiondetail.js @@ -0,0 +1,51 @@ +'use strict'; +const { Model } = require('sequelize'); + +module.exports = (sequelize, DataTypes) => { + class TransactionDetail extends Model { + static associate(models) { + // Definisi asosiasi + TransactionDetail.belongsTo(models.Transaction, { foreignKey: 'TransactionId' }); + TransactionDetail.belongsTo(models.Lecture, { foreignKey: 'LectureId' }); + } + } + + TransactionDetail.init({ + TransactionId: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { msg: 'Transaction ID is required' } + } + }, + LectureId: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + notNull: { msg: 'Lecture ID is required' } + } + }, + price: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + min: { + args: [0], + msg: 'Price cannot be negative' + } + } + } + }, { + sequelize, + modelName: 'TransactionDetail', + indexes: [ + { + unique: true, + fields: ['TransactionId', 'LectureId'], + name: 'unique_transaction_lecture' + } + ] + }); + + return TransactionDetail; +}; \ No newline at end of file diff --git a/server/models/user.js b/server/models/user.js index da075143..e563d971 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -4,12 +4,11 @@ const { hashPassword } = require("../helpers/bcrypt"); module.exports = (sequelize, DataTypes) => { class User extends Model { static associate(models) { - // Relasi User → Lecture (One-to-Many, untuk pengelola lecture) User.hasMany(models.Lecture); - // Relasi User ↔ Lecture (Many-to-Many melalui Cart) User.belongsToMany(models.Lecture, { through: models.Cart, }); + User.hasMany(models.Transaction, { foreignKey: 'UserId' }); } } User.init( diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js index d3d14136..074c0acb 100644 --- a/server/routes/adminRoutes.js +++ b/server/routes/adminRoutes.js @@ -2,6 +2,8 @@ const express = require("express"); const router = express.Router(); const CategoryController = require("../controllers/categoryController"); const LectureController = require("../controllers/lectureController"); +const UserController = require("../controllers/userController"); +const AdminController = require("../controllers/adminController"); const authentication = require("../middlewares/authentication"); const { adminAuthorization } = require("../middlewares/authorization"); @@ -9,6 +11,13 @@ const { adminAuthorization } = require("../middlewares/authorization"); router.use(authentication); router.use(adminAuthorization); +// Dashboard APIs +router.get("/statistics", AdminController.getStatistics); +router.get("/recent-users", AdminController.getRecentUsers); // Ubah nama route untuk menghindari konflik +router.get("/orders", AdminController.getRecentOrders); +router.get("/categories/stats", AdminController.getCategoriesStats); +router.get("/orders/monthly", AdminController.getMonthlySales); + // Categories routes router.get("/categories", CategoryController.getAllCategories); router.post("/categories", CategoryController.createCategory); @@ -21,4 +30,10 @@ router.post("/lectures", LectureController.createLecture); router.put("/lectures/:id", LectureController.updateLecture); router.delete("/lectures/:id", LectureController.deleteLecture); +// Users routes +router.get("/users", UserController.getAllUsers); +router.post("/users", UserController.createUser); +router.put("/users/:id", UserController.updateUser); +router.delete("/users/:id", UserController.deleteUser); + module.exports = router; \ No newline at end of file diff --git a/server/routes/index.js b/server/routes/index.js index 41b2986a..3dd750b3 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -6,6 +6,7 @@ const lectureRoutes = require("./lectureRoutes"); const cartRoutes = require("./cartRoutes"); const publicRoutes = require("./publicRoutes"); const adminRoutes = require("./adminRoutes"); // Tambahkan ini +const transactionRoutes = require("./transactionRoutes"); // Tambahkan ini // Public routes - tidak memerlukan authentication router.use("/public", publicRoutes); @@ -16,5 +17,6 @@ router.use("/categories", categoryRoutes); router.use("/lectures", lectureRoutes); router.use("/carts", cartRoutes); router.use("/admin", adminRoutes); // Tambahkan ini +router.use("/transactions", transactionRoutes); // Tambahkan ini module.exports = router; \ No newline at end of file diff --git a/server/routes/transactionRoutes.js b/server/routes/transactionRoutes.js new file mode 100644 index 00000000..90de39a2 --- /dev/null +++ b/server/routes/transactionRoutes.js @@ -0,0 +1,19 @@ +const express = require("express"); +const router = express.Router(); +const TransactionController = require("../controllers/transactionController"); +const authentication = require("../middlewares/authentication"); +const { adminAuthorization } = require("../middlewares/authorization"); + +// Semua route transaction memerlukan autentikasi +router.use(authentication); + +// Route untuk user +router.get("/user", TransactionController.getUserTransactions); +router.post("/", TransactionController.createTransaction); +router.get("/:id", TransactionController.getTransactionById); + +// Route untuk admin +router.get("/", adminAuthorization, TransactionController.getAllTransactions); +router.put("/:id/status", adminAuthorization, TransactionController.updateTransactionStatus); + +module.exports = router; \ No newline at end of file From ef864c4f8e98e8e2bcde1c1a9434e33bbd9576b6 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Wed, 30 Apr 2025 09:07:35 +0700 Subject: [PATCH 19/51] chore: update using Midtrans API for payment --- client/index.html | 5 + client/src/App.jsx | 6 + client/src/components/AdminSidebar.jsx | 9 + client/src/pages/Admin/Payments.jsx | 188 +++++++++++ client/src/pages/Checkout.jsx | 407 +++++++++--------------- client/src/pages/PaymentResult.jsx | 205 ++++++++++++ server/controllers/cartController.js | 50 +-- server/controllers/paymentController.js | 196 ++++++++++++ server/helpers/midtrans.js | 79 +++++ server/package-lock.json | 40 +++ server/package.json | 1 + server/routes/cartRoutes.js | 1 + server/routes/index.js | 2 + server/routes/paymentRoutes.js | 15 + 14 files changed, 915 insertions(+), 289 deletions(-) create mode 100644 client/src/pages/Admin/Payments.jsx create mode 100644 client/src/pages/PaymentResult.jsx create mode 100644 server/controllers/paymentController.js create mode 100644 server/helpers/midtrans.js create mode 100644 server/routes/paymentRoutes.js diff --git a/client/index.html b/client/index.html index c2fbac1d..557879c3 100644 --- a/client/index.html +++ b/client/index.html @@ -11,6 +11,11 @@ integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous" /> + diff --git a/client/src/App.jsx b/client/src/App.jsx index 9985b630..753a0af2 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -18,6 +18,7 @@ import Checkout from "./pages/Checkout"; import UserProfile from "./pages/UserProfile"; import Login from "./pages/Login"; import Register from "./pages/Register"; +import PaymentResult from "./pages/PaymentResult"; // Tambahkan import // Admin Pages import AdminDashboard from "./pages/Admin/Dashboard"; @@ -80,6 +81,11 @@ function AppRoutes() { + {/* Payment Result Routes */} + } /> + } /> + } /> + {/* 404 Not Found */} } /> diff --git a/client/src/components/AdminSidebar.jsx b/client/src/components/AdminSidebar.jsx index 3dc81350..77cab917 100644 --- a/client/src/components/AdminSidebar.jsx +++ b/client/src/components/AdminSidebar.jsx @@ -74,6 +74,15 @@ export default function AdminSidebar() { Categories +
  • + + + Payments + +
  • diff --git a/client/src/pages/Admin/Payments.jsx b/client/src/pages/Admin/Payments.jsx new file mode 100644 index 00000000..ecd3c858 --- /dev/null +++ b/client/src/pages/Admin/Payments.jsx @@ -0,0 +1,188 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router'; +import api from '../../utils/api'; + +export default function AdminPayments() { + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [statusFilter, setStatusFilter] = useState(""); + + useEffect(() => { + fetchTransactions(); + }, [currentPage, statusFilter]); + + const fetchTransactions = async () => { + try { + setLoading(true); + const params = new URLSearchParams(); + params.append("page", currentPage); + if (statusFilter) params.append("status", statusFilter); + + const { data } = await api.get(`/admin/transactions?${params}`); + setTransactions(data.transactions); + setTotalPages(data.totalPages || 1); + } catch (error) { + console.error("Error fetching transactions:", error); + setError("Failed to load transactions"); + } finally { + setLoading(false); + } + }; + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const getStatusBadgeClass = (status) => { + switch (status) { + case 'Completed': + return 'bg-success'; + case 'Processing': + return 'bg-info'; + case 'Pending': + return 'bg-warning'; + case 'Cancelled': + case 'Failed': + return 'bg-danger'; + case 'Refunded': + return 'bg-secondary'; + default: + return 'bg-secondary'; + } + }; + + // Additional functions would go here for transaction management + + if (loading && transactions.length === 0) { + return ( +
    +

    Payment Transactions

    +
    +
    + Loading... +
    +
    +
    + ); + } + + return ( +
    +

    Payment Transactions

    + +
    +
    +
    Payment List
    +
    + +
    +
    +
    + {error ? ( +
    {error}
    + ) : ( +
    + + + + + + + + + + + + + + {transactions.length === 0 ? ( + + + + ) : ( + transactions.map((transaction) => ( + + + + + + + + + + )) + )} + +
    InvoiceCustomerDateAmountStatusPayment MethodActions
    No transactions found
    {transaction.invoice_number}{transaction.User?.username}{new Date(transaction.createdAt).toLocaleDateString()}{formatToIDR(transaction.total_amount)} + + {transaction.status} + + {transaction.payment_method} + + View + +
    +
    + )} + + {/* Pagination controls */} + {totalPages > 1 && ( + + )} +
    +
    +
    + ); +} \ No newline at end of file diff --git a/client/src/pages/Checkout.jsx b/client/src/pages/Checkout.jsx index 063dd39a..99f1e8db 100644 --- a/client/src/pages/Checkout.jsx +++ b/client/src/pages/Checkout.jsx @@ -1,72 +1,30 @@ -import { useState, useEffect } from "react"; -import { Link, useNavigate } from "react-router"; -import api from "../utils/api"; -import { useAuth } from "../context/AuthContext"; +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import api from '../utils/api'; export default function Checkout() { const navigate = useNavigate(); - const { user } = useAuth(); const [cartItems, setCartItems] = useState([]); const [loading, setLoading] = useState(true); const [processing, setProcessing] = useState(false); const [error, setError] = useState(null); - const [formData, setFormData] = useState({ - fullName: user?.username || "", - email: user?.email || "", - phone: user?.phoneNumber || "", - address: user?.address || "", - paymentMethod: "bank_transfer" - }); - + useEffect(() => { - const fetchCartItems = async () => { + const fetchCart = async () => { try { - const { data } = await api.get("/carts"); - setCartItems(data); + const response = await api.get('/carts'); + setCartItems(response.data); } catch (error) { - console.error("Error fetching cart:", error); - setError("Failed to load your cart items. Please try again."); + console.error('Error fetching cart:', error); + setError('Failed to load your cart. Please try again.'); } finally { setLoading(false); } }; - - fetchCartItems(); - }, []); - - const handleChange = (e) => { - const { name, value } = e.target; - setFormData(prevData => ({ - ...prevData, - [name]: value - })); - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - setProcessing(true); - try { - // Create order from cart - const response = await api.post("/orders", { - ...formData, - items: cartItems.map(item => ({ - lectureId: item.LectureId, - price: item.Lecture.price - })) - }); - - // Redirect to success page with order ID - navigate(`/checkout/success?orderId=${response.data.id}`); - - } catch (error) { - console.error("Checkout error:", error); - setError(error.response?.data?.message || "Failed to process your order. Please try again."); - } finally { - setProcessing(false); - } - }; - + fetchCart(); + }, []); + const formatToIDR = (price) => { return new Intl.NumberFormat('id-ID', { style: 'currency', @@ -74,249 +32,168 @@ export default function Checkout() { minimumFractionDigits: 0 }).format(price); }; - + const calculateTotal = () => { return cartItems.reduce((sum, item) => sum + item.Lecture.price, 0); }; - + + const handlePayment = async () => { + try { + setProcessing(true); + + const response = await api.post('/payments/create'); + + // Handle client-side Midtrans configuration + const { token, redirect_url } = response.data.payment; + + if (redirect_url) { + // Redirect to Midtrans payment page + window.location.href = redirect_url; + } else if (token) { + // Use Snap.js to show popup payment + window.snap.pay(token, { + onSuccess: function(result) { + navigate(`/payment/success?order_id=${response.data.transaction.invoice_number}`); + }, + onPending: function(result) { + navigate(`/payment/pending?order_id=${response.data.transaction.invoice_number}`); + }, + onError: function(result) { + navigate(`/payment/failed?order_id=${response.data.transaction.invoice_number}`); + }, + onClose: function() { + alert('You closed the payment window. Please complete your payment to access the courses.'); + } + }); + } + } catch (error) { + console.error('Payment error:', error); + setError('Failed to process payment. Please try again.'); + setProcessing(false); + } + }; + if (loading) { return ( -
    -
    - Loading... +
    +
    +
    + Loading... +
    ); } - + if (cartItems.length === 0) { return (
    -
    - -

    Your cart is empty

    -

    You need to add courses to your cart before checkout.

    - Explore Courses +
    +
    +

    Your cart is empty

    +

    Please add courses to your cart before checkout

    + +
    ); } - + return (
    -

    Checkout

    - - {error && ( -
    - {error} +
    +
    +
    +
    +
    Order Summary
    +
    +
    + + + + + + + + + {cartItems.map(item => ( + + + + + ))} + + + + + + + +
    CoursePrice
    +
    +
    {item.Lecture.name}
    +

    {item.Lecture.technique}

    + {item.Lecture.category?.name} +
    +
    {formatToIDR(item.Lecture.price)}
    Total{formatToIDR(calculateTotal())}
    +
    +
    - )} - -
    -
    -

    Billing Details

    -
    -
    -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    + +
    +
    +
    +
    Payment
    - -
    - -

    Payment Method

    - -
    -
    - - -
    +
    +

    Click the button below to proceed with payment through Midtrans secure payment gateway.

    -
    - - +
    +
    -
    - - -
    -
    - - {formData.paymentMethod === "credit_card" && ( -
    -
    - - - Full name as displayed on card + {error && ( +
    + {error}
    - -
    - - -
    - -
    - - -
    - -
    - - -
    -
    - )} - -
    - - - -
    - -
    -
    -
    -

    Order Summary

    -
    -
    -
      - {cartItems.map((item) => ( -
    • -
      -
      {item.Lecture.name}
      - {item.Lecture.technique} -
      - {formatToIDR(item.Lecture.price)} -
    • - ))} - -
    • - Total - {formatToIDR(calculateTotal())} -
    • -
    - - Return to Cart - + + By clicking "Pay Now", you will be redirected to Midtrans secure payment page. +
    -
    +
    -
    Why Choose Our Courses
    -
      -
    • - - Industry-recognized certifications -
    • -
    • - - Expert instructors with real-world experience -
    • -
    • - - Lifetime access to course materials -
    • -
    • - - 30-day money-back guarantee -
    • -
    +
    Accepted Payment Methods
    +
    + BCA + Mandiri + BNI + BRI + GoPay +
    diff --git a/client/src/pages/PaymentResult.jsx b/client/src/pages/PaymentResult.jsx new file mode 100644 index 00000000..f0891c61 --- /dev/null +++ b/client/src/pages/PaymentResult.jsx @@ -0,0 +1,205 @@ +import { useState, useEffect } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router'; +import api from '../utils/api'; + +export default function PaymentResult() { + const location = useLocation(); + const navigate = useNavigate(); + const [transaction, setTransaction] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Get query params + const params = new URLSearchParams(location.search); + const orderId = params.get('order_id'); + const status = location.pathname.split('/').pop(); // success, failed, or pending + + useEffect(() => { + if (!orderId) { + navigate('/'); + return; + } + + const fetchTransactionStatus = async () => { + try { + const response = await api.get(`/payments/status/${orderId}`); + setTransaction(response.data); + } catch (error) { + console.error('Error fetching transaction:', error); + setError('Failed to load transaction details.'); + } finally { + setLoading(false); + } + }; + + fetchTransactionStatus(); + }, [orderId, navigate]); + + const formatToIDR = (price) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0 + }).format(price); + }; + + const getStatusDetails = () => { + switch (status) { + case 'success': + return { + title: 'Payment Successful!', + message: 'Thank you for your purchase. You can now access your courses.', + icon: 'bi-check-circle-fill text-success', + variant: 'success' + }; + case 'pending': + return { + title: 'Payment is Processing', + message: 'Your payment is being processed. We\'ll notify you once it\'s complete.', + icon: 'bi-hourglass-split text-warning', + variant: 'warning' + }; + case 'failed': + return { + title: 'Payment Failed', + message: 'Sorry, your payment could not be processed. Please try again.', + icon: 'bi-x-circle-fill text-danger', + variant: 'danger' + }; + default: + return { + title: 'Payment Status', + message: 'Here\'s the status of your payment.', + icon: 'bi-info-circle-fill text-info', + variant: 'info' + }; + } + }; + + const statusDetails = getStatusDetails(); + + if (loading) { + return ( +
    +
    + Loading... +
    +

    Retrieving your payment information...

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

    {statusDetails.title}

    +

    {statusDetails.message}

    +
    +
    + + {transaction && ( +
    +
    +
    +
    Payment Details
    + + + + + + + + + + + + + + + + + + + + + + + +
    Invoice Number: {transaction.invoice_number}
    Date: {new Date(transaction.createdAt).toLocaleString()}
    Total Amount: {formatToIDR(transaction.total_amount)}
    Payment Method: {transaction.payment_method}
    Status + : + {transaction.status} + +
    +
    + +
    +
    Purchased Courses
    + {transaction.TransactionDetails?.length > 0 ? ( +
    + {transaction.TransactionDetails.map(detail => ( +
    +
    +
    {detail.Lecture?.name}
    + {formatToIDR(detail.price)} +
    +

    {detail.Lecture?.technique}

    + {transaction.status === 'Completed' && ( + + + View Course + + + )} +
    + ))} +
    + ) : ( +

    No course details available

    + )} +
    +
    + +
    + + Go to Home + + + + Browse More Courses + + + {transaction.status === 'Pending' && ( + + Complete Payment + + )} +
    +
    + )} + + {error && ( +
    +
    + {error} +
    +
    + Back to Home +
    +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/server/controllers/cartController.js b/server/controllers/cartController.js index 310481a4..8f8c87d1 100644 --- a/server/controllers/cartController.js +++ b/server/controllers/cartController.js @@ -4,7 +4,7 @@ class CartController { static async getUserCart(req, res, next) { try { const userId = req.user.id; - + const carts = await Cart.findAll({ where: { UserId: userId }, include: [ @@ -13,13 +13,13 @@ class CartController { include: [ { model: Category, - as: 'category' - } - ] - } - ] + as: "category", + }, + ], + }, + ], }); - + res.status(200).json(carts); } catch (err) { next(err); @@ -30,30 +30,30 @@ class CartController { try { const UserId = req.user.id; const { lectureId } = req.body; - + // Check if lecture exists const lecture = await Lecture.findByPk(lectureId); if (!lecture) { throw { name: "NotFound", message: "Lecture not found" }; } - + // Check if already in cart const existingCart = await Cart.findOne({ - where: { - UserId, - LectureId: lectureId - } + where: { + UserId, + LectureId: lectureId, + }, }); - + if (existingCart) { throw { name: "BadRequest", message: "Lecture already in cart" }; } - + const newCart = await Cart.create({ UserId, - LectureId: lectureId + LectureId: lectureId, }); - + res.status(201).json(newCart); } catch (err) { next(err); @@ -64,22 +64,24 @@ class CartController { try { const UserId = req.user.id; const { id } = req.params; - + const cart = await Cart.findOne({ - where: { id, UserId } + where: { id, UserId }, }); - + if (!cart) { throw { name: "NotFound", message: "Cart item not found" }; } - + await cart.destroy(); - - res.status(200).json({ message: "Lecture removed from cart successfully" }); + + res + .status(200) + .json({ message: "Lecture removed from cart successfully" }); } catch (err) { next(err); } } } -module.exports = CartController; \ No newline at end of file +module.exports = CartController; diff --git a/server/controllers/paymentController.js b/server/controllers/paymentController.js new file mode 100644 index 00000000..80eef871 --- /dev/null +++ b/server/controllers/paymentController.js @@ -0,0 +1,196 @@ +const { + Transaction, + User, + Cart, + TransactionDetail, + Lecture, + sequelize, +} = require("../models"); +const { + createPaymentToken, + handleNotification, +} = require("../helpers/midtrans"); + +class PaymentController { + // Create payment for a transaction + static async createPayment(req, res, next) { + const t = await sequelize.transaction(); + + try { + const UserId = req.user.id; + + // Get user details + const user = await User.findByPk(UserId); + if (!user) { + throw { name: "NotFound", message: "User not found" }; + } + + // Get cart items + const cartItems = await Cart.findAll({ + where: { UserId }, + include: [{ model: Lecture }], + transaction: t, + }); + + if (!cartItems.length) { + throw { name: "BadRequest", message: "Your cart is empty" }; + } + + // Calculate total amount + const total_amount = cartItems.reduce( + (sum, item) => sum + item.Lecture.price, + 0 + ); + + // Generate unique invoice number + const date = new Date(); + const dateStr = date.toISOString().slice(0, 10).replace(/-/g, ""); + const random = Math.floor(10000 + Math.random() * 90000); // 5 digit random + const invoice_number = `INV-${dateStr}-${random}`; + + // Create new transaction + const transaction = await Transaction.create( + { + UserId, + total_amount, + payment_method: "Midtrans", + status: "Pending", + invoice_number, + }, + { transaction: t } + ); + + // Create transaction details + const transactionDetails = cartItems.map((item) => ({ + TransactionId: transaction.id, + LectureId: item.Lecture.id, + price: item.Lecture.price, + })); + + await TransactionDetail.bulkCreate(transactionDetails, { + transaction: t, + }); + + // Clear cart + await Cart.destroy({ + where: { UserId }, + transaction: t, + }); + + // Create Midtrans payment token + const transactionWithUser = { + ...transaction.toJSON(), + User: user, + }; + + const paymentToken = await createPaymentToken(transactionWithUser); + + await t.commit(); + + // Return payment details + res.status(201).json({ + message: "Payment initiated successfully", + transaction: { + id: transaction.id, + invoice_number: transaction.invoice_number, + total_amount: transaction.total_amount, + status: transaction.status, + }, + payment: paymentToken, + }); + } catch (error) { + await t.rollback(); + next(error); + } + } + + // Handle notification from Midtrans + static async handleNotification(req, res, next) { + try { + const notificationJson = req.body; + + const { orderId, transactionStatus, fraudStatus } = + await handleNotification(notificationJson); + + // Find our transaction by invoice number (order_id in Midtrans) + const transaction = await Transaction.findOne({ + where: { invoice_number: orderId }, + }); + + if (!transaction) { + throw { name: "NotFound", message: "Transaction not found" }; + } + + // Update transaction status based on Midtrans status + let newStatus; + + if (transactionStatus == "capture") { + if (fraudStatus == "accept") { + newStatus = "Completed"; + } else if (fraudStatus == "challenge") { + newStatus = "Processing"; + } + } else if (transactionStatus == "settlement") { + newStatus = "Completed"; + } else if (transactionStatus == "pending") { + newStatus = "Pending"; + } else if ( + transactionStatus == "deny" || + transactionStatus == "cancel" || + transactionStatus == "expire" + ) { + newStatus = "Cancelled"; + } else if (transactionStatus == "refund") { + newStatus = "Refunded"; + } + + if (newStatus) { + await transaction.update({ status: newStatus }); + } + + res.status(200).json({ message: "Notification processed" }); + } catch (error) { + console.error("Error processing notification:", error); + next(error); + } + } + + // Get payment status by invoice number + static async getPaymentStatus(req, res, next) { + try { + const { invoice } = req.params; + + const transaction = await Transaction.findOne({ + where: { invoice_number: invoice }, + include: [ + { + model: User, + attributes: ["id", "username", "email"], + }, + { + model: TransactionDetail, + include: [{ model: Lecture }], + }, + ], + }); + + if (!transaction) { + throw { name: "NotFound", message: "Transaction not found" }; + } + + // Check if user is authorized to view this transaction + if (transaction.UserId !== req.user.id && req.user.role !== "Admin") { + throw { + name: "Forbidden", + message: "You are not authorized to view this transaction", + }; + } + + res.status(200).json(transaction); + } catch (error) { + next(error); + } + } +} + +module.exports = PaymentController; diff --git a/server/helpers/midtrans.js b/server/helpers/midtrans.js new file mode 100644 index 00000000..991c5647 --- /dev/null +++ b/server/helpers/midtrans.js @@ -0,0 +1,79 @@ +const midtransClient = require('midtrans-client'); + +// Create Snap API instance +const snap = new midtransClient.Snap({ + isProduction: process.env.MIDTRANS_IS_PRODUCTION === 'true', + serverKey: process.env.MIDTRANS_SERVER_KEY, + clientKey: process.env.MIDTRANS_CLIENT_KEY +}); + +// Create Core API instance (for handling notifications) +const coreApi = new midtransClient.CoreApi({ + isProduction: process.env.MIDTRANS_IS_PRODUCTION === 'true', + serverKey: process.env.MIDTRANS_SERVER_KEY, + clientKey: process.env.MIDTRANS_CLIENT_KEY +}); + +// Function to create transaction token +const createPaymentToken = async (transaction) => { + const { id, invoice_number, total_amount, User } = transaction; + + const parameter = { + transaction_details: { + order_id: invoice_number, + gross_amount: total_amount + }, + credit_card: { + secure: true + }, + customer_details: { + first_name: User.username || 'Customer', + email: User.email, + phone: User.phoneNumber || '' + }, + callbacks: { + finish: `${process.env.CLIENT_URL}/payment/success?order_id=${invoice_number}`, + error: `${process.env.CLIENT_URL}/payment/failed?order_id=${invoice_number}`, + pending: `${process.env.CLIENT_URL}/payment/pending?order_id=${invoice_number}` + } + }; + + try { + const transaction = await snap.createTransaction(parameter); + return { + token: transaction.token, + redirect_url: transaction.redirect_url + }; + } catch (error) { + console.error('Error creating Midtrans transaction:', error); + throw error; + } +}; + +// Function to handle webhook notifications +const handleNotification = async (notificationJson) => { + try { + const statusResponse = await coreApi.transaction.notification(notificationJson); + const orderId = statusResponse.order_id; + const transactionStatus = statusResponse.transaction_status; + const fraudStatus = statusResponse.fraud_status; + + console.log(`Transaction notification received. Order ID: ${orderId}. Transaction status: ${transactionStatus}. Fraud status: ${fraudStatus}`); + + // Return the details as an object for further processing + return { + orderId, + transactionStatus, + fraudStatus, + statusResponse + }; + } catch (error) { + console.error('Error handling notification:', error); + throw error; + } +}; + +module.exports = { + createPaymentToken, + handleNotification +}; \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 7b36853f..2b6552dc 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,6 +15,7 @@ "express": "^5.1.0", "google-auth-library": "^9.15.1", "jsonwebtoken": "^9.0.2", + "midtrans-client": "^1.4.2", "pg": "^8.15.6", "sequelize": "^6.37.7" }, @@ -1363,6 +1364,15 @@ "node": ">= 4.0.0" } }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2732,6 +2742,26 @@ "node": ">=8" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4744,6 +4774,16 @@ "node": ">=8.6" } }, + "node_modules/midtrans-client": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/midtrans-client/-/midtrans-client-1.4.2.tgz", + "integrity": "sha512-hGT6UDF6WsmOprJYdgxReT5qxOPj+9VGVbJTe6txYICkadI01yC1ApBlkf+5AH/2v4fWNo03421VVpNfJDFAyg==", + "license": "MIT", + "dependencies": { + "axios": "^0.26.0", + "lodash": "^4.17.21" + } + }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", diff --git a/server/package.json b/server/package.json index 4585fa5f..0d948bc1 100644 --- a/server/package.json +++ b/server/package.json @@ -17,6 +17,7 @@ "express": "^5.1.0", "google-auth-library": "^9.15.1", "jsonwebtoken": "^9.0.2", + "midtrans-client": "^1.4.2", "pg": "^8.15.6", "sequelize": "^6.37.7" }, diff --git a/server/routes/cartRoutes.js b/server/routes/cartRoutes.js index a92e0629..6380676e 100644 --- a/server/routes/cartRoutes.js +++ b/server/routes/cartRoutes.js @@ -9,4 +9,5 @@ router.get("/", CartController.getUserCart); router.post("/add", CartController.addToCart); router.delete("/:id", CartController.removeFromCart); + module.exports = router; \ No newline at end of file diff --git a/server/routes/index.js b/server/routes/index.js index 3dd750b3..345e65c6 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -7,6 +7,7 @@ const cartRoutes = require("./cartRoutes"); const publicRoutes = require("./publicRoutes"); const adminRoutes = require("./adminRoutes"); // Tambahkan ini const transactionRoutes = require("./transactionRoutes"); // Tambahkan ini +const paymentRoutes = require('./paymentRoutes'); // Tambahkan ini // Public routes - tidak memerlukan authentication router.use("/public", publicRoutes); @@ -18,5 +19,6 @@ router.use("/lectures", lectureRoutes); router.use("/carts", cartRoutes); router.use("/admin", adminRoutes); // Tambahkan ini router.use("/transactions", transactionRoutes); // Tambahkan ini +router.use('/payments', paymentRoutes); // Tambahkan ini module.exports = router; \ No newline at end of file diff --git a/server/routes/paymentRoutes.js b/server/routes/paymentRoutes.js new file mode 100644 index 00000000..5ddeb9b3 --- /dev/null +++ b/server/routes/paymentRoutes.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const PaymentController = require('../controllers/paymentController'); +const authentication = require('../middlewares/authentication'); + +// Create payment (requires authentication) +router.post('/create', authentication, PaymentController.createPayment); + +// Check payment status (requires authentication) +router.get('/status/:invoice', authentication, PaymentController.getPaymentStatus); + +// Handle notifications from Midtrans +router.post('/notification', PaymentController.handleNotification); + +module.exports = router; \ No newline at end of file From d064fde57b572e0480ea2fcb0f27fd84fa181690 Mon Sep 17 00:00:00 2001 From: hafizhabdul Date: Wed, 30 Apr 2025 13:13:05 +0700 Subject: [PATCH 20/51] chore: update Dialogflow API for chatbot --- client/index.html | 2 +- client/public/technical-support.png | Bin 0 -> 23336 bytes client/src/assets/react.svg | 1 - client/src/components/Chatbot.jsx | 210 ++++ client/src/layouts/MainLayout.jsx | 3 + client/src/pages/Categories.jsx | 6 +- client/src/styles/chatbot.css | 310 ++++++ server/.gitignore | 5 +- server/app.js | 1 - server/controllers/chatbotController.js | 53 + server/data/lectures.json | 34 +- server/helpers/dialogflow.js | 52 + server/package-lock.json | 1286 ++++++++++++++++++++++- server/package.json | 5 +- server/routes/chatbotRoutes.js | 8 + server/routes/index.js | 2 + server/test-dialogflow.js | 48 + 17 files changed, 1955 insertions(+), 71 deletions(-) create mode 100644 client/public/technical-support.png delete mode 100644 client/src/assets/react.svg create mode 100644 client/src/components/Chatbot.jsx create mode 100644 client/src/styles/chatbot.css create mode 100644 server/controllers/chatbotController.js create mode 100644 server/helpers/dialogflow.js create mode 100644 server/routes/chatbotRoutes.js create mode 100644 server/test-dialogflow.js diff --git a/client/index.html b/client/index.html index 557879c3..29363bdb 100644 --- a/client/index.html +++ b/client/index.html @@ -14,7 +14,7 @@ diff --git a/client/public/technical-support.png b/client/public/technical-support.png new file mode 100644 index 0000000000000000000000000000000000000000..65379070fa27ff1d7d165d30b794279b5daf87d7 GIT binary patch literal 23336 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 JrfK);{{TQX;Wq#P literal 0 HcmV?d00001 diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/components/Chatbot.jsx b/client/src/components/Chatbot.jsx new file mode 100644 index 00000000..67f7ad39 --- /dev/null +++ b/client/src/components/Chatbot.jsx @@ -0,0 +1,210 @@ +import { useState, useEffect, useRef } from 'react'; +import api from '../utils/api'; +import '../styles/chatbot.css'; // Kita akan buat file CSS ini + +export default function Chatbot() { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { + sender: 'bot', + text: 'Halo! Saya SNS Assistant. Ada yang bisa saya bantu tentang kursus NDT?', + timestamp: new Date() + } + ]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [sessionId, setSessionId] = useState(''); + const messagesEndRef = useRef(null); + + // Generate session ID saat komponen dimount + useEffect(() => { + setSessionId(`user-${Date.now()}-${Math.floor(Math.random() * 1000)}`); + }, []); + + // Auto-scroll ke pesan terbaru + useEffect(() => { + if (isOpen) { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [messages, isOpen]); + + const handleToggleChat = () => { + setIsOpen(!isOpen); + }; + + const handleInputChange = (e) => { + setInput(e.target.value); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!input.trim()) return; + + const userMessage = { + sender: 'user', + text: input, + timestamp: new Date() + }; + + setMessages(prev => [...prev, userMessage]); + setInput(''); + setLoading(true); + + try { + const response = await api.post('/chatbot/send', { + message: userMessage.text, + sessionId + }); + + // Tambahkan respons bot + setMessages(prev => [ + ...prev, + { + sender: 'bot', + text: response.data.text, + courses: response.data.courses, // Jika ada data kursus + timestamp: new Date() + } + ]); + } catch (error) { + console.error('Error sending message to chatbot:', error); + setMessages(prev => [ + ...prev, + { + sender: 'bot', + text: 'Maaf, saya mengalami kendala teknis. Silakan coba lagi nanti.', + timestamp: new Date() + } + ]); + } finally { + setLoading(false); + } + }; + + const formatTime = (date) => { + return new Date(date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + return ( +
    + {/* Tombol untuk membuka/menutup chatbot */} + + + {/* Chat window */} +
    +
    +
    + SNS NDT +
    +
    SNS Assistant
    + Online +
    +
    + +
    + +
    + {messages.map((msg, index) => ( +
    + {msg.sender === 'bot' && ( +
    + Bot +
    + )} + +
    +
    +

    {msg.text}

    + {formatTime(msg.timestamp)} +
    + + {/* Tampilkan course cards jika ada */} + {msg.courses && msg.courses.length > 0 && ( +
    + {msg.courses.map(course => ( +
    +
    {course.technique}
    +

    {course.name}

    +

    + {new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + maximumFractionDigits: 0 + }).format(course.price)} +

    + + Lihat Detail + +
    + ))} +
    + )} +
    +
    + ))} +
    + + {/* Loading indicator */} + {loading && ( +
    +
    + Bot +
    +
    +
    + + + +
    +
    +
    + )} +
    + +
    + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/client/src/layouts/MainLayout.jsx b/client/src/layouts/MainLayout.jsx index 59e6abb1..f2bd2e3e 100644 --- a/client/src/layouts/MainLayout.jsx +++ b/client/src/layouts/MainLayout.jsx @@ -1,6 +1,8 @@ import { Outlet } from "react-router"; import Navbar from "../components/Navbar"; import Footer from "../components/Footer"; +// Import komponen Chatbot +import Chatbot from "../components/Chatbot"; export default function MainLayout() { return ( @@ -10,6 +12,7 @@ export default function MainLayout() {