From 8a1868482c25df7cde651e1e8bacf1288be4d585 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 14:18:03 +0300 Subject: [PATCH 001/147] =?UTF-8?q?feat:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=B0=20=D1=81=20=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20?= =?UTF-8?q?TypeScript=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Переписаны основные файлы сервера с JavaScript на TypeScript. - Добавлен новый обработчик ошибок с логированием в базу данных. - Обновлен Dockerfile для поддержки сборки TypeScript. - Изменены настройки окружения для MongoDB в docker-compose. - Удалены устаревшие файлы и добавлены новые модели и утилиты для работы с MongoDB. - Обновлены зависимости в package.json и package-lock.json. --- Dockerfile | 34 +- d-scripts/rerun.sh | 12 +- docker-compose.yaml | 10 +- eslint.config.mjs | 2 +- package-lock.json | 291 +++++- package.json | 16 +- server/error.js | 13 - server/error.ts | 28 + server/index.js | 99 -- server/index.ts | 150 +++ server/io.js | 13 - server/io.ts | 13 + server/models/ErrorLog.ts | 16 + .../{questionnaire.js => questionnaire.ts} | 11 +- server/root.js | 33 - server/routers/edateam-legacy/index.js | 15 - server/routers/edateam-legacy/index.ts | 21 + server/server.ts | 962 ++++++++++++++++++ server/utils/{common.js => common.ts} | 4 +- server/utils/const.js | 4 - server/utils/const.ts | 4 + server/utils/{mongo.js => mongo.ts} | 7 +- server/utils/mongoose.js | 5 - server/utils/mongoose.ts | 11 + tsconfig.json | 112 ++ 25 files changed, 1669 insertions(+), 217 deletions(-) delete mode 100644 server/error.js create mode 100644 server/error.ts delete mode 100644 server/index.js create mode 100644 server/index.ts delete mode 100644 server/io.js create mode 100644 server/io.ts create mode 100644 server/models/ErrorLog.ts rename server/models/{questionnaire.js => questionnaire.ts} (88%) delete mode 100644 server/root.js delete mode 100644 server/routers/edateam-legacy/index.js create mode 100644 server/routers/edateam-legacy/index.ts create mode 100644 server/server.ts rename server/utils/{common.js => common.ts} (75%) delete mode 100644 server/utils/const.js create mode 100644 server/utils/const.ts rename server/utils/{mongo.js => mongo.ts} (70%) delete mode 100644 server/utils/mongoose.js create mode 100644 server/utils/mongoose.ts create mode 100644 tsconfig.json diff --git a/Dockerfile b/Dockerfile index d37b6fc..577a78b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,38 @@ -FROM node:20 +FROM node:22 AS builder + +WORKDIR /usr/src/app/ + +# Сначала копируем только файлы, необходимые для установки зависимостей +COPY ./package.json /usr/src/app/package.json +COPY ./package-lock.json /usr/src/app/package-lock.json + +# Устанавливаем все зависимости +RUN npm ci + +# Затем копируем исходный код проекта и файлы конфигурации +COPY ./tsconfig.json /usr/src/app/tsconfig.json +COPY ./server /usr/src/app/server + +# Сборка проекта +RUN npm run build + +# Вторая стадия - рабочий образ +FROM node:22 RUN mkdir -p /usr/src/app/server/log/ WORKDIR /usr/src/app/ -COPY ./server /usr/src/app/server +# Копирование только package.json/package-lock.json для продакшн зависимостей COPY ./package.json /usr/src/app/package.json COPY ./package-lock.json /usr/src/app/package-lock.json -COPY ./.serverrc.js /usr/src/app/.serverrc.js -# COPY ./.env /usr/src/app/.env -# RUN npm i --omit=dev -RUN npm ci +# Установка только продакшн зависимостей +RUN npm ci --production + +# Копирование собранного приложения из билдера +COPY --from=builder /usr/src/app/dist /usr/src/app/dist +COPY --from=builder /usr/src/app/server /usr/src/app/server + EXPOSE 8044 CMD ["npm", "run", "up:prod"] diff --git a/d-scripts/rerun.sh b/d-scripts/rerun.sh index 03dbb37..d6ee428 100644 --- a/d-scripts/rerun.sh +++ b/d-scripts/rerun.sh @@ -1,6 +1,12 @@ #!/bin/sh docker stop ms-mongo -docker volume remove ms_volume -docker volume create ms_volume -docker run --rm -v ms_volume:/data/db --name ms-mongo -p 27017:27017 -d mongo:8.0.3 +docker volume remove ms_volume8 +docker volume create ms_volume8 +docker run --rm \ + -v ms_volume8:/data/db \ + --name ms-mongo \ + -p 27018:27017 \ + -e MONGO_INITDB_ROOT_USERNAME=qqq \ + -e MONGO_INITDB_ROOT_PASSWORD=qqq \ + -d mongo:8.0.3 diff --git a/docker-compose.yaml b/docker-compose.yaml index 76b4b32..0eb409d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,10 +10,12 @@ services: volumes: - ms_volume8:/data/db restart: always - # ports: - # - 27017:27017 + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD} + ports: + - 27018:27017 multy-stubs: - # build: . image: bro.js/ms/bh:$TAG restart: always volumes: @@ -22,4 +24,4 @@ services: - 8044:8044 environment: - TZ=Europe/Moscow - - MONGO_ADDR=mongodb \ No newline at end of file + - MONGO_ADDR=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017 \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 21a1f6d..bcdabf1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,7 @@ import pluginJs from "@eslint/js"; export default [ { ignores: ['server/routers/old/*'] }, - { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, + { files: ["**/*.js"], languageOptions: { } }, { languageOptions: { globals: globals.node } }, pluginJs.configs.recommended, { diff --git a/package-lock.json b/package-lock.json index e200158..8a9bf9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,9 @@ "jest": "^29.7.0", "mockingoose": "^2.16.2", "nodemon": "3.1.9", - "supertest": "^7.0.0" + "supertest": "^7.0.0", + "ts-node-dev": "2.0.0", + "typescript": "5.7.3" } }, "node_modules/@ai-sdk/provider": { @@ -852,6 +854,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", @@ -1747,6 +1773,34 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1882,6 +1936,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1954,6 +2022,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2121,6 +2202,13 @@ "node": ">= 6" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2846,6 +2934,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -3062,6 +3157,16 @@ "wrappy": "1" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", @@ -3104,6 +3209,16 @@ "node": ">= 0.4" } }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -5774,6 +5889,13 @@ "semver": "bin/semver.js" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -7876,6 +7998,142 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7930,6 +8188,20 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -8037,6 +8309,13 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8286,6 +8565,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index c7384b9..75e1d2d 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,12 @@ "name": "multi-stub", "version": "1.2.1", "description": "", - "main": "index.js", + "main": "server/index.ts", + "type": "commonjs", "scripts": { - "start": "cross-env PORT=8033 npx nodemon ./server", - "up:prod": "cross-env NODE_ENV=\"production\" node ./server", - "deploy:d:stop": "docker compose down", - "deploy:d:build": "docker compose build", - "deploy:d:up": "docker compose up -d", - "redeploy": "npm run deploy:d:stop && npm run deploy:d:build && npm run deploy:d:up", + "start": "cross-env NODE_ENV=\"development\" ts-node-dev .", + "build": "tsc", + "up:prod": "node dist/server/index.js", "eslint": "npx eslint ./server", "eslint:fix": "npx eslint ./server --fix", "test": "jest" @@ -55,6 +53,8 @@ "jest": "^29.7.0", "mockingoose": "^2.16.2", "nodemon": "3.1.9", - "supertest": "^7.0.0" + "supertest": "^7.0.0", + "ts-node-dev": "2.0.0", + "typescript": "5.7.3" } } diff --git a/server/error.js b/server/error.js deleted file mode 100644 index b8db25c..0000000 --- a/server/error.js +++ /dev/null @@ -1,13 +0,0 @@ -const noToken = 'No authorization token was found' - -module.exports = (err, req, res, next) => { - if (err.message === noToken) { - res.status(400).send({ - success: false, error: 'Токен авторизации не найден', - }) - } - - res.status(400).send({ - success: false, error: err.message || 'Что-то пошло не так', - }) -} diff --git a/server/error.ts b/server/error.ts new file mode 100644 index 0000000..1919737 --- /dev/null +++ b/server/error.ts @@ -0,0 +1,28 @@ +import { ErrorLog } from './models/ErrorLog' + +const noToken = 'No authorization token was found' + +export const errorHandler = (err, req, res, next) => { + // Сохраняем ошибку в базу данных + const errorLog = new ErrorLog({ + message: err.message || 'Неизвестная ошибка', + stack: err.stack, + path: req.path, + method: req.method, + query: req.query, + body: req.body + }) + + errorLog.save() + .catch(saveErr => console.error('Ошибка при сохранении лога ошибки:', saveErr)) + + if (err.message === noToken) { + res.status(400).send({ + success: false, error: 'Токен авторизации не найден', + }) + } + + res.status(400).send({ + success: false, error: err.message || 'Что-то пошло не так', + }) +} diff --git a/server/index.js b/server/index.js deleted file mode 100644 index 2a179cd..0000000 --- a/server/index.js +++ /dev/null @@ -1,99 +0,0 @@ -const express = require("express") -const bodyParser = require("body-parser") -const cookieParser = require("cookie-parser") -const session = require("express-session") -const morgan = require("morgan") -const path = require("path") -const rfs = require("rotating-file-stream") - -const app = express() -require("dotenv").config() -exports.app = app - -const accessLogStream = rfs.createStream("access.log", { - size: "10M", - interval: "1d", - compress: "gzip", - path: path.join(__dirname, "log"), -}) - -const errorLogStream = rfs.createStream("error.log", { - size: "10M", - interval: "1d", - compress: "gzip", - path: path.join(__dirname, "log"), -}) - -const config = require("../.serverrc") -const { setIo } = require("./io") - -app.use(cookieParser()) -app.use( - morgan("combined", { - stream: accessLogStream, - skip: function (req, res) { - return res.statusCode >= 400 - }, - }) -) - -// log all requests to access.log -app.use( - morgan("combined", { - stream: errorLogStream, - skip: function (req, res) { - console.log('statusCode', res.statusCode, res.statusCode <= 400) - return res.statusCode < 400 - }, - }) -) - -const server = setIo(app) - -const sess = { - secret: "super-secret-key", - resave: true, - saveUninitialized: true, - cookie: {}, -} -if (app.get("env") === "production") { - app.set("trust proxy", 1) - sess.cookie.secure = true -} -app.use(session(sess)) - -app.use( - bodyParser.json({ - limit: "50mb", - }) -) -app.use( - bodyParser.urlencoded({ - limit: "50mb", - extended: true, - }) -) -app.use(require("./root")) - -/** - * Добавляйте сюда свои routers. - */ -app.use("/kfu-m-24-1", require("./routers/kfu-m-24-1")) -app.use("/epja-2024-1", require("./routers/epja-2024-1")) -app.use("/v1/todo", require("./routers/todo")) -app.use("/dogsitters-finder", require("./routers/dogsitters-finder")) -app.use("/kazan-explore", require("./routers/kazan-explore")) -app.use("/edateam", require("./routers/edateam-legacy")) -app.use("/dry-wash", require("./routers/dry-wash")) -app.use("/freetracker", require("./routers/freetracker")) -app.use("/dhs-testing", require("./routers/dhs-testing")) -app.use("/gamehub", require("./routers/gamehub")) -app.use("/esc", require("./routers/esc")) -app.use('/connectme', require('./routers/connectme')) -app.use('/questioneer', require('./routers/questioneer')) - -app.use(require("./error")) - -server.listen(config.port, () => - console.log(`Listening on http://localhost:${config.port}`) -) diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..d39f65f --- /dev/null +++ b/server/index.ts @@ -0,0 +1,150 @@ +import express from 'express' +import cookieParser from 'cookie-parser' +import session from 'express-session' +import morgan from 'morgan' +import path from 'path' +import 'dotenv/config' + +import root from './server' +import { errorHandler } from './error' +import kfuM241Router from './routers/kfu-m-24-1' +import epja20241Router from './routers/epja-2024-1' +import todoRouter from './routers/todo' +import dogsittersFinderRouter from './routers/dogsitters-finder' +import kazanExploreRouter from './routers/kazan-explore' +import edateamRouter from './routers/edateam-legacy' +import dryWashRouter from './routers/dry-wash' +import freetrackerRouter from './routers/freetracker' +import dhsTestingRouter from './routers/dhs-testing' +import gamehubRouter from './routers/gamehub' +import escRouter from './routers/esc' +import connectmeRouter from './routers/connectme' +import questioneerRouter from './routers/questioneer' +import { setIo } from './io' + +export const app = express() + +// Динамический импорт rotating-file-stream +const initServer = async () => { + const rfs = await import('rotating-file-stream') + const accessLogStream = rfs.createStream("access.log", { + size: "10M", + interval: "1d", + compress: "gzip", + path: path.join(__dirname, "log"), + }) + + const errorLogStream = rfs.createStream("error.log", { + size: "10M", + interval: "1d", + compress: "gzip", + path: path.join(__dirname, "log"), + }) + + app.use(cookieParser()) + app.use( + morgan("combined", { + stream: accessLogStream, + skip: function (req, res) { + return res.statusCode >= 400 + }, + }) + ) + + // log all requests to access.log + app.use( + morgan("combined", { + stream: errorLogStream, + skip: function (req, res) { + console.log('statusCode', res.statusCode, res.statusCode <= 400) + return res.statusCode < 400 + }, + }) + ) + + console.log('warming up 🔥') + + const server = setIo(app) + + const sess = { + secret: "super-secret-key", + resave: true, + saveUninitialized: true, + cookie: {}, + } + if (app.get("env") !== "development") { + app.set("trust proxy", 1) + } + app.use(session(sess)) + + app.use( + express.json({ + limit: "50mb", + }) + ) + app.use( + express.urlencoded({ + limit: "50mb", + extended: true, + }) + ) + app.use(root) + + /** + * Добавляйте сюда свои routers. + */ + app.use("/kfu-m-24-1", kfuM241Router) + app.use("/epja-2024-1", epja20241Router) + app.use("/v1/todo", todoRouter) + app.use("/dogsitters-finder", dogsittersFinderRouter) + app.use("/kazan-explore", kazanExploreRouter) + app.use("/edateam", edateamRouter) + app.use("/dry-wash", dryWashRouter) + app.use("/freetracker", freetrackerRouter) + app.use("/dhs-testing", dhsTestingRouter) + app.use("/gamehub", gamehubRouter) + app.use("/esc", escRouter) + app.use('/connectme', connectmeRouter) + app.use('/questioneer', questioneerRouter) + + app.use(errorHandler) + + server.listen(process.env.PORT ?? 8044, () => + console.log(`🚀 Сервер запущен на http://localhost:${process.env.PORT ?? 8044}`) + ) + + // Обработка сигналов завершения процесса + process.on('SIGTERM', () => { + console.log('🛑 Получен сигнал SIGTERM. Выполняется корректное завершение...') + server.close(() => { + console.log('✅ Сервер успешно остановлен') + process.exit(0) + }) + }) + + process.on('SIGINT', () => { + console.log('🛑 Получен сигнал SIGINT. Выполняется корректное завершение...') + server.close(() => { + console.log('✅ Сервер успешно остановлен') + process.exit(0) + }) + }) + + // Обработка необработанных исключений + process.on('uncaughtException', (err) => { + console.error('❌ Необработанное исключение:', err) + server.close(() => { + process.exit(1) + }) + }) + + // Обработка необработанных отклонений промисов + process.on('unhandledRejection', (reason, promise) => { + console.error('⚠️ Необработанное отклонение промиса:', reason) + server.close(() => { + process.exit(1) + }) + }) +} + +initServer().catch(console.error) diff --git a/server/io.js b/server/io.js deleted file mode 100644 index 8df2c10..0000000 --- a/server/io.js +++ /dev/null @@ -1,13 +0,0 @@ -const { Server } = require('socket.io') -const { createServer } = require('http') - -let io = null - -module.exports.setIo = (app) => { - const server = createServer(app) - io = new Server(server, {}) - - return server -} - -module.exports.getIo = () => io diff --git a/server/io.ts b/server/io.ts new file mode 100644 index 0000000..71833d6 --- /dev/null +++ b/server/io.ts @@ -0,0 +1,13 @@ +import { Server } from 'socket.io' +import { createServer } from 'http' + +let io = null + +export const setIo = (app) => { + const server = createServer(app) + io = new Server(server, {}) + + return server +} + +export const getIo = () => io diff --git a/server/models/ErrorLog.ts b/server/models/ErrorLog.ts new file mode 100644 index 0000000..5923615 --- /dev/null +++ b/server/models/ErrorLog.ts @@ -0,0 +1,16 @@ +import mongoose from 'mongoose' + +const ErrorLogSchema = new mongoose.Schema({ + message: { type: String, required: true }, + stack: { type: String }, + path: { type: String }, + method: { type: String }, + query: { type: Object }, + body: { type: Object }, + createdAt: { type: Date, default: Date.now }, +}) + +// Индекс для быстрого поиска по дате создания +ErrorLogSchema.index({ createdAt: 1 }) + +export const ErrorLog = mongoose.model('ErrorLog', ErrorLogSchema) \ No newline at end of file diff --git a/server/models/questionnaire.js b/server/models/questionnaire.ts similarity index 88% rename from server/models/questionnaire.js rename to server/models/questionnaire.ts index e08928e..aa39e23 100644 --- a/server/models/questionnaire.js +++ b/server/models/questionnaire.ts @@ -1,7 +1,7 @@ const mongoose = require('mongoose'); // Типы вопросов -const QUESTION_TYPES = { +export const QUESTION_TYPES = { SINGLE_CHOICE: 'single_choice', // Один вариант MULTIPLE_CHOICE: 'multiple_choice', // Несколько вариантов TEXT: 'text', // Текстовый ответ @@ -10,7 +10,7 @@ const QUESTION_TYPES = { }; // Типы отображения -const DISPLAY_TYPES = { +export const DISPLAY_TYPES = { DEFAULT: 'default', TAG_CLOUD: 'tag_cloud', VOTING: 'voting', @@ -51,10 +51,5 @@ const questionnaireSchema = new mongoose.Schema({ publicLink: { type: String, required: true } // ссылка для голосования }); -const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema); +export const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema); -module.exports = { - Questionnaire, - QUESTION_TYPES, - DISPLAY_TYPES -}; \ No newline at end of file diff --git a/server/root.js b/server/root.js deleted file mode 100644 index cb3cc8a..0000000 --- a/server/root.js +++ /dev/null @@ -1,33 +0,0 @@ -const fs = require('fs') -const path = require('path') -const router = require('express').Router() -const mongoose = require('mongoose') - -const pkg = require('../package.json') - -require('./utils/mongoose') -const folderPath = path.resolve(__dirname, './routers') -const folders = fs.readdirSync(folderPath) - -router.get('/', async (req, res) => { - // throw new Error('check error message') - res.send(` -

multy stub is working v${pkg.version}

- - -

models

- - `) -}) - -module.exports = router diff --git a/server/routers/edateam-legacy/index.js b/server/routers/edateam-legacy/index.js deleted file mode 100644 index 0892e93..0000000 --- a/server/routers/edateam-legacy/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const router = require('express').Router(); - -router.get('/recipe-data', (request, response) => { - response.send(require('./json/recipe-data/success.json')) - }) - -router.get('/userpage-data', (req, res)=>{ - res.send(require('./json/userpage-data/success.json')) -}) - -router.get('/homepage-data', (req, res)=>{ - res.send(require('./json/homepage-data/success.json')) -}) - -module.exports = router; \ No newline at end of file diff --git a/server/routers/edateam-legacy/index.ts b/server/routers/edateam-legacy/index.ts new file mode 100644 index 0000000..1e3833b --- /dev/null +++ b/server/routers/edateam-legacy/index.ts @@ -0,0 +1,21 @@ +import { Router } from 'express'; + +import recipeData from './json/recipe-data/success.json'; +import userpageData from './json/userpage-data/success.json'; +import homepageData from './json/homepage-data/success.json'; + +const router = Router(); + +router.get('/recipe-data', (request, response) => { + response.send(recipeData) + }) + +router.get('/userpage-data', (req, res)=>{ + res.send(userpageData) +}) + +router.get('/homepage-data', (req, res)=>{ + res.send(homepageData) +}) + +export default router; diff --git a/server/server.ts b/server/server.ts new file mode 100644 index 0000000..1217ac2 --- /dev/null +++ b/server/server.ts @@ -0,0 +1,962 @@ +import fs from 'fs' +import path from 'path' +import { Router } from 'express' +import mongoose from 'mongoose' + +import pkg from '../package.json' + +import './utils/mongoose' +import { ErrorLog } from './models/ErrorLog' + +const folderPath = path.resolve(__dirname, './routers') +const folders = fs.readdirSync(folderPath) + +// Определение типов +interface FileInfo { + path: string; + type: 'file'; + endpoints: number; +} + +interface DirectoryInfo { + path: string; + type: 'directory'; + endpoints: number; + children: (FileInfo | DirectoryInfo)[]; +} + +interface DirScanResult { + items: (FileInfo | DirectoryInfo)[]; + totalEndpoints: number; +} + +// Функция для поиска эндпоинтов в файлах +function countEndpoints(filePath) { + if (!fs.existsSync(filePath) || !filePath.endsWith('.js') && !filePath.endsWith('.ts')) { + return 0; + } + + try { + const content = fs.readFileSync(filePath, 'utf8'); + const httpMethods = ['get', 'post', 'put', 'delete', 'patch']; + + return httpMethods.reduce((count, method) => { + const regex = new RegExp(`router\\.${method}\\(`, 'gi'); + const matches = content.match(regex) || []; + return count + matches.length; + }, 0); + } catch (err) { + return 0; + } +} + +// Функция для рекурсивного обхода директорий +function getAllDirs(dir, basePath = ''): DirScanResult { + const items: (FileInfo | DirectoryInfo)[] = []; + let totalEndpoints = 0; + + try { + const dirItems = fs.readdirSync(dir); + + for (const item of dirItems) { + const itemPath = path.join(dir, item); + const relativePath = path.join(basePath, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + const dirResult = getAllDirs(itemPath, relativePath); + totalEndpoints += dirResult.totalEndpoints; + + items.push({ + path: relativePath, + type: 'directory', + endpoints: dirResult.totalEndpoints, + children: dirResult.items + }); + } else { + const fileEndpoints = countEndpoints(itemPath); + totalEndpoints += fileEndpoints; + + items.push({ + path: relativePath, + type: 'file', + endpoints: fileEndpoints + }); + } + } + } catch (err) { + console.error(`Ошибка при чтении директории ${dir}:`, err); + } + + return { + items, + totalEndpoints + }; +} + +// Функция для генерации HTML-списка директорий +function generateDirList(dirs: (FileInfo | DirectoryInfo)[], level = 0) { + if (dirs.length === 0) return ''; + + const indent = level * 20; + + return ``; +} + +const router = Router() + +// Эндпоинт для получения содержимого файла +router.get('/file-content', async (req, res) => { + try { + const filePath = req.query.path as string; + if (!filePath) { + return res.status(400).json({ error: 'Путь к файлу не указан' }); + } + + // Используем корень проекта для получения абсолютного пути к файлу + const projectRoot = path.resolve(__dirname, 'routers'); + const absolutePath = path.join(projectRoot, filePath); + + // Проверка, что путь не выходит за пределы проекта + if (!absolutePath.startsWith(projectRoot)) { + return res.status(403).json({ error: 'Доступ запрещен' }); + } + + if (!fs.existsSync(absolutePath)) { + return res.status(404).json({ error: 'Файл не найден' }); + } + + // Проверяем, что это файл, а не директория + const stat = fs.statSync(absolutePath); + if (!stat.isFile()) { + return res.status(400).json({ error: 'Указанный путь не является файлом' }); + } + + // Проверяем размер файла, чтобы не загружать слишком большие файлы + const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB + if (stat.size > MAX_FILE_SIZE) { + return res.status(413).json({ + error: 'Файл слишком большой', + size: stat.size, + maxSize: MAX_FILE_SIZE + }); + } + + const content = fs.readFileSync(absolutePath, 'utf8'); + const fileExtension = path.extname(absolutePath).slice(1); + + res.json({ + content, + extension: fileExtension, + fileName: path.basename(absolutePath) + }); + } catch (err) { + console.error('Ошибка при чтении файла:', err); + res.status(500).json({ error: 'Ошибка при чтении файла' }); + } +}); + +router.get('/', async (req, res) => { + // throw new Error('check error message') + // Используем корень проекта вместо только директории routers + const projectRoot = path.resolve(__dirname, 'routers'); + const routersPath = path.resolve(__dirname, 'routers'); + const routerFolders = fs.readdirSync(routersPath); + + const directoryResult = getAllDirs(projectRoot); + const totalEndpoints = directoryResult.totalEndpoints; + + // Получаем последние 10 ошибок + const latestErrors = await ErrorLog.find().sort({ createdAt: -1 }).limit(10); + + // Сформируем HTML для секции с ошибками + let errorsHtml = ''; + if (latestErrors.length > 0) { + errorsHtml = latestErrors.map(error => ` +
+
+

${error.message}

+ ${new Date(error.createdAt).toLocaleString()} +
+
+ ${error.path ? `
${error.method || 'GET'} ${error.path}
` : ''} + ${error.stack ? `
${error.stack}
` : ''} +
+
+ `).join(''); + } else { + errorsHtml = '

Нет зарегистрированных ошибок

'; + } + + // Создаем JavaScript для клиентской части + const clientScript = ` + document.addEventListener('DOMContentLoaded', function() { + // Директории + document.querySelectorAll('.dir-item[data-expandable="true"]').forEach(item => { + item.addEventListener('click', function(e) { + const subdirectory = this.nextElementSibling; + const isExpanded = this.classList.toggle('expanded'); + + if (isExpanded) { + subdirectory.style.display = 'block'; + this.querySelector('.expand-icon').textContent = '▼'; + } else { + subdirectory.style.display = 'none'; + this.querySelector('.expand-icon').textContent = '▶'; + } + + e.stopPropagation(); + }); + }); + + // Модальное окно + const modal = document.getElementById('fileModal'); + const closeBtn = document.querySelector('.close-modal'); + const fileContent = document.getElementById('fileContent'); + const fileLoader = document.getElementById('fileLoader'); + const modalFileName = document.getElementById('modalFileName'); + + // Закрытие модального окна + closeBtn.addEventListener('click', function() { + modal.style.display = 'none'; + }); + + window.addEventListener('click', function(event) { + if (event.target == modal) { + modal.style.display = 'none'; + } + }); + + // Обработчик для файлов + document.querySelectorAll('.file-item').forEach(item => { + item.addEventListener('click', async function() { + const filePath = this.getAttribute('data-path'); + if (!filePath) return; + + // Показываем модальное окно и лоадер + modal.style.display = 'block'; + fileContent.style.display = 'none'; + fileLoader.style.display = 'block'; + modalFileName.textContent = 'Загрузка...'; + + try { + const response = await fetch('/file-content?path=' + encodeURIComponent(filePath)); + if (!response.ok) { + throw new Error('Ошибка при загрузке файла'); + } + + const data = await response.json(); + + // Отображаем содержимое файла + fileLoader.style.display = 'none'; + fileContent.style.display = 'block'; + fileContent.textContent = data.content; + modalFileName.textContent = data.fileName; + + // Подсветка синтаксиса + const extensionMap = { + 'js': 'javascript', + 'ts': 'typescript', + 'json': 'json', + 'css': 'css', + 'html': 'xml', + 'xml': 'xml', + 'md': 'markdown', + 'yaml': 'yaml', + 'yml': 'yaml', + 'sh': 'bash', + 'bash': 'bash' + }; + + const language = extensionMap[data.extension] || ''; + if (language) { + fileContent.className = 'language-' + language; + hljs.highlightElement(fileContent); + } + } catch (error) { + fileLoader.style.display = 'none'; + fileContent.style.display = 'block'; + fileContent.textContent = 'Ошибка при загрузке файла: ' + error.message; + modalFileName.textContent = 'Ошибка'; + } + }); + }); + + // Обработчик кнопки очистки ошибок + const clearErrorsBtn = document.getElementById('clearErrorsBtn'); + const successAction = document.getElementById('successAction'); + + clearErrorsBtn.addEventListener('click', async function() { + try { + const response = await fetch('/clear-old-errors', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + if (!response.ok) { + throw new Error('Ошибка при очистке старых ошибок'); + } + + const data = await response.json(); + + // Показываем сообщение об успехе + successAction.textContent = 'Удалено ' + data.deletedCount + ' записей'; + successAction.style.display = 'block'; + + // Перезагружаем страницу через 2 секунды + setTimeout(() => { + window.location.reload(); + }, 2000); + + } catch (error) { + console.error('Ошибка:', error); + alert('Произошла ошибка: ' + error.message); + } + }); + }); + `; + + res.send(` + + + + Multy Stub v${pkg.version} + + + + + + + + + + + + + + + + +
+
+

Multy Stub v${pkg.version}

+
+
${totalEndpoints}
+
Всего эндпоинтов
+
+
+ +
+

Routers:

+
+ ${routerFolders.map((f) => ` +
+
${f}
+
+ `).join('')} +
+
+ +
+

Структура директорий проекта:

+ ${generateDirList(directoryResult.items)} +
+ +
+

Models:

+
+ ${ + (await Promise.all( + (await mongoose.modelNames()).map(async (name) => { + const model = mongoose.model(name); + const count = await model.countDocuments(); + // Получаем информацию о полях модели + const schema = model.schema; + const fields = Object.keys(schema.paths).filter(field => !['__v', '_id'].includes(field)); + + return ` +
+
+

${name}

+

${count} документов

+
+
+
+

Поля:

+
    + ${fields.map(field => { + const fieldType = schema.paths[field].instance; + return `
  • ${field}: ${fieldType}
  • `; + }).join('')} +
+
+
+
+ `; + } + ) + )).join('') + } +
+
+ +
+

Последние ошибки:

+ +
+ ${errorsHtml} +
+
+
+ + + + +
Операция выполнена успешно
+ + + + + `) +}) + +// Эндпоинт для очистки ошибок старше 10 дней +router.post('/clear-old-errors', async (req, res) => { + try { + const tenDaysAgo = new Date(); + tenDaysAgo.setDate(tenDaysAgo.getDate() - 10); + + const result = await ErrorLog.deleteMany({ createdAt: { $lt: tenDaysAgo } }); + + res.json({ + success: true, + deletedCount: result.deletedCount || 0 + }); + } catch (error) { + console.error('Ошибка при очистке старых ошибок:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при очистке старых ошибок' + }); + } +}); + +export default router diff --git a/server/utils/common.js b/server/utils/common.ts similarity index 75% rename from server/utils/common.js rename to server/utils/common.ts index e0aee4f..42c19e7 100644 --- a/server/utils/common.js +++ b/server/utils/common.ts @@ -1,4 +1,4 @@ -exports.getAnswer = (errors, data, success = true) => { +export const getAnswer = (errors, data, success = true) => { if (errors) { return { success: false, @@ -12,7 +12,7 @@ exports.getAnswer = (errors, data, success = true) => { } } -exports.getResponse = (errors, data, success = true) => { +export const getResponse = (errors, data, success = true) => { if (errors.length) { return { success: false, diff --git a/server/utils/const.js b/server/utils/const.js deleted file mode 100644 index bc12a52..0000000 --- a/server/utils/const.js +++ /dev/null @@ -1,4 +0,0 @@ -const rc = require('../../.serverrc') - -// Connection URL -exports.mongoUrl = `mongodb://${rc.mongoAddr}:${rc.mongoPort}` diff --git a/server/utils/const.ts b/server/utils/const.ts new file mode 100644 index 0000000..5d19080 --- /dev/null +++ b/server/utils/const.ts @@ -0,0 +1,4 @@ +import 'dotenv/config'; + +// Connection URL +export const mongoUrl = process.env.MONGO_ADDR diff --git a/server/utils/mongo.js b/server/utils/mongo.ts similarity index 70% rename from server/utils/mongo.js rename to server/utils/mongo.ts index 258067c..821b9c4 100644 --- a/server/utils/mongo.js +++ b/server/utils/mongo.ts @@ -10,8 +10,11 @@ const mongoDBConnect = async () => { const MongoClient = new MDBClient(mongoUrl, { useUnifiedTopology: true, }) - return await MongoClient.connect() + const client = await MongoClient.connect() + console.log('Подключение к MongoDB успешно') + return client } catch (error) { + console.log('Неудачная попытка подключения к MongoDB') console.error(error) } } @@ -27,6 +30,6 @@ const getDB = async (dbName) => { } } -module.exports = { +export { getDB, } diff --git a/server/utils/mongoose.js b/server/utils/mongoose.js deleted file mode 100644 index d11d7bc..0000000 --- a/server/utils/mongoose.js +++ /dev/null @@ -1,5 +0,0 @@ -const mongoose = require('mongoose') - -const { mongoUrl } = require('./const') - -mongoose.connect(`${mongoUrl}/mongoose`) diff --git a/server/utils/mongoose.ts b/server/utils/mongoose.ts new file mode 100644 index 0000000..d018e66 --- /dev/null +++ b/server/utils/mongoose.ts @@ -0,0 +1,11 @@ +import mongoose from 'mongoose' + +import { mongoUrl } from './const' + +mongoose.connect(`${mongoUrl}`).then(() => { + console.log('Подключение к MongoDB успешно') +}).catch((err) => { + console.log('Неудачная попытка подключения к MongoDB') + console.error(err) +}) + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..72ecfc4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,112 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "NodeNext", /* Specify what module code is generated. */ + "rootDir": ".", /* Specify the root folder within your source files. */ + "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": false, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "exclude": [ + "node_modules", + "legacy/**/*.ts" + ] +} From e5d6b7cecde576ebe89a126d73271a03777c5df9 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 14:30:39 +0300 Subject: [PATCH 002/147] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=D0=B0=20postinstall=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20package-lock.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен скрипт postinstall для автоматического создания файлов .env и .env.example. - Обновлен package-lock.json для отражения изменений в зависимостях. --- .env.example | 9 +++++++++ package-lock.json | 1 + package.json | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3aaec2b --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Application settings +TZ=Europe/Moscow +APP_PORT=8044 + +MONGO_INITDB_ROOT_USERNAME=qqq +MONGO_INITDB_ROOT_PASSWORD=qqq + +# MongoDB connection string +MONGO_ADDR=mongodb://qqq:qqq@127.0.0.1:27018 diff --git a/package-lock.json b/package-lock.json index 8a9bf9e..99d42eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "multi-stub", "version": "1.2.1", + "hasInstallScript": true, "license": "MIT", "dependencies": { "ai": "^4.1.13", diff --git a/package.json b/package.json index 75e1d2d..cc261fe 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "up:prod": "node dist/server/index.js", "eslint": "npx eslint ./server", "eslint:fix": "npx eslint ./server --fix", - "test": "jest" + "test": "jest", + "postinstall": "node -e \"const fs = require('fs'); const envExample = `# Настройки сервера\\nPORT=3000\\nNODE_ENV=development\\n\\n# Настройки базы данных\\nMONGODB_URI=mongodb://localhost:27017/multi-stub\\n\\n# Настройки JWT\\nJWT_SECRET=your_jwt_secret\\nJWT_EXPIRES_IN=7d\\n\\n# Прочие настройки\\nLOG_LEVEL=info`; if (!fs.existsSync('.env.example')) { fs.writeFileSync('.env.example', envExample); console.log('Created .env.example file'); } if (!fs.existsSync('.env')) { fs.copyFileSync('.env.example', '.env'); console.log('Created .env file from .env.example'); }\"" }, "repository": { "type": "git", From c8f7e471819232312f6b60e5d04310ea2409a249 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:15:07 +0300 Subject: [PATCH 003/147] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20Docker=20Compose?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20MongoDB=20=D0=B8=20multy-stubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создан файл docker-compose.yml для настройки сервисов MongoDB и multy-stubs. - Определены необходимые переменные окружения и порты для взаимодействия сервисов. --- docker-compose.yaml => docker-compose.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker-compose.yaml => docker-compose.yml (100%) diff --git a/docker-compose.yaml b/docker-compose.yml similarity index 100% rename from docker-compose.yaml rename to docker-compose.yml From 7f57b2a4d3b86a4c7af71bd4118c8c8a7d758e5b Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:18:48 +0300 Subject: [PATCH 004/147] =?UTF-8?q?fix:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=B0?= =?UTF-8?q?=20postinstall=20=D0=B8=D0=B7=20package.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удален скрипт postinstall, который создавал файлы .env и .env.example. - Обновлен файл package.json для упрощения конфигурации проекта. --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index cc261fe..75e1d2d 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "up:prod": "node dist/server/index.js", "eslint": "npx eslint ./server", "eslint:fix": "npx eslint ./server --fix", - "test": "jest", - "postinstall": "node -e \"const fs = require('fs'); const envExample = `# Настройки сервера\\nPORT=3000\\nNODE_ENV=development\\n\\n# Настройки базы данных\\nMONGODB_URI=mongodb://localhost:27017/multi-stub\\n\\n# Настройки JWT\\nJWT_SECRET=your_jwt_secret\\nJWT_EXPIRES_IN=7d\\n\\n# Прочие настройки\\nLOG_LEVEL=info`; if (!fs.existsSync('.env.example')) { fs.writeFileSync('.env.example', envExample); console.log('Created .env.example file'); } if (!fs.existsSync('.env')) { fs.copyFileSync('.env.example', '.env'); console.log('Created .env file from .env.example'); }\"" + "test": "jest" }, "repository": { "type": "git", From b83e0d603c931e8e100112fac1172ad869e572a3 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:22:49 +0300 Subject: [PATCH 005/147] =?UTF-8?q?-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=20mongoDb=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B4=20multy-stubs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0eb409d..7811e58 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,16 +5,6 @@ volumes: ms_logs: services: - mongoDb: - image: mongo:8.0.3 - volumes: - - ms_volume8:/data/db - restart: always - environment: - - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME} - - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD} - ports: - - 27018:27017 multy-stubs: image: bro.js/ms/bh:$TAG restart: always @@ -24,4 +14,17 @@ services: - 8044:8044 environment: - TZ=Europe/Moscow - - MONGO_ADDR=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017 \ No newline at end of file + - MONGO_ADDR=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017 + depends_on: + mongoDb: + condition: service_started + mongoDb: + image: mongo:8.0.3 + volumes: + - ms_volume8:/data/db + restart: always + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD} + ports: + - 27018:27017 \ No newline at end of file From e7d114a9d91b38800ad52aeaf9ce64244415f12c Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:23:27 +0300 Subject: [PATCH 006/147] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=82=D1=81=D1=82?= =?UTF-8?q?=D1=83=D0=BF=D0=BE=D0=B2=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20docker-compo?= =?UTF-8?q?se.yml=20=D0=B4=D0=BB=D1=8F=20mongoDb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлены отступы в секции depends_on для корректного форматирования файла. --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7811e58..6d5a60b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,9 @@ services: environment: - TZ=Europe/Moscow - MONGO_ADDR=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017 - depends_on: - mongoDb: - condition: service_started + depends_on: + mongoDb: + condition: service_started mongoDb: image: mongo:8.0.3 volumes: From f909d90b6f3c8ce3ecc672dbbd722dd9db1b27b2 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:36:52 +0300 Subject: [PATCH 007/147] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20docker-compose.yml?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20mongoDb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Изменена переменная окружения MONGO_ADDR для использования значения из окружения вместо жестко закодированного адреса. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6d5a60b..71ffe2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: - 8044:8044 environment: - TZ=Europe/Moscow - - MONGO_ADDR=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@mongodb:27017 + - MONGO_ADDR=${MONGO_ADDR} depends_on: mongoDb: condition: service_started From 48167530fd32932ad34abde4264e82cb5f0b84f7 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:43:11 +0300 Subject: [PATCH 008/147] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20mongoUrl=20=D0=B2=20?= =?UTF-8?q?=D1=83=D1=82=D0=B8=D0=BB=D0=B8=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены консольные логи для переменной mongoUrl в файле const.ts для упрощения отладки подключения к MongoDB. --- server/utils/const.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/utils/const.ts b/server/utils/const.ts index 5d19080..07c5175 100644 --- a/server/utils/const.ts +++ b/server/utils/const.ts @@ -2,3 +2,7 @@ import 'dotenv/config'; // Connection URL export const mongoUrl = process.env.MONGO_ADDR + +console.log('=======================================================') +console.log('mongoUrl', mongoUrl) +console.log('=======================================================') From 95bcaf3c5e1b74b369d92aae386f9de2935c5685 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:48:02 +0300 Subject: [PATCH 009/147] 2 --- server/utils/const.ts | 4 ---- server/utils/mongo.ts | 13 ++++++++----- server/utils/mongoose.ts | 5 +++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/server/utils/const.ts b/server/utils/const.ts index 07c5175..5d19080 100644 --- a/server/utils/const.ts +++ b/server/utils/const.ts @@ -2,7 +2,3 @@ import 'dotenv/config'; // Connection URL export const mongoUrl = process.env.MONGO_ADDR - -console.log('=======================================================') -console.log('mongoUrl', mongoUrl) -console.log('=======================================================') diff --git a/server/utils/mongo.ts b/server/utils/mongo.ts index 821b9c4..e98dfac 100644 --- a/server/utils/mongo.ts +++ b/server/utils/mongo.ts @@ -1,15 +1,18 @@ -const MDBClient = require('mongodb').MongoClient +import { MongoClient as MDBClient } from 'mongodb' -const { mongoUrl } = require('./const') +import { mongoUrl } from './const' const dbInstanses = { } +console.log('=======================================================') +console.log('mongoUrl', mongoUrl) +console.log('=======================================================') + + const mongoDBConnect = async () => { try { - const MongoClient = new MDBClient(mongoUrl, { - useUnifiedTopology: true, - }) + const MongoClient = new MDBClient(mongoUrl, {}) const client = await MongoClient.connect() console.log('Подключение к MongoDB успешно') return client diff --git a/server/utils/mongoose.ts b/server/utils/mongoose.ts index d018e66..257b430 100644 --- a/server/utils/mongoose.ts +++ b/server/utils/mongoose.ts @@ -2,6 +2,11 @@ import mongoose from 'mongoose' import { mongoUrl } from './const' +console.log('=======================================================') +console.log('mongoUrl2', mongoUrl) +console.log('=======================================================') + + mongoose.connect(`${mongoUrl}`).then(() => { console.log('Подключение к MongoDB успешно') }).catch((err) => { From ab555cd70e6d727feb20a33f848b50f9e219295e Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 15:53:59 +0300 Subject: [PATCH 010/147] =?UTF-8?q?fix:=20=D1=83=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20mongoUrl=20=D0=B2=20=D1=83?= =?UTF-8?q?=D1=82=D0=B8=D0=BB=D0=B8=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Обновлены консольные логи для переменной mongoUrl в файлах mongo.ts и mongoose.ts для более удобного отображения. - Упрощена инициализация MongoClient, убрав лишние параметры. --- server/utils/mongo.ts | 4 ++-- server/utils/mongoose.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/utils/mongo.ts b/server/utils/mongo.ts index e98dfac..59344d0 100644 --- a/server/utils/mongo.ts +++ b/server/utils/mongo.ts @@ -6,13 +6,13 @@ const dbInstanses = { } console.log('=======================================================') -console.log('mongoUrl', mongoUrl) +console.log(`mongoUrl ->${mongoUrl}<-`) console.log('=======================================================') const mongoDBConnect = async () => { try { - const MongoClient = new MDBClient(mongoUrl, {}) + const MongoClient = new MDBClient(mongoUrl) const client = await MongoClient.connect() console.log('Подключение к MongoDB успешно') return client diff --git a/server/utils/mongoose.ts b/server/utils/mongoose.ts index 257b430..bbfaf12 100644 --- a/server/utils/mongoose.ts +++ b/server/utils/mongoose.ts @@ -3,11 +3,11 @@ import mongoose from 'mongoose' import { mongoUrl } from './const' console.log('=======================================================') -console.log('mongoUrl2', mongoUrl) +console.log(`mongoUrl ->${mongoUrl}<-`) console.log('=======================================================') -mongoose.connect(`${mongoUrl}`).then(() => { +mongoose.connect(mongoUrl).then(() => { console.log('Подключение к MongoDB успешно') }).catch((err) => { console.log('Неудачная попытка подключения к MongoDB') From 3c22354130277e2484dc31c908d71041fc4ef41b Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 16:13:53 +0300 Subject: [PATCH 011/147] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20docker-compose.yml?= =?UTF-8?q?=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8?= =?UTF-8?q?=20URL=20=D0=B2=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Закомментированы секции mongoDb в docker-compose.yml для упрощения конфигурации. - Добавлена функция getUrl для динамического формирования URL в server.ts, что улучшает обработку запросов в зависимости от окружения. - Удалены лишние консольные логи из файлов mongo.ts и mongoose.ts для повышения читаемости кода. --- docker-compose.yml | 26 +++++++++++++------------- server/server.ts | 7 ++++--- server/utils/mongo.ts | 5 ----- server/utils/mongoose.ts | 5 ----- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 71ffe2b..17fefd4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,16 +15,16 @@ services: environment: - TZ=Europe/Moscow - MONGO_ADDR=${MONGO_ADDR} - depends_on: - mongoDb: - condition: service_started - mongoDb: - image: mongo:8.0.3 - volumes: - - ms_volume8:/data/db - restart: always - environment: - - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME} - - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD} - ports: - - 27018:27017 \ No newline at end of file + # depends_on: + # mongoDb: + # condition: service_started + # mongoDb: + # image: mongo:8.0.3 + # volumes: + # - ms_volume8:/data/db + # restart: always + # environment: + # - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME} + # - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD} + # ports: + # - 27018:27017 \ No newline at end of file diff --git a/server/server.ts b/server/server.ts index 1217ac2..2529983 100644 --- a/server/server.ts +++ b/server/server.ts @@ -9,7 +9,8 @@ import './utils/mongoose' import { ErrorLog } from './models/ErrorLog' const folderPath = path.resolve(__dirname, './routers') -const folders = fs.readdirSync(folderPath) + +const getUrl = (url) => `${process.env.NODE_ENV === 'development' ? '' : '/ms'}${url}` // Определение типов interface FileInfo { @@ -275,7 +276,7 @@ router.get('/', async (req, res) => { modalFileName.textContent = 'Загрузка...'; try { - const response = await fetch('/file-content?path=' + encodeURIComponent(filePath)); + const response = await fetch('${getUrl('/file-content?path=')}' + encodeURIComponent(filePath)); if (!response.ok) { throw new Error('Ошибка при загрузке файла'); } @@ -323,7 +324,7 @@ router.get('/', async (req, res) => { clearErrorsBtn.addEventListener('click', async function() { try { - const response = await fetch('/clear-old-errors', { + const response = await fetch('${getUrl('/clear-old-errors')}', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/server/utils/mongo.ts b/server/utils/mongo.ts index 59344d0..ea2d6d0 100644 --- a/server/utils/mongo.ts +++ b/server/utils/mongo.ts @@ -5,11 +5,6 @@ import { mongoUrl } from './const' const dbInstanses = { } -console.log('=======================================================') -console.log(`mongoUrl ->${mongoUrl}<-`) -console.log('=======================================================') - - const mongoDBConnect = async () => { try { const MongoClient = new MDBClient(mongoUrl) diff --git a/server/utils/mongoose.ts b/server/utils/mongoose.ts index bbfaf12..f797605 100644 --- a/server/utils/mongoose.ts +++ b/server/utils/mongoose.ts @@ -2,11 +2,6 @@ import mongoose from 'mongoose' import { mongoUrl } from './const' -console.log('=======================================================') -console.log(`mongoUrl ->${mongoUrl}<-`) -console.log('=======================================================') - - mongoose.connect(mongoUrl).then(() => { console.log('Подключение к MongoDB успешно') }).catch((err) => { From 2d0b97be441e2cbf8542d90a24619e59daf8bad2 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 18:25:37 +0300 Subject: [PATCH 012/147] update statistics screen --- server/server.ts | 168 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 143 insertions(+), 25 deletions(-) diff --git a/server/server.ts b/server/server.ts index 2529983..8c94b68 100644 --- a/server/server.ts +++ b/server/server.ts @@ -13,48 +13,110 @@ const folderPath = path.resolve(__dirname, './routers') const getUrl = (url) => `${process.env.NODE_ENV === 'development' ? '' : '/ms'}${url}` // Определение типов +interface EndpointStats { + total: number; + mock: number; + real: number; +} + interface FileInfo { path: string; type: 'file'; - endpoints: number; + endpoints: EndpointStats; } interface DirectoryInfo { path: string; type: 'directory'; - endpoints: number; + endpoints: EndpointStats; children: (FileInfo | DirectoryInfo)[]; } interface DirScanResult { items: (FileInfo | DirectoryInfo)[]; - totalEndpoints: number; + totalEndpoints: EndpointStats; } // Функция для поиска эндпоинтов в файлах function countEndpoints(filePath) { if (!fs.existsSync(filePath) || !filePath.endsWith('.js') && !filePath.endsWith('.ts')) { - return 0; + return { total: 0, mock: 0, real: 0 }; } try { const content = fs.readFileSync(filePath, 'utf8'); const httpMethods = ['get', 'post', 'put', 'delete', 'patch']; - return httpMethods.reduce((count, method) => { - const regex = new RegExp(`router\\.${method}\\(`, 'gi'); - const matches = content.match(regex) || []; - return count + matches.length; - }, 0); + const endpointMatches = []; + let totalCount = 0; + + // Собираем все эндпоинты и их контекст + httpMethods.forEach(method => { + const regex = new RegExp(`router\\.${method}\\([^{]*{([\\s\\S]*?)(?:}\\s*\\)|},)`, 'gi'); + let match; + + while ((match = regex.exec(content)) !== null) { + totalCount++; + endpointMatches.push({ + method, + body: match[1] + }); + } + }); + + // Проверяем каждый эндпоинт - работает с БД или моковый + let mockCount = 0; + let realCount = 0; + + endpointMatches.forEach(endpoint => { + const body = endpoint.body; + + // Признаки работы с базой данных - модели Mongoose и их методы + const hasDbInteraction = /\b(find|findOne|findById|create|update|delete|remove|aggregate|count|model)\b.*\(/.test(body) || + /\bmongoose\b/.test(body) || + /\.[a-zA-Z]+Model\b/.test(body); + + // Проверка на отправку файлов (считается реальным эндпоинтом) + const hasFileServing = /res\.sendFile\(/.test(body); + + // Проверка на отправку ошибок в JSON (считается реальным эндпоинтом) + const hasErrorResponse = /res\.json\(\s*{.*?(error|success\s*:\s*false).*?}\)/.test(body) || + /res\.status\(.*?\)\.json\(/.test(body); + + // Признаки моковых данных - только явный импорт JSON файлов или отправка JSON без ошибок + const hasMockJsonImport = /require\s*\(\s*['"`].*\.json['"`]\s*\)/.test(body) || + /import\s+.*\s+from\s+['"`].*\.json['"`]/.test(body); + + // JSON ответ, который не является ошибкой (упрощенная проверка) + const hasJsonResponse = /res\.json\(/.test(body) && !hasErrorResponse; + + // Определяем тип эндпоинта + if (hasDbInteraction || hasFileServing || hasErrorResponse) { + // Если работает с БД, отправляет файлы или возвращает ошибки - считаем реальным + realCount++; + } else if (hasMockJsonImport || hasJsonResponse) { + // Если импортирует JSON или отправляет JSON без ошибок - считаем моком + mockCount++; + } else { + // По умолчанию считаем реальным + realCount++; + } + }); + + return { + total: totalCount, + mock: mockCount, + real: realCount + }; } catch (err) { - return 0; + return { total: 0, mock: 0, real: 0 }; } } // Функция для рекурсивного обхода директорий function getAllDirs(dir, basePath = ''): DirScanResult { const items: (FileInfo | DirectoryInfo)[] = []; - let totalEndpoints = 0; + let totalEndpoints = { total: 0, mock: 0, real: 0 }; try { const dirItems = fs.readdirSync(dir); @@ -66,7 +128,9 @@ function getAllDirs(dir, basePath = ''): DirScanResult { if (stat.isDirectory()) { const dirResult = getAllDirs(itemPath, relativePath); - totalEndpoints += dirResult.totalEndpoints; + totalEndpoints.total += dirResult.totalEndpoints.total; + totalEndpoints.mock += dirResult.totalEndpoints.mock; + totalEndpoints.real += dirResult.totalEndpoints.real; items.push({ path: relativePath, @@ -76,7 +140,9 @@ function getAllDirs(dir, basePath = ''): DirScanResult { }); } else { const fileEndpoints = countEndpoints(itemPath); - totalEndpoints += fileEndpoints; + totalEndpoints.total += fileEndpoints.total; + totalEndpoints.mock += fileEndpoints.mock; + totalEndpoints.real += fileEndpoints.real; items.push({ path: relativePath, @@ -103,9 +169,13 @@ function generateDirList(dirs: (FileInfo | DirectoryInfo)[], level = 0) { return `
    ${dirs.map(item => { if (item.type === 'directory') { - const endpointClass = item.endpoints > 0 ? 'has-endpoints' : ''; - const endpointInfo = item.endpoints > 0 - ? `${item.endpoints}` + const endpointClass = item.endpoints.total > 0 ? 'has-endpoints' : ''; + const endpointInfo = item.endpoints.total > 0 + ? `
    + ${item.endpoints.total} + ${item.endpoints.mock} + ${item.endpoints.real} +
    ` : ''; return ` @@ -121,9 +191,13 @@ function generateDirList(dirs: (FileInfo | DirectoryInfo)[], level = 0) { `; } else { - const endpointClass = item.endpoints > 0 ? 'has-endpoints' : ''; - const endpointInfo = item.endpoints > 0 - ? `${item.endpoints}` + const endpointClass = item.endpoints.total > 0 ? 'has-endpoints' : ''; + const endpointInfo = item.endpoints.total > 0 + ? `
    + ${item.endpoints.total} + ${item.endpoints.mock} + ${item.endpoints.real} +
    ` : ''; return ` @@ -200,7 +274,9 @@ router.get('/', async (req, res) => { const routerFolders = fs.readdirSync(routersPath); const directoryResult = getAllDirs(projectRoot); - const totalEndpoints = directoryResult.totalEndpoints; + const totalEndpoints = directoryResult.totalEndpoints.total; + const mockEndpoints = directoryResult.totalEndpoints.mock; + const realEndpoints = directoryResult.totalEndpoints.real; // Получаем последние 10 ошибок const latestErrors = await ErrorLog.find().sort({ createdAt: -1 }).limit(10); @@ -376,6 +452,8 @@ router.get('/', async (req, res) => { :root { --primary-color: #3f51b5; --secondary-color: #f50057; + --mock-color: #ff9800; + --real-color: #4caf50; --bg-color: #f9f9f9; --card-bg: #ffffff; --text-color: #333333; @@ -501,16 +579,32 @@ router.get('/', async (req, res) => { transform: rotate(90deg); } - .endpoint-count { + .endpoint-stats { + display: flex; + gap: 5px; margin-left: 10px; + } + + .endpoint-count { font-size: 0.75rem; color: #fff; - background-color: var(--secondary-color); padding: 2px 8px; border-radius: 12px; font-weight: 500; } + .endpoint-count.total { + background-color: var(--secondary-color); + } + + .endpoint-count.mock { + background-color: var(--mock-color); + } + + .endpoint-count.real { + background-color: var(--real-color); + } + .has-endpoints .dir-item { border-left: 3px solid var(--secondary-color); } @@ -519,6 +613,20 @@ router.get('/', async (req, res) => { border-left: 3px solid var(--secondary-color); } + .stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 15px; + } + + .stats-card.mock { + background: linear-gradient(135deg, #ff9800, #f57c00); + } + + .stats-card.real { + background: linear-gradient(135deg, #4caf50, #388e3c); + } + .section { margin-bottom: 30px; padding: 20px; @@ -848,9 +956,19 @@ router.get('/', async (req, res) => {

    Multy Stub v${pkg.version}

    -
    -
    ${totalEndpoints}
    -
    Всего эндпоинтов
    +
    +
    +
    ${totalEndpoints}
    +
    Всего эндпоинтов
    +
    +
    +
    ${mockEndpoints}
    +
    Моковые эндпоинты
    +
    +
    +
    ${realEndpoints}
    +
    Реальные эндпоинты
    +
    From bde6ab4c7aa77cd322ecd909a7efe63acca13233 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 18:36:13 +0300 Subject: [PATCH 013/147] progress bars --- server/server.ts | 130 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/server/server.ts b/server/server.ts index 8c94b68..801959b 100644 --- a/server/server.ts +++ b/server/server.ts @@ -427,6 +427,47 @@ router.get('/', async (req, res) => { alert('Произошла ошибка: ' + error.message); } }); + + // Обработчик кнопок очистки коллекций + document.querySelectorAll('.clear-model-btn').forEach(button => { + button.addEventListener('click', async function() { + const modelName = this.getAttribute('data-model'); + if (!modelName) return; + + if (!confirm(\`Вы уверены, что хотите очистить коллекцию \${modelName}?\`)) { + return; + } + + try { + const response = await fetch('' + getUrl('/clear-collection'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ modelName }) + }); + + if (!response.ok) { + throw new Error('Ошибка при очистке коллекции'); + } + + const data = await response.json(); + + // Показываем сообщение об успехе + successAction.textContent = 'Коллекция ' + data.model + ' очищена. Удалено ' + data.deletedCount + ' записей'; + successAction.style.display = 'block'; + + // Перезагружаем страницу через 2 секунды + setTimeout(() => { + window.location.reload(); + }, 2000); + + } catch (error) { + console.error('Ошибка:', error); + alert('Произошла ошибка: ' + error.message); + } + }); + }); }); `; @@ -792,6 +833,52 @@ router.get('/', async (req, res) => { font-weight: 500; } + .progress-bar-container { + width: 100%; + background-color: #f0f0f0; + border-radius: 10px; + margin: 10px 0; + overflow: hidden; + } + + .progress-bar { + height: 10px; + background: linear-gradient(90deg, var(--primary-color), #5c6bc0); + border-radius: 10px; + transition: width 0.5s ease; + } + + .model-actions { + display: flex; + justify-content: flex-end; + margin-top: 10px; + } + + .model-button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 5px 10px; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 0.8rem; + font-weight: 500; + transition: var(--transition); + } + + .model-button:hover { + background-color: #303f9f; + box-shadow: var(--shadow); + } + + .model-button-danger { + background-color: #f44336; + } + + .model-button-danger:hover { + background-color: #d32f2f; + } + .model-details { padding: 15px; } @@ -1007,6 +1094,9 @@ router.get('/', async (req, res) => {

    ${count} документов

    +
    +
    +

    Поля:

      @@ -1016,6 +1106,9 @@ router.get('/', async (req, res) => { }).join('')}
    +
    + +
    `; @@ -1078,4 +1171,41 @@ router.post('/clear-old-errors', async (req, res) => { } }); +// Эндпоинт для очистки отдельной коллекции +router.post('/clear-collection', async (req, res) => { + try { + const { modelName } = req.body; + + if (!modelName) { + return res.status(400).json({ + success: false, + error: 'Имя модели не указано' + }); + } + + // Проверяем, существует ли такая модель + if (!mongoose.modelNames().includes(modelName)) { + return res.status(404).json({ + success: false, + error: 'Модель не найдена' + }); + } + + const model = mongoose.model(modelName); + const result = await model.deleteMany({}); + + res.json({ + success: true, + deletedCount: result.deletedCount || 0, + model: modelName + }); + } catch (error) { + console.error(`Ошибка при очистке коллекции:`, error); + res.status(500).json({ + success: false, + error: 'Ошибка при очистке коллекции' + }); + } +}); + export default router From d90fee82d58c781627bbe7a07118c4d09b386dac Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Thu, 8 May 2025 18:36:25 +0300 Subject: [PATCH 014/147] 2.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99d42eb..dd33865 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-stub", - "version": "1.2.1", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-stub", - "version": "1.2.1", + "version": "2.0.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 75e1d2d..3cc5bad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-stub", - "version": "1.2.1", + "version": "2.0.0", "description": "", "main": "server/index.ts", "type": "commonjs", From f89729dbeb2a26961fe08371490d2af598b9da0d Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 18 May 2025 21:58:54 +0300 Subject: [PATCH 015/147] feature/add supabase auth --- package-lock.json | 111 ++++++++++++++++++ package.json | 1 + server/routers/kfu-m-24-1/index.js | 1 + server/routers/kfu-m-24-1/sber_mobile/auth.js | 54 +++++++++ .../kfu-m-24-1/sber_mobile/get-constants.js | 18 +++ .../routers/kfu-m-24-1/sber_mobile/index.js | 6 + 6 files changed, 191 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/auth.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/get-constants.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/index.js diff --git a/package-lock.json b/package-lock.json index 8a9bf9e..c964dd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.2.1", "license": "MIT", "dependencies": { + "@supabase/supabase-js": "^2.49.4", "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", @@ -1773,6 +1774,101 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@supabase/auth-js": { + "version": "2.69.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", + "integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", + "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.49.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz", + "integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.69.1", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.4", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1929,6 +2025,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1965,6 +2067,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", diff --git a/package.json b/package.json index 75e1d2d..e8ad598 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "license": "MIT", "homepage": "https://bitbucket.org/online-mentor/multi-stub#readme", "dependencies": { + "@supabase/supabase-js": "^2.49.4", "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", diff --git a/server/routers/kfu-m-24-1/index.js b/server/routers/kfu-m-24-1/index.js index 609da3e..c65cfc8 100644 --- a/server/routers/kfu-m-24-1/index.js +++ b/server/routers/kfu-m-24-1/index.js @@ -4,6 +4,7 @@ const router = Router() router.use('/eng-it-lean', require('./eng-it-lean/index')) router.use('/sberhubproject', require('./sberhubproject/index')) router.use('/sber_web', require('./sber_web/index')) +router.use('/sber_mobile', require('./sber_mobile/index')) module.exports = router diff --git a/server/routers/kfu-m-24-1/sber_mobile/auth.js b/server/routers/kfu-m-24-1/sber_mobile/auth.js new file mode 100644 index 0000000..6ee3f2e --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/auth.js @@ -0,0 +1,54 @@ +const router = require('express').Router(); +const { createClient } = require('@supabase/supabase-js'); +const { getSupabaseUrl, getSupabaseKey } = require('./get-constants'); + + +(async () => { + const supabaseUrl = await getSupabaseUrl(); + const supabaseAnonKey = await getSupabaseKey(); + supabase = createClient(supabaseUrl, supabaseAnonKey); // supabase — глобальная переменная +})(); + +// POST /sign-in +router.post('/sign-in', async (req, res) => { + const { email, password } = req.body; + const { data, error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// POST /sign-up +router.post('/sign-up', async (req, res) => { + const { email, password } = req.body; + const { data, error } = await supabase.auth.signUp({ email, password }); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// POST /sign-out +router.post('/sign-out', async (req, res) => { + const { access_token } = req.body; + supabase.auth.setSession({ access_token, refresh_token: '' }); + const { error } = await supabase.auth.signOut(); + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true }); +}); + +// POST /reset-password +router.post('/reset-password', async (req, res) => { + const { email } = req.body; + const { data, error } = await supabase.auth.resetPasswordForEmail(email); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// POST /update-password +router.post('/update-password', async (req, res) => { + const { access_token, newPassword } = req.body; + supabase.auth.setSession({ access_token, refresh_token: '' }); + const { data, error } = await supabase.auth.updateUser({ password: newPassword }); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js new file mode 100644 index 0000000..0834951 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js @@ -0,0 +1,18 @@ +const fetch = require('node-fetch'); + +const getSupabaseUrl = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].SUPABASE_URL.value; +}; + +const getSupabaseKey = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].SUPABASE_KEY.value; +}; + +module.exports = { + getSupabaseUrl, + getSupabaseKey, +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js new file mode 100644 index 0000000..8429488 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -0,0 +1,6 @@ +const router = require('express').Router(); +const authRouter = require('./auth'); + +module.exports = router; + +router.use('/auth', authRouter); \ No newline at end of file From 0fbbe33e8a2b8930116879d1d5db70671259accb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Tue, 20 May 2025 13:47:10 +0300 Subject: [PATCH 016/147] fix/fix supabase --- server/routers/kfu-m-24-1/sber_mobile/auth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routers/kfu-m-24-1/sber_mobile/auth.js b/server/routers/kfu-m-24-1/sber_mobile/auth.js index 6ee3f2e..faff6fa 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/auth.js +++ b/server/routers/kfu-m-24-1/sber_mobile/auth.js @@ -2,6 +2,7 @@ const router = require('express').Router(); const { createClient } = require('@supabase/supabase-js'); const { getSupabaseUrl, getSupabaseKey } = require('./get-constants'); +let supabase; (async () => { const supabaseUrl = await getSupabaseUrl(); From ddcf27b02246e58b9dbdea2becc14ed9324bd667 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 May 2025 16:24:30 +0300 Subject: [PATCH 017/147] add supabase refresh --- server/routers/kfu-m-24-1/sber_mobile/auth.js | 16 +++----- .../routers/kfu-m-24-1/sber_mobile/index.js | 4 +- .../kfu-m-24-1/sber_mobile/supabaseClient.js | 37 +++++++++++++++++++ 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/auth.js b/server/routers/kfu-m-24-1/sber_mobile/auth.js index faff6fa..39ef7bd 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/auth.js +++ b/server/routers/kfu-m-24-1/sber_mobile/auth.js @@ -1,18 +1,10 @@ const router = require('express').Router(); -const { createClient } = require('@supabase/supabase-js'); -const { getSupabaseUrl, getSupabaseKey } = require('./get-constants'); - -let supabase; - -(async () => { - const supabaseUrl = await getSupabaseUrl(); - const supabaseAnonKey = await getSupabaseKey(); - supabase = createClient(supabaseUrl, supabaseAnonKey); // supabase — глобальная переменная -})(); +const { getSupabaseClient } = require('./supabaseClient'); // POST /sign-in router.post('/sign-in', async (req, res) => { const { email, password } = req.body; + const supabase = getSupabaseClient(); const { data, error } = await supabase.auth.signInWithPassword({ email, password }); if (error) return res.status(400).json({ error: error.message }); res.json(data); @@ -21,6 +13,7 @@ router.post('/sign-in', async (req, res) => { // POST /sign-up router.post('/sign-up', async (req, res) => { const { email, password } = req.body; + const supabase = getSupabaseClient(); const { data, error } = await supabase.auth.signUp({ email, password }); if (error) return res.status(400).json({ error: error.message }); res.json(data); @@ -29,6 +22,7 @@ router.post('/sign-up', async (req, res) => { // POST /sign-out router.post('/sign-out', async (req, res) => { const { access_token } = req.body; + const supabase = getSupabaseClient(); supabase.auth.setSession({ access_token, refresh_token: '' }); const { error } = await supabase.auth.signOut(); if (error) return res.status(400).json({ error: error.message }); @@ -38,6 +32,7 @@ router.post('/sign-out', async (req, res) => { // POST /reset-password router.post('/reset-password', async (req, res) => { const { email } = req.body; + const supabase = getSupabaseClient(); const { data, error } = await supabase.auth.resetPasswordForEmail(email); if (error) return res.status(400).json({ error: error.message }); res.json(data); @@ -46,6 +41,7 @@ router.post('/reset-password', async (req, res) => { // POST /update-password router.post('/update-password', async (req, res) => { const { access_token, newPassword } = req.body; + const supabase = getSupabaseClient(); supabase.auth.setSession({ access_token, refresh_token: '' }); const { data, error } = await supabase.auth.updateUser({ password: newPassword }); if (error) return res.status(400).json({ error: error.message }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 8429488..e93bb14 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,6 +1,8 @@ const router = require('express').Router(); const authRouter = require('./auth'); +const supabaseRouter = require('./supabaseClient'); module.exports = router; -router.use('/auth', authRouter); \ No newline at end of file +router.use('/auth', authRouter); +router.use('/supabase', supabaseRouter); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js new file mode 100644 index 0000000..8b1c7b7 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -0,0 +1,37 @@ +const { createClient } = require('@supabase/supabase-js'); +const { getSupabaseUrl, getSupabaseKey } = require('./get-constants'); + +let supabase = null; + +async function initSupabaseClient() { + const supabaseUrl = await getSupabaseUrl(); + const supabaseAnonKey = await getSupabaseKey(); + supabase = createClient(supabaseUrl, supabaseAnonKey); + return supabase; +} + +function getSupabaseClient() { + if (!supabase) { + throw new Error('Supabase client is not initialized. Call initSupabaseClient first.'); + } + return supabase; +} + +// POST /refresh-supabase-client +router.post('/refresh-supabase-client', async (req, res) => { +try { + await initSupabaseClient(); + res.json({ success: true, message: 'Supabase client refreshed' }); +} catch (error) { + res.status(500).json({ error: error.message }); +} +}); + +// Инициализация клиента при старте +(async () => { + await initSupabaseClient(); +})(); + +module.exports = { + getSupabaseClient, +}; \ No newline at end of file From 6b5ae7bce1f89cc103c29bcc784b81493c321f9f Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 May 2025 16:43:09 +0300 Subject: [PATCH 018/147] refactor code --- server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 8b1c7b7..fd32b92 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -7,7 +7,6 @@ async function initSupabaseClient() { const supabaseUrl = await getSupabaseUrl(); const supabaseAnonKey = await getSupabaseKey(); supabase = createClient(supabaseUrl, supabaseAnonKey); - return supabase; } function getSupabaseClient() { @@ -16,7 +15,7 @@ function getSupabaseClient() { } return supabase; } - + // POST /refresh-supabase-client router.post('/refresh-supabase-client', async (req, res) => { try { @@ -31,7 +30,7 @@ try { (async () => { await initSupabaseClient(); })(); - + module.exports = { getSupabaseClient, }; \ No newline at end of file From 72d298ef2f62718106c88cd71a2a1c52f03018d4 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 May 2025 20:43:57 +0300 Subject: [PATCH 019/147] fix router --- server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index fd32b92..cbc2248 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -1,3 +1,4 @@ +const router = require('express').Router(); const { createClient } = require('@supabase/supabase-js'); const { getSupabaseUrl, getSupabaseKey } = require('./get-constants'); From 337e3ee2bf6f30625b0bcc4effd53ee30be67364 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 May 2025 20:50:27 +0300 Subject: [PATCH 020/147] fix exports --- server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index cbc2248..0712909 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -34,4 +34,5 @@ try { module.exports = { getSupabaseClient, + router }; \ No newline at end of file From 6835c84cc46f09e78716949b68e1f1beb35469c9 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 May 2025 21:06:51 +0300 Subject: [PATCH 021/147] fix export js object --- server/routers/kfu-m-24-1/sber_mobile/auth.js | 2 +- server/routers/kfu-m-24-1/sber_mobile/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/auth.js b/server/routers/kfu-m-24-1/sber_mobile/auth.js index 39ef7bd..48e781a 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/auth.js +++ b/server/routers/kfu-m-24-1/sber_mobile/auth.js @@ -1,5 +1,5 @@ const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); +const getSupabaseClient = require('./supabaseClient').getSupabaseClient; // POST /sign-in router.post('/sign-in', async (req, res) => { diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index e93bb14..c7e6553 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,6 +1,6 @@ const router = require('express').Router(); const authRouter = require('./auth'); -const supabaseRouter = require('./supabaseClient'); +const supabaseRouter = require('./supabaseClient').router; module.exports = router; From a9490da5a6b37ef3ce4d79710e6dfe67b90f82d9 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 31 May 2025 11:44:46 +0300 Subject: [PATCH 022/147] refactor code --- server/routers/kfu-m-24-1/sber_mobile/auth.js | 2 +- server/routers/kfu-m-24-1/sber_mobile/index.js | 2 +- server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/auth.js b/server/routers/kfu-m-24-1/sber_mobile/auth.js index 48e781a..39ef7bd 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/auth.js +++ b/server/routers/kfu-m-24-1/sber_mobile/auth.js @@ -1,5 +1,5 @@ const router = require('express').Router(); -const getSupabaseClient = require('./supabaseClient').getSupabaseClient; +const { getSupabaseClient } = require('./supabaseClient'); // POST /sign-in router.post('/sign-in', async (req, res) => { diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index c7e6553..be3d4d2 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,6 +1,6 @@ const router = require('express').Router(); const authRouter = require('./auth'); -const supabaseRouter = require('./supabaseClient').router; +const { supabaseRouter } = require('./supabaseClient'); module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 0712909..bac0342 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -34,5 +34,5 @@ try { module.exports = { getSupabaseClient, - router + supabaseRouter: router }; \ No newline at end of file From 539b1d2277b0c0892ceb6c1147846df694a68087 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 31 May 2025 19:13:39 +0300 Subject: [PATCH 023/147] add getting profile proto --- .../kfu-m-24-1/sber_mobile/users/index.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/users/index.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/users/index.js b/server/routers/kfu-m-24-1/sber_mobile/users/index.js new file mode 100644 index 0000000..a32e262 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/users/index.js @@ -0,0 +1,28 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// POST /profile +router.get('/profile', async (req, res) => { + const { user_id } = req.body; + const supabase = getSupabaseClient(); + const { data, error } = await supabase.from('user_profiles').select(` + id, + full_name, + avatar_url, + updated_at, + auth.users(phone) + `).eq('id', user_id); + console.log('@@@@@@@@@@@@@@@@@@@@@@@@'); + console.log(data); + if (error) return res.status(400).json({ error: error.message }); + res.json({ + id: data.id, + username: data.full_name, + avatar_url: data.avatar_url, + phone: data.users.phone, + apartment: '9', + updated_at: data.updated_at + }); +}); + +module.exports = router; \ No newline at end of file From 36107afbc2c318e91e11a152266d6f72ec892cd4 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 31 May 2025 19:17:10 +0300 Subject: [PATCH 024/147] add router --- server/routers/kfu-m-24-1/sber_mobile/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index be3d4d2..d645f49 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,8 +1,10 @@ const router = require('express').Router(); const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); +const profileRouter = require('./users/index'); module.exports = router; -router.use('/auth', authRouter); -router.use('/supabase', supabaseRouter); \ No newline at end of file +router.use('/auth', authRouter); +router.use('/supabase', supabaseRouter); +router.use('', profileRouter); \ No newline at end of file From b5f6f6d30f76696129cebab5cc58191f4eeeb33e Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 31 May 2025 19:19:25 +0300 Subject: [PATCH 025/147] fix router --- server/routers/kfu-m-24-1/sber_mobile/users/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/users/index.js b/server/routers/kfu-m-24-1/sber_mobile/users/index.js index a32e262..6aae4f3 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/users/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/users/index.js @@ -1,5 +1,5 @@ const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); +const { getSupabaseClient } = require('../supabaseClient'); // POST /profile router.get('/profile', async (req, res) => { From ca4bfdade4599ec5125afe8ca448f9c768c957fe Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 31 May 2025 19:27:45 +0300 Subject: [PATCH 026/147] change users --- server/routers/kfu-m-24-1/sber_mobile/users/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/users/index.js b/server/routers/kfu-m-24-1/sber_mobile/users/index.js index 6aae4f3..673b873 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/users/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/users/index.js @@ -10,7 +10,7 @@ router.get('/profile', async (req, res) => { full_name, avatar_url, updated_at, - auth.users(phone) + users(phone) `).eq('id', user_id); console.log('@@@@@@@@@@@@@@@@@@@@@@@@'); console.log(data); From 8031938b2fd46feea7ca0d0c1a5c53006fcfefd2 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 31 May 2025 19:37:17 +0300 Subject: [PATCH 027/147] change request --- .../kfu-m-24-1/sber_mobile/users/index.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/users/index.js b/server/routers/kfu-m-24-1/sber_mobile/users/index.js index 673b873..45e16bf 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/users/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/users/index.js @@ -5,23 +5,21 @@ const { getSupabaseClient } = require('../supabaseClient'); router.get('/profile', async (req, res) => { const { user_id } = req.body; const supabase = getSupabaseClient(); - const { data, error } = await supabase.from('user_profiles').select(` + const { data, error } = await supabase.from('users').select(` id, - full_name, - avatar_url, - updated_at, - users(phone) + phone, + user_profiles(full_name,avatar_url,updated_at) `).eq('id', user_id); console.log('@@@@@@@@@@@@@@@@@@@@@@@@'); console.log(data); if (error) return res.status(400).json({ error: error.message }); res.json({ id: data.id, - username: data.full_name, - avatar_url: data.avatar_url, - phone: data.users.phone, + username: data.user_profiles.full_name, + avatar_url: data.user_profiles.avatar_url, + phone: data.phone, apartment: '9', - updated_at: data.updated_at + updated_at: data.user_profiles.updated_at }); }); From c251a640b652af7d3b5667b03402dab8e85ea68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Tue, 3 Jun 2025 12:24:19 +0300 Subject: [PATCH 028/147] add profile --- .../kfu-m-24-1/sber_mobile/get-constants.js | 7 +++ .../routers/kfu-m-24-1/sber_mobile/index.js | 2 +- .../routers/kfu-m-24-1/sber_mobile/profile.js | 54 +++++++++++++++++++ .../kfu-m-24-1/sber_mobile/supabaseClient.js | 5 +- .../kfu-m-24-1/sber_mobile/users/index.js | 26 --------- 5 files changed, 65 insertions(+), 29 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/profile.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/users/index.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js index 0834951..fba7807 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js +++ b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js @@ -12,7 +12,14 @@ const getSupabaseKey = async () => { return data.features['sber_mobile'].SUPABASE_KEY.value; }; +const getSupabaseServiceKey = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].SUPABASE_SERVICE_KEY.value; +}; + module.exports = { getSupabaseUrl, getSupabaseKey, + getSupabaseServiceKey }; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index d645f49..f8dd26f 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,7 +1,7 @@ const router = require('express').Router(); const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); -const profileRouter = require('./users/index'); +const profileRouter = require('./profile'); module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/profile.js b/server/routers/kfu-m-24-1/sber_mobile/profile.js new file mode 100644 index 0000000..cb4612a --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/profile.js @@ -0,0 +1,54 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// GET /profile +router.get('/profile', async (req, res) => { + const { user_id } = req.body; + const supabase = getSupabaseClient(); + let { data: userData, error: userError } = await supabase.auth.admin.getUserById(user_id); + + if (userError) return res.status(400).json({ error: userError.message }); + + let { data: profileData, error: profileError } = await supabase.from('user_profiles').select(` + id, + full_name, + avatar_url, + updated_at + `).eq('id', user_id).single(); + + if (profileError) return res.status(400).json({ error: profileError.message }); + + res.json({ + id: profileData.id, + username: profileData.full_name, + avatar_url: profileData.avatar_url, + phone: userData.user.phone, + apartment: '9', + updated_at: profileData.updated_at + }); +}); + +// POST /profile +router.post('/profile', async (req, res) => { + const { user_id, data } = req.body; + const supabase = getSupabaseClient(); + + const { data: userData, error: userError } = await supabase.auth.admin.updateUserById( + user_id, + { phone: data.phone } + ) + + if (userError) return res.status(400).json({ error: userError.message }); + + let { error: profileError } = await supabase.from('user_profiles').update({ + full_name: data.username, + avatar_url: data.avatar_url, + // apartment: data.apartment + }).eq('id', user_id).single(); + + if (profileError) return res.status(400).json({ error: profileError.message }); + + res.json({ success: true }); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index bac0342..938cc18 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -1,13 +1,14 @@ const router = require('express').Router(); const { createClient } = require('@supabase/supabase-js'); -const { getSupabaseUrl, getSupabaseKey } = require('./get-constants'); +const { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey } = require('./get-constants'); let supabase = null; async function initSupabaseClient() { const supabaseUrl = await getSupabaseUrl(); const supabaseAnonKey = await getSupabaseKey(); - supabase = createClient(supabaseUrl, supabaseAnonKey); + const supabaseServiceRoleKey = await getSupabaseServiceKey(); + supabase = createClient(supabaseUrl, supabaseServiceRoleKey); } function getSupabaseClient() { diff --git a/server/routers/kfu-m-24-1/sber_mobile/users/index.js b/server/routers/kfu-m-24-1/sber_mobile/users/index.js deleted file mode 100644 index 45e16bf..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/users/index.js +++ /dev/null @@ -1,26 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('../supabaseClient'); - -// POST /profile -router.get('/profile', async (req, res) => { - const { user_id } = req.body; - const supabase = getSupabaseClient(); - const { data, error } = await supabase.from('users').select(` - id, - phone, - user_profiles(full_name,avatar_url,updated_at) - `).eq('id', user_id); - console.log('@@@@@@@@@@@@@@@@@@@@@@@@'); - console.log(data); - if (error) return res.status(400).json({ error: error.message }); - res.json({ - id: data.id, - username: data.user_profiles.full_name, - avatar_url: data.user_profiles.avatar_url, - phone: data.phone, - apartment: '9', - updated_at: data.user_profiles.updated_at - }); -}); - -module.exports = router; \ No newline at end of file From ea691536ac5bb9330d0ac2413a1dedfd5e6facef Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 18:49:25 +0300 Subject: [PATCH 029/147] add db api --- .../sber_mobile/additional_services.js | 56 ++++++++++++ .../kfu-m-24-1/sber_mobile/apartments.js | 14 +++ .../kfu-m-24-1/sber_mobile/buildings.js | 14 +++ .../routers/kfu-m-24-1/sber_mobile/cameras.js | 28 ++++++ .../routers/kfu-m-24-1/sber_mobile/chats.js | 28 ++++++ .../routers/kfu-m-24-1/sber_mobile/index.js | 26 +++++- .../kfu-m-24-1/sber_mobile/initiatives.js | 90 +++++++++++++++++++ .../kfu-m-24-1/sber_mobile/messages.js | 14 +++ .../routers/kfu-m-24-1/sber_mobile/profile.js | 14 +++ .../routers/kfu-m-24-1/sber_mobile/tickets.js | 14 +++ .../kfu-m-24-1/sber_mobile/user_apartments.js | 18 ++++ .../sber_mobile/utility_payments.js | 17 ++++ .../routers/kfu-m-24-1/sber_mobile/votes.js | 44 +++++++++ 13 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/additional_services.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/apartments.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/buildings.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/cameras.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/chats.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/messages.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/tickets.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/user_apartments.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/utility_payments.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/votes.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/additional_services.js b/server/routers/kfu-m-24-1/sber_mobile/additional_services.js new file mode 100644 index 0000000..1861a60 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/additional_services.js @@ -0,0 +1,56 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все дополнительные сервисы (по УК) +router.get('/additional-services', async (req, res) => { + const supabase = getSupabaseClient(); + const { management_company_id } = req.query; + let query = supabase.from('additional_services').select('*'); + if (management_company_id) query = query.eq('management_company_id', management_company_id); + const { data, error } = await query; + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить сервис по id +router.get('/additional-services/:id', async (req, res) => { + const supabase = getSupabaseClient(); + const { id } = req.params; + const { data, error } = await supabase.from('additional_services').select('*').eq('id', id).single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Создать сервис +router.post('/additional-services', async (req, res) => { + const supabase = getSupabaseClient(); + const { title, description, category, price, image_url } = req.body; + const { data, error } = await supabase.from('additional_services').insert([ + { title, description, category, price, image_url } + ]).select().single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Обновить сервис +router.put('/additional-services/:id', async (req, res) => { + const supabase = getSupabaseClient(); + const { id } = req.params; + const { title, description, category, price, image_url } = req.body; + const { data, error } = await supabase.from('additional_services').update({ + title, description, category, price, image_url + }).eq('id', id).select().single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Удалить сервис +router.delete('/additional-services/:id', async (req, res) => { + const supabase = getSupabaseClient(); + const { id } = req.params; + const { error } = await supabase.from('additional_services').delete().eq('id', id); + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true }); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/apartments.js b/server/routers/kfu-m-24-1/sber_mobile/apartments.js new file mode 100644 index 0000000..5182e78 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/apartments.js @@ -0,0 +1,14 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все квартиры по дому +router.get('/apartments', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + if (!building_id) return res.status(400).json({ error: 'building_id required' }); + const { data, error } = await supabase.from('apartments').select('*').eq('building_id', building_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/buildings.js b/server/routers/kfu-m-24-1/sber_mobile/buildings.js new file mode 100644 index 0000000..8230923 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/buildings.js @@ -0,0 +1,14 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все дома по УК +router.get('/buildings', async (req, res) => { + const supabase = getSupabaseClient(); + const { management_company_id } = req.query; + if (!management_company_id) return res.status(400).json({ error: 'management_company_id required' }); + const { data, error } = await supabase.from('buildings').select('*').eq('management_company_id', management_company_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/cameras.js b/server/routers/kfu-m-24-1/sber_mobile/cameras.js new file mode 100644 index 0000000..1425b9f --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/cameras.js @@ -0,0 +1,28 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все камеры по дому +router.get('/cameras', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + if (!building_id) return res.status(400).json({ error: 'building_id required' }); + const { data, error } = await supabase.from('cameras').select('*').eq('building_id', building_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить все камеры по квартире (через building_id) +router.get('/cameras/by-apartment', async (req, res) => { + const supabase = getSupabaseClient(); + const { apartment_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); + // Получаем building_id квартиры и сразу камеры этого дома + const { data, error } = await supabase + .from('cameras') + .select('*, apartments!inner(id, building_id)') + .eq('apartments.id', apartment_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js new file mode 100644 index 0000000..983f0dd --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -0,0 +1,28 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все чаты по дому +router.get('/chats', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + if (!building_id) return res.status(400).json({ error: 'building_id required' }); + const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить все чаты по квартире (через building_id) +router.get('/chats/by-apartment', async (req, res) => { + const supabase = getSupabaseClient(); + const { apartment_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); + // Получаем building_id квартиры и сразу чаты этого дома + const { data, error } = await supabase + .from('chats') + .select('*, apartments!inner(id, building_id)') + .eq('apartments.id', apartment_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index f8dd26f..5c4af66 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -2,9 +2,33 @@ const router = require('express').Router(); const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); const profileRouter = require('./profile'); +const initiativesRouter = require('./initiatives'); +const votesRouter = require('./votes'); +const paymentServicesRouter = require('./payment_services'); +const additionalServicesRouter = require('./additional_services'); +const chatsRouter = require('./chats'); +const camerasRouter = require('./cameras'); +const ticketsRouter = require('./tickets'); +const messagesRouter = require('./messages'); +const utilityPaymentsRouter = require('./utility_payments'); +const apartmentsRouter = require('./apartments'); +const buildingsRouter = require('./buildings'); +const userApartmentsRouter = require('./user_apartments'); module.exports = router; router.use('/auth', authRouter); router.use('/supabase', supabaseRouter); -router.use('', profileRouter); \ No newline at end of file +router.use('', profileRouter); +router.use('', initiativesRouter); +router.use('', votesRouter); +router.use('', paymentServicesRouter); +router.use('', additionalServicesRouter); +router.use('', chatsRouter); +router.use('', camerasRouter); +router.use('', ticketsRouter); +router.use('', messagesRouter); +router.use('', utilityPaymentsRouter); +router.use('', apartmentsRouter); +router.use('', buildingsRouter); +router.use('', userApartmentsRouter); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js new file mode 100644 index 0000000..4e0469b --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js @@ -0,0 +1,90 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все инициативы (по дому) +router.get('/initiatives', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + let query = supabase.from('initiatives').select('*'); + if (building_id) query = query.eq('building_id', building_id); + const { data, error } = await query; + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить инициативу по id (и optionally building_id) +router.get('/initiatives/:id', async (req, res) => { + const supabase = getSupabaseClient(); + const { id } = req.params; + const { building_id } = req.query; + let query = supabase.from('initiatives').select('*').eq('id', id); + if (building_id) query = query.eq('building_id', building_id); + const { data, error } = await query.single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Создать инициативу +router.post('/initiatives', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id, creator_id, title, description, status, target_amount, image_url } = req.body; + const { data, error } = await supabase.from('initiatives').insert([ + { building_id, creator_id, title, description, status, target_amount, image_url } + ]).select().single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Обновить инициативу +router.put('/initiatives/:id', async (req, res) => { + const supabase = getSupabaseClient(); + const { id } = req.params; + const { title, description, status, target_amount, current_amount, image_url } = req.body; + const { data, error } = await supabase.from('initiatives').update({ + title, description, status, target_amount, current_amount, image_url + }).eq('id', id).select().single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Удалить инициативу +router.delete('/initiatives/:id', async (req, res) => { + const supabase = getSupabaseClient(); + const { id } = req.params; + const { error } = await supabase.from('initiatives').delete().eq('id', id); + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true }); +}); + +// Получить все инициативы по квартире с голосами пользователя +router.get('/initiatives/by-apartment', async (req, res) => { + const supabase = getSupabaseClient(); + const { apartment_id, user_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); + // Получаем building_id квартиры + const { data: apartments, error: err1 } = await supabase + .from('apartments') + .select('building_id') + .eq('id', apartment_id) + .single(); + if (err1) return res.status(400).json({ error: err1.message }); + const building_id = apartments.building_id; + // Получаем инициативы этого дома с голосами пользователя (если user_id передан) + let selectStr = '*, votes:initiatives(id, votes!left(user_id, vote_type))'; + if (!user_id) selectStr = '*'; + const { data, error } = await supabase + .from('initiatives') + .select(selectStr) + .eq('building_id', building_id); + if (error) return res.status(400).json({ error: error.message }); + // Если user_id передан, фильтруем только голос текущего пользователя + if (user_id && data) { + data.forEach(initiative => { + initiative.user_vote = (initiative.votes || []).find(v => v.user_id === user_id) || null; + delete initiative.votes; + }); + } + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js new file mode 100644 index 0000000..b1a8188 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -0,0 +1,14 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все сообщения в чате +router.get('/messages', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.query; + if (!chat_id) return res.status(400).json({ error: 'chat_id required' }); + const { data, error } = await supabase.from('messages').select('*').eq('chat_id', chat_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/profile.js b/server/routers/kfu-m-24-1/sber_mobile/profile.js index cb4612a..09c97c6 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/profile.js +++ b/server/routers/kfu-m-24-1/sber_mobile/profile.js @@ -51,4 +51,18 @@ router.post('/profile', async (req, res) => { res.json({ success: true }); }); +// Получить управляющую компанию по квартире +router.get('/management-company', async (req, res) => { + const supabase = getSupabaseClient(); + const { apartment_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); + const { data: apartment, error: err1 } = await supabase.from('apartments').select('building_id').eq('id', apartment_id).single(); + if (err1) return res.status(400).json({ error: err1.message }); + const { data: building, error: err2 } = await supabase.from('buildings').select('management_company_id').eq('id', apartment.building_id).single(); + if (err2) return res.status(400).json({ error: err2.message }); + const { data: company, error: err3 } = await supabase.from('management_companies').select('*').eq('id', building.management_company_id).single(); + if (err3) return res.status(400).json({ error: err3.message }); + res.json(company); +}); + module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/tickets.js b/server/routers/kfu-m-24-1/sber_mobile/tickets.js new file mode 100644 index 0000000..5ff082d --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/tickets.js @@ -0,0 +1,14 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все тикеты по дому +router.get('/tickets', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + if (!building_id) return res.status(400).json({ error: 'building_id required' }); + const { data, error } = await supabase.from('tickets').select('*').eq('building_id', building_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/user_apartments.js b/server/routers/kfu-m-24-1/sber_mobile/user_apartments.js new file mode 100644 index 0000000..e5421ba --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/user_apartments.js @@ -0,0 +1,18 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все квартиры пользователя +router.get('/user-apartments', async (req, res) => { + const supabase = getSupabaseClient(); + const { user_id } = req.query; + if (!user_id) return res.status(400).json({ error: 'user_id required' }); + const { data: links, error: err1 } = await supabase.from('apartment_residents').select('apartment_id').eq('user_id', user_id); + if (err1) return res.status(400).json({ error: err1.message }); + const apartmentIds = links.map(l => l.apartment_id); + if (!apartmentIds.length) return res.json([]); + const { data, error } = await supabase.from('apartments').select('*').in('id', apartmentIds); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js new file mode 100644 index 0000000..a6e8b98 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js @@ -0,0 +1,17 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все платежи по конкретной квартире с данными сервиса +router.get('/utility-payments', async (req, res) => { + const supabase = getSupabaseClient(); + const { apartment_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); + const { data, error } = await supabase + .from('utility_payments') + .select('*, payment_services(*)') + .eq('apartment_id', apartment_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/votes.js b/server/routers/kfu-m-24-1/sber_mobile/votes.js new file mode 100644 index 0000000..d46df22 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/votes.js @@ -0,0 +1,44 @@ +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); + +// Получить все голоса по инициативе +router.get('/votes/:initiative_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { initiative_id } = req.params; + const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить голос пользователя по инициативе +router.get('/votes/:initiative_id/:user_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { initiative_id, user_id } = req.params; + const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id).eq('user_id', user_id).single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить все голоса по инициативе (через query) +router.get('/votes', async (req, res) => { + const supabase = getSupabaseClient(); + const { initiative_id } = req.query; + if (!initiative_id) return res.status(400).json({ error: 'initiative_id required' }); + const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Проголосовать (создать или обновить голос) +router.post('/votes', async (req, res) => { + const supabase = getSupabaseClient(); + const { initiative_id, user_id, vote_type } = req.body; + // upsert: если голос уже есть, обновить, иначе создать + const { data, error } = await supabase.from('votes').upsert([ + { initiative_id, user_id, vote_type } + ], { onConflict: ['initiative_id', 'user_id'] }).select().single(); + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +module.exports = router; \ No newline at end of file From 0500497fc108b1e10b89dfdc0e72393ebbb3b932 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 7 Jun 2025 00:48:51 +0300 Subject: [PATCH 030/147] fix api and add apartment info --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 222 ++++++++++++++++++ .../kfu-m-24-1/sber_mobile/apartments.js | 31 +++ .../routers/kfu-m-24-1/sber_mobile/index.js | 2 - .../routers/kfu-m-24-1/sber_mobile/profile.js | 3 +- 4 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt new file mode 100644 index 0000000..81dade2 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -0,0 +1,222 @@ +-- Расширение для генерации UUID +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 1. Управляющие компании +CREATE TABLE management_companies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + logo_url TEXT, + contact_phone TEXT NOT NULL, + email TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 2. Жилые дома +CREATE TABLE buildings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + management_company_id UUID NOT NULL REFERENCES management_companies(id), + name TEXT, + address TEXT NOT NULL, + floors INTEGER, + entrances INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 3. Профили пользователей +CREATE TABLE user_profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id), + full_name TEXT, + avatar_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 4. Квартиры +CREATE TABLE apartments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + number TEXT NOT NULL, + area DECIMAL(10, 2), + floor INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 5. Связь пользователей с квартирами +CREATE TABLE apartment_residents ( + apartment_id UUID NOT NULL REFERENCES apartments(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + is_owner BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (apartment_id, user_id) +); + +-- 6. Сервисы УК +CREATE TABLE management_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + management_company_id UUID NOT NULL REFERENCES management_companies(id), + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL, + base_price DECIMAL(10, 2), + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 7. Связь сервисов УК с домами +CREATE TABLE building_management_services ( + building_id UUID NOT NULL REFERENCES buildings(id), + service_id UUID NOT NULL REFERENCES management_services(id), + custom_price DECIMAL(10, 2), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (building_id, service_id) +); + +-- 8. Платежные сервисы +CREATE TABLE payment_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + logo_url TEXT, + provider_name TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 9. Дополнительные сервисы +CREATE TABLE additional_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL, + price DECIMAL(10, 2), + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 10. Инициативы +CREATE TABLE initiatives ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + creator_id UUID NOT NULL REFERENCES auth.users(id), + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK ( + status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected') + ), + target_amount DECIMAL(10, 2), + current_amount DECIMAL(10, 2) DEFAULT 0, + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 11. Голосования +CREATE TABLE votes ( + initiative_id UUID NOT NULL REFERENCES initiatives(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')), + created_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (initiative_id, user_id) +); + +-- 12. Чат +CREATE TABLE chats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + name TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 13. Сообщения +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chat_id UUID NOT NULL REFERENCES chats(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + text TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 14. Камеры +CREATE TABLE cameras ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + location TEXT NOT NULL, + stream_url TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 15. Платежи ЖКХ (исправленная версия) +CREATE TABLE utility_payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + apartment_id UUID NOT NULL REFERENCES apartments(id), + service_id UUID NOT NULL REFERENCES payment_services(id), + amount DECIMAL(10, 2) NOT NULL, + period DATE NOT NULL, + status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 16. Заявки +CREATE TABLE tickets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id), + building_id UUID NOT NULL REFERENCES buildings(id), + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), + category TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Индексы +CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); +CREATE INDEX idx_management_services_company ON management_services(management_company_id); +CREATE INDEX idx_building_services_building ON building_management_services(building_id); +CREATE INDEX idx_initiatives_building ON initiatives(building_id); +CREATE INDEX idx_votes_initiative ON votes(initiative_id); +CREATE INDEX idx_messages_chat ON messages(chat_id); +CREATE INDEX idx_cameras_building ON cameras(building_id); +CREATE INDEX idx_tickets_user ON tickets(user_id); +CREATE INDEX idx_apartments_building ON apartments(building_id); +CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); +CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); +CREATE INDEX idx_payments_apartment ON utility_payments(apartment_id); +CREATE INDEX idx_payments_service ON utility_payments(service_id); + +-- Триггеры для обновления updated_at +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Применяем триггеры ко всем таблицам с updated_at +DO $$ +DECLARE + t record; +BEGIN + FOR t IN + SELECT table_name + FROM information_schema.columns + WHERE column_name = 'updated_at' + AND table_schema = 'public' + LOOP + EXECUTE format('CREATE TRIGGER trigger_%s_updated_at + BEFORE UPDATE ON %I + FOR EACH ROW EXECUTE FUNCTION update_updated_at()', + t.table_name, t.table_name); + END LOOP; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/apartments.js b/server/routers/kfu-m-24-1/sber_mobile/apartments.js index 5182e78..fd360ef 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/apartments.js +++ b/server/routers/kfu-m-24-1/sber_mobile/apartments.js @@ -11,4 +11,35 @@ router.get('/apartments', async (req, res) => { res.json(data); }); +// Получить адрес квартиры и название дома по id квартиры +router.get('/apartment-info', async (req, res) => { + const supabase = getSupabaseClient(); + const { apartment_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); + + // Получаем квартиру с building_id и номером + const { data: apartment, error: err1 } = await supabase + .from('apartments') + .select('id, number, building_id') + .eq('id', apartment_id) + .single(); + if (err1) return res.status(400).json({ error: err1.message }); + + // Получаем дом по building_id + const { data: building, error: err2 } = await supabase + .from('buildings') + .select('id, name, address') + .eq('id', apartment.building_id) + .single(); + if (err2) return res.status(400).json({ error: err2.message }); + + res.json({ + apartment_id: apartment.id, + apartment_number: apartment.number, + building_id: building.id, + building_name: building.name, + building_address: building.address + }); +}); + module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 5c4af66..ea0ca85 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -4,7 +4,6 @@ const { supabaseRouter } = require('./supabaseClient'); const profileRouter = require('./profile'); const initiativesRouter = require('./initiatives'); const votesRouter = require('./votes'); -const paymentServicesRouter = require('./payment_services'); const additionalServicesRouter = require('./additional_services'); const chatsRouter = require('./chats'); const camerasRouter = require('./cameras'); @@ -22,7 +21,6 @@ router.use('/supabase', supabaseRouter); router.use('', profileRouter); router.use('', initiativesRouter); router.use('', votesRouter); -router.use('', paymentServicesRouter); router.use('', additionalServicesRouter); router.use('', chatsRouter); router.use('', camerasRouter); diff --git a/server/routers/kfu-m-24-1/sber_mobile/profile.js b/server/routers/kfu-m-24-1/sber_mobile/profile.js index 09c97c6..982a17e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/profile.js +++ b/server/routers/kfu-m-24-1/sber_mobile/profile.js @@ -3,7 +3,7 @@ const { getSupabaseClient } = require('./supabaseClient'); // GET /profile router.get('/profile', async (req, res) => { - const { user_id } = req.body; + const { user_id } = req.query; const supabase = getSupabaseClient(); let { data: userData, error: userError } = await supabase.auth.admin.getUserById(user_id); @@ -23,7 +23,6 @@ router.get('/profile', async (req, res) => { username: profileData.full_name, avatar_url: profileData.avatar_url, phone: userData.user.phone, - apartment: '9', updated_at: profileData.updated_at }); }); From 904a227adb5274d655d1ba9a3cda07976aec9fe4 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 7 Jun 2025 11:11:45 +0000 Subject: [PATCH 031/147] delete server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 222 ------------------ 1 file changed, 222 deletions(-) delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt deleted file mode 100644 index 81dade2..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ /dev/null @@ -1,222 +0,0 @@ --- Расширение для генерации UUID -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- 1. Управляющие компании -CREATE TABLE management_companies ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - logo_url TEXT, - contact_phone TEXT NOT NULL, - email TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 2. Жилые дома -CREATE TABLE buildings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - management_company_id UUID NOT NULL REFERENCES management_companies(id), - name TEXT, - address TEXT NOT NULL, - floors INTEGER, - entrances INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 3. Профили пользователей -CREATE TABLE user_profiles ( - id UUID PRIMARY KEY REFERENCES auth.users(id), - full_name TEXT, - avatar_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 4. Квартиры -CREATE TABLE apartments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - number TEXT NOT NULL, - area DECIMAL(10, 2), - floor INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 5. Связь пользователей с квартирами -CREATE TABLE apartment_residents ( - apartment_id UUID NOT NULL REFERENCES apartments(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - is_owner BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (apartment_id, user_id) -); - --- 6. Сервисы УК -CREATE TABLE management_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - management_company_id UUID NOT NULL REFERENCES management_companies(id), - title TEXT NOT NULL, - description TEXT, - category TEXT NOT NULL, - base_price DECIMAL(10, 2), - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 7. Связь сервисов УК с домами -CREATE TABLE building_management_services ( - building_id UUID NOT NULL REFERENCES buildings(id), - service_id UUID NOT NULL REFERENCES management_services(id), - custom_price DECIMAL(10, 2), - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (building_id, service_id) -); - --- 8. Платежные сервисы -CREATE TABLE payment_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - logo_url TEXT, - provider_name TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 9. Дополнительные сервисы -CREATE TABLE additional_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title TEXT NOT NULL, - description TEXT, - category TEXT NOT NULL, - price DECIMAL(10, 2), - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 10. Инициативы -CREATE TABLE initiatives ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - creator_id UUID NOT NULL REFERENCES auth.users(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - status TEXT NOT NULL CHECK ( - status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected') - ), - target_amount DECIMAL(10, 2), - current_amount DECIMAL(10, 2) DEFAULT 0, - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 11. Голосования -CREATE TABLE votes ( - initiative_id UUID NOT NULL REFERENCES initiatives(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')), - created_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (initiative_id, user_id) -); - --- 12. Чат -CREATE TABLE chats ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - name TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 13. Сообщения -CREATE TABLE messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - chat_id UUID NOT NULL REFERENCES chats(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - text TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 14. Камеры -CREATE TABLE cameras ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - location TEXT NOT NULL, - stream_url TEXT NOT NULL, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 15. Платежи ЖКХ (исправленная версия) -CREATE TABLE utility_payments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - apartment_id UUID NOT NULL REFERENCES apartments(id), - service_id UUID NOT NULL REFERENCES payment_services(id), - amount DECIMAL(10, 2) NOT NULL, - period DATE NOT NULL, - status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 16. Заявки -CREATE TABLE tickets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id), - building_id UUID NOT NULL REFERENCES buildings(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), - category TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Индексы -CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); -CREATE INDEX idx_management_services_company ON management_services(management_company_id); -CREATE INDEX idx_building_services_building ON building_management_services(building_id); -CREATE INDEX idx_initiatives_building ON initiatives(building_id); -CREATE INDEX idx_votes_initiative ON votes(initiative_id); -CREATE INDEX idx_messages_chat ON messages(chat_id); -CREATE INDEX idx_cameras_building ON cameras(building_id); -CREATE INDEX idx_tickets_user ON tickets(user_id); -CREATE INDEX idx_apartments_building ON apartments(building_id); -CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); -CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); -CREATE INDEX idx_payments_apartment ON utility_payments(apartment_id); -CREATE INDEX idx_payments_service ON utility_payments(service_id); - --- Триггеры для обновления updated_at -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Применяем триггеры ко всем таблицам с updated_at -DO $$ -DECLARE - t record; -BEGIN - FOR t IN - SELECT table_name - FROM information_schema.columns - WHERE column_name = 'updated_at' - AND table_schema = 'public' - LOOP - EXECUTE format('CREATE TRIGGER trigger_%s_updated_at - BEFORE UPDATE ON %I - FOR EACH ROW EXECUTE FUNCTION update_updated_at()', - t.table_name, t.table_name); - END LOOP; -END; -$$ LANGUAGE plpgsql; \ No newline at end of file From 18cfa427d2d9fcfe53bfae09daa66fa33f4ae83c Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 7 Jun 2025 15:13:36 +0300 Subject: [PATCH 032/147] add sql --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt new file mode 100644 index 0000000..81dade2 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -0,0 +1,222 @@ +-- Расширение для генерации UUID +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 1. Управляющие компании +CREATE TABLE management_companies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + logo_url TEXT, + contact_phone TEXT NOT NULL, + email TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 2. Жилые дома +CREATE TABLE buildings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + management_company_id UUID NOT NULL REFERENCES management_companies(id), + name TEXT, + address TEXT NOT NULL, + floors INTEGER, + entrances INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 3. Профили пользователей +CREATE TABLE user_profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id), + full_name TEXT, + avatar_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 4. Квартиры +CREATE TABLE apartments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + number TEXT NOT NULL, + area DECIMAL(10, 2), + floor INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 5. Связь пользователей с квартирами +CREATE TABLE apartment_residents ( + apartment_id UUID NOT NULL REFERENCES apartments(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + is_owner BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (apartment_id, user_id) +); + +-- 6. Сервисы УК +CREATE TABLE management_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + management_company_id UUID NOT NULL REFERENCES management_companies(id), + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL, + base_price DECIMAL(10, 2), + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 7. Связь сервисов УК с домами +CREATE TABLE building_management_services ( + building_id UUID NOT NULL REFERENCES buildings(id), + service_id UUID NOT NULL REFERENCES management_services(id), + custom_price DECIMAL(10, 2), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (building_id, service_id) +); + +-- 8. Платежные сервисы +CREATE TABLE payment_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + logo_url TEXT, + provider_name TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 9. Дополнительные сервисы +CREATE TABLE additional_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL, + price DECIMAL(10, 2), + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 10. Инициативы +CREATE TABLE initiatives ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + creator_id UUID NOT NULL REFERENCES auth.users(id), + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK ( + status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected') + ), + target_amount DECIMAL(10, 2), + current_amount DECIMAL(10, 2) DEFAULT 0, + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 11. Голосования +CREATE TABLE votes ( + initiative_id UUID NOT NULL REFERENCES initiatives(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')), + created_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (initiative_id, user_id) +); + +-- 12. Чат +CREATE TABLE chats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + name TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 13. Сообщения +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chat_id UUID NOT NULL REFERENCES chats(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + text TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 14. Камеры +CREATE TABLE cameras ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + location TEXT NOT NULL, + stream_url TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 15. Платежи ЖКХ (исправленная версия) +CREATE TABLE utility_payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + apartment_id UUID NOT NULL REFERENCES apartments(id), + service_id UUID NOT NULL REFERENCES payment_services(id), + amount DECIMAL(10, 2) NOT NULL, + period DATE NOT NULL, + status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 16. Заявки +CREATE TABLE tickets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id), + building_id UUID NOT NULL REFERENCES buildings(id), + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), + category TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Индексы +CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); +CREATE INDEX idx_management_services_company ON management_services(management_company_id); +CREATE INDEX idx_building_services_building ON building_management_services(building_id); +CREATE INDEX idx_initiatives_building ON initiatives(building_id); +CREATE INDEX idx_votes_initiative ON votes(initiative_id); +CREATE INDEX idx_messages_chat ON messages(chat_id); +CREATE INDEX idx_cameras_building ON cameras(building_id); +CREATE INDEX idx_tickets_user ON tickets(user_id); +CREATE INDEX idx_apartments_building ON apartments(building_id); +CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); +CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); +CREATE INDEX idx_payments_apartment ON utility_payments(apartment_id); +CREATE INDEX idx_payments_service ON utility_payments(service_id); + +-- Триггеры для обновления updated_at +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Применяем триггеры ко всем таблицам с updated_at +DO $$ +DECLARE + t record; +BEGIN + FOR t IN + SELECT table_name + FROM information_schema.columns + WHERE column_name = 'updated_at' + AND table_schema = 'public' + LOOP + EXECUTE format('CREATE TRIGGER trigger_%s_updated_at + BEFORE UPDATE ON %I + FOR EACH ROW EXECUTE FUNCTION update_updated_at()', + t.table_name, t.table_name); + END LOOP; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file From 01b6e4ae72f6fdba7c63f2912e06da3cb7f2d7a4 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 7 Jun 2025 15:20:23 +0300 Subject: [PATCH 033/147] delete sql --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 222 ------------------ 1 file changed, 222 deletions(-) delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt deleted file mode 100644 index 81dade2..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ /dev/null @@ -1,222 +0,0 @@ --- Расширение для генерации UUID -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- 1. Управляющие компании -CREATE TABLE management_companies ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - logo_url TEXT, - contact_phone TEXT NOT NULL, - email TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 2. Жилые дома -CREATE TABLE buildings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - management_company_id UUID NOT NULL REFERENCES management_companies(id), - name TEXT, - address TEXT NOT NULL, - floors INTEGER, - entrances INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 3. Профили пользователей -CREATE TABLE user_profiles ( - id UUID PRIMARY KEY REFERENCES auth.users(id), - full_name TEXT, - avatar_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 4. Квартиры -CREATE TABLE apartments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - number TEXT NOT NULL, - area DECIMAL(10, 2), - floor INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 5. Связь пользователей с квартирами -CREATE TABLE apartment_residents ( - apartment_id UUID NOT NULL REFERENCES apartments(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - is_owner BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (apartment_id, user_id) -); - --- 6. Сервисы УК -CREATE TABLE management_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - management_company_id UUID NOT NULL REFERENCES management_companies(id), - title TEXT NOT NULL, - description TEXT, - category TEXT NOT NULL, - base_price DECIMAL(10, 2), - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 7. Связь сервисов УК с домами -CREATE TABLE building_management_services ( - building_id UUID NOT NULL REFERENCES buildings(id), - service_id UUID NOT NULL REFERENCES management_services(id), - custom_price DECIMAL(10, 2), - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (building_id, service_id) -); - --- 8. Платежные сервисы -CREATE TABLE payment_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - logo_url TEXT, - provider_name TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 9. Дополнительные сервисы -CREATE TABLE additional_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title TEXT NOT NULL, - description TEXT, - category TEXT NOT NULL, - price DECIMAL(10, 2), - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 10. Инициативы -CREATE TABLE initiatives ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - creator_id UUID NOT NULL REFERENCES auth.users(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - status TEXT NOT NULL CHECK ( - status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected') - ), - target_amount DECIMAL(10, 2), - current_amount DECIMAL(10, 2) DEFAULT 0, - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 11. Голосования -CREATE TABLE votes ( - initiative_id UUID NOT NULL REFERENCES initiatives(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')), - created_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (initiative_id, user_id) -); - --- 12. Чат -CREATE TABLE chats ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - name TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 13. Сообщения -CREATE TABLE messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - chat_id UUID NOT NULL REFERENCES chats(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - text TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 14. Камеры -CREATE TABLE cameras ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - location TEXT NOT NULL, - stream_url TEXT NOT NULL, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 15. Платежи ЖКХ (исправленная версия) -CREATE TABLE utility_payments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - apartment_id UUID NOT NULL REFERENCES apartments(id), - service_id UUID NOT NULL REFERENCES payment_services(id), - amount DECIMAL(10, 2) NOT NULL, - period DATE NOT NULL, - status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 16. Заявки -CREATE TABLE tickets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id), - building_id UUID NOT NULL REFERENCES buildings(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), - category TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- Индексы -CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); -CREATE INDEX idx_management_services_company ON management_services(management_company_id); -CREATE INDEX idx_building_services_building ON building_management_services(building_id); -CREATE INDEX idx_initiatives_building ON initiatives(building_id); -CREATE INDEX idx_votes_initiative ON votes(initiative_id); -CREATE INDEX idx_messages_chat ON messages(chat_id); -CREATE INDEX idx_cameras_building ON cameras(building_id); -CREATE INDEX idx_tickets_user ON tickets(user_id); -CREATE INDEX idx_apartments_building ON apartments(building_id); -CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); -CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); -CREATE INDEX idx_payments_apartment ON utility_payments(apartment_id); -CREATE INDEX idx_payments_service ON utility_payments(service_id); - --- Триггеры для обновления updated_at -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Применяем триггеры ко всем таблицам с updated_at -DO $$ -DECLARE - t record; -BEGIN - FOR t IN - SELECT table_name - FROM information_schema.columns - WHERE column_name = 'updated_at' - AND table_schema = 'public' - LOOP - EXECUTE format('CREATE TRIGGER trigger_%s_updated_at - BEFORE UPDATE ON %I - FOR EACH ROW EXECUTE FUNCTION update_updated_at()', - t.table_name, t.table_name); - END LOOP; -END; -$$ LANGUAGE plpgsql; \ No newline at end of file From a0c9c5bab169508f538a6dd5fb5c72d9d6891eb7 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 7 Jun 2025 15:26:22 +0300 Subject: [PATCH 034/147] add db scheme --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt new file mode 100644 index 0000000..81dade2 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -0,0 +1,222 @@ +-- Расширение для генерации UUID +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- 1. Управляющие компании +CREATE TABLE management_companies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + logo_url TEXT, + contact_phone TEXT NOT NULL, + email TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 2. Жилые дома +CREATE TABLE buildings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + management_company_id UUID NOT NULL REFERENCES management_companies(id), + name TEXT, + address TEXT NOT NULL, + floors INTEGER, + entrances INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 3. Профили пользователей +CREATE TABLE user_profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id), + full_name TEXT, + avatar_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 4. Квартиры +CREATE TABLE apartments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + number TEXT NOT NULL, + area DECIMAL(10, 2), + floor INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 5. Связь пользователей с квартирами +CREATE TABLE apartment_residents ( + apartment_id UUID NOT NULL REFERENCES apartments(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + is_owner BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (apartment_id, user_id) +); + +-- 6. Сервисы УК +CREATE TABLE management_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + management_company_id UUID NOT NULL REFERENCES management_companies(id), + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL, + base_price DECIMAL(10, 2), + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 7. Связь сервисов УК с домами +CREATE TABLE building_management_services ( + building_id UUID NOT NULL REFERENCES buildings(id), + service_id UUID NOT NULL REFERENCES management_services(id), + custom_price DECIMAL(10, 2), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (building_id, service_id) +); + +-- 8. Платежные сервисы +CREATE TABLE payment_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + logo_url TEXT, + provider_name TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 9. Дополнительные сервисы +CREATE TABLE additional_services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL, + price DECIMAL(10, 2), + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 10. Инициативы +CREATE TABLE initiatives ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + creator_id UUID NOT NULL REFERENCES auth.users(id), + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK ( + status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected') + ), + target_amount DECIMAL(10, 2), + current_amount DECIMAL(10, 2) DEFAULT 0, + image_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 11. Голосования +CREATE TABLE votes ( + initiative_id UUID NOT NULL REFERENCES initiatives(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')), + created_at TIMESTAMPTZ DEFAULT NOW(), + PRIMARY KEY (initiative_id, user_id) +); + +-- 12. Чат +CREATE TABLE chats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + name TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 13. Сообщения +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chat_id UUID NOT NULL REFERENCES chats(id), + user_id UUID NOT NULL REFERENCES auth.users(id), + text TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 14. Камеры +CREATE TABLE cameras ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + building_id UUID NOT NULL REFERENCES buildings(id), + location TEXT NOT NULL, + stream_url TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 15. Платежи ЖКХ (исправленная версия) +CREATE TABLE utility_payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + apartment_id UUID NOT NULL REFERENCES apartments(id), + service_id UUID NOT NULL REFERENCES payment_services(id), + amount DECIMAL(10, 2) NOT NULL, + period DATE NOT NULL, + status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 16. Заявки +CREATE TABLE tickets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id), + building_id UUID NOT NULL REFERENCES buildings(id), + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), + category TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Индексы +CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); +CREATE INDEX idx_management_services_company ON management_services(management_company_id); +CREATE INDEX idx_building_services_building ON building_management_services(building_id); +CREATE INDEX idx_initiatives_building ON initiatives(building_id); +CREATE INDEX idx_votes_initiative ON votes(initiative_id); +CREATE INDEX idx_messages_chat ON messages(chat_id); +CREATE INDEX idx_cameras_building ON cameras(building_id); +CREATE INDEX idx_tickets_user ON tickets(user_id); +CREATE INDEX idx_apartments_building ON apartments(building_id); +CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); +CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); +CREATE INDEX idx_payments_apartment ON utility_payments(apartment_id); +CREATE INDEX idx_payments_service ON utility_payments(service_id); + +-- Триггеры для обновления updated_at +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Применяем триггеры ко всем таблицам с updated_at +DO $$ +DECLARE + t record; +BEGIN + FOR t IN + SELECT table_name + FROM information_schema.columns + WHERE column_name = 'updated_at' + AND table_schema = 'public' + LOOP + EXECUTE format('CREATE TRIGGER trigger_%s_updated_at + BEFORE UPDATE ON %I + FOR EACH ROW EXECUTE FUNCTION update_updated_at()', + t.table_name, t.table_name); + END LOOP; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file From 0c0c62fe1b211f01024d5fccd7945a88f476c2c2 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 7 Jun 2025 16:12:29 +0300 Subject: [PATCH 035/147] add avatar getting --- server/routers/kfu-m-24-1/sber_mobile/index.js | 4 +++- server/routers/kfu-m-24-1/sber_mobile/media.js | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/media.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index ea0ca85..3a655bc 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -13,6 +13,7 @@ const utilityPaymentsRouter = require('./utility_payments'); const apartmentsRouter = require('./apartments'); const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); +const avatarRouter = require('./media'); module.exports = router; @@ -29,4 +30,5 @@ router.use('', messagesRouter); router.use('', utilityPaymentsRouter); router.use('', apartmentsRouter); router.use('', buildingsRouter); -router.use('', userApartmentsRouter); \ No newline at end of file +router.use('', userApartmentsRouter); +router.use('', avatarRouter); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/media.js b/server/routers/kfu-m-24-1/sber_mobile/media.js new file mode 100644 index 0000000..e2a8df9 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/media.js @@ -0,0 +1,15 @@ +const router = require('express').Router(); +const { supabaseRouter } = require('./supabaseClient'); + + +// GET /avatar +router.get('/avatar', async (req, res) => { + const supabase = getSupabaseClient(); + const { user_id } = req.query; + if (!user_id) return res.status(400).json({ error: 'user_id required' }); + const { data, error } = await supabase.storage.from('avatars').download(`avatar_${user_id}.png`); + if (error) return res.status(400).json({ error: error.message }); + res.blob(data); + }); + +module.exports = router; \ No newline at end of file From e4e00184a59d70c497e9169094eca50744c96643 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 8 Jun 2025 19:46:23 +0300 Subject: [PATCH 036/147] change services api --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 47 ++++++++------ .../sber_mobile/utility_payments.js | 61 +++++++++++++++---- 2 files changed, 79 insertions(+), 29 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt index 81dade2..ef5844a 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -77,16 +77,6 @@ CREATE TABLE building_management_services ( PRIMARY KEY (building_id, service_id) ); --- 8. Платежные сервисы -CREATE TABLE payment_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - logo_url TEXT, - provider_name TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - -- 9. Дополнительные сервисы CREATE TABLE additional_services ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -153,19 +143,40 @@ CREATE TABLE cameras ( updated_at TIMESTAMPTZ DEFAULT NOW() ); --- 15. Платежи ЖКХ (исправленная версия) -CREATE TABLE utility_payments ( +-- 15. Агрегаторы платежных сервисов (ЖКХ, Интернет и т.д.) +CREATE TABLE payment_services ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - apartment_id UUID NOT NULL REFERENCES apartments(id), - service_id UUID NOT NULL REFERENCES payment_services(id), - amount DECIMAL(10, 2) NOT NULL, - period DATE NOT NULL, - status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), + name TEXT NOT NULL, -- Например, "ЖКХ", "Интернет" + icon TEXT, -- Можно хранить название иконки или url created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); --- 16. Заявки +-- 16. Детализация услуг внутри агрегатора (отопление, вода и т.д.) +CREATE TABLE payment_service_details ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + service_id UUID NOT NULL REFERENCES payment_services(id), + name TEXT NOT NULL, -- Например, "Отопление" + description TEXT, -- Описание услуги + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 17. Платежи пользователя по деталям услуг +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id), + apartment_id UUID REFERENCES apartments(id), + detail_id UUID NOT NULL REFERENCES payment_service_details(id), + amount DECIMAL(10, 2) NOT NULL, + period DATE NOT NULL, + status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), + payment_method TEXT CHECK (payment_method IN ('card', 'sber')), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 18. Заявки CREATE TABLE tickets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), diff --git a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js index a6e8b98..2675059 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js +++ b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js @@ -1,17 +1,56 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить все платежи по конкретной квартире с данными сервиса -router.get('/utility-payments', async (req, res) => { +// Получить агрегированные сервисы с деталями и статусами оплаты для квартиры +router.get('/payment-services', async (req, res) => { const supabase = getSupabaseClient(); - const { apartment_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); - const { data, error } = await supabase - .from('utility_payments') - .select('*, payment_services(*)') - .eq('apartment_id', apartment_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + const { apartment_id, user_id } = req.query; + if (!apartment_id || !user_id) return res.status(400).json({ error: 'apartment_id и user_id обязательны' }); + + // Получаем все агрегаторы + const { data: services, error: servicesError } = await supabase + .from('payment_services') + .select('id, name, icon'); + if (servicesError) return res.status(400).json({ error: servicesError.message }); + + // Получаем детали по агрегаторам + const { data: details, error: detailsError } = await supabase + .from('payment_service_details') + .select('id, service_id, name, description'); + if (detailsError) return res.status(400).json({ error: detailsError.message }); + + // Получаем платежи пользователя по деталям + const { data: payments, error: paymentsError } = await supabase + .from('payments') + .select('id, detail_id, amount, period, status, payment_method') + .eq('apartment_id', apartment_id) + .eq('user_id', user_id); + if (paymentsError) return res.status(400).json({ error: paymentsError.message }); + + // Формируем структуру для фронта + const result = services.map(service => { + const serviceDetails = details.filter(d => d.service_id === service.id).map(detail => { + const payment = payments.find(p => p.detail_id === detail.id) || {}; + return { + id: detail.id, + name: detail.name, + description: detail.description, + amount: payment.amount || null, + period: payment.period || null, + status: payment.status || 'pending', + paymentMethod: payment.payment_method || null, + paymentId: payment.id || null, + }; + }); + return { + id: service.id, + name: service.name, + icon: service.icon, + details: serviceDetails, + }; + }); + + res.json(result); }); -module.exports = router; \ No newline at end of file +module.exports = router; \ No newline at end of file From 18b33ae10a4b92a79dd760120f4bf840cda694cb Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sun, 8 Jun 2025 21:36:08 +0300 Subject: [PATCH 037/147] add initiatives --- .../kfu-m-24-1/sber_mobile/initiatives.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js index 4e0469b..3ca0562 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js @@ -1,11 +1,22 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить все инициативы (по дому) -router.get('/initiatives', async (req, res) => { +// Получить все предложения, инициативы status=review (по дому) +router.get('/initiatives-review', async (req, res) => { const supabase = getSupabaseClient(); const { building_id } = req.query; - let query = supabase.from('initiatives').select('*'); + let query = supabase.from('initiatives').select('*').eq('status', 'review'); + if (building_id) query = query.eq('building_id', building_id); + const { data, error } = await query; + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить все сборы, инициативы status=fundraising (по дому) +router.get('/initiatives-fundraising', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + let query = supabase.from('initiatives').select('*').eq('status', 'fundraising'); if (building_id) query = query.eq('building_id', building_id); const { data, error } = await query; if (error) return res.status(400).json({ error: error.message }); From 46ad6ea9f3bccea49e0242ae08f592846b1cfe42 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 8 Jun 2025 22:24:19 +0300 Subject: [PATCH 038/147] change services in db --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 42 +++++--------- .../sber_mobile/utility_payments.js | 58 +++++++++---------- 2 files changed, 41 insertions(+), 59 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt index ef5844a..fa55eb6 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -143,40 +143,30 @@ CREATE TABLE cameras ( updated_at TIMESTAMPTZ DEFAULT NOW() ); --- 15. Агрегаторы платежных сервисов (ЖКХ, Интернет и т.д.) +-- 15. Платежки по квартире (ЖКХ, Интернет и т.д.) CREATE TABLE payment_services ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + apartment_id UUID NOT NULL REFERENCES apartments(id), name TEXT NOT NULL, -- Например, "ЖКХ", "Интернет" icon TEXT, -- Можно хранить название иконки или url - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 16. Детализация услуг внутри агрегатора (отопление, вода и т.д.) -CREATE TABLE payment_service_details ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - service_id UUID NOT NULL REFERENCES payment_services(id), - name TEXT NOT NULL, -- Например, "Отопление" - description TEXT, -- Описание услуги - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 17. Платежи пользователя по деталям услуг -CREATE TABLE payments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id), - apartment_id UUID REFERENCES apartments(id), - detail_id UUID NOT NULL REFERENCES payment_service_details(id), - amount DECIMAL(10, 2) NOT NULL, - period DATE NOT NULL, - status TEXT NOT NULL CHECK (status IN ('paid', 'pending', 'overdue')), + amount DECIMAL(10, 2) NOT NULL, -- Общая сумма по платежке + is_paid BOOLEAN DEFAULT FALSE, -- Оплачен ли весь агрегатор payment_method TEXT CHECK (payment_method IN ('card', 'sber')), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); --- 18. Заявки +-- 16. Детализация по платежке (например, отопление, вода и т.д.) +CREATE TABLE payment_service_details ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + payment_service_id UUID NOT NULL REFERENCES payment_services(id) ON DELETE CASCADE, + name TEXT NOT NULL, -- Например, "Отопление" + amount DECIMAL(10, 2) NOT NULL, -- Сумма по детализации + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 17. Заявки CREATE TABLE tickets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), @@ -201,8 +191,6 @@ CREATE INDEX idx_tickets_user ON tickets(user_id); CREATE INDEX idx_apartments_building ON apartments(building_id); CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); -CREATE INDEX idx_payments_apartment ON utility_payments(apartment_id); -CREATE INDEX idx_payments_service ON utility_payments(service_id); -- Триггеры для обновления updated_at CREATE OR REPLACE FUNCTION update_updated_at() diff --git a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js index 2675059..61b2ae4 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js +++ b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js @@ -1,52 +1,46 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить агрегированные сервисы с деталями и статусами оплаты для квартиры +// Получить платежки с деталями для квартиры router.get('/payment-services', async (req, res) => { const supabase = getSupabaseClient(); - const { apartment_id, user_id } = req.query; - if (!apartment_id || !user_id) return res.status(400).json({ error: 'apartment_id и user_id обязательны' }); + const { apartment_id } = req.query; + if (!apartment_id) return res.status(400).json({ error: 'apartment_id обязателен' }); - // Получаем все агрегаторы + // Получаем все платежки по квартире const { data: services, error: servicesError } = await supabase .from('payment_services') - .select('id, name, icon'); + .select('id, name, icon, amount, is_paid, payment_method') + .eq('apartment_id', apartment_id); if (servicesError) return res.status(400).json({ error: servicesError.message }); - // Получаем детали по агрегаторам - const { data: details, error: detailsError } = await supabase - .from('payment_service_details') - .select('id, service_id, name, description'); - if (detailsError) return res.status(400).json({ error: detailsError.message }); - - // Получаем платежи пользователя по деталям - const { data: payments, error: paymentsError } = await supabase - .from('payments') - .select('id, detail_id, amount, period, status, payment_method') - .eq('apartment_id', apartment_id) - .eq('user_id', user_id); - if (paymentsError) return res.status(400).json({ error: paymentsError.message }); + // Получаем детализацию по всем платежкам + const serviceIds = services.map(s => s.id); + let details = []; + if (serviceIds.length > 0) { + const { data: detailsData, error: detailsError } = await supabase + .from('payment_service_details') + .select('id, payment_service_id, name, amount') + .in('payment_service_id', serviceIds); + if (detailsError) return res.status(400).json({ error: detailsError.message }); + details = detailsData; + } // Формируем структуру для фронта const result = services.map(service => { - const serviceDetails = details.filter(d => d.service_id === service.id).map(detail => { - const payment = payments.find(p => p.detail_id === detail.id) || {}; - return { - id: detail.id, - name: detail.name, - description: detail.description, - amount: payment.amount || null, - period: payment.period || null, - status: payment.status || 'pending', - paymentMethod: payment.payment_method || null, - paymentId: payment.id || null, - }; - }); + const serviceDetails = details.filter(d => d.payment_service_id === service.id).map(detail => ({ + id: detail.id, + name: detail.name, + amount: detail.amount + })); return { id: service.id, name: service.name, icon: service.icon, - details: serviceDetails, + amount: service.amount, + isPaid: service.is_paid, + paymentMethod: service.payment_method, + details: serviceDetails }; }); From b9f6e4d7aac9d18b12ff238daf82d9f51a3c2fdc Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Tue, 10 Jun 2025 21:45:57 +0300 Subject: [PATCH 039/147] fix outgoing json --- server/routers/kfu-m-24-1/sber_mobile/utility_payments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js index 61b2ae4..b5082b6 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js +++ b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js @@ -35,7 +35,7 @@ router.get('/payment-services', async (req, res) => { })); return { id: service.id, - name: service.name, + title: service.name, icon: service.icon, amount: service.amount, isPaid: service.is_paid, From da7e25d33929574afb3bab2f21fb6a88abe713b3 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 10 Jun 2025 23:25:51 +0300 Subject: [PATCH 040/147] add support table and api --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 9 ++++++++ .../routers/kfu-m-24-1/sber_mobile/index.js | 4 +++- .../kfu-m-24-1/sber_mobile/supportApi.js | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/supportApi.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt index fa55eb6..0a7b25a 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -179,6 +179,15 @@ CREATE TABLE tickets ( updated_at TIMESTAMPTZ DEFAULT NOW() ); +-- 18. Сообщения в службу поддержки +CREATE TABLE support ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id), + message TEXT NOT NULL, + is_from_user BOOLEAN NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + -- Индексы CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); CREATE INDEX idx_management_services_company ON management_services(management_company_id); diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 3a655bc..e419be7 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -14,6 +14,7 @@ const apartmentsRouter = require('./apartments'); const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); +const supportRouter = require('./supportApi'); module.exports = router; @@ -31,4 +32,5 @@ router.use('', utilityPaymentsRouter); router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); -router.use('', avatarRouter); \ No newline at end of file +router.use('', avatarRouter); +router.use('', supportRouter); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js new file mode 100644 index 0000000..a741e89 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -0,0 +1,22 @@ +const express = require('express'); +const router = express.Router(); +const { supabase } = require('./supabaseClient'); + +// POST /api/support +router.post('/support', async (req, res) => { + const { user_id, message } = req.body; + if (!user_id || !message) { + return res.status(400).json({ error: 'user_id и message обязательны' }); + } + try { + const { data, error } = await supabase + .from('support') + .insert([{ user_id, message, is_from_user: true }]); + if (error) throw error; + return res.json({ reply: 'Спасибо за ваше сообщение! Служба поддержки свяжется с вами в ближайшее время.' }); + } catch (err) { + return res.status(500).json({ error: 'Ошибка при сохранении сообщения' }); + } +}); + +module.exports = router; \ No newline at end of file From 7503d076e8605561e1dc04046024d2d99a09b1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Wed, 11 Jun 2025 18:51:32 +0300 Subject: [PATCH 041/147] fix supabase insert --- server/routers/kfu-m-24-1/sber_mobile/supportApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index a741e89..6dda12e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -9,9 +9,9 @@ router.post('/support', async (req, res) => { return res.status(400).json({ error: 'user_id и message обязательны' }); } try { - const { data, error } = await supabase + const { error } = await supabase .from('support') - .insert([{ user_id, message, is_from_user: true }]); + .insert({ user_id, message, is_from_user: true }); if (error) throw error; return res.json({ reply: 'Спасибо за ваше сообщение! Служба поддержки свяжется с вами в ближайшее время.' }); } catch (err) { From 5c1421242944fb2244de3bf51103b909f6cb448a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Wed, 11 Jun 2025 19:03:58 +0300 Subject: [PATCH 042/147] fix router --- .../kfu-m-24-1/sber_mobile/supportApi.js | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index 6dda12e..71eef57 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -1,22 +1,16 @@ -const express = require('express'); -const router = express.Router(); -const { supabase } = require('./supabaseClient'); +const router = require('express').Router(); +const { getSupabaseClient } = require('./supabaseClient'); // POST /api/support router.post('/support', async (req, res) => { + const supabase = getSupabaseClient(); const { user_id, message } = req.body; - if (!user_id || !message) { - return res.status(400).json({ error: 'user_id и message обязательны' }); - } - try { - const { error } = await supabase - .from('support') - .insert({ user_id, message, is_from_user: true }); - if (error) throw error; - return res.json({ reply: 'Спасибо за ваше сообщение! Служба поддержки свяжется с вами в ближайшее время.' }); - } catch (err) { - return res.status(500).json({ error: 'Ошибка при сохранении сообщения' }); - } + if (!user_id || !message) return res.status(400).json({ error: 'user_id и message обязательны' }); + const { error } = await supabase + .from('support') + .insert({ user_id, message, is_from_user: true }); + if (error) return res.status(400).json({ error: error.message }); + res.json({ reply: 'Спасибо за ваше сообщение! Служба поддержки свяжется с вами в ближайшее время.' }); }); module.exports = router; \ No newline at end of file From 4cf29c97b91caea52fd023017a642626ec63f823 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Thu, 12 Jun 2025 16:21:46 +0300 Subject: [PATCH 043/147] add chats api --- .../routers/kfu-m-24-1/sber_mobile/chats.js | 216 ++++++++++- .../kfu-m-24-1/sber_mobile/messages.js | 204 ++++++++++- .../kfu-m-24-1/sber_mobile/socket-chat.js | 340 ++++++++++++++++++ 3 files changed, 752 insertions(+), 8 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/socket-chat.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js index 983f0dd..a20d9df 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -3,12 +3,33 @@ const { getSupabaseClient } = require('./supabaseClient'); // Получить все чаты по дому router.get('/chats', async (req, res) => { + console.log('🏠 [Server] GET /chats запрос получен'); + console.log('🏠 [Server] Query параметры:', req.query); + const supabase = getSupabaseClient(); const { building_id } = req.query; - if (!building_id) return res.status(400).json({ error: 'building_id required' }); - const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + + if (!building_id) { + console.log('❌ [Server] Ошибка: building_id обязателен'); + return res.status(400).json({ error: 'building_id required' }); + } + + try { + console.log('🔍 [Server] Выполняем запрос к Supabase для здания:', building_id); + + const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); + + if (error) { + console.log('❌ [Server] Ошибка Supabase:', error); + return res.status(400).json({ error: error.message }); + } + + console.log('✅ [Server] Чаты получены:', data?.length || 0, 'шт.'); + res.json(data || []); + } catch (err) { + console.log('❌ [Server] Неожиданная ошибка:', err); + res.status(500).json({ error: 'Internal server error' }); + } }); // Получить все чаты по квартире (через building_id) @@ -25,4 +46,191 @@ router.get('/chats/by-apartment', async (req, res) => { res.json(data); }); +// Создать новый чат +router.post('/chats', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id, name } = req.body; + + if (!building_id) { + return res.status(400).json({ error: 'building_id is required' }); + } + + const { data, error } = await supabase + .from('chats') + .insert({ building_id, name }) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить конкретный чат по ID +router.get('/chats/:chat_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + const { data, error } = await supabase + .from('chats') + .select('*') + .eq('id', chat_id) + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Обновить чат +router.put('/chats/:chat_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + const { name } = req.body; + + const { data, error } = await supabase + .from('chats') + .update({ name }) + .eq('id', chat_id) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Удалить чат +router.delete('/chats/:chat_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + const { error } = await supabase + .from('chats') + .delete() + .eq('id', chat_id); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true, message: 'Chat deleted successfully' }); +}); + +// Получить статистику чата (количество сообщений, участников и т.д.) +router.get('/chats/:chat_id/stats', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + try { + // Получаем количество сообщений + const { count: messageCount, error: messageError } = await supabase + .from('messages') + .select('*', { count: 'exact', head: true }) + .eq('chat_id', chat_id); + + if (messageError) throw messageError; + + // Получаем информацию о чате с домом + const { data: chatInfo, error: chatError } = await supabase + .from('chats') + .select(` + *, + buildings ( + id, + name, + address, + apartments ( + apartment_residents ( + user_id + ) + ) + ) + `) + .eq('id', chat_id) + .single(); + + if (chatError) throw chatError; + + // Собираем уникальные user_id жителей дома + const userIds = new Set(); + chatInfo.buildings.apartments.forEach(apartment => { + apartment.apartment_residents.forEach(resident => { + userIds.add(resident.user_id); + }); + }); + + // Получаем профили всех жителей + let uniqueResidents = []; + if (userIds.size > 0) { + const { data: profiles } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .in('id', Array.from(userIds)); + + uniqueResidents = profiles || []; + } + + res.json({ + chat_id, + chat_name: chatInfo.name, + building: { + id: chatInfo.buildings.id, + name: chatInfo.buildings.name, + address: chatInfo.buildings.address + }, + message_count: messageCount || 0, + total_residents: uniqueResidents.length, + residents: uniqueResidents + }); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +// Получить последнее сообщение в чате +router.get('/chats/:chat_id/last-message', async (req, res) => { + console.log('💬 [Server] GET /chats/:chat_id/last-message запрос получен'); + console.log('💬 [Server] Chat ID:', req.params.chat_id); + + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + try { + console.log('🔍 [Server] Выполняем запрос последнего сообщения для чата:', chat_id); + + // Получаем последнее сообщение + const { data: lastMessage, error } = await supabase + .from('messages') + .select('*') + .eq('chat_id', chat_id) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + let data = null; + + if (error && error.code === 'PGRST116') { + console.log('ℹ️ [Server] Сообщений в чате нет (PGRST116)'); + data = null; + } else if (error) { + console.log('❌ [Server] Ошибка Supabase при получении последнего сообщения:', error); + return res.status(400).json({ error: error.message }); + } else if (lastMessage) { + // Получаем профиль пользователя для сообщения + const { data: userProfile } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', lastMessage.user_id) + .single(); + + // Объединяем сообщение с профилем + data = { + ...lastMessage, + user_profiles: userProfile || null + }; + console.log('✅ [Server] Последнее сообщение получено для чата:', chat_id); + } + + res.json(data); + } catch (err) { + console.log('❌ [Server] Неожиданная ошибка при получении последнего сообщения:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index b1a8188..6c0bfa3 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,14 +1,210 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить все сообщения в чате +// Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { + console.log('📬 [Server] GET /messages запрос получен'); + console.log('📬 [Server] Query параметры:', req.query); + const supabase = getSupabaseClient(); - const { chat_id } = req.query; - if (!chat_id) return res.status(400).json({ error: 'chat_id required' }); - const { data, error } = await supabase.from('messages').select('*').eq('chat_id', chat_id); + const { chat_id, limit = 50, offset = 0 } = req.query; + + if (!chat_id) { + console.log('❌ [Server] Ошибка: chat_id обязателен'); + return res.status(400).json({ error: 'chat_id required' }); + } + + try { + console.log('🔍 [Server] Выполняем запрос к Supabase для чата:', chat_id); + + // Получаем сообщения + const { data: messages, error } = await supabase + .from('messages') + .select('*') + .eq('chat_id', chat_id) + .order('created_at', { ascending: false }) + .limit(limit) + .range(offset, offset + limit - 1); + + if (error) { + console.log('❌ [Server] Ошибка получения сообщений:', error); + return res.status(400).json({ error: error.message }); + } + + // Получаем профили пользователей для всех уникальных user_id + let data = messages || []; + if (data.length > 0) { + const userIds = [...new Set(data.map(msg => msg.user_id))]; + console.log('👥 [Server] Получаем профили для пользователей:', userIds); + + const { data: profiles, error: profilesError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .in('id', userIds); + + if (!profilesError && profiles) { + // Объединяем сообщения с профилями + data = data.map(msg => ({ + ...msg, + user_profiles: profiles.find(profile => profile.id === msg.user_id) || null + })); + console.log('✅ [Server] Профили пользователей добавлены к сообщениям'); + } else { + console.log('⚠️ [Server] Ошибка получения профилей пользователей:', profilesError); + } + } + + console.log('✅ [Server] Сообщения получены:', data?.length || 0, 'шт.'); + res.json(data?.reverse() || []); // Возвращаем в хронологическом порядке + } catch (err) { + console.log('❌ [Server] Неожиданная ошибка:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Создать новое сообщение +router.post('/messages', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id, user_id, text } = req.body; + + if (!chat_id || !user_id || !text) { + return res.status(400).json({ + error: 'chat_id, user_id, and text are required' + }); + } + + // Создаем сообщение + const { data: newMessage, error } = await supabase + .from('messages') + .insert({ chat_id, user_id, text }) + .select('*') + .single(); + if (error) return res.status(400).json({ error: error.message }); + + // Получаем профиль пользователя + const { data: userProfile } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', user_id) + .single(); + + // Объединяем сообщение с профилем + const data = { + ...newMessage, + user_profiles: userProfile || null + }; + res.json(data); }); +// Получить конкретное сообщение +router.get('/messages/:message_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { message_id } = req.params; + + // Получаем сообщение + const { data: message, error } = await supabase + .from('messages') + .select('*') + .eq('id', message_id) + .single(); + + if (error) return res.status(400).json({ error: error.message }); + + // Получаем профиль пользователя + const { data: userProfile } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', message.user_id) + .single(); + + // Объединяем сообщение с профилем + const data = { + ...message, + user_profiles: userProfile || null + }; + + res.json(data); +}); + +// Получить последние сообщения для каждого чата (для списка чатов) +router.get('/chats/last-messages', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + + if (!building_id) { + return res.status(400).json({ error: 'building_id required' }); + } + + // Получаем чаты и их последние сообщения через обычные запросы + const { data: chats, error: chatsError } = await supabase + .from('chats') + .select('*') + .eq('building_id', building_id); + + if (chatsError) return res.status(400).json({ error: chatsError.message }); + + // Для каждого чата получаем последнее сообщение + const chatsWithMessages = await Promise.all( + chats.map(async (chat) => { + const { data: lastMessage } = await supabase + .from('messages') + .select(` + *, + user_profiles:user_id ( + id, + full_name, + avatar_url + ) + `) + .eq('chat_id', chat.id) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + return { + ...chat, + last_message: lastMessage || null + }; + }) + ); + + res.json(chatsWithMessages); +}); + +// Удалить сообщение (только для автора) +router.delete('/messages/:message_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { message_id } = req.params; + const { user_id } = req.body; + + if (!user_id) { + return res.status(400).json({ error: 'user_id required' }); + } + + // Проверяем, что пользователь является автором сообщения + const { data: message, error: fetchError } = await supabase + .from('messages') + .select('user_id') + .eq('id', message_id) + .single(); + + if (fetchError) return res.status(400).json({ error: fetchError.message }); + + if (message.user_id !== user_id) { + return res.status(403).json({ error: 'You can only delete your own messages' }); + } + + const { error } = await supabase + .from('messages') + .delete() + .eq('id', message_id); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true, message: 'Message deleted successfully' }); +}); + + + module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js new file mode 100644 index 0000000..d35c2a2 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js @@ -0,0 +1,340 @@ +const { getSupabaseClient } = require('./supabaseClient'); + +class ChatSocketHandler { + constructor(io) { + this.io = io; + this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info + this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) + this.setupSocketHandlers(); + } + + setupSocketHandlers() { + this.io.on('connection', (socket) => { + console.log(`User connected: ${socket.id}`); + + // Аутентификация пользователя + socket.on('authenticate', async (data) => { + await this.handleAuthentication(socket, data); + }); + + // Присоединение к чату + socket.on('join_chat', async (data) => { + await this.handleJoinChat(socket, data); + }); + + // Покидание чата + socket.on('leave_chat', (data) => { + this.handleLeaveChat(socket, data); + }); + + // Отправка сообщения + socket.on('send_message', async (data) => { + await this.handleSendMessage(socket, data); + }); + + // Пользователь начал печатать + socket.on('typing_start', (data) => { + this.handleTypingStart(socket, data); + }); + + // Пользователь закончил печатать + socket.on('typing_stop', (data) => { + this.handleTypingStop(socket, data); + }); + + // Отключение пользователя + socket.on('disconnect', () => { + this.handleDisconnect(socket); + }); + }); + } + + async handleAuthentication(socket, data) { + try { + const { user_id, token } = data; + + if (!user_id) { + socket.emit('auth_error', { message: 'user_id is required' }); + return; + } + + // Получаем информацию о пользователе из базы данных + const supabase = getSupabaseClient(); + const { data: userProfile, error } = await supabase + .from('user_profiles') + .select('*') + .eq('id', user_id) + .single(); + + if (error) { + socket.emit('auth_error', { message: 'User not found' }); + return; + } + + // Сохраняем информацию о пользователе + this.onlineUsers.set(socket.id, { + user_id, + socket_id: socket.id, + profile: userProfile, + last_seen: new Date() + }); + + socket.user_id = user_id; + socket.emit('authenticated', { + message: 'Successfully authenticated', + user: userProfile + }); + + console.log(`User ${user_id} authenticated with socket ${socket.id}`); + } catch (error) { + console.error('Authentication error:', error); + socket.emit('auth_error', { message: 'Authentication failed' }); + } + } + + async handleJoinChat(socket, data) { + try { + const { chat_id } = data; + + if (!socket.user_id) { + socket.emit('error', { message: 'Not authenticated' }); + return; + } + + if (!chat_id) { + socket.emit('error', { message: 'chat_id is required' }); + return; + } + + // Проверяем, что чат существует и пользователь имеет доступ к нему + const supabase = getSupabaseClient(); + const { data: chat, error } = await supabase + .from('chats') + .select(` + *, + buildings ( + management_company_id, + apartments ( + apartment_residents ( + user_id + ) + ) + ) + `) + .eq('id', chat_id) + .single(); + + if (error || !chat) { + socket.emit('error', { message: 'Chat not found' }); + return; + } + + // Проверяем доступ пользователя к чату через квартиры в доме + const hasAccess = chat.buildings.apartments.some(apartment => + apartment.apartment_residents.some(resident => + resident.user_id === socket.user_id + ) + ); + + if (!hasAccess) { + socket.emit('error', { message: 'Access denied to this chat' }); + return; + } + + // Добавляем сокет в комнату + socket.join(chat_id); + + // Обновляем список участников комнаты + if (!this.chatRooms.has(chat_id)) { + this.chatRooms.set(chat_id, new Set()); + } + this.chatRooms.get(chat_id).add(socket.id); + + socket.emit('joined_chat', { + chat_id, + chat: chat, + message: 'Successfully joined chat' + }); + + // Уведомляем других участников о подключении + const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_joined', { + chat_id, + user: userInfo?.profile, + timestamp: new Date() + }); + + console.log(`User ${socket.user_id} joined chat ${chat_id}`); + } catch (error) { + console.error('Join chat error:', error); + socket.emit('error', { message: 'Failed to join chat' }); + } + } + + handleLeaveChat(socket, data) { + const { chat_id } = data; + + if (!chat_id) return; + + socket.leave(chat_id); + + // Удаляем из списка участников + if (this.chatRooms.has(chat_id)) { + this.chatRooms.get(chat_id).delete(socket.id); + + // Если комната пуста, удаляем её + if (this.chatRooms.get(chat_id).size === 0) { + this.chatRooms.delete(chat_id); + } + } + + // Уведомляем других участников об отключении + const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_left', { + chat_id, + user: userInfo?.profile, + timestamp: new Date() + }); + + console.log(`User ${socket.user_id} left chat ${chat_id}`); + } + + async handleSendMessage(socket, data) { + try { + const { chat_id, text } = data; + + if (!socket.user_id) { + socket.emit('error', { message: 'Not authenticated' }); + return; + } + + if (!chat_id || !text) { + socket.emit('error', { message: 'chat_id and text are required' }); + return; + } + + // Сохраняем сообщение в базу данных + const supabase = getSupabaseClient(); + const { data: message, error } = await supabase + .from('messages') + .insert({ + chat_id, + user_id: socket.user_id, + text + }) + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .single(); + + if (error) { + socket.emit('error', { message: 'Failed to save message' }); + return; + } + + // Отправляем сообщение всем участникам чата + this.io.to(chat_id).emit('new_message', { + message, + timestamp: new Date() + }); + + console.log(`Message sent to chat ${chat_id} by user ${socket.user_id}`); + } catch (error) { + console.error('Send message error:', error); + socket.emit('error', { message: 'Failed to send message' }); + } + } + + handleTypingStart(socket, data) { + const { chat_id } = data; + + if (!socket.user_id || !chat_id) return; + + const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_typing_start', { + chat_id, + user: userInfo?.profile, + timestamp: new Date() + }); + } + + handleTypingStop(socket, data) { + const { chat_id } = data; + + if (!socket.user_id || !chat_id) return; + + const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_typing_stop', { + chat_id, + user: userInfo?.profile, + timestamp: new Date() + }); + } + + handleDisconnect(socket) { + console.log(`User disconnected: ${socket.id}`); + + // Удаляем пользователя из всех комнат + this.chatRooms.forEach((participants, chat_id) => { + if (participants.has(socket.id)) { + participants.delete(socket.id); + + // Уведомляем других участников об отключении + const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_left', { + chat_id, + user: userInfo?.profile, + timestamp: new Date() + }); + + // Если комната пуста, удаляем её + if (participants.size === 0) { + this.chatRooms.delete(chat_id); + } + } + }); + + // Удаляем пользователя из списка онлайн + this.onlineUsers.delete(socket.id); + } + + // Получение списка онлайн пользователей в чате + getOnlineUsersInChat(chat_id) { + const participants = this.chatRooms.get(chat_id) || new Set(); + const onlineUsers = []; + + participants.forEach(socketId => { + const userInfo = this.onlineUsers.get(socketId); + if (userInfo) { + onlineUsers.push(userInfo.profile); + } + }); + + return onlineUsers; + } + + // Отправка системного сообщения в чат + async sendSystemMessage(chat_id, text) { + this.io.to(chat_id).emit('system_message', { + chat_id, + text, + timestamp: new Date() + }); + } +} + +// Функция инициализации Socket.IO для чатов +function initializeChatSocket(io) { + const chatHandler = new ChatSocketHandler(io); + return chatHandler; +} + +module.exports = { + ChatSocketHandler, + initializeChatSocket +}; \ No newline at end of file From 7ecb73ac6e5c2b8f96b44fac6ce236a7bcdd7882 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 12 Jun 2025 16:50:44 +0300 Subject: [PATCH 044/147] add constants --- .../kfu-m-24-1/sber_mobile/get-constants.js | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js index fba7807..7c9e18d 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js +++ b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js @@ -18,8 +18,74 @@ const getSupabaseServiceKey = async () => { return data.features['sber_mobile'].SUPABASE_SERVICE_KEY.value; }; +const getGigaAuth = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].GIGA_AUTH.value; +}; + +const getLangsmithApiKey = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].LANGSMITH_API_KEY.value; +}; + +const getLangsmithEndpoint = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].LANGSMITH_ENDPOINT.value; +}; + +const getLangsmithTracing = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].LANGSMITH_TRACING.value; +}; + +const getLangsmithProject = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].LANGSMITH_PROJECT.value; +}; + +const getTavilyApiKey = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].TAVILY_API_KEY.value; +}; + +const getRagSupabaseServiceRoleKey = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].RAG_SUPABASE_SERVICE_ROLE_KEY.value; +}; + +const getRagSupabaseUrl = async () => { + const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); + const data = await response.json(); + return data.features['sber_mobile'].RAG_SUPABASE_URL.value; +}; + module.exports = { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey -}; \ No newline at end of file +}; + +// IIFE для установки переменных окружения +(async () => { + try { + process.env.GIGA_AUTH = await getGigaAuth(); + process.env.LANGSMITH_API_KEY = await getLangsmithApiKey(); + process.env.LANGSMITH_ENDPOINT = await getLangsmithEndpoint(); + process.env.LANGSMITH_TRACING = await getLangsmithTracing(); + process.env.LANGSMITH_PROJECT = await getLangsmithProject(); + process.env.TAVILY_API_KEY = await getTavilyApiKey(); + process.env.RAG_SUPABASE_SERVICE_ROLE_KEY = await getRagSupabaseServiceRoleKey(); + process.env.RAG_SUPABASE_URL = await getRagSupabaseUrl(); + + console.log('Environment variables loaded successfully'); + } catch (error) { + console.error('Error loading environment variables:', error); + } +})(); \ No newline at end of file From 09174abaa42bf581951305d2829c368dbfda9f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Thu, 12 Jun 2025 19:39:57 +0300 Subject: [PATCH 045/147] add ai_initiatives --- .../initiatives-ai-agents/moderation.ts | 57 +++++++++++++++ .../initiatives-ai-agents/picture.ts | 37 ++++++++++ .../kfu-m-24-1/sber_mobile/moderate.js | 73 +++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts new file mode 100644 index 0000000..00e5e0b --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -0,0 +1,57 @@ +import { Agent } from 'node:https'; +import { GigaChat } from "langchain-gigachat"; +import { z } from "zod"; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +const llm = new GigaChat({ + credentials: process.env.GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2', + httpsAgent, +}); + +// возвращаю комментарий + исправленное предложение + булево значение +const moderationLlm = llm.withStructuredOutput(z.object({ + comment: z.string(), + fixedText: z.string().optional(), + isApproved: z.boolean(), +}) as any) + +export const moderationText = async (title: string, body: string): Promise<[string, string | undefined, boolean]> => { + const prompt = ` + Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, + не имеющая отношения к Управляющей компании). + + Заголовок: ${title} + Основной текст: ${body} + + Твои задачи: + 1. Проверь предложение и заголовок на спам. + 2. Проверь, чтобы заголовок и текст были на одну тему. + 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. + 4. Проверь грамматику. + 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. + 6. Не должно быть рекламы, ссылок и т.д. + 7. Проверь предложение на информативность, оно не должно быть слишком коротким. + 8. Предложение должно быть в вежливой форме. + + - Если все правила соблюдены, то предложение принимается! + + Правила написания комментария: + - Если предложение отклоняется, верни комментарий со следующей формулировкой: + "Предложение отклонено. Причина: (укажи проблему)" + Правила написания fixedBody: + - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. + - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, + которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. + - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, + по которому оно не прошло. + -Если предложение принимается, то ничего не возвращай в поле fixedBody. + ` + const result = await moderationLlm.invoke(prompt); + + return [result.comment, result.fixedText, result.isApproved]; +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts new file mode 100644 index 0000000..febea0b --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -0,0 +1,37 @@ +import { GigaChat, detectImage } from 'gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +export const llm = new GigaChat({ + credentials: process.env.GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); + +export const generatePicture = async (prompt: string) => { + const resp = await llm.chat({ + messages: [ + { + "role": "system", + "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" + }, + { + role: "user", + content: `Старайся передать атмосферу уюта и безопасности. + Нарисуй картинку подходящую для такого события: ${prompt} + В картинке не должно быть текста, только изображение.`, + }, + ], + function_call: 'auto', + }); + + // Получение изображения по идентификатору + const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); + const image = await llm.getImage(detectedImage?.uuid ?? ''); + + // Возвращаем содержимое изображения + return image.content; +} diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js new file mode 100644 index 0000000..76cccd3 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -0,0 +1,73 @@ +const router = require('express').Router(); +const { moderationText } = require('./initiatives-ai-agents/moderation'); +const { generatePicture } = require('./initiatives-ai-agents/picture'); +const { getSupabaseClient } = require('./supabaseClient'); + +// Обработчик для модерации текста +router.post('/moderate', async (req, res) => { + try { + const { title, body } = req.body; + if (!title || !body) { + res.status(400).json({ error: 'Заголовок и текст обязательны' }); + return; + } + + const [comment, fixedText, isApproved] = await moderationText(title, body); + res.json({ + comment, + fixedText, + isApproved + }); + } catch (error) { + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } +}); + +// Обработчик для генерации изображений +router.post('/generate-image', async (req, res) => { + try { + const { prompt, userId } = req.body; + if (!prompt) { + res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); + return; + } + + // Получаем изображение + const imageBuffer = await generatePicture(prompt); + + // Получаем Supabase клиент + const supabase = getSupabaseClient(); + + // Генерируем уникальное имя файла + const timestamp = Date.now(); + const filename = `image_${userId || 'user'}_${timestamp}.jpg`; + + // Загружаем в Supabase + const { data, error } = await supabase.storage + .from('images') + .upload(filename, imageBuffer, { + contentType: 'image/jpeg', + upsert: true + }); + + if (error) { + res.status(500).json({ error: 'Ошибка при сохранении изображения' }); + return; + } + + // Получаем публичный URL изображения + const { data: urlData } = supabase.storage + .from('images') + .getPublicUrl(filename); + + res.json({ + success: true, + imageUrl: urlData.publicUrl, + imagePath: filename + }); + } catch (error) { + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } +}); + +module.exports = router; \ No newline at end of file From 548dbfcc9dfcb49a3a40c5ecc469b66ad6787ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Thu, 12 Jun 2025 20:48:56 +0300 Subject: [PATCH 046/147] fix error --- server/routers/kfu-m-24-1/sber_mobile/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index e419be7..98ba2e2 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,6 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); +const moderateRouter = require('./moderate'); module.exports = router; @@ -33,4 +34,5 @@ router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); router.use('', avatarRouter); -router.use('', supportRouter); \ No newline at end of file +router.use('', supportRouter); +router.use('', moderateRouter); \ No newline at end of file From ec6b30e2209ce97167429b79e99f65ba3ed6e897 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 12 Jun 2025 20:58:54 +0300 Subject: [PATCH 047/147] add support ai-agent --- package-lock.json | 2357 ++++++++++++++++- package.json | 7 +- .../kfu-m-24-1/sber_mobile/get-constants.js | 2 - .../sber_mobile/support-ai-agent/README.md | 63 + .../sber_mobile/support-ai-agent/gigachat.ts | 18 + .../support-ai-agent/support-agent.ts | 131 + .../kfu-m-24-1/sber_mobile/supportApi.js | 149 +- 7 files changed, 2611 insertions(+), 116 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts diff --git a/package-lock.json b/package-lock.json index 57a84e7..0205535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,11 @@ "": { "name": "multi-stub", "version": "2.0.0", - "hasInstallScript": true, "license": "MIT", "dependencies": { + "@langchain/community": "^0.3.41", + "@langchain/core": "^0.3.46", + "@langchain/langgraph": "^0.2.65", "@supabase/supabase-js": "^2.49.4", "ai": "^4.1.13", "axios": "^1.7.7", @@ -23,8 +25,10 @@ "express": "5.0.1", "express-jwt": "^8.5.1", "express-session": "^1.18.1", + "gigachat": "^0.0.14", "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", + "langchain-gigachat": "^0.0.11", "mongodb": "^6.12.0", "mongoose": "^8.9.2", "mongoose-sequence": "^6.0.1", @@ -33,7 +37,7 @@ "pbkdf2-password": "^1.2.1", "rotating-file-stream": "^3.2.5", "socket.io": "^4.8.1", - "uuid": "^11.0.3" + "zod": "^3.24.3" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -146,6 +150,60 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.27.3.tgz", + "integrity": "sha512-IjLt0gd3L4jlOfilxVXTifn42FnVffMgDC04RJK1KDZpmkBWLv0XC92MVVmkxrFZNS/7l3xWgP/I3nqtX1sQHw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.111", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz", + "integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/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", + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "peer": true + }, "node_modules/@asamuzakjp/css-color": { "version": "2.8.3", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.3.tgz", @@ -856,6 +914,86 @@ "dev": true, "license": "MIT" }, + "node_modules/@browserbasehq/sdk": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@browserbasehq/sdk/-/sdk-2.6.0.tgz", + "integrity": "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@browserbasehq/sdk/node_modules/@types/node": { + "version": "18.19.111", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz", + "integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@browserbasehq/sdk/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", + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@browserbasehq/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "peer": true + }, + "node_modules/@browserbasehq/stagehand": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@browserbasehq/stagehand/-/stagehand-1.14.0.tgz", + "integrity": "sha512-Hi/EzgMFWz+FKyepxHTrqfTPjpsuBS4zRy3e9sbMpBgLPv+9c0R+YZEvS7Bw4mTS66QtvvURRT6zgDGFotthVQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@anthropic-ai/sdk": "^0.27.3", + "@browserbasehq/sdk": "^2.0.0", + "ws": "^8.18.0", + "zod-to-json-schema": "^3.23.5" + }, + "peerDependencies": { + "@playwright/test": "^1.42.1", + "deepmerge": "^4.3.1", + "dotenv": "^16.4.5", + "openai": "^4.62.1", + "zod": "^3.23.8" + } + }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1178,6 +1316,46 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1244,6 +1422,38 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ibm-cloud/watsonx-ai": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@ibm-cloud/watsonx-ai/-/watsonx-ai-1.6.7.tgz", + "integrity": "sha512-lyHG5pjIINc+3fVbodD+ui0kvs7xk6TRAPJasK+8d8+j/FXS6TNsNGjvP79nfQJPTTYYy9IXxFc/3Z4jAqfD7w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/node": "^18.0.0", + "extend": "3.0.2", + "ibm-cloud-sdk-core": "^5.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ibm-cloud/watsonx-ai/node_modules/@types/node": { + "version": "18.19.111", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz", + "integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@ibm-cloud/watsonx-ai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "peer": true + }, "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", @@ -1706,6 +1916,786 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@langchain/community": { + "version": "0.3.46", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.46.tgz", + "integrity": "sha512-loix9LkoNcn1gQlVCopmrJW9TmgZb+YpZw7nkFzXT6ozR8ZDh1XlFq1ymR5gTFtdNzF0neK2oJtE9iEl1lm7Dw==", + "license": "MIT", + "dependencies": { + "@langchain/openai": ">=0.2.0 <0.6.0", + "@langchain/weaviate": "^0.2.0", + "binary-extensions": "^2.2.0", + "expr-eval": "^2.0.2", + "flat": "^5.0.2", + "js-yaml": "^4.1.0", + "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", + "langsmith": "^0.3.29", + "uuid": "^10.0.0", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@arcjet/redact": "^v1.0.0-alpha.23", + "@aws-crypto/sha256-js": "^5.0.0", + "@aws-sdk/client-bedrock-agent-runtime": "^3.749.0", + "@aws-sdk/client-bedrock-runtime": "^3.749.0", + "@aws-sdk/client-dynamodb": "^3.749.0", + "@aws-sdk/client-kendra": "^3.749.0", + "@aws-sdk/client-lambda": "^3.749.0", + "@aws-sdk/client-s3": "^3.749.0", + "@aws-sdk/client-sagemaker-runtime": "^3.749.0", + "@aws-sdk/client-sfn": "^3.749.0", + "@aws-sdk/credential-provider-node": "^3.388.0", + "@azure/search-documents": "^12.0.0", + "@azure/storage-blob": "^12.15.0", + "@browserbasehq/sdk": "*", + "@browserbasehq/stagehand": "^1.0.0", + "@clickhouse/client": "^0.2.5", + "@cloudflare/ai": "*", + "@datastax/astra-db-ts": "^1.0.0", + "@elastic/elasticsearch": "^8.4.0", + "@getmetal/metal-sdk": "*", + "@getzep/zep-cloud": "^1.0.6", + "@getzep/zep-js": "^0.9.0", + "@gomomento/sdk": "^1.51.1", + "@gomomento/sdk-core": "^1.51.1", + "@google-ai/generativelanguage": "*", + "@google-cloud/storage": "^6.10.1 || ^7.7.0", + "@gradientai/nodejs-sdk": "^1.2.0", + "@huggingface/inference": "^2.6.4", + "@huggingface/transformers": "^3.2.3", + "@ibm-cloud/watsonx-ai": "*", + "@lancedb/lancedb": "^0.12.0", + "@langchain/core": ">=0.3.58 <0.4.0", + "@layerup/layerup-security": "^1.5.12", + "@libsql/client": "^0.14.0", + "@mendable/firecrawl-js": "^1.4.3", + "@mlc-ai/web-llm": "*", + "@mozilla/readability": "*", + "@neondatabase/serverless": "*", + "@notionhq/client": "^2.2.10", + "@opensearch-project/opensearch": "*", + "@pinecone-database/pinecone": "*", + "@planetscale/database": "^1.8.0", + "@premai/prem-sdk": "^0.3.25", + "@qdrant/js-client-rest": "^1.8.2", + "@raycast/api": "^1.55.2", + "@rockset/client": "^0.9.1", + "@smithy/eventstream-codec": "^2.0.5", + "@smithy/protocol-http": "^3.0.6", + "@smithy/signature-v4": "^2.0.10", + "@smithy/util-utf8": "^2.0.0", + "@spider-cloud/spider-client": "^0.0.21", + "@supabase/supabase-js": "^2.45.0", + "@tensorflow-models/universal-sentence-encoder": "*", + "@tensorflow/tfjs-converter": "*", + "@tensorflow/tfjs-core": "*", + "@upstash/ratelimit": "^1.1.3 || ^2.0.3", + "@upstash/redis": "^1.20.6", + "@upstash/vector": "^1.1.1", + "@vercel/kv": "*", + "@vercel/postgres": "*", + "@writerai/writer-sdk": "^0.40.2", + "@xata.io/client": "^0.28.0", + "@zilliz/milvus2-sdk-node": ">=2.3.5", + "apify-client": "^2.7.1", + "assemblyai": "^4.6.0", + "azion": "^1.11.1", + "better-sqlite3": ">=9.4.0 <12.0.0", + "cassandra-driver": "^4.7.2", + "cborg": "^4.1.1", + "cheerio": "^1.0.0-rc.12", + "chromadb": "*", + "closevector-common": "0.1.3", + "closevector-node": "0.1.6", + "closevector-web": "0.1.6", + "cohere-ai": "*", + "convex": "^1.3.1", + "crypto-js": "^4.2.0", + "d3-dsv": "^2.0.0", + "discord.js": "^14.14.1", + "dria": "^0.0.3", + "duck-duck-scrape": "^2.2.5", + "epub2": "^3.0.1", + "fast-xml-parser": "*", + "firebase-admin": "^11.9.0 || ^12.0.0", + "google-auth-library": "*", + "googleapis": "*", + "hnswlib-node": "^3.0.0", + "html-to-text": "^9.0.5", + "ibm-cloud-sdk-core": "*", + "ignore": "^5.2.0", + "interface-datastore": "^8.2.11", + "ioredis": "^5.3.2", + "it-all": "^3.0.4", + "jsdom": "*", + "jsonwebtoken": "^9.0.2", + "llmonitor": "^0.5.9", + "lodash": "^4.17.21", + "lunary": "^0.7.10", + "mammoth": "^1.6.0", + "mariadb": "^3.4.0", + "mem0ai": "^2.1.8", + "mongodb": ">=5.2.0", + "mysql2": "^3.9.8", + "neo4j-driver": "*", + "notion-to-md": "^3.1.0", + "officeparser": "^4.0.4", + "openai": "*", + "pdf-parse": "1.1.1", + "pg": "^8.11.0", + "pg-copy-streams": "^6.0.5", + "pickleparser": "^0.2.1", + "playwright": "^1.32.1", + "portkey-ai": "^0.1.11", + "puppeteer": "*", + "pyodide": ">=0.24.1 <0.27.0", + "redis": "*", + "replicate": "*", + "sonix-speech-recognition": "^2.1.1", + "srt-parser-2": "^1.2.3", + "typeorm": "^0.3.20", + "typesense": "^1.5.3", + "usearch": "^1.1.1", + "voy-search": "0.6.2", + "weaviate-client": "^3.5.2", + "web-auth-library": "^1.0.3", + "word-extractor": "*", + "ws": "^8.14.2", + "youtubei.js": "*" + }, + "peerDependenciesMeta": { + "@arcjet/redact": { + "optional": true + }, + "@aws-crypto/sha256-js": { + "optional": true + }, + "@aws-sdk/client-bedrock-agent-runtime": { + "optional": true + }, + "@aws-sdk/client-bedrock-runtime": { + "optional": true + }, + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "@aws-sdk/client-kendra": { + "optional": true + }, + "@aws-sdk/client-lambda": { + "optional": true + }, + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/client-sagemaker-runtime": { + "optional": true + }, + "@aws-sdk/client-sfn": { + "optional": true + }, + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@aws-sdk/dsql-signer": { + "optional": true + }, + "@azure/search-documents": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@browserbasehq/sdk": { + "optional": true + }, + "@clickhouse/client": { + "optional": true + }, + "@cloudflare/ai": { + "optional": true + }, + "@datastax/astra-db-ts": { + "optional": true + }, + "@elastic/elasticsearch": { + "optional": true + }, + "@getmetal/metal-sdk": { + "optional": true + }, + "@getzep/zep-cloud": { + "optional": true + }, + "@getzep/zep-js": { + "optional": true + }, + "@gomomento/sdk": { + "optional": true + }, + "@gomomento/sdk-core": { + "optional": true + }, + "@google-ai/generativelanguage": { + "optional": true + }, + "@google-cloud/storage": { + "optional": true + }, + "@gradientai/nodejs-sdk": { + "optional": true + }, + "@huggingface/inference": { + "optional": true + }, + "@huggingface/transformers": { + "optional": true + }, + "@lancedb/lancedb": { + "optional": true + }, + "@layerup/layerup-security": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@mendable/firecrawl-js": { + "optional": true + }, + "@mlc-ai/web-llm": { + "optional": true + }, + "@mozilla/readability": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@notionhq/client": { + "optional": true + }, + "@opensearch-project/opensearch": { + "optional": true + }, + "@pinecone-database/pinecone": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@premai/prem-sdk": { + "optional": true + }, + "@qdrant/js-client-rest": { + "optional": true + }, + "@raycast/api": { + "optional": true + }, + "@rockset/client": { + "optional": true + }, + "@smithy/eventstream-codec": { + "optional": true + }, + "@smithy/protocol-http": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "@smithy/util-utf8": { + "optional": true + }, + "@spider-cloud/spider-client": { + "optional": true + }, + "@supabase/supabase-js": { + "optional": true + }, + "@tensorflow-models/universal-sentence-encoder": { + "optional": true + }, + "@tensorflow/tfjs-converter": { + "optional": true + }, + "@tensorflow/tfjs-core": { + "optional": true + }, + "@upstash/ratelimit": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@upstash/vector": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@writerai/writer-sdk": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "@zilliz/milvus2-sdk-node": { + "optional": true + }, + "apify-client": { + "optional": true + }, + "assemblyai": { + "optional": true + }, + "azion": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "cassandra-driver": { + "optional": true + }, + "cborg": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "chromadb": { + "optional": true + }, + "closevector-common": { + "optional": true + }, + "closevector-node": { + "optional": true + }, + "closevector-web": { + "optional": true + }, + "cohere-ai": { + "optional": true + }, + "convex": { + "optional": true + }, + "crypto-js": { + "optional": true + }, + "d3-dsv": { + "optional": true + }, + "discord.js": { + "optional": true + }, + "dria": { + "optional": true + }, + "duck-duck-scrape": { + "optional": true + }, + "epub2": { + "optional": true + }, + "fast-xml-parser": { + "optional": true + }, + "firebase-admin": { + "optional": true + }, + "google-auth-library": { + "optional": true + }, + "googleapis": { + "optional": true + }, + "hnswlib-node": { + "optional": true + }, + "html-to-text": { + "optional": true + }, + "ignore": { + "optional": true + }, + "interface-datastore": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "it-all": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "jsonwebtoken": { + "optional": true + }, + "llmonitor": { + "optional": true + }, + "lodash": { + "optional": true + }, + "lunary": { + "optional": true + }, + "mammoth": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mem0ai": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "neo4j-driver": { + "optional": true + }, + "notion-to-md": { + "optional": true + }, + "officeparser": { + "optional": true + }, + "pdf-parse": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-copy-streams": { + "optional": true + }, + "pickleparser": { + "optional": true + }, + "playwright": { + "optional": true + }, + "portkey-ai": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "pyodide": { + "optional": true + }, + "redis": { + "optional": true + }, + "replicate": { + "optional": true + }, + "sonix-speech-recognition": { + "optional": true + }, + "srt-parser-2": { + "optional": true + }, + "typeorm": { + "optional": true + }, + "typesense": { + "optional": true + }, + "usearch": { + "optional": true + }, + "voy-search": { + "optional": true + }, + "weaviate-client": { + "optional": true + }, + "web-auth-library": { + "optional": true + }, + "word-extractor": { + "optional": true + }, + "ws": { + "optional": true + }, + "youtubei.js": { + "optional": true + } + } + }, + "node_modules/@langchain/community/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/core": { + "version": "0.3.58", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.58.tgz", + "integrity": "sha512-HLkOtVofgBHefaUae/+2fLNkpMLzEjHSavTmUF0YC7bDa5NPIZGlP80CGrSFXAeJ+WCPd8rIK8K/p6AW94inUQ==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.29", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.32", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@langchain/core/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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@langchain/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph": { + "version": "0.2.74", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz", + "integrity": "sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "~0.0.17", + "@langchain/langgraph-sdk": "~0.0.32", + "uuid": "^10.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz", + "integrity": "sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==", + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.31 <0.4.0" + } + }, + "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "0.0.84", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.84.tgz", + "integrity": "sha512-l0PFQyJ+6m6aclORNPPWlcRwgKcXVXsPaJCbCUYFABR3yf4cOpsjhUNR0cJ7+2cS400oieHjGRdGGyO/hbSjhg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.31 <0.4.0", + "react": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/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/@langchain/langgraph/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/openai": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.5.13.tgz", + "integrity": "sha512-t5UsO7XYE+DBQlXQ21QK74Y+LH4It20wnENrmueNvxIWTn0nHDIGVmO6wo4rJxbmOOPRQ4l/oAxGRnYU8B8v6w==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^4.96.0", + "zod": "3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.3.58 <0.4.0" + } + }, + "node_modules/@langchain/openai/node_modules/zod": { + "version": "3.25.32", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.32.tgz", + "integrity": "sha512-OSm2xTIRfW8CV5/QKgngwmQW/8aPfGdaQFlrGoErlgg/Epm7cjb6K6VEyExfe65a3VybUOnu381edLb0dfJl0g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@langchain/textsplitters": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", + "integrity": "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, + "node_modules/@langchain/weaviate": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@langchain/weaviate/-/weaviate-0.2.0.tgz", + "integrity": "sha512-gAtTCxSllR8Z92qAuRn2ir0cop241VmftQHQN+UYtTeoLge8hvZT5k0j55PDVaXTVpjx0ecx6DKv5I/wLRQI+A==", + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0", + "weaviate-client": "^3.5.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, + "node_modules/@langchain/weaviate/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -1725,6 +2715,26 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/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/@mongodb-js/saslprep": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", @@ -1743,6 +2753,86 @@ "node": ">=8.0.0" } }, + "node_modules/@playwright/test": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", + "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright": "1.53.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1826,27 +2916,6 @@ "ws": "^8.18.0" } }, - "node_modules/@supabase/realtime-js/node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@supabase/storage-js": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", @@ -1870,6 +2939,13 @@ "@supabase/storage-js": "2.7.1" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT", + "peer": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1952,6 +3028,16 @@ "@types/node": "*" } }, + "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", + "peer": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/diff-match-patch": { "version": "1.0.36", "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", @@ -2006,7 +3092,6 @@ "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/jsonwebtoken": { @@ -2017,6 +3102,13 @@ "@types/node": "*" } }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/node": { "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", @@ -2026,12 +3118,28 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/phoenix": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", "license": "MIT" }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2053,6 +3161,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -2099,6 +3220,24 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abort-controller-x": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", + "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==", + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2179,6 +3318,18 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ai": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/ai/-/ai-4.1.13.tgz", @@ -2253,7 +3404,6 @@ "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" @@ -2325,7 +3475,6 @@ "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/array-flatten": { @@ -2353,9 +3502,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "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", @@ -2494,6 +3643,26 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "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/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -2538,7 +3707,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, "engines": { "node": ">=8" } @@ -2641,6 +3809,31 @@ "node": ">=16.20.1" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "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", @@ -2745,7 +3938,6 @@ "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", @@ -2842,7 +4034,6 @@ "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", @@ -2875,7 +4066,6 @@ "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" @@ -2888,7 +4078,6 @@ "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/color-support": { @@ -2945,6 +4134,15 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, + "node_modules/console-table-printer": { + "version": "2.14.3", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.14.3.tgz", + "integrity": "sha512-X5OCFnjYlXzRuC8ac5hPA2QflRjJvNKJocMhlnqK/Ap7q3DHXr0NJ0TGzwmEKOiOdJrjsSwEd0m+a32JAYPrKQ==", + "license": "MIT", + "dependencies": { + "simple-wcswidth": "^1.0.1" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -3070,6 +4268,35 @@ "yarn": ">=1" } }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-fetch/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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3164,6 +4391,15 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -3195,7 +4431,6 @@ "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" @@ -3430,6 +4665,27 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -3485,7 +4741,6 @@ "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" @@ -3712,6 +4967,31 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", @@ -3770,6 +5050,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expr-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz", + "integrity": "sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==", + "license": "MIT" + }, "node_modules/express": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", @@ -4067,6 +5353,13 @@ "node": ">= 0.8" } }, + "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", + "peer": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4129,6 +5422,24 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4186,6 +5497,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -4240,6 +5560,25 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/formidable": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", @@ -4299,6 +5638,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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", @@ -4341,7 +5694,6 @@ "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.*" @@ -4406,6 +5758,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gigachat": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.14.tgz", + "integrity": "sha512-BwXDecDxF6aKJT+juuoATrBnFLDBg5Vho1dxYRsgM18zgZ55q5SwNiOgC05/J7rhGY66Pj6Wsnvk3FC6K4IMQw==", + "license": "ISC", + "dependencies": { + "axios": "^1.8.2", + "uuid": "^11.0.3" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4470,11 +5832,32 @@ "dev": true, "license": "ISC" }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", + "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "cross-fetch": "^3.1.5" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, "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" @@ -4640,6 +6023,113 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ibm-cloud-sdk-core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.4.0.tgz", + "integrity": "sha512-c4cwOuUDbMiFROYM/Ti1aC+Umi1v3TdvC2DO5zR7w44FYY/3xrs79+3DVPXt/nRhJeaMHN2L9XwlXsPSoVDHJA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@types/debug": "^4.1.12", + "@types/node": "^18.19.80", + "@types/tough-cookie": "^4.0.0", + "axios": "^1.8.2", + "camelcase": "^6.3.0", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "extend": "3.0.2", + "file-type": "16.5.4", + "form-data": "4.0.0", + "isstream": "0.1.2", + "jsonwebtoken": "^9.0.2", + "mime-types": "2.1.35", + "retry-axios": "^2.6.0", + "tough-cookie": "^4.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/@types/node": { + "version": "18.19.111", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz", + "integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ibm-cloud-sdk-core/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", + "peer": true + }, + "node_modules/ibm-cloud-sdk-core/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ibm-cloud-sdk-core/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "peer": true + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4651,11 +6141,32 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "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, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -4854,6 +6365,13 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT", + "peer": true + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -5549,6 +7067,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-tiktoken": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.20.tgz", + "integrity": "sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5560,7 +7087,6 @@ "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" @@ -5688,27 +7214,6 @@ "node": ">=18" } }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -5798,6 +7303,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -5873,6 +7387,168 @@ "node": ">=6" } }, + "node_modules/langchain": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.28.tgz", + "integrity": "sha512-h4GGlBJNGU/Sj2PipW9kL+ewj7To3c+SnnNKH3HZaVHEqGPMHVB96T1lLjtCLcZCyUfabMr/zFIkLNI4War+Xg==", + "license": "MIT", + "dependencies": { + "@langchain/openai": ">=0.1.0 <0.6.0", + "@langchain/textsplitters": ">=0.0.0 <0.2.0", + "js-tiktoken": "^1.0.12", + "js-yaml": "^4.1.0", + "jsonpointer": "^5.0.1", + "langsmith": "^0.3.29", + "openapi-types": "^12.1.3", + "p-retry": "4", + "uuid": "^10.0.0", + "yaml": "^2.2.1", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/anthropic": "*", + "@langchain/aws": "*", + "@langchain/cerebras": "*", + "@langchain/cohere": "*", + "@langchain/core": ">=0.3.58 <0.4.0", + "@langchain/deepseek": "*", + "@langchain/google-genai": "*", + "@langchain/google-vertexai": "*", + "@langchain/google-vertexai-web": "*", + "@langchain/groq": "*", + "@langchain/mistralai": "*", + "@langchain/ollama": "*", + "@langchain/xai": "*", + "axios": "*", + "cheerio": "*", + "handlebars": "^4.7.8", + "peggy": "^3.0.2", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/aws": { + "optional": true + }, + "@langchain/cerebras": { + "optional": true + }, + "@langchain/cohere": { + "optional": true + }, + "@langchain/deepseek": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/google-vertexai": { + "optional": true + }, + "@langchain/google-vertexai-web": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "@langchain/xai": { + "optional": true + }, + "axios": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "handlebars": { + "optional": true + }, + "peggy": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, + "node_modules/langchain-gigachat": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/langchain-gigachat/-/langchain-gigachat-0.0.11.tgz", + "integrity": "sha512-2hYES1Dt0U/p/h+F+/1lDfmaYTWQyuHG5KAAIQGYygursAUGDDoyKQlGywbJ4JgmENy4u5fv7keVC9+k0X8tbQ==", + "license": "MIT", + "dependencies": { + "gigachat": "^0.0.14", + "uuid": "^11.0.5", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, + "node_modules/langchain/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/langsmith": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.31.tgz", + "integrity": "sha512-9lwuLZuN3tXFYQ6eMg0rmbBw7oxQo4bu1NYeylbjz27bOdG1XB9XNoxaiIArkK4ciLdOIOhPMBXP4bkvZOgHRw==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "openai": "*" + }, + "peerDependenciesMeta": { + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5926,6 +7602,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -5968,16 +7650,11 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" }, "node_modules/make-dir": { "version": "3.1.0", @@ -6467,6 +8144,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -6500,28 +8186,59 @@ "node": ">= 0.6" } }, + "node_modules/nice-grpc": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.12.tgz", + "integrity": "sha512-J1n4Wg+D3IhRhGQb+iqh2OpiM0GzTve/kf2lnlW4S+xczmIEd0aHUDV1OsJ5a3q8GSTqJf+s4Rgg1M8uJltarw==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.13.1", + "abort-controller-x": "^0.4.0", + "nice-grpc-common": "^2.0.2" + } + }, + "node_modules/nice-grpc-client-middleware-retry": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nice-grpc-client-middleware-retry/-/nice-grpc-client-middleware-retry-3.1.11.tgz", + "integrity": "sha512-xW/imz/kNG2g0DwTfH2eYEGrg1chSLrXtvGp9fg2qkhTgGFfAS/Pq3+t+9G8KThcC4hK/xlEyKvZWKk++33S6A==", + "license": "MIT", + "dependencies": { + "abort-controller-x": "^0.4.0", + "nice-grpc-common": "^2.0.2" + } + }, + "node_modules/nice-grpc-common": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz", + "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", + "license": "MIT", + "dependencies": { + "ts-error": "^1.0.6" + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", - "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-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" } }, "node_modules/node-int64": { @@ -6727,6 +8444,77 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.111", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz", + "integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/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/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6745,6 +8533,15 @@ "node": ">= 0.8.0" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6777,6 +8574,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -6888,6 +8726,20 @@ "fastfall": "^1.2.3" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -6986,6 +8838,38 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", + "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright-core": "1.53.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", + "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7024,6 +8908,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -7044,6 +8938,30 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7062,6 +8980,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -7109,6 +9040,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT", + "peer": true + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -7178,6 +9116,50 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "peer": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7194,12 +9176,18 @@ "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/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "peer": true + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -7261,6 +9249,28 @@ "node": ">=10" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-axios": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz", + "integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.7.0" + }, + "peerDependencies": { + "axios": "*" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7362,12 +9372,10 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -7569,6 +9577,12 @@ "node": ">=10" } }, + "node_modules/simple-wcswidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", + "integrity": "sha512-xMO/8eNREtaROt7tJvWJqHBDTMFN4eiQ5I4JRMuilwfnFcV5W9u7RUkueNkdw0jPqGMX36iCywelS5yilTuOxg==", + "license": "MIT" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -7637,6 +9651,27 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -7849,6 +9884,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -7926,7 +9979,6 @@ "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" @@ -8066,6 +10118,24 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -8120,6 +10190,12 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz", + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", + "license": "MIT" + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -8337,6 +10413,16 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8386,6 +10472,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", @@ -8473,6 +10570,47 @@ "makeerror": "1.0.12" } }, + "node_modules/weaviate-client": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/weaviate-client/-/weaviate-client-3.6.1.tgz", + "integrity": "sha512-C/sVYLqLuRQn2FeP3YQFNSOV61gNNtswNDRnb117ZfweSU/DIGAYeZZn63DpNI11qDSWTS3Cbz/aQjUzZXbA6Q==", + "license": "BSD 3-Clause", + "dependencies": { + "abort-controller-x": "^0.4.3", + "graphql": "^16.10.0", + "graphql-request": "^6.1.0", + "long": "^5.2.4", + "nice-grpc": "^2.1.11", + "nice-grpc-client-middleware-retry": "^3.1.10", + "nice-grpc-common": "^2.0.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/weaviate-client/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/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -8556,7 +10694,6 @@ "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", @@ -8590,9 +10727,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8637,7 +10774,6 @@ "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" @@ -8648,11 +10784,22 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "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", @@ -8671,7 +10818,6 @@ "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" @@ -8701,11 +10847,10 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.63", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.63.tgz", + "integrity": "sha512-3ttCkqhtpncYXfP0f6dsyabbYV/nEUW+Xlu89jiXbTBifUfjaSqXOG6JnQPLtqt87n7KAmnMqcjay6c0Wq0Vbw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 17ea786..10a157b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "homepage": "https://bitbucket.org/online-mentor/multi-stub#readme", "dependencies": { "@supabase/supabase-js": "^2.49.4", + "@langchain/community": "^0.3.41", + "@langchain/core": "^0.3.46", + "@langchain/langgraph": "^0.2.65", "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", @@ -34,8 +37,10 @@ "express": "5.0.1", "express-jwt": "^8.5.1", "express-session": "^1.18.1", + "gigachat": "^0.0.14", "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", + "langchain-gigachat": "^0.0.11", "mongodb": "^6.12.0", "mongoose": "^8.9.2", "mongoose-sequence": "^6.0.1", @@ -44,7 +49,7 @@ "pbkdf2-password": "^1.2.1", "rotating-file-stream": "^3.2.5", "socket.io": "^4.8.1", - "uuid": "^11.0.3" + "zod": "^3.24.3" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js index 7c9e18d..0d7c355 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js +++ b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js @@ -1,5 +1,3 @@ -const fetch = require('node-fetch'); - const getSupabaseUrl = async () => { const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); const data = await response.json(); diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md new file mode 100644 index 0000000..ccad3cd --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md @@ -0,0 +1,63 @@ +# AI Support Agent + +AI-агент поддержки интегрирован в существующий `supportApi.js`. + +## Структура + +``` +support-ai-agent/ +├── gigachat.ts # Конфигурация GigaChat +├── support-agent.ts # Основной класс агента +└── README.md # Документация +``` + +## API + +### POST `/support` +Отправить сообщение в службу поддержки (теперь с AI-агентом). + +**Запрос:** +```json +{ + "user_id": "string", // Обязательно + "message": "string", // Обязательно + "system_prompt": "string" // Опционально - настройка поведения агента +} +``` + +**Ответ:** +```json +{ + "reply": "Ответ AI-агента", + "success": true +} +``` + +### POST `/support/configure` +Настроить системный промпт для конкретного пользователя. + +### DELETE `/support/history/:userId` +Очистить историю диалога пользователя. + +## Возможности + +- 🤖 Интеллектуальные ответы на основе GigaChat +- 💾 Сохранение всех сообщений в базу данных Supabase +- 🧠 Память контекста диалога для каждого пользователя +- ⚙️ Настраиваемые системные промпты +- 📊 Поддержка множественных пользователей + +## Примеры системных промптов + +### Техническая поддержка +``` +Ты - специалист технической поддержки мобильного приложения "Умный дом". +Помогай пользователям решать проблемы, объясняй функции простым языком, +проводи диагностику пошагово. Всегда будь дружелюбным и терпеливым. +``` + +### Общая поддержка клиентов +``` +Ты - профессиональный агент службы поддержки. Помогай решать вопросы +пользователей, отвечай вежливо и по существу, проявляй эмпатию. +``` \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts new file mode 100644 index 0000000..e46c067 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts @@ -0,0 +1,18 @@ +import { Agent } from 'node:https'; +import { GigaChat } from 'langchain-gigachat'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js) +export const gigachat = new GigaChat({ + model: 'GigaChat-2', + temperature: 0.7, + scope: 'GIGACHAT_API_PERS', + streaming: false, + credentials: process.env.GIGA_AUTH, + httpsAgent +}); + +export default gigachat; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts new file mode 100644 index 0000000..514a3c8 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts @@ -0,0 +1,131 @@ +import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from '@langchain/core/messages'; +import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import { MemorySaver } from '@langchain/langgraph'; +import gigachat from './gigachat'; + +export interface SupportAgentConfig { + systemPrompt?: string; + temperature?: number; + threadId?: string; +} + +export interface SupportResponse { + content: string; + success: boolean; + error?: string; +} + +export class SupportAgent { + private llm: any; + private memorySaver: MemorySaver; + private agent: any; + private systemPrompt: string; + private threadId: string; + + constructor(config: SupportAgentConfig = {}) { + this.systemPrompt = config.systemPrompt || this.getDefaultSystemPrompt(); + this.threadId = config.threadId || 'default'; + this.memorySaver = new MemorySaver(); + + // Настраиваем модель с заданной температурой + this.llm = gigachat; + if (config.temperature !== undefined) { + this.llm.temperature = config.temperature; + } + + // Создаем агента без инструментов для простого чата + this.agent = createReactAgent({ + llm: this.llm, + tools: [], + checkpointSaver: this.memorySaver + }); + } + + /** + * Получить системный промпт по умолчанию для агента поддержки + */ + private getDefaultSystemPrompt(): string { + return `Ты - профессиональный агент службы поддержки. + +Твои основные задачи: +- Помогать пользователям решать их вопросы и проблемы +- Отвечать вежливо, профессионально и по существу +- Предоставлять четкие и понятные инструкции +- Проявлять эмпатию к проблемам пользователей +- Если не знаешь ответ, честно сообщить об этом и предложить альтернативные способы получения помощи + +Всегда отвечай на русском языке и старайся быть максимально полезным.`; + } + + /** + * Обновить системный промпт + */ + public updateSystemPrompt(newPrompt: string): void { + this.systemPrompt = newPrompt; + } + + /** + * Получить текущий системный промпт + */ + public getSystemPrompt(): string { + return this.systemPrompt; + } + + /** + * Обработать сообщение пользователя и получить ответ + */ + public async processMessage(userMessage: string): Promise { + try { + // Создаем сообщения с системным промптом + const messages = [ + new SystemMessage(this.systemPrompt), + new HumanMessage(userMessage) + ]; + + // Получаем ответ от агента + const response = await this.agent.invoke({ + messages: messages + }, { + configurable: { + thread_id: this.threadId + } + }); + + // Извлекаем последнее сообщение от ассистента + const lastMessage = response.messages[response.messages.length - 1]; + + return { + content: lastMessage.content || 'Извините, не удалось сформировать ответ.', + success: true + }; + + } catch (error) { + console.error('Ошибка при обработке сообщения:', error); + return { + content: 'Извините, произошла ошибка при обработке вашего запроса. Попробуйте позже.', + success: false, + error: error instanceof Error ? error.message : 'Неизвестная ошибка' + }; + } + } + + /** + * Очистить историю диалога + */ + public async clearHistory(): Promise { + this.memorySaver = new MemorySaver(); + this.agent = createReactAgent({ + llm: this.llm, + tools: [], + checkpointSaver: this.memorySaver + }); + } + + /** + * Изменить ID потока (для работы с разными пользователями) + */ + public setThreadId(threadId: string): void { + this.threadId = threadId; + } +} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index 71eef57..de908a8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -1,16 +1,151 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); +const { SupportAgent } = require('./support-ai-agent/support-agent'); + +// Хранилище агентов для разных пользователей +const userAgents = new Map(); + +/** + * Получить или создать агента для пользователя + */ +function getUserAgent(userId, systemPrompt) { + if (!userAgents.has(userId)) { + const config = { + threadId: userId, + temperature: 0.7 + }; + if (systemPrompt) { + config.systemPrompt = systemPrompt; + } + userAgents.set(userId, new SupportAgent(config)); + } + return userAgents.get(userId); +} // POST /api/support router.post('/support', async (req, res) => { const supabase = getSupabaseClient(); - const { user_id, message } = req.body; - if (!user_id || !message) return res.status(400).json({ error: 'user_id и message обязательны' }); - const { error } = await supabase - .from('support') - .insert({ user_id, message, is_from_user: true }); - if (error) return res.status(400).json({ error: error.message }); - res.json({ reply: 'Спасибо за ваше сообщение! Служба поддержки свяжется с вами в ближайшее время.' }); + const { user_id, message, system_prompt } = req.body; + + if (!user_id || !message) { + return res.status(400).json({ error: 'user_id и message обязательны' }); + } + + try { + // Сохраняем сообщение пользователя в базу данных + const { error: insertError } = await supabase + .from('support') + .insert({ user_id, message, is_from_user: true }); + + if (insertError) { + return res.status(400).json({ error: insertError.message }); + } + + // Получаем агента для пользователя + const agent = getUserAgent(user_id, system_prompt); + + // Обновляем системный промпт если передан + if (system_prompt) { + agent.updateSystemPrompt(system_prompt); + } + + // Получаем ответ от AI-агента + const aiResponse = await agent.processMessage(message); + + if (!aiResponse.success) { + console.error('Ошибка AI-агента:', aiResponse.error); + return res.status(500).json({ + error: 'Ошибка при генерации ответа', + reply: 'Извините, произошла ошибка. Попробуйте позже.' + }); + } + + // Сохраняем ответ агента в базу данных + const { error: responseError } = await supabase + .from('support') + .insert({ + user_id, + message: aiResponse.content, + is_from_user: false + }); + + if (responseError) { + console.error('Ошибка сохранения ответа:', responseError); + // Не возвращаем ошибку пользователю, так как ответ уже сгенерирован + } + + // Возвращаем ответ пользователю + res.json({ + reply: aiResponse.content, + success: true + }); + + } catch (error) { + console.error('Ошибка в supportApi:', error); + res.status(500).json({ + error: 'Внутренняя ошибка сервера', + reply: 'Извините, произошла ошибка. Попробуйте позже.' + }); + } +}); + +// POST /api/support/configure - Настройка системного промпта +router.post('/support/configure', async (req, res) => { + const { user_id, system_prompt } = req.body; + + if (!user_id) { + return res.status(400).json({ error: 'user_id обязателен' }); + } + + try { + const agent = getUserAgent(user_id, system_prompt); + + if (system_prompt) { + agent.updateSystemPrompt(system_prompt); + } + + res.json({ + message: 'Конфигурация агента обновлена', + current_system_prompt: agent.getSystemPrompt(), + success: true + }); + + } catch (error) { + console.error('Ошибка в /support/configure:', error); + res.status(500).json({ + error: 'Внутренняя ошибка сервера', + success: false + }); + } +}); + +// DELETE /api/support/history/:userId - Очистка истории диалога +router.delete('/support/history/:userId', async (req, res) => { + const { userId } = req.params; + + try { + if (userAgents.has(userId)) { + const agent = userAgents.get(userId); + await agent.clearHistory(); + + res.json({ + message: 'История диалога очищена', + success: true + }); + } else { + res.json({ + message: 'Агент для данного пользователя не найден', + success: true + }); + } + + } catch (error) { + console.error('Ошибка в /support/history:', error); + res.status(500).json({ + error: 'Внутренняя ошибка сервера', + success: false + }); + } }); module.exports = router; \ No newline at end of file From 24ff71230641ee4167d4fa422b49529f23876b8b Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Thu, 12 Jun 2025 21:04:12 +0300 Subject: [PATCH 048/147] add sockets and change subscription --- server/index.ts | 13 +- server/io.ts | 9 +- .../routers/kfu-m-24-1/sber_mobile/chats.js | 22 +-- .../routers/kfu-m-24-1/sber_mobile/index.js | 1 + .../kfu-m-24-1/sber_mobile/messages.js | 90 ++++++----- .../kfu-m-24-1/sber_mobile/socket-chat.js | 147 ++++++++++++++++-- 6 files changed, 199 insertions(+), 83 deletions(-) diff --git a/server/index.ts b/server/index.ts index d39f65f..3d4f58e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,7 +20,9 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' -import { setIo } from './io' +import { setIo, getIo } from './io' +// Импорт обработчика чата +const { initializeChatSocket } = require('./routers/kfu-m-24-1/sber_mobile/socket-chat') export const app = express() @@ -65,6 +67,15 @@ const initServer = async () => { console.log('warming up 🔥') const server = setIo(app) + + // Инициализация Socket.IO для чата + const io = getIo() + if (io) { + const chatHandler = initializeChatSocket(io) + // Сохраняем ссылку на chat handler для доступа из эндпоинтов + io.chatHandler = chatHandler + console.log('✅ Socket.IO для чата инициализирован') + } const sess = { secret: "super-secret-key", diff --git a/server/io.ts b/server/io.ts index 71833d6..4121b06 100644 --- a/server/io.ts +++ b/server/io.ts @@ -5,7 +5,14 @@ let io = null export const setIo = (app) => { const server = createServer(app) - io = new Server(server, {}) + io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"], + credentials: false + }, + transports: ['websocket', 'polling'] + }) return server } diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js index a20d9df..047ac37 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -3,31 +3,22 @@ const { getSupabaseClient } = require('./supabaseClient'); // Получить все чаты по дому router.get('/chats', async (req, res) => { - console.log('🏠 [Server] GET /chats запрос получен'); - console.log('🏠 [Server] Query параметры:', req.query); - const supabase = getSupabaseClient(); const { building_id } = req.query; if (!building_id) { - console.log('❌ [Server] Ошибка: building_id обязателен'); return res.status(400).json({ error: 'building_id required' }); } try { - console.log('🔍 [Server] Выполняем запрос к Supabase для здания:', building_id); - const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); if (error) { - console.log('❌ [Server] Ошибка Supabase:', error); return res.status(400).json({ error: error.message }); } - console.log('✅ [Server] Чаты получены:', data?.length || 0, 'шт.'); res.json(data || []); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка:', err); res.status(500).json({ error: 'Internal server error' }); } }); @@ -184,15 +175,10 @@ router.get('/chats/:chat_id/stats', async (req, res) => { // Получить последнее сообщение в чате router.get('/chats/:chat_id/last-message', async (req, res) => { - console.log('💬 [Server] GET /chats/:chat_id/last-message запрос получен'); - console.log('💬 [Server] Chat ID:', req.params.chat_id); - const supabase = getSupabaseClient(); const { chat_id } = req.params; try { - console.log('🔍 [Server] Выполняем запрос последнего сообщения для чата:', chat_id); - // Получаем последнее сообщение const { data: lastMessage, error } = await supabase .from('messages') @@ -205,10 +191,8 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { let data = null; if (error && error.code === 'PGRST116') { - console.log('ℹ️ [Server] Сообщений в чате нет (PGRST116)'); data = null; } else if (error) { - console.log('❌ [Server] Ошибка Supabase при получении последнего сообщения:', error); return res.status(400).json({ error: error.message }); } else if (lastMessage) { // Получаем профиль пользователя для сообщения @@ -223,12 +207,10 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { ...lastMessage, user_profiles: userProfile || null }; - console.log('✅ [Server] Последнее сообщение получено для чата:', chat_id); - } + } - res.json(data); + res.json(data); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка при получении последнего сообщения:', err); res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index e419be7..4dfad13 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,6 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); +const { getIo } = require('../../../io'); module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index 6c0bfa3..f729679 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,64 +1,59 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); +const { getIo } = require('../../../io'); // Импортируем Socket.IO // Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { - console.log('📬 [Server] GET /messages запрос получен'); - console.log('📬 [Server] Query параметры:', req.query); - - const supabase = getSupabaseClient(); - const { chat_id, limit = 50, offset = 0 } = req.query; - - if (!chat_id) { - console.log('❌ [Server] Ошибка: chat_id обязателен'); - return res.status(400).json({ error: 'chat_id required' }); - } - try { - console.log('🔍 [Server] Выполняем запрос к Supabase для чата:', chat_id); - - // Получаем сообщения - const { data: messages, error } = await supabase - .from('messages') - .select('*') - .eq('chat_id', chat_id) - .order('created_at', { ascending: false }) - .limit(limit) - .range(offset, offset + limit - 1); - - if (error) { - console.log('❌ [Server] Ошибка получения сообщений:', error); - return res.status(400).json({ error: error.message }); + const { chat_id, limit = 50, offset = 0 } = req.query; + + if (!chat_id) { + return res.status(400).json({ error: 'chat_id is required' }); } + + const supabase = getSupabaseClient(); - // Получаем профили пользователей для всех уникальных user_id - let data = messages || []; - if (data.length > 0) { - const userIds = [...new Set(data.map(msg => msg.user_id))]; - console.log('👥 [Server] Получаем профили для пользователей:', userIds); - + const { data, error } = await supabase + .from('messages') + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .eq('chat_id', chat_id) + .order('created_at', { ascending: true }) + .range(offset, offset + limit - 1); + + if (error) { + return res.status(500).json({ error: 'Failed to fetch messages' }); + } + + // Получаем уникальные ID пользователей из сообщений, у которых нет профиля + const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles); + const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))]; + + if (userIds.length > 0) { const { data: profiles, error: profilesError } = await supabase .from('user_profiles') .select('id, full_name, avatar_url') .in('id', userIds); - + if (!profilesError && profiles) { - // Объединяем сообщения с профилями - data = data.map(msg => ({ - ...msg, - user_profiles: profiles.find(profile => profile.id === msg.user_id) || null - })); - console.log('✅ [Server] Профили пользователей добавлены к сообщениям'); - } else { - console.log('⚠️ [Server] Ошибка получения профилей пользователей:', profilesError); + // Добавляем профили к сообщениям + data.forEach(message => { + if (!message.user_profiles) { + message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null; + } + }); } - } - - console.log('✅ [Server] Сообщения получены:', data?.length || 0, 'шт.'); - res.json(data?.reverse() || []); // Возвращаем в хронологическом порядке + } + + res.json(data); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка:', err); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({ error: 'Unexpected error occurred' }); } }); @@ -94,6 +89,9 @@ router.post('/messages', async (req, res) => { ...newMessage, user_profiles: userProfile || null }; + + // Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку + // Это предотвращает дублирование сообщений res.json(data); }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js index d35c2a2..62102c5 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js +++ b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js @@ -5,13 +5,29 @@ class ChatSocketHandler { this.io = io; this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) + this.realtimeSubscription = null; // Ссылка на подписку для управления + this.setupSocketHandlers(); + + try { + this.setupRealtimeSubscription(); // Добавляем Real-time подписки + } catch (error) { + // Ignore error + } + + // Запускаем тестирование через 2 секунды после инициализации + setTimeout(() => { + this.testRealtimeConnection(); + }, 2000); + + // Проверяем статус подписки через 5 секунд + setTimeout(() => { + this.checkSubscriptionStatus(); + }, 5000); } setupSocketHandlers() { this.io.on('connection', (socket) => { - console.log(`User connected: ${socket.id}`); - // Аутентификация пользователя socket.on('authenticate', async (data) => { await this.handleAuthentication(socket, data); @@ -84,10 +100,7 @@ class ChatSocketHandler { message: 'Successfully authenticated', user: userProfile }); - - console.log(`User ${user_id} authenticated with socket ${socket.id}`); } catch (error) { - console.error('Authentication error:', error); socket.emit('auth_error', { message: 'Authentication failed' }); } } @@ -105,7 +118,6 @@ class ChatSocketHandler { socket.emit('error', { message: 'chat_id is required' }); return; } - // Проверяем, что чат существует и пользователь имеет доступ к нему const supabase = getSupabaseClient(); const { data: chat, error } = await supabase @@ -140,7 +152,6 @@ class ChatSocketHandler { socket.emit('error', { message: 'Access denied to this chat' }); return; } - // Добавляем сокет в комнату socket.join(chat_id); @@ -148,8 +159,11 @@ class ChatSocketHandler { if (!this.chatRooms.has(chat_id)) { this.chatRooms.set(chat_id, new Set()); } + + const participantsBefore = this.chatRooms.get(chat_id).size; this.chatRooms.get(chat_id).add(socket.id); - + const participantsAfter = this.chatRooms.get(chat_id).size; + socket.emit('joined_chat', { chat_id, chat: chat, @@ -158,15 +172,13 @@ class ChatSocketHandler { // Уведомляем других участников о подключении const userInfo = this.onlineUsers.get(socket.id); + socket.to(chat_id).emit('user_joined', { chat_id, user: userInfo?.profile, timestamp: new Date() }); - - console.log(`User ${socket.user_id} joined chat ${chat_id}`); } catch (error) { - console.error('Join chat error:', error); socket.emit('error', { message: 'Failed to join chat' }); } } @@ -196,7 +208,7 @@ class ChatSocketHandler { timestamp: new Date() }); - console.log(`User ${socket.user_id} left chat ${chat_id}`); + } async handleSendMessage(socket, data) { @@ -243,9 +255,7 @@ class ChatSocketHandler { timestamp: new Date() }); - console.log(`Message sent to chat ${chat_id} by user ${socket.user_id}`); } catch (error) { - console.error('Send message error:', error); socket.emit('error', { message: 'Failed to send message' }); } } @@ -277,7 +287,6 @@ class ChatSocketHandler { } handleDisconnect(socket) { - console.log(`User disconnected: ${socket.id}`); // Удаляем пользователя из всех комнат this.chatRooms.forEach((participants, chat_id) => { @@ -326,11 +335,119 @@ class ChatSocketHandler { timestamp: new Date() }); } + + // Тестирование Real-time подписки + async testRealtimeConnection() { + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + // Создаем тестовый канал для проверки подключения + const testChannel = supabase + .channel('test_connection') + .subscribe((status, error) => { + if (status === 'SUBSCRIBED') { + // Отписываемся от тестового канала + setTimeout(() => { + testChannel.unsubscribe(); + }, 2000); + } + }); + + return true; + } catch (error) { + return false; + } + } + + // Проверка статуса подписки + checkSubscriptionStatus() { + if (this.realtimeSubscription) { + return true; + } else { + return false; + } + } + + setupRealtimeSubscription() { + // Добавляем небольшую задержку, чтобы убедиться, что Supabase клиент инициализирован + setTimeout(() => { + this._doSetupRealtimeSubscription(); + }, 1000); + } + + _doSetupRealtimeSubscription() { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + return; + } + + // Подписываемся на изменения в таблице messages + const subscription = supabase + .channel('messages_changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const newMessage = payload.new; + if (!newMessage) { + return; + } + + if (!newMessage.chat_id) { + return; + } + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', newMessage.user_id) + .single(); + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...newMessage, + user_profiles: userProfile || null + }; + + // Проверяем, есть ли участники в чате + const chatRoomParticipants = this.chatRooms.get(newMessage.chat_id); + + // Отправляем сообщение через Socket.IO всем участникам чата + this.io.to(newMessage.chat_id).emit('new_message', { + message: messageWithProfile, + timestamp: new Date() + }); + } catch (callbackError) { + // Ignore error + } + } + ) + .subscribe(); + + // Сохраняем ссылку на подписку для возможности отписки + this.realtimeSubscription = subscription; + + } catch (error) { + // Ignore error + } + } } // Функция инициализации Socket.IO для чатов function initializeChatSocket(io) { const chatHandler = new ChatSocketHandler(io); + return chatHandler; } From 39a62818e923e300c5182238d173b561cdfa3ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Thu, 12 Jun 2025 21:07:06 +0300 Subject: [PATCH 049/147] fix error --- .../routers/kfu-m-24-1/sber_mobile/index.js | 2 +- .../initiatives-ai-agents/moderation.js | 56 +++++++++++++++++++ .../initiatives-ai-agents/picture.js | 36 ++++++++++++ .../sber_mobile/{moderate.js => moderate.ts} | 25 +++++---- 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js rename server/routers/kfu-m-24-1/sber_mobile/{moderate.js => moderate.ts} (76%) diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 98ba2e2..a13e53e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,7 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const moderateRouter = require('./moderate'); +const moderateRouter = require('./moderate.ts').default; module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js new file mode 100644 index 0000000..88896b8 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js @@ -0,0 +1,56 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.moderationText = void 0; +const node_https_1 = require("node:https"); +const langchain_gigachat_1 = require("langchain-gigachat"); +const zod_1 = require("zod"); +const httpsAgent = new node_https_1.Agent({ + rejectUnauthorized: false, +}); +const llm = new langchain_gigachat_1.GigaChat({ + credentials: process.env.GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2', + httpsAgent, +}); +// возвращаю комментарий + исправленное предложение + булево значение +const moderationLlm = llm.withStructuredOutput(zod_1.z.object({ + comment: zod_1.z.string(), + fixedText: zod_1.z.string().optional(), + isApproved: zod_1.z.boolean(), +})); +const moderationText = async (title, body) => { + const prompt = ` + Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, + не имеющая отношения к Управляющей компании). + + Заголовок: ${title} + Основной текст: ${body} + + Твои задачи: + 1. Проверь предложение и заголовок на спам. + 2. Проверь, чтобы заголовок и текст были на одну тему. + 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. + 4. Проверь грамматику. + 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. + 6. Не должно быть рекламы, ссылок и т.д. + 7. Проверь предложение на информативность, оно не должно быть слишком коротким. + 8. Предложение должно быть в вежливой форме. + + - Если все правила соблюдены, то предложение принимается! + + Правила написания комментария: + - Если предложение отклоняется, верни комментарий со следующей формулировкой: + "Предложение отклонено. Причина: (укажи проблему)" + Правила написания fixedBody: + - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. + - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, + которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. + - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, + по которому оно не прошло. + -Если предложение принимается, то ничего не возвращай в поле fixedBody. + `; + const result = await moderationLlm.invoke(prompt); + return [result.comment, result.fixedText, result.isApproved]; +}; +exports.moderationText = moderationText; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js new file mode 100644 index 0000000..0b54adc --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generatePicture = exports.llm = void 0; +const gigachat_1 = require("gigachat"); +const node_https_1 = require("node:https"); +const httpsAgent = new node_https_1.Agent({ + rejectUnauthorized: false, +}); +exports.llm = new gigachat_1.GigaChat({ + credentials: process.env.GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); +const generatePicture = async (prompt) => { + const resp = await exports.llm.chat({ + messages: [ + { + "role": "system", + "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" + }, + { + role: "user", + content: `Старайся передать атмосферу уюта и безопасности. + Нарисуй картинку подходящую для такого события: ${prompt} + В картинке не должно быть текста, только изображение.`, + }, + ], + function_call: 'auto', + }); + // Получение изображения по идентификатору + const detectedImage = (0, gigachat_1.detectImage)(resp.choices[0]?.message.content ?? ''); + const image = await exports.llm.getImage(detectedImage?.uuid ?? ''); + // Возвращаем содержимое изображения + return image.content; +}; +exports.generatePicture = generatePicture; diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.ts similarity index 76% rename from server/routers/kfu-m-24-1/sber_mobile/moderate.js rename to server/routers/kfu-m-24-1/sber_mobile/moderate.ts index 76cccd3..332554a 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.ts @@ -1,10 +1,13 @@ -const router = require('express').Router(); -const { moderationText } = require('./initiatives-ai-agents/moderation'); -const { generatePicture } = require('./initiatives-ai-agents/picture'); +import { Router, Request, Response } from 'express'; +import { moderationText } from './initiatives-ai-agents/moderation'; +import { generatePicture } from './initiatives-ai-agents/picture'; + const { getSupabaseClient } = require('./supabaseClient'); +const router = Router(); + // Обработчик для модерации текста -router.post('/moderate', async (req, res) => { +router.post('/moderate', async (req: Request, res: Response) => { try { const { title, body } = req.body; if (!title || !body) { @@ -18,13 +21,14 @@ router.post('/moderate', async (req, res) => { fixedText, isApproved }); - } catch (error) { - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } catch (error: any) { + console.error('Error in moderation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); } }); // Обработчик для генерации изображений -router.post('/generate-image', async (req, res) => { +router.post('/generate-image', async (req: Request, res: Response) => { try { const { prompt, userId } = req.body; if (!prompt) { @@ -65,9 +69,10 @@ router.post('/generate-image', async (req, res) => { imageUrl: urlData.publicUrl, imagePath: filename }); - } catch (error) { - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } catch (error: any) { + console.error('Error in image generation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); } }); -module.exports = router; \ No newline at end of file +export default router; \ No newline at end of file From 3af82f74787f28da146c9acd9d35f6b811d56801 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 13 Jun 2025 19:44:45 +0300 Subject: [PATCH 050/147] fix system prompt --- .../sber_mobile/support-ai-agent/README.md | 63 ------------------- .../support-ai-agent/support-agent.ts | 42 +++++-------- .../kfu-m-24-1/sber_mobile/supportApi.js | 42 +------------ 3 files changed, 20 insertions(+), 127 deletions(-) delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md deleted file mode 100644 index ccad3cd..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# AI Support Agent - -AI-агент поддержки интегрирован в существующий `supportApi.js`. - -## Структура - -``` -support-ai-agent/ -├── gigachat.ts # Конфигурация GigaChat -├── support-agent.ts # Основной класс агента -└── README.md # Документация -``` - -## API - -### POST `/support` -Отправить сообщение в службу поддержки (теперь с AI-агентом). - -**Запрос:** -```json -{ - "user_id": "string", // Обязательно - "message": "string", // Обязательно - "system_prompt": "string" // Опционально - настройка поведения агента -} -``` - -**Ответ:** -```json -{ - "reply": "Ответ AI-агента", - "success": true -} -``` - -### POST `/support/configure` -Настроить системный промпт для конкретного пользователя. - -### DELETE `/support/history/:userId` -Очистить историю диалога пользователя. - -## Возможности - -- 🤖 Интеллектуальные ответы на основе GigaChat -- 💾 Сохранение всех сообщений в базу данных Supabase -- 🧠 Память контекста диалога для каждого пользователя -- ⚙️ Настраиваемые системные промпты -- 📊 Поддержка множественных пользователей - -## Примеры системных промптов - -### Техническая поддержка -``` -Ты - специалист технической поддержки мобильного приложения "Умный дом". -Помогай пользователям решать проблемы, объясняй функции простым языком, -проводи диагностику пошагово. Всегда будь дружелюбным и терпеливым. -``` - -### Общая поддержка клиентов -``` -Ты - профессиональный агент службы поддержки. Помогай решать вопросы -пользователей, отвечай вежливо и по существу, проявляй эмпатию. -``` \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts index 514a3c8..3ea39b7 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts @@ -5,7 +5,6 @@ import { MemorySaver } from '@langchain/langgraph'; import gigachat from './gigachat'; export interface SupportAgentConfig { - systemPrompt?: string; temperature?: number; threadId?: string; } @@ -22,11 +21,13 @@ export class SupportAgent { private agent: any; private systemPrompt: string; private threadId: string; + private isFirstMessage: boolean; constructor(config: SupportAgentConfig = {}) { - this.systemPrompt = config.systemPrompt || this.getDefaultSystemPrompt(); + this.systemPrompt = this.getDefaultSystemPrompt(); this.threadId = config.threadId || 'default'; this.memorySaver = new MemorySaver(); + this.isFirstMessage = true; // Настраиваем модель с заданной температурой this.llm = gigachat; @@ -58,30 +59,24 @@ export class SupportAgent { Всегда отвечай на русском языке и старайся быть максимально полезным.`; } - /** - * Обновить системный промпт - */ - public updateSystemPrompt(newPrompt: string): void { - this.systemPrompt = newPrompt; - } - /** - * Получить текущий системный промпт - */ - public getSystemPrompt(): string { - return this.systemPrompt; - } /** * Обработать сообщение пользователя и получить ответ */ public async processMessage(userMessage: string): Promise { try { - // Создаем сообщения с системным промптом - const messages = [ - new SystemMessage(this.systemPrompt), - new HumanMessage(userMessage) - ]; + // Создаем массив сообщений + const messages: BaseMessage[] = []; + + // Добавляем системный промпт только в первом сообщении + if (this.isFirstMessage) { + messages.push(new SystemMessage(this.systemPrompt)); + this.isFirstMessage = false; + } + + // Добавляем сообщение пользователя + messages.push(new HumanMessage(userMessage)); // Получаем ответ от агента const response = await this.agent.invoke({ @@ -120,12 +115,9 @@ export class SupportAgent { tools: [], checkpointSaver: this.memorySaver }); + // Сбрасываем флаг первого сообщения + this.isFirstMessage = true; } - /** - * Изменить ID потока (для работы с разными пользователями) - */ - public setThreadId(threadId: string): void { - this.threadId = threadId; - } + } \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index de908a8..88f488c 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -8,15 +8,12 @@ const userAgents = new Map(); /** * Получить или создать агента для пользователя */ -function getUserAgent(userId, systemPrompt) { +function getUserAgent(userId) { if (!userAgents.has(userId)) { const config = { threadId: userId, temperature: 0.7 }; - if (systemPrompt) { - config.systemPrompt = systemPrompt; - } userAgents.set(userId, new SupportAgent(config)); } return userAgents.get(userId); @@ -25,7 +22,7 @@ function getUserAgent(userId, systemPrompt) { // POST /api/support router.post('/support', async (req, res) => { const supabase = getSupabaseClient(); - const { user_id, message, system_prompt } = req.body; + const { user_id, message } = req.body; if (!user_id || !message) { return res.status(400).json({ error: 'user_id и message обязательны' }); @@ -42,12 +39,7 @@ router.post('/support', async (req, res) => { } // Получаем агента для пользователя - const agent = getUserAgent(user_id, system_prompt); - - // Обновляем системный промпт если передан - if (system_prompt) { - agent.updateSystemPrompt(system_prompt); - } + const agent = getUserAgent(user_id); // Получаем ответ от AI-агента const aiResponse = await agent.processMessage(message); @@ -89,35 +81,7 @@ router.post('/support', async (req, res) => { } }); -// POST /api/support/configure - Настройка системного промпта -router.post('/support/configure', async (req, res) => { - const { user_id, system_prompt } = req.body; - - if (!user_id) { - return res.status(400).json({ error: 'user_id обязателен' }); - } - try { - const agent = getUserAgent(user_id, system_prompt); - - if (system_prompt) { - agent.updateSystemPrompt(system_prompt); - } - - res.json({ - message: 'Конфигурация агента обновлена', - current_system_prompt: agent.getSystemPrompt(), - success: true - }); - - } catch (error) { - console.error('Ошибка в /support/configure:', error); - res.status(500).json({ - error: 'Внутренняя ошибка сервера', - success: false - }); - } -}); // DELETE /api/support/history/:userId - Очистка истории диалога router.delete('/support/history/:userId', async (req, res) => { From 8dd8ec89300ca2aabc9712759e58d6fd655af143 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 13 Jun 2025 21:07:13 +0300 Subject: [PATCH 051/147] add getting support chat history --- .../kfu-m-24-1/sber_mobile/supportApi.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index 88f488c..39db5a7 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -19,6 +19,41 @@ function getUserAgent(userId) { return userAgents.get(userId); } +// GET /api/support - Получить историю сообщений пользователя +router.get('/support', async (req, res) => { + const supabase = getSupabaseClient(); + const { user_id } = req.query; + + if (!user_id) { + return res.status(400).json({ error: 'user_id обязателен' }); + } + + try { + // Получаем все сообщения пользователя из базы данных + const { data: messages, error } = await supabase + .from('support') + .select('*') + .eq('user_id', user_id) + .order('created_at', { ascending: true }); + + if (error) { + return res.status(400).json({ error: error.message }); + } + + res.json({ + messages: messages || [], + success: true + }); + + } catch (error) { + console.error('Ошибка в GET /support:', error); + res.status(500).json({ + error: 'Внутренняя ошибка сервера', + success: false + }); + } +}); + // POST /api/support router.post('/support', async (req, res) => { const supabase = getSupabaseClient(); From 5886270e298e119d14a34ff582c1ea02cc934459 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 13 Jun 2025 22:31:32 +0300 Subject: [PATCH 052/147] add history tool --- .../support-ai-agent/support-agent.ts | 44 +++++++-------- .../support-ai-agent/support-context-tool.ts | 56 +++++++++++++++++++ 2 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts index 3ea39b7..0850b95 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts @@ -3,6 +3,7 @@ import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts import { createReactAgent } from '@langchain/langgraph/prebuilt'; import { MemorySaver } from '@langchain/langgraph'; import gigachat from './gigachat'; +import { SupportContextTool } from './support-context-tool'; export interface SupportAgentConfig { temperature?: number; @@ -22,30 +23,31 @@ export class SupportAgent { private systemPrompt: string; private threadId: string; private isFirstMessage: boolean; + private userId: string; constructor(config: SupportAgentConfig = {}) { this.systemPrompt = this.getDefaultSystemPrompt(); this.threadId = config.threadId || 'default'; + this.userId = this.threadId; this.memorySaver = new MemorySaver(); this.isFirstMessage = true; - // Настраиваем модель с заданной температурой this.llm = gigachat; if (config.temperature !== undefined) { this.llm.temperature = config.temperature; } - // Создаем агента без инструментов для простого чата + const tools = [ + new SupportContextTool(this.userId) + ]; + this.agent = createReactAgent({ llm: this.llm, - tools: [], + tools: tools, checkpointSaver: this.memorySaver }); } - /** - * Получить системный промпт по умолчанию для агента поддержки - */ private getDefaultSystemPrompt(): string { return `Ты - профессиональный агент службы поддержки. @@ -56,29 +58,26 @@ export class SupportAgent { - Проявлять эмпатию к проблемам пользователей - Если не знаешь ответ, честно сообщить об этом и предложить альтернативные способы получения помощи +ВАЖНО: У тебя есть доступ к инструменту get_support_context, который позволяет получить историю предыдущих сообщений пользователя. +ВСЕГДА используй этот инструмент ПЕРВЫМ ДЕЛОМ при получении каждого нового сообщения, чтобы понять контекст и предыдущие обращения пользователя. +Только после получения контекста отвечай на вопрос пользователя. + +Если в истории есть предыдущие обращения, обязательно ссылайся на них в своем ответе, показывая что помнишь предыдущее общение. + Всегда отвечай на русском языке и старайся быть максимально полезным.`; } - - - /** - * Обработать сообщение пользователя и получить ответ - */ public async processMessage(userMessage: string): Promise { try { - // Создаем массив сообщений const messages: BaseMessage[] = []; - // Добавляем системный промпт только в первом сообщении if (this.isFirstMessage) { messages.push(new SystemMessage(this.systemPrompt)); this.isFirstMessage = false; } - // Добавляем сообщение пользователя messages.push(new HumanMessage(userMessage)); - // Получаем ответ от агента const response = await this.agent.invoke({ messages: messages }, { @@ -87,7 +86,6 @@ export class SupportAgent { } }); - // Извлекаем последнее сообщение от ассистента const lastMessage = response.messages[response.messages.length - 1]; return { @@ -105,19 +103,19 @@ export class SupportAgent { } } - /** - * Очистить историю диалога - */ public async clearHistory(): Promise { this.memorySaver = new MemorySaver(); + + const tools = [ + new SupportContextTool(this.userId) + ]; + this.agent = createReactAgent({ llm: this.llm, - tools: [], + tools: tools, checkpointSaver: this.memorySaver }); - // Сбрасываем флаг первого сообщения + this.isFirstMessage = true; } - - } \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts new file mode 100644 index 0000000..94a573c --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts @@ -0,0 +1,56 @@ +import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools'; +import { z } from 'zod'; +import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; +import { getSupabaseClient } from '../supabaseClient'; + +export class SupportContextTool extends StructuredTool { + name = 'get_support_context'; + description = 'Получает последние 10 сообщений из истории поддержки для понимания контекста разговора. Используй этот инструмент в начале разговора.'; + + schema = z.object({}); + + private userId: string; + + constructor(userId: string) { + super(); + this.userId = userId; + } + + protected async _call( + arg: z.infer, + runManager?: CallbackManagerForToolRun, + parentConfig?: ToolRunnableConfig> + ): Promise { + try { + const supabase = getSupabaseClient(); + + const { data: messages, error } = await supabase + .from('support') + .select('message, is_from_user, created_at') + .eq('user_id', this.userId) + .order('created_at', { ascending: false }) + .limit(10); + + if (error) { + return 'Не удалось получить историю сообщений.'; + } + + if (!messages || messages.length === 0) { + return 'История сообщений поддержки пуста. Это первое обращение пользователя.'; + } + + const chronologicalMessages = messages.reverse(); + + const contextMessages = chronologicalMessages.map((msg, index) => { + const role = msg.is_from_user ? 'Пользователь' : 'Агент поддержки'; + const time = new Date(msg.created_at).toLocaleString('ru-RU'); + return `${index + 1}. [${time}] ${role}: ${msg.message}`; + }).join('\n'); + + return `Последние сообщения из истории поддержки (${messages.length} сообщений):\n\n${contextMessages}\n\nИспользуй этот контекст для понимания предыдущих обращений пользователя и предоставления более точных ответов.`; + + } catch (error) { + return 'Произошла ошибка при получении истории сообщений.'; + } + } +} \ No newline at end of file From 1aeb62d490ff7fe586b040d3116a94909a75705f Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 13 Jun 2025 23:15:13 +0300 Subject: [PATCH 053/147] add rag tool --- package.json | 1 + .../support-ai-agent/knowledge-base-tool.ts | 41 +++++++++++++++++++ .../support-ai-agent/support-agent.ts | 26 +++++++++--- .../support-ai-agent/vector-store.ts | 33 +++++++++++++++ 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts diff --git a/package.json b/package.json index 10a157b..a43b5aa 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "gigachat": "^0.0.14", "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", + "langchain": "^0.3.7", "langchain-gigachat": "^0.0.11", "mongodb": "^6.12.0", "mongoose": "^8.9.2", diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts new file mode 100644 index 0000000..84a681d --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts @@ -0,0 +1,41 @@ +import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools'; +import { z } from 'zod'; +import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; +import { getVectorStore } from './vector-store'; + +export class KnowledgeBaseTool extends StructuredTool { + name = 'search_knowledge_base'; + description = 'Ищет информацию в базе знаний компании о процессах, оплатах, подаче заявок, правилах и документах УК. Используй этот инструмент для вопросов, требующих специфических знаний о компании.'; + + schema = z.object({ + query: z.string().describe('Поисковый запрос для поиска в базе знаний'), + }); + + protected async _call( + arg: z.infer, + runManager?: CallbackManagerForToolRun, + parentConfig?: ToolRunnableConfig> + ): Promise { + try { + const vectorStore = getVectorStore(); + const retriever = vectorStore.asRetriever({ + k: 5 + }); + + const relevantDocs = await retriever.getRelevantDocuments(arg.query); + + if (!relevantDocs || relevantDocs.length === 0) { + return 'В базе знаний не найдено информации по данному запросу. Возможно, стоит переформулировать вопрос или обратиться к специалисту.'; + } + + const formattedDocs = relevantDocs.map((doc, index) => { + return `Документ ${index + 1}:\n${doc.pageContent}\n`; + }).join('\n---\n'); + + return `Найдена следующая информация в базе знаний компании:\n\n${formattedDocs}\n\nИспользуй эту информацию для ответа на вопрос пользователя.`; + + } catch (error) { + return 'Произошла ошибка при поиске в базе знаний. Попробуйте переформулировать запрос.'; + } + } +} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts index 0850b95..56578ec 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts @@ -4,6 +4,7 @@ import { createReactAgent } from '@langchain/langgraph/prebuilt'; import { MemorySaver } from '@langchain/langgraph'; import gigachat from './gigachat'; import { SupportContextTool } from './support-context-tool'; +import { KnowledgeBaseTool } from './knowledge-base-tool'; export interface SupportAgentConfig { temperature?: number; @@ -38,7 +39,8 @@ export class SupportAgent { } const tools = [ - new SupportContextTool(this.userId) + new SupportContextTool(this.userId), + new KnowledgeBaseTool() ]; this.agent = createReactAgent({ @@ -58,11 +60,22 @@ export class SupportAgent { - Проявлять эмпатию к проблемам пользователей - Если не знаешь ответ, честно сообщить об этом и предложить альтернативные способы получения помощи -ВАЖНО: У тебя есть доступ к инструменту get_support_context, который позволяет получить историю предыдущих сообщений пользователя. -ВСЕГДА используй этот инструмент ПЕРВЫМ ДЕЛОМ при получении каждого нового сообщения, чтобы понять контекст и предыдущие обращения пользователя. -Только после получения контекста отвечай на вопрос пользователя. +У тебя есть доступ к двум инструментам: -Если в истории есть предыдущие обращения, обязательно ссылайся на них в своем ответе, показывая что помнишь предыдущее общение. +1. get_support_context - получает историю предыдущих сообщений пользователя + ВСЕГДА используй этот инструмент ПЕРВЫМ ДЕЛОМ при получении каждого нового сообщения + +2. search_knowledge_base - ищет информацию в базе знаний компании + Используй этот инструмент для вопросов о: + - Процессах оплаты и тарифах + - Подаче заявок и документооборота + - Правилах и регламентах УК + - Технических вопросах приложения + - Любых специфических вопросах о компании + +ВАЖНО: Сначала получи контекст, затем при необходимости найди информацию в базе знаний, и только после этого отвечай пользователю. + +Если в истории есть предыдущие обращения, обязательно ссылайся на них в своем ответе. Всегда отвечай на русском языке и старайся быть максимально полезным.`; } @@ -107,7 +120,8 @@ export class SupportAgent { this.memorySaver = new MemorySaver(); const tools = [ - new SupportContextTool(this.userId) + new SupportContextTool(this.userId), + new KnowledgeBaseTool() ]; this.agent = createReactAgent({ diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts new file mode 100644 index 0000000..3841672 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts @@ -0,0 +1,33 @@ +import { createClient } from '@supabase/supabase-js'; +import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'; +import { GigaChatEmbeddings } from 'langchain-gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +let vectorStoreInstance: SupabaseVectorStore | null = null; + +export function getVectorStore(): SupabaseVectorStore { + if (!vectorStoreInstance) { + const client = createClient( + process.env.RAG_SUPABASE_URL!, + process.env.RAG_SUPABASE_SERVICE_ROLE_KEY!, + ); + + vectorStoreInstance = new SupabaseVectorStore( + new GigaChatEmbeddings({ + credentials: process.env.GIGA_AUTH, + httpsAgent, + }), + { + client, + tableName: 'slon', + queryName: 'match_slon' + } + ); + } + + return vectorStoreInstance; +} \ No newline at end of file From 7bd82fedced6f62bd7bd555de797dd90659b9f68 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Fri, 13 Jun 2025 23:52:02 +0300 Subject: [PATCH 054/147] change socket settings --- server/index.ts | 13 +------------ server/routers/kfu-m-24-1/sber_mobile/index.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/server/index.ts b/server/index.ts index 3d4f58e..d39f65f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,9 +20,7 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' -import { setIo, getIo } from './io' -// Импорт обработчика чата -const { initializeChatSocket } = require('./routers/kfu-m-24-1/sber_mobile/socket-chat') +import { setIo } from './io' export const app = express() @@ -67,15 +65,6 @@ const initServer = async () => { console.log('warming up 🔥') const server = setIo(app) - - // Инициализация Socket.IO для чата - const io = getIo() - if (io) { - const chatHandler = initializeChatSocket(io) - // Сохраняем ссылку на chat handler для доступа из эндпоинтов - io.chatHandler = chatHandler - console.log('✅ Socket.IO для чата инициализирован') - } const sess = { secret: "super-secret-key", diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 4dfad13..854576e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,7 +15,10 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const { getIo } = require('../../../io'); +const {setIo, getIo } = require('../../../io'); +// Импорт обработчика чата +const { initializeChatSocket } = require('./socket-chat') + module.exports = router; @@ -34,4 +37,14 @@ router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); router.use('', avatarRouter); -router.use('', supportRouter); \ No newline at end of file +router.use('', supportRouter); + + + // Инициализация Socket.IO для чата + const io = getIo() + if (io) { + const chatHandler = initializeChatSocket(io) + // Сохраняем ссылку на chat handler для доступа из эндпоинтов + io.chatHandler = chatHandler + console.log('✅ Socket.IO для чата инициализирован') + } \ No newline at end of file From ca81e19d14ab5064dcfab15d241dc9de6ba2d927 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 14 Jun 2025 00:16:02 +0300 Subject: [PATCH 055/147] add tickets creation --- .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 3 +- .../support-ai-agent/create-ticket-tool.ts | 66 +++++++++++++++++ .../support-ai-agent/support-agent.ts | 73 +++++++++++++------ .../kfu-m-24-1/sber_mobile/supportApi.js | 8 +- 4 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt index 0a7b25a..367f42a 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt @@ -170,7 +170,7 @@ CREATE TABLE payment_service_details ( CREATE TABLE tickets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), - building_id UUID NOT NULL REFERENCES buildings(id), + apartment_id UUID NOT NULL REFERENCES apartments(id), title TEXT NOT NULL, description TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), @@ -197,6 +197,7 @@ CREATE INDEX idx_votes_initiative ON votes(initiative_id); CREATE INDEX idx_messages_chat ON messages(chat_id); CREATE INDEX idx_cameras_building ON cameras(building_id); CREATE INDEX idx_tickets_user ON tickets(user_id); +CREATE INDEX idx_tickets_apartment ON tickets(apartment_id); CREATE INDEX idx_apartments_building ON apartments(building_id); CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts new file mode 100644 index 0000000..ff88787 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts @@ -0,0 +1,66 @@ +import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools'; +import { z } from 'zod'; +import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; +import { getSupabaseClient } from '../supabaseClient'; + +export class CreateTicketTool extends StructuredTool { + name = 'create_ticket'; + description = 'Создает заявку в системе. ВАЖНО: используй этот инструмент ТОЛЬКО после получения явного согласия пользователя на создание заявки с конкретным текстом.'; + + schema = z.object({ + title: z.string().describe('Заголовок заявки'), + description: z.string().describe('Подробное описание проблемы'), + category: z.string().describe('Категория заявки (например: ремонт, уборка, техническая_поддержка, жалоба)'), + }); + + private userId: string; + private apartmentId: string; + + constructor(userId: string, apartmentId: string) { + super(); + this.userId = userId; + this.apartmentId = apartmentId; + } + + protected async _call( + arg: z.infer, + runManager?: CallbackManagerForToolRun, + parentConfig?: ToolRunnableConfig> + ): Promise { + try { + if (!this.apartmentId) { + return 'Не удалось определить вашу квартиру. Обратитесь к администратору для создания заявки.'; + } + + const supabase = getSupabaseClient(); + + const { data: ticket, error } = await supabase + .from('tickets') + .insert({ + user_id: this.userId, + apartment_id: this.apartmentId, + title: arg.title, + description: arg.description, + category: arg.category, + status: 'open' + }) + .select() + .single(); + + if (error) { + return 'Произошла ошибка при создании заявки. Попробуйте позже или обратитесь к администратору.'; + } + + return `Заявка успешно создана! +Номер заявки: ${ticket.id} +Заголовок: ${ticket.title} +Статус: Открыта +Дата создания: ${new Date(ticket.created_at).toLocaleString('ru-RU')} + +Ваша заявка принята в работу. Мы свяжемся с вами в ближайшее время.`; + + } catch (error) { + return 'Произошла техническая ошибка при создании заявки. Пожалуйста, попробуйте позже.'; + } + } +} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts index 56578ec..93816e6 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts @@ -5,6 +5,7 @@ import { MemorySaver } from '@langchain/langgraph'; import gigachat from './gigachat'; import { SupportContextTool } from './support-context-tool'; import { KnowledgeBaseTool } from './knowledge-base-tool'; +import { CreateTicketTool } from './create-ticket-tool'; export interface SupportAgentConfig { temperature?: number; @@ -51,36 +52,52 @@ export class SupportAgent { } private getDefaultSystemPrompt(): string { - return `Ты - профессиональный агент службы поддержки. + return `Ты - профессиональный агент службы поддержки управляющей компании. -Твои основные задачи: -- Помогать пользователям решать их вопросы и проблемы -- Отвечать вежливо, профессионально и по существу -- Предоставлять четкие и понятные инструкции -- Проявлять эмпатию к проблемам пользователей -- Если не знаешь ответ, честно сообщить об этом и предложить альтернативные способы получения помощи +ОСНОВНЫЕ ПРИНЦИПЫ: +- Помогай только с реальными проблемами и вопросами, связанными с ЖКХ, управляющей компанией и приложением +- Будь вежливым, профессиональным и по существу +- Если вопрос неуместен, не связан с твоими обязанностями или является развлекательным - вежливо откажись и перенаправь к основным темам -У тебя есть доступ к двум инструментам: +ДОСТУПНЫЕ ИНСТРУМЕНТЫ: -1. get_support_context - получает историю предыдущих сообщений пользователя - ВСЕГДА используй этот инструмент ПЕРВЫМ ДЕЛОМ при получении каждого нового сообщения +1. get_support_context - получает историю сообщений пользователя + ВСЕГДА используй ПЕРВЫМ при каждом новом сообщении -2. search_knowledge_base - ищет информацию в базе знаний компании - Используй этот инструмент для вопросов о: - - Процессах оплаты и тарифах - - Подаче заявок и документооборота +2. search_knowledge_base - поиск в базе знаний компании + Используй ТОЛЬКО для серьезных вопросов о: + - Процессах оплаты ЖКХ и тарифах + - Подаче заявок и документообороте - Правилах и регламентах УК - Технических вопросах приложения - - Любых специфических вопросах о компании + - Процедурах и инструкциях компании -ВАЖНО: Сначала получи контекст, затем при необходимости найди информацию в базе знаний, и только после этого отвечай пользователю. +3. create_ticket - создание заявки в системе + Используй ТОЛЬКО когда: + - Пользователь сообщает о реальной проблеме (поломка, неисправность, жалоба) + - Проблема требует вмешательства УК или технических служб + - ОБЯЗАТЕЛЬНО сначала покажи пользователю полный текст заявки + - Получи ЯВНОЕ согласие пользователя перед созданием + - НЕ создавай заявки для консультационных вопросов -Если в истории есть предыдущие обращения, обязательно ссылайся на них в своем ответе. +ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ: +- НЕ используй search_knowledge_base и create_ticket для: + * Общих вопросов и болтовни + * Развлекательных запросов + * Вопросов не по теме ЖКХ/УК + * Простых консультаций, которые можно решить обычным ответом -Всегда отвечай на русском языке и старайся быть максимально полезным.`; +АЛГОРИТМ РАБОТЫ: +1. Получи контекст истории сообщений +2. Определи, является ли вопрос уместным и серьезным +3. Если нужна специфическая информация - найди в базе знаний +4. Если нужно создать заявку - покажи текст и получи согласие +5. Дай полный и полезный ответ + +Всегда отвечай на русском языке и фокусируйся на помощи с реальными проблемами ЖКХ.`; } - public async processMessage(userMessage: string): Promise { + public async processMessage(userMessage: string, apartmentId?: string): Promise { try { const messages: BaseMessage[] = []; @@ -91,7 +108,21 @@ export class SupportAgent { messages.push(new HumanMessage(userMessage)); - const response = await this.agent.invoke({ + // Создаем инструменты с актуальным apartmentId + const tools = [ + new SupportContextTool(this.userId), + new KnowledgeBaseTool(), + new CreateTicketTool(this.userId, apartmentId || '') + ]; + + // Пересоздаем агента с обновленными инструментами + const tempAgent = createReactAgent({ + llm: this.llm, + tools: tools, + checkpointSaver: this.memorySaver + }); + + const response = await tempAgent.invoke({ messages: messages }, { configurable: { @@ -102,7 +133,7 @@ export class SupportAgent { const lastMessage = response.messages[response.messages.length - 1]; return { - content: lastMessage.content || 'Извините, не удалось сформировать ответ.', + content: typeof lastMessage.content === 'string' ? lastMessage.content : 'Извините, не удалось сформировать ответ.', success: true }; diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index 39db5a7..1babe43 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -57,7 +57,7 @@ router.get('/support', async (req, res) => { // POST /api/support router.post('/support', async (req, res) => { const supabase = getSupabaseClient(); - const { user_id, message } = req.body; + const { user_id, message, apartment_id } = req.body; if (!user_id || !message) { return res.status(400).json({ error: 'user_id и message обязательны' }); @@ -76,8 +76,8 @@ router.post('/support', async (req, res) => { // Получаем агента для пользователя const agent = getUserAgent(user_id); - // Получаем ответ от AI-агента - const aiResponse = await agent.processMessage(message); + // Получаем ответ от AI-агента, передавая apartment_id + const aiResponse = await agent.processMessage(message, apartment_id); if (!aiResponse.success) { console.error('Ошибка AI-агента:', aiResponse.error); @@ -116,8 +116,6 @@ router.post('/support', async (req, res) => { } }); - - // DELETE /api/support/history/:userId - Очистка истории диалога router.delete('/support/history/:userId', async (req, res) => { const { userId } = req.params; From a7be7936085d1af15b4ddc46ba485c3c53fa1200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Sat, 14 Jun 2025 02:01:19 +0300 Subject: [PATCH 056/147] change file type and fix agents --- .../routers/kfu-m-24-1/sber_mobile/index.js | 2 +- .../initiatives-ai-agents/moderation.js | 56 -------- .../initiatives-ai-agents/moderation.ts | 24 +++- .../initiatives-ai-agents/picture.js | 36 ----- .../initiatives-ai-agents/picture.ts | 18 ++- .../kfu-m-24-1/sber_mobile/moderate.js | 124 ++++++++++++++++++ .../kfu-m-24-1/sber_mobile/moderate.ts | 78 ----------- 7 files changed, 157 insertions(+), 181 deletions(-) delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index a13e53e..da592c7 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,7 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const moderateRouter = require('./moderate.ts').default; +const moderateRouter = require('./moderate.js'); module.exports = router; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js deleted file mode 100644 index 88896b8..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.moderationText = void 0; -const node_https_1 = require("node:https"); -const langchain_gigachat_1 = require("langchain-gigachat"); -const zod_1 = require("zod"); -const httpsAgent = new node_https_1.Agent({ - rejectUnauthorized: false, -}); -const llm = new langchain_gigachat_1.GigaChat({ - credentials: process.env.GIGA_AUTH, - temperature: 0.2, - model: 'GigaChat-2', - httpsAgent, -}); -// возвращаю комментарий + исправленное предложение + булево значение -const moderationLlm = llm.withStructuredOutput(zod_1.z.object({ - comment: zod_1.z.string(), - fixedText: zod_1.z.string().optional(), - isApproved: zod_1.z.boolean(), -})); -const moderationText = async (title, body) => { - const prompt = ` - Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, - не имеющая отношения к Управляющей компании). - - Заголовок: ${title} - Основной текст: ${body} - - Твои задачи: - 1. Проверь предложение и заголовок на спам. - 2. Проверь, чтобы заголовок и текст были на одну тему. - 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. - 4. Проверь грамматику. - 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. - 6. Не должно быть рекламы, ссылок и т.д. - 7. Проверь предложение на информативность, оно не должно быть слишком коротким. - 8. Предложение должно быть в вежливой форме. - - - Если все правила соблюдены, то предложение принимается! - - Правила написания комментария: - - Если предложение отклоняется, верни комментарий со следующей формулировкой: - "Предложение отклонено. Причина: (укажи проблему)" - Правила написания fixedBody: - - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. - - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, - которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. - - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, - по которому оно не прошло. - -Если предложение принимается, то ничего не возвращай в поле fixedBody. - `; - const result = await moderationLlm.invoke(prompt); - return [result.comment, result.fixedText, result.isApproved]; -}; -exports.moderationText = moderationText; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts index 00e5e0b..7a34fdb 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -35,23 +35,33 @@ export const moderationText = async (title: string, body: string): Promise<[stri 4. Проверь грамматику. 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. 6. Не должно быть рекламы, ссылок и т.д. - 7. Проверь предложение на информативность, оно не должно быть слишком коротким. + 7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы. 8. Предложение должно быть в вежливой форме. - Если все правила соблюдены, то предложение принимается! + - Если предложение отклонено, всегда пиши комментарий и fixedText! + Правила написания комментария: - - Если предложение отклоняется, верни комментарий со следующей формулировкой: + - Если предложение отклоняется, пиши комментарий со следующей формулировкой: "Предложение отклонено. Причина: (укажи проблему)" - Правила написания fixedBody: - - Если предложение отклонено, то верни в поле "fixedBody" новый текст, который будет соответствовать правилам. + + Правила написания fixedText: + - Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам. - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. - - Если текст не представляет никакой ценности, возврати в поле "fixedBody" правило, + - Если текст не представляет никакой ценности, возврати в поле "fixedText" правило, по которому оно не прошло. - -Если предложение принимается, то ничего не возвращай в поле fixedBody. + -Если предложение принимается, то ничего не возвращай в поле fixedText. ` + const result = await moderationLlm.invoke(prompt); - + console.log(result) + // Дополнительная проверка + if(!result.isApproved && result.comment.trim() === '' && result.fixedText.trim() === '') { + result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', + result.fixedText = body + } + return [result.comment, result.fixedText, result.isApproved]; }; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js deleted file mode 100644 index 0b54adc..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePicture = exports.llm = void 0; -const gigachat_1 = require("gigachat"); -const node_https_1 = require("node:https"); -const httpsAgent = new node_https_1.Agent({ - rejectUnauthorized: false, -}); -exports.llm = new gigachat_1.GigaChat({ - credentials: process.env.GIGA_AUTH, - model: 'GigaChat-2', - httpsAgent, -}); -const generatePicture = async (prompt) => { - const resp = await exports.llm.chat({ - messages: [ - { - "role": "system", - "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" - }, - { - role: "user", - content: `Старайся передать атмосферу уюта и безопасности. - Нарисуй картинку подходящую для такого события: ${prompt} - В картинке не должно быть текста, только изображение.`, - }, - ], - function_call: 'auto', - }); - // Получение изображения по идентификатору - const detectedImage = (0, gigachat_1.detectImage)(resp.choices[0]?.message.content ?? ''); - const image = await exports.llm.getImage(detectedImage?.uuid ?? ''); - // Возвращаем содержимое изображения - return image.content; -}; -exports.generatePicture = generatePicture; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts index febea0b..2544dd3 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -3,6 +3,7 @@ import { Agent } from 'node:https'; const httpsAgent = new Agent({ rejectUnauthorized: false, + timeout: 60000 }); export const llm = new GigaChat({ @@ -30,8 +31,19 @@ export const generatePicture = async (prompt: string) => { // Получение изображения по идентификатору const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); - const image = await llm.getImage(detectedImage?.uuid ?? ''); + + if (!detectedImage?.uuid) { + throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); + } + + const image = await llm.getImage(detectedImage.uuid); - // Возвращаем содержимое изображения - return image.content; + // Возвращаем содержимое изображения, убеждаясь что это Buffer + if (Buffer.isBuffer(image.content)) { + return image.content; + } else if (typeof image.content === 'string') { + return Buffer.from(image.content, 'binary'); + } else { + throw new Error('Unexpected image content type: ' + typeof image.content); + } } diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js new file mode 100644 index 0000000..25b5e3a --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -0,0 +1,124 @@ +const router = require('express').Router(); +const { moderationText } = require('./initiatives-ai-agents/moderation.ts'); +const { generatePicture } = require('./initiatives-ai-agents/picture.ts'); +const { getSupabaseClient } = require('./supabaseClient'); + +// Обработчик для модерации текста +router.post('/moderate', async (req, res) => { + try { + const { title, body } = req.body; + if (!title || !body) { + res.status(400).json({ error: 'Заголовок и текст обязательны' }); + return; + } + + console.log('Запрос на модерацию:', { title: title.substring(0, 50), body: body.substring(0, 100) }); + + const [comment, fixedText, isApproved] = await moderationText(title, body); + + console.log('Результат модерации получен:', { comment, fixedText: fixedText?.substring(0, 100), isApproved }); + + // Дополнительная проверка на стороне сервера + if (!isApproved && (!comment || comment.trim() === '')) { + console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); + } + + res.json({ + comment, + fixedText, + isApproved + }); + } catch (error) { + console.error('Error in moderation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); + } +}); + +// Обработчик для генерации изображений +router.post('/generate-image', async (req, res) => { + try { + const { prompt, userId } = req.body; + if (!prompt) { + res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); + return; + } + + // Генерируем изображение + const imageBuffer = await generatePicture(prompt); + + //console.log('Изображение получено, размер буфера:', imageBuffer?.length || 0, 'байт'); + if (!imageBuffer || imageBuffer.length === 0) { + res.status(500).json({ error: 'Получен пустой буфер изображения' }); + return; + } + + //console.log('Начинаем загрузку в Supabase Storage...'); + + // Получаем Supabase клиент и создаем имя файла + const supabase = getSupabaseClient(); + const timestamp = Date.now(); + const filename = `image_${userId || 'user'}_${timestamp}.jpg`; + + let uploadResult; + let retries = 0; + const maxRetries = 5; + + while (retries < maxRetries) { + try { + uploadResult = await supabase.storage + .from('images') + .upload(filename, imageBuffer, { + contentType: 'image/jpeg', + upsert: true + }); + + if (!uploadResult.error) { + break; // Успешная загрузка + } + + //console.warn(`Попытка загрузки ${retries + 1} неудачна:`, uploadResult.error); + retries++; + + if (retries < maxRetries) { + // Ждем перед повторной попыткой + await new Promise(resolve => setTimeout(resolve, 1000 * retries)); + } + } catch (error) { + //console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); + retries++; + + if (retries < maxRetries) { + // Ждем перед повторной попыткой + await new Promise(resolve => setTimeout(resolve, 1000 * retries)); + } else { + throw error; // Перебрасываем ошибку после всех попыток + } + } + } + + if (uploadResult?.error) { + //console.error('Supabase storage error after all retries:', uploadResult.error); + res.status(500).json({ error: 'Ошибка при сохранении изображения после нескольких попыток' }); + return; + } + + //console.log('Изображение успешно загружено в Supabase Storage:', filename); + + // Получаем публичный URL + const { data: urlData } = supabase.storage + .from('images') + .getPublicUrl(filename); + + res.json({ + success: true, + imageUrl: urlData.publicUrl, + imagePath: filename + }); + + } catch (error) { + //console.error('Error in image generation:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.ts b/server/routers/kfu-m-24-1/sber_mobile/moderate.ts deleted file mode 100644 index 332554a..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Router, Request, Response } from 'express'; -import { moderationText } from './initiatives-ai-agents/moderation'; -import { generatePicture } from './initiatives-ai-agents/picture'; - -const { getSupabaseClient } = require('./supabaseClient'); - -const router = Router(); - -// Обработчик для модерации текста -router.post('/moderate', async (req: Request, res: Response) => { - try { - const { title, body } = req.body; - if (!title || !body) { - res.status(400).json({ error: 'Заголовок и текст обязательны' }); - return; - } - - const [comment, fixedText, isApproved] = await moderationText(title, body); - res.json({ - comment, - fixedText, - isApproved - }); - } catch (error: any) { - console.error('Error in moderation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -// Обработчик для генерации изображений -router.post('/generate-image', async (req: Request, res: Response) => { - try { - const { prompt, userId } = req.body; - if (!prompt) { - res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); - return; - } - - // Получаем изображение - const imageBuffer = await generatePicture(prompt); - - // Получаем Supabase клиент - const supabase = getSupabaseClient(); - - // Генерируем уникальное имя файла - const timestamp = Date.now(); - const filename = `image_${userId || 'user'}_${timestamp}.jpg`; - - // Загружаем в Supabase - const { data, error } = await supabase.storage - .from('images') - .upload(filename, imageBuffer, { - contentType: 'image/jpeg', - upsert: true - }); - - if (error) { - res.status(500).json({ error: 'Ошибка при сохранении изображения' }); - return; - } - - // Получаем публичный URL изображения - const { data: urlData } = supabase.storage - .from('images') - .getPublicUrl(filename); - - res.json({ - success: true, - imageUrl: urlData.publicUrl, - imagePath: filename - }); - } catch (error: any) { - console.error('Error in image generation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -export default router; \ No newline at end of file From bde67dc7c310a1e542d2d378267ae9ef78b82a72 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 14 Jun 2025 10:30:12 +0300 Subject: [PATCH 057/147] fix socket server --- server/io.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server/io.ts b/server/io.ts index 4121b06..71833d6 100644 --- a/server/io.ts +++ b/server/io.ts @@ -5,14 +5,7 @@ let io = null export const setIo = (app) => { const server = createServer(app) - io = new Server(server, { - cors: { - origin: "*", - methods: ["GET", "POST"], - credentials: false - }, - transports: ['websocket', 'polling'] - }) + io = new Server(server, {}) return server } From 580651094f42183c44d45b919cd5260c5c1d591f Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 14 Jun 2025 13:36:06 +0300 Subject: [PATCH 058/147] remove websocket add polling --- server/index.ts | 18 +- .../routers/kfu-m-24-1/sber_mobile/index.js | 14 +- .../kfu-m-24-1/sber_mobile/polling-chat.js | 822 ++++++++++++++++++ .../kfu-m-24-1/sber_mobile/socket-chat.js | 457 ---------- .../kfu-m-24-1/sber_mobile/supabaseClient.js | 65 +- 5 files changed, 894 insertions(+), 482 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/polling-chat.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/socket-chat.js diff --git a/server/index.ts b/server/index.ts index d39f65f..7f1d913 100644 --- a/server/index.ts +++ b/server/index.ts @@ -21,6 +21,7 @@ import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' import { setIo } from './io' +const { createChatPollingRouter } = require('./routers/kfu-m-24-1/sber_mobile/polling-chat') export const app = express() @@ -64,8 +65,6 @@ const initServer = async () => { console.log('warming up 🔥') - const server = setIo(app) - const sess = { secret: "super-secret-key", resave: true, @@ -90,10 +89,18 @@ const initServer = async () => { ) app.use(root) + // Инициализация Polling для чата (после настройки middleware) + const { router: chatPollingRouter, chatHandler } = createChatPollingRouter(express) + + /** * Добавляйте сюда свои routers. */ app.use("/kfu-m-24-1", kfuM241Router) + + // Добавляем Polling роутер для чата + app.use("/kfu-m-24-1/sber_mobile", chatPollingRouter) + app.use("/epja-2024-1", epja20241Router) app.use("/v1/todo", todoRouter) app.use("/dogsitters-finder", dogsittersFinderRouter) @@ -109,9 +116,10 @@ const initServer = async () => { app.use(errorHandler) - server.listen(process.env.PORT ?? 8044, () => + // Создаем обычный HTTP сервер + const server = app.listen(process.env.PORT ?? 8044, () => { console.log(`🚀 Сервер запущен на http://localhost:${process.env.PORT ?? 8044}`) - ) + }) // Обработка сигналов завершения процесса process.on('SIGTERM', () => { @@ -145,6 +153,8 @@ const initServer = async () => { process.exit(1) }) }) + + return server } initServer().catch(console.error) diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 854576e..2fdc6bd 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,9 +15,6 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); -const {setIo, getIo } = require('../../../io'); -// Импорт обработчика чата -const { initializeChatSocket } = require('./socket-chat') module.exports = router; @@ -39,12 +36,5 @@ router.use('', userApartmentsRouter); router.use('', avatarRouter); router.use('', supportRouter); - - // Инициализация Socket.IO для чата - const io = getIo() - if (io) { - const chatHandler = initializeChatSocket(io) - // Сохраняем ссылку на chat handler для доступа из эндпоинтов - io.chatHandler = chatHandler - console.log('✅ Socket.IO для чата инициализирован') - } \ No newline at end of file + + diff --git a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js new file mode 100644 index 0000000..db528ca --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js @@ -0,0 +1,822 @@ +const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); + +class ChatPollingHandler { + constructor() { + this.connectedClients = new Map(); // user_id -> { user_info, chats: Set(), lastActivity: Date } + this.chatParticipants = new Map(); // chat_id -> Set(user_id) + this.userEventQueues = new Map(); // user_id -> [{id, event, data, timestamp}] + this.eventIdCounter = 0; + this.realtimeSubscription = null; + + // Инициализируем Supabase подписку с задержкой и проверками + this.initializeWithRetry(); + + // Очистка старых событий каждые 5 минут + setInterval(() => { + this.cleanupOldEvents(); + }, 5 * 60 * 1000); + } + + // Инициализация с повторными попытками + async initializeWithRetry() { + try { + // Сначала ждем завершения основной инициализации + await initializationPromise; + + this.setupRealtimeSubscription(); + this.testRealtimeConnection(); + return; + + } catch (error) { + console.log('❌ [Supabase] Основная инициализация неудачна, пробуем альтернативный подход'); + } + + // Если основная инициализация не удалась, используем повторные попытки + let attempts = 0; + const maxAttempts = 10; + const baseDelay = 2000; // 2 секунды + + while (attempts < maxAttempts) { + try { + attempts++; + + // Ждем перед попыткой + await new Promise(resolve => setTimeout(resolve, baseDelay * attempts)); + + // Проверяем готовность Supabase клиента + const supabase = getSupabaseClient(); + if (supabase) { + this.setupRealtimeSubscription(); + this.testRealtimeConnection(); + return; // Успех, выходим + } + } catch (error) { + console.log(`❌ [Supabase] Попытка #${attempts} неудачна:`, error.message); + + if (attempts === maxAttempts) { + console.error('❌ [Supabase] Все попытки инициализации исчерпаны'); + console.error('❌ [Supabase] Realtime подписка будет недоступна'); + return; + } + } + } + } + + // Аутентификация пользователя + async handleAuthentication(req, res) { + const { user_id, token } = req.body; + + if (!user_id) { + res.status(400).json({ error: 'user_id is required' }); + return; + } + + try { + // Проверяем пользователя в базе данных + const supabase = getSupabaseClient(); + const { data: userProfile, error } = await supabase + .from('user_profiles') + .select('*') + .eq('id', user_id) + .single(); + + if (error) { + console.log('❌ [Polling Server] Пользователь не найден:', error); + res.status(401).json({ error: 'User not found' }); + return; + } + + // Регистрируем пользователя + this.connectedClients.set(user_id, { + user_info: { + user_id, + profile: userProfile, + last_seen: new Date() + }, + chats: new Set(), + lastActivity: new Date() + }); + + // Создаем очередь событий для пользователя + if (!this.userEventQueues.has(user_id)) { + this.userEventQueues.set(user_id, []); + } + + // Добавляем событие аутентификации в очередь + this.addEventToQueue(user_id, 'authenticated', { + message: 'Successfully authenticated', + user: userProfile + }); + + res.json({ + success: true, + message: 'Successfully authenticated', + user: userProfile + }); + + } catch (error) { + console.error('❌ [Polling Server] Ошибка аутентификации:', error); + res.status(500).json({ error: 'Authentication failed' }); + } + } + + // Эндпоинт для получения событий (polling) + async handleGetEvents(req, res) { + try { + const { user_id, last_event_id } = req.query; + + if (!user_id) { + res.status(400).json({ error: 'user_id is required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // Обновляем время последней активности + client.lastActivity = new Date(); + + // Получаем очередь событий пользователя + const eventQueue = this.userEventQueues.get(user_id) || []; + + // Фильтруем события после last_event_id + const lastEventId = parseInt(last_event_id) || 0; + const newEvents = eventQueue.filter(event => event.id > lastEventId); + + res.json({ + success: true, + events: newEvents, + last_event_id: eventQueue.length > 0 ? Math.max(...eventQueue.map(e => e.id)) : lastEventId + }); + + } catch (error) { + console.error('❌ [Polling Server] Ошибка получения событий:', error); + res.status(500).json({ error: 'Failed to get events' }); + } + } + + // HTTP эндпоинт для присоединения к чату + async handleJoinChat(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // Проверяем, что чат существует и пользователь имеет доступ к нему + const supabase = getSupabaseClient(); + const { data: chat, error } = await supabase + .from('chats') + .select(` + *, + buildings ( + management_company_id, + apartments ( + apartment_residents ( + user_id + ) + ) + ) + `) + .eq('id', chat_id) + .single(); + + if (error || !chat) { + res.status(404).json({ error: 'Chat not found' }); + return; + } + + // Проверяем доступ пользователя к чату через квартиры в доме + const hasAccess = chat.buildings.apartments.some(apartment => + apartment.apartment_residents.some(resident => + resident.user_id === user_id + ) + ); + + if (!hasAccess) { + res.status(403).json({ error: 'Access denied to this chat' }); + return; + } + + // Добавляем пользователя в чат + client.chats.add(chat_id); + + if (!this.chatParticipants.has(chat_id)) { + this.chatParticipants.set(chat_id, new Set()); + } + this.chatParticipants.get(chat_id).add(user_id); + + // Добавляем событие присоединения в очередь пользователя + this.addEventToQueue(user_id, 'joined_chat', { + chat_id, + chat: chat, + message: 'Successfully joined chat' + }); + + // Уведомляем других участников о подключении + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_joined', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Joined chat successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to join chat' }); + } + } + + // HTTP эндпоинт для покидания чата + async handleLeaveChat(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // Удаляем пользователя из чата + client.chats.delete(chat_id); + + if (this.chatParticipants.has(chat_id)) { + this.chatParticipants.get(chat_id).delete(user_id); + + // Если чат пуст, удаляем его + if (this.chatParticipants.get(chat_id).size === 0) { + this.chatParticipants.delete(chat_id); + } + } + + // Уведомляем других участников об отключении + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Left chat successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to leave chat' }); + } + } + + // HTTP эндпоинт для отправки сообщения + async handleSendMessage(req, res) { + try { + const { user_id, chat_id, text } = req.body; + + if (!user_id || !chat_id || !text) { + res.status(400).json({ error: 'user_id, chat_id and text are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + // Сохраняем сообщение в базу данных + const supabase = getSupabaseClient(); + const { data: message, error } = await supabase + .from('messages') + .insert({ + chat_id, + user_id, + text + }) + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .single(); + + if (error) { + res.status(500).json({ error: 'Failed to save message' }); + return; + } + + // Отправляем сообщение всем участникам чата + this.broadcastToChat(chat_id, 'new_message', { + message, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Message sent successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send message' }); + } + } + + // HTTP эндпоинт для индикации печатания + async handleTypingStart(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_start', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send typing indicator' }); + } + } + + // HTTP эндпоинт для остановки индикации печатания + async handleTypingStop(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_stop', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send typing indicator' }); + } + } + + // Обработка отключения клиента + handleClientDisconnect(user_id) { + const client = this.connectedClients.get(user_id); + if (!client) return; + + // Удаляем пользователя из всех чатов + client.chats.forEach(chat_id => { + if (this.chatParticipants.has(chat_id)) { + this.chatParticipants.get(chat_id).delete(user_id); + + // Уведомляем других участников об отключении + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + // Если чат пуст, удаляем его + if (this.chatParticipants.get(chat_id).size === 0) { + this.chatParticipants.delete(chat_id); + } + } + }); + + // Удаляем клиента + this.connectedClients.delete(user_id); + } + + // Добавление события в очередь пользователя + addEventToQueue(user_id, event, data) { + if (!this.userEventQueues.has(user_id)) { + this.userEventQueues.set(user_id, []); + } + + const eventQueue = this.userEventQueues.get(user_id); + const eventId = ++this.eventIdCounter; + + eventQueue.push({ + id: eventId, + event, + data, + timestamp: new Date() + }); + + // Ограничиваем размер очереди (последние 100 событий) + if (eventQueue.length > 100) { + eventQueue.splice(0, eventQueue.length - 100); + } + } + + // Рассылка события всем участникам чата + broadcastToChat(chat_id, event, data) { + const participants = this.chatParticipants.get(chat_id); + if (!participants) return; + + participants.forEach(user_id => { + this.addEventToQueue(user_id, event, data); + }); + } + + // Рассылка события всем участникам чата кроме отправителя + broadcastToChatExcludeUser(chat_id, exclude_user_id, event, data) { + const participants = this.chatParticipants.get(chat_id); + if (!participants) return; + + participants.forEach(user_id => { + if (user_id !== exclude_user_id) { + this.addEventToQueue(user_id, event, data); + } + }); + } + + // Получение списка онлайн пользователей в чате + getOnlineUsersInChat(chat_id) { + const participants = this.chatParticipants.get(chat_id) || new Set(); + const onlineUsers = []; + const now = new Date(); + const ONLINE_THRESHOLD = 2 * 60 * 1000; // 2 минуты + + participants.forEach(user_id => { + const client = this.connectedClients.get(user_id); + if (client && (now - client.lastActivity) < ONLINE_THRESHOLD) { + onlineUsers.push(client.user_info.profile); + } + }); + + return onlineUsers; + } + + // Отправка системного сообщения в чат + async sendSystemMessage(chat_id, text) { + this.broadcastToChat(chat_id, 'system_message', { + chat_id, + text, + timestamp: new Date() + }); + } + + // Очистка старых событий + cleanupOldEvents() { + const now = new Date(); + const MAX_EVENT_AGE = 24 * 60 * 60 * 1000; // 24 часа + const INACTIVE_USER_THRESHOLD = 60 * 60 * 1000; // 1 час + + // Очищаем старые события + this.userEventQueues.forEach((eventQueue, user_id) => { + const filteredEvents = eventQueue.filter(event => + (now - event.timestamp) < MAX_EVENT_AGE + ); + + if (filteredEvents.length !== eventQueue.length) { + this.userEventQueues.set(user_id, filteredEvents); + } + }); + + // Удаляем неактивных пользователей + this.connectedClients.forEach((client, user_id) => { + if ((now - client.lastActivity) > INACTIVE_USER_THRESHOLD) { + this.handleClientDisconnect(user_id); + this.userEventQueues.delete(user_id); + } + }); + } + + // Тестирование Real-time подписки + async testRealtimeConnection() { + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + // Создаем тестовый канал для проверки подключения + const testChannel = supabase + .channel('test_connection') + .subscribe((status, error) => { + if (error) { + console.error('❌ [Supabase] Тестовый канал - ошибка:', error); + } + + if (status === 'SUBSCRIBED') { + // Отписываемся от тестового канала + setTimeout(() => { + testChannel.unsubscribe(); + }, 2000); + } + }); + + return true; + } catch (error) { + console.error('❌ [Supabase] Ошибка тестирования Realtime:', error); + return false; + } + } + + // Проверка статуса подписки + checkSubscriptionStatus() { + if (this.realtimeSubscription) { + return true; + } else { + return false; + } + } + + setupRealtimeSubscription() { + // Убираем setTimeout, вызываем сразу + this._doSetupRealtimeSubscription(); + } + + _doSetupRealtimeSubscription() { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + console.log('❌ [Supabase] Supabase клиент не найден'); + throw new Error('Supabase client not available'); + } + + // Подписываемся на изменения в таблице messages + const subscription = supabase + .channel('messages_changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const newMessage = payload.new; + if (!newMessage) { + return; + } + + if (!newMessage.chat_id) { + return; + } + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', newMessage.user_id) + .single(); + + if (profileError) { + console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); + } + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...newMessage, + user_profiles: userProfile || null + }; + + // Отправляем сообщение всем участникам чата + this.broadcastToChat(newMessage.chat_id, 'new_message', { + message: messageWithProfile, + timestamp: new Date() + }); + + } catch (callbackError) { + console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError); + } + } + ) + .subscribe((status, error) => { + if (error) { + console.error('❌ [Supabase] Ошибка подписки:', error); + } + + if (status === 'CHANNEL_ERROR') { + console.error('❌ [Supabase] Ошибка канала'); + } else if (status === 'TIMED_OUT') { + console.error('❌ [Supabase] Таймаут подписки'); + } + }); + + // Сохраняем ссылку на подписку для возможности отписки + this.realtimeSubscription = subscription; + + } catch (error) { + console.error('❌ [Supabase] Критическая ошибка при настройке подписки:', error); + throw error; // Пробрасываем ошибку для обработки в initializeWithRetry + } + } + + // Получение статистики подключений + getConnectionStats() { + return { + connectedClients: this.connectedClients.size, + activeChats: this.chatParticipants.size, + totalChatParticipants: Array.from(this.chatParticipants.values()) + .reduce((total, participants) => total + participants.size, 0), + totalEventQueues: this.userEventQueues.size, + totalEvents: Array.from(this.userEventQueues.values()) + .reduce((total, queue) => total + queue.length, 0) + }; + } +} + +// Функция для создания роутера с polling эндпоинтами +function createChatPollingRouter(express) { + const router = express.Router(); + const chatHandler = new ChatPollingHandler(); + + // CORS middleware для всех запросов + router.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Authorization'); + res.header('Access-Control-Allow-Credentials', 'true'); + + // Обрабатываем OPTIONS запросы + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + next(); + }); + + // Эндпоинт для аутентификации + router.post('/auth', (req, res) => { + chatHandler.handleAuthentication(req, res); + }); + + // Эндпоинт для получения событий (polling) + router.get('/events', (req, res) => { + chatHandler.handleGetEvents(req, res); + }); + + // HTTP эндпоинты для действий + router.post('/join-chat', (req, res) => { + chatHandler.handleJoinChat(req, res); + }); + + router.post('/leave-chat', (req, res) => { + chatHandler.handleLeaveChat(req, res); + }); + + router.post('/send-message', (req, res) => { + chatHandler.handleSendMessage(req, res); + }); + + router.post('/typing-start', (req, res) => { + chatHandler.handleTypingStart(req, res); + }); + + router.post('/typing-stop', (req, res) => { + chatHandler.handleTypingStop(req, res); + }); + + // Эндпоинт для получения онлайн пользователей в чате + router.get('/online-users/:chat_id', (req, res) => { + const { chat_id } = req.params; + const onlineUsers = chatHandler.getOnlineUsersInChat(chat_id); + res.json({ onlineUsers }); + }); + + // Эндпоинт для получения статистики + router.get('/stats', (req, res) => { + const stats = chatHandler.getConnectionStats(); + res.json(stats); + }); + + // Эндпоинт для проверки статуса Supabase подписки + router.get('/supabase-status', (req, res) => { + const isConnected = chatHandler.checkSubscriptionStatus(); + res.json({ + supabaseSubscriptionActive: isConnected, + subscriptionExists: !!chatHandler.realtimeSubscription, + subscriptionInfo: chatHandler.realtimeSubscription ? { + channel: chatHandler.realtimeSubscription.topic, + state: chatHandler.realtimeSubscription.state + } : null + }); + }); + + // Эндпоинт для принудительного переподключения к Supabase + router.post('/reconnect-supabase', (req, res) => { + try { + // Отписываемся от текущей подписки + if (chatHandler.realtimeSubscription) { + chatHandler.realtimeSubscription.unsubscribe(); + chatHandler.realtimeSubscription = null; + } + + // Создаем новую подписку + chatHandler.setupRealtimeSubscription(); + + res.json({ + success: true, + message: 'Reconnection initiated' + }); + } catch (error) { + console.error('❌ [Polling Server] Ошибка переподключения:', error); + res.status(500).json({ + success: false, + error: 'Reconnection failed', + details: error.message + }); + } + }); + + // Тестовый эндпоинт для создания сообщения в обход API + router.post('/test-message', async (req, res) => { + const { chat_id, user_id, text } = req.body; + + if (!chat_id || !user_id || !text) { + res.status(400).json({ error: 'chat_id, user_id и text обязательны' }); + return; + } + + try { + // Создаем тестовое событие напрямую + chatHandler.broadcastToChat(chat_id, 'new_message', { + message: { + id: `test_${Date.now()}`, + chat_id, + user_id, + text, + created_at: new Date().toISOString(), + user_profiles: { + id: user_id, + full_name: 'Test User', + avatar_url: null + } + }, + timestamp: new Date() + }); + + res.json({ + success: true, + message: 'Test message sent to polling clients' + }); + } catch (error) { + console.error('❌ [Polling Server] Ошибка отправки тестового сообщения:', error); + res.status(500).json({ + success: false, + error: 'Failed to send test message', + details: error.message + }); + } + }); + + return { router, chatHandler }; +} + +module.exports = { + ChatPollingHandler, + createChatPollingRouter +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js deleted file mode 100644 index 62102c5..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js +++ /dev/null @@ -1,457 +0,0 @@ -const { getSupabaseClient } = require('./supabaseClient'); - -class ChatSocketHandler { - constructor(io) { - this.io = io; - this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info - this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) - this.realtimeSubscription = null; // Ссылка на подписку для управления - - this.setupSocketHandlers(); - - try { - this.setupRealtimeSubscription(); // Добавляем Real-time подписки - } catch (error) { - // Ignore error - } - - // Запускаем тестирование через 2 секунды после инициализации - setTimeout(() => { - this.testRealtimeConnection(); - }, 2000); - - // Проверяем статус подписки через 5 секунд - setTimeout(() => { - this.checkSubscriptionStatus(); - }, 5000); - } - - setupSocketHandlers() { - this.io.on('connection', (socket) => { - // Аутентификация пользователя - socket.on('authenticate', async (data) => { - await this.handleAuthentication(socket, data); - }); - - // Присоединение к чату - socket.on('join_chat', async (data) => { - await this.handleJoinChat(socket, data); - }); - - // Покидание чата - socket.on('leave_chat', (data) => { - this.handleLeaveChat(socket, data); - }); - - // Отправка сообщения - socket.on('send_message', async (data) => { - await this.handleSendMessage(socket, data); - }); - - // Пользователь начал печатать - socket.on('typing_start', (data) => { - this.handleTypingStart(socket, data); - }); - - // Пользователь закончил печатать - socket.on('typing_stop', (data) => { - this.handleTypingStop(socket, data); - }); - - // Отключение пользователя - socket.on('disconnect', () => { - this.handleDisconnect(socket); - }); - }); - } - - async handleAuthentication(socket, data) { - try { - const { user_id, token } = data; - - if (!user_id) { - socket.emit('auth_error', { message: 'user_id is required' }); - return; - } - - // Получаем информацию о пользователе из базы данных - const supabase = getSupabaseClient(); - const { data: userProfile, error } = await supabase - .from('user_profiles') - .select('*') - .eq('id', user_id) - .single(); - - if (error) { - socket.emit('auth_error', { message: 'User not found' }); - return; - } - - // Сохраняем информацию о пользователе - this.onlineUsers.set(socket.id, { - user_id, - socket_id: socket.id, - profile: userProfile, - last_seen: new Date() - }); - - socket.user_id = user_id; - socket.emit('authenticated', { - message: 'Successfully authenticated', - user: userProfile - }); - } catch (error) { - socket.emit('auth_error', { message: 'Authentication failed' }); - } - } - - async handleJoinChat(socket, data) { - try { - const { chat_id } = data; - - if (!socket.user_id) { - socket.emit('error', { message: 'Not authenticated' }); - return; - } - - if (!chat_id) { - socket.emit('error', { message: 'chat_id is required' }); - return; - } - // Проверяем, что чат существует и пользователь имеет доступ к нему - const supabase = getSupabaseClient(); - const { data: chat, error } = await supabase - .from('chats') - .select(` - *, - buildings ( - management_company_id, - apartments ( - apartment_residents ( - user_id - ) - ) - ) - `) - .eq('id', chat_id) - .single(); - - if (error || !chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // Проверяем доступ пользователя к чату через квартиры в доме - const hasAccess = chat.buildings.apartments.some(apartment => - apartment.apartment_residents.some(resident => - resident.user_id === socket.user_id - ) - ); - - if (!hasAccess) { - socket.emit('error', { message: 'Access denied to this chat' }); - return; - } - // Добавляем сокет в комнату - socket.join(chat_id); - - // Обновляем список участников комнаты - if (!this.chatRooms.has(chat_id)) { - this.chatRooms.set(chat_id, new Set()); - } - - const participantsBefore = this.chatRooms.get(chat_id).size; - this.chatRooms.get(chat_id).add(socket.id); - const participantsAfter = this.chatRooms.get(chat_id).size; - - socket.emit('joined_chat', { - chat_id, - chat: chat, - message: 'Successfully joined chat' - }); - - // Уведомляем других участников о подключении - const userInfo = this.onlineUsers.get(socket.id); - - socket.to(chat_id).emit('user_joined', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } catch (error) { - socket.emit('error', { message: 'Failed to join chat' }); - } - } - - handleLeaveChat(socket, data) { - const { chat_id } = data; - - if (!chat_id) return; - - socket.leave(chat_id); - - // Удаляем из списка участников - if (this.chatRooms.has(chat_id)) { - this.chatRooms.get(chat_id).delete(socket.id); - - // Если комната пуста, удаляем её - if (this.chatRooms.get(chat_id).size === 0) { - this.chatRooms.delete(chat_id); - } - } - - // Уведомляем других участников об отключении - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_left', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - - } - - async handleSendMessage(socket, data) { - try { - const { chat_id, text } = data; - - if (!socket.user_id) { - socket.emit('error', { message: 'Not authenticated' }); - return; - } - - if (!chat_id || !text) { - socket.emit('error', { message: 'chat_id and text are required' }); - return; - } - - // Сохраняем сообщение в базу данных - const supabase = getSupabaseClient(); - const { data: message, error } = await supabase - .from('messages') - .insert({ - chat_id, - user_id: socket.user_id, - text - }) - .select(` - *, - user_profiles ( - id, - full_name, - avatar_url - ) - `) - .single(); - - if (error) { - socket.emit('error', { message: 'Failed to save message' }); - return; - } - - // Отправляем сообщение всем участникам чата - this.io.to(chat_id).emit('new_message', { - message, - timestamp: new Date() - }); - - } catch (error) { - socket.emit('error', { message: 'Failed to send message' }); - } - } - - handleTypingStart(socket, data) { - const { chat_id } = data; - - if (!socket.user_id || !chat_id) return; - - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_typing_start', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } - - handleTypingStop(socket, data) { - const { chat_id } = data; - - if (!socket.user_id || !chat_id) return; - - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_typing_stop', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } - - handleDisconnect(socket) { - - // Удаляем пользователя из всех комнат - this.chatRooms.forEach((participants, chat_id) => { - if (participants.has(socket.id)) { - participants.delete(socket.id); - - // Уведомляем других участников об отключении - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_left', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - // Если комната пуста, удаляем её - if (participants.size === 0) { - this.chatRooms.delete(chat_id); - } - } - }); - - // Удаляем пользователя из списка онлайн - this.onlineUsers.delete(socket.id); - } - - // Получение списка онлайн пользователей в чате - getOnlineUsersInChat(chat_id) { - const participants = this.chatRooms.get(chat_id) || new Set(); - const onlineUsers = []; - - participants.forEach(socketId => { - const userInfo = this.onlineUsers.get(socketId); - if (userInfo) { - onlineUsers.push(userInfo.profile); - } - }); - - return onlineUsers; - } - - // Отправка системного сообщения в чат - async sendSystemMessage(chat_id, text) { - this.io.to(chat_id).emit('system_message', { - chat_id, - text, - timestamp: new Date() - }); - } - - // Тестирование Real-time подписки - async testRealtimeConnection() { - try { - const supabase = getSupabaseClient(); - if (!supabase) { - return false; - } - - // Создаем тестовый канал для проверки подключения - const testChannel = supabase - .channel('test_connection') - .subscribe((status, error) => { - if (status === 'SUBSCRIBED') { - // Отписываемся от тестового канала - setTimeout(() => { - testChannel.unsubscribe(); - }, 2000); - } - }); - - return true; - } catch (error) { - return false; - } - } - - // Проверка статуса подписки - checkSubscriptionStatus() { - if (this.realtimeSubscription) { - return true; - } else { - return false; - } - } - - setupRealtimeSubscription() { - // Добавляем небольшую задержку, чтобы убедиться, что Supabase клиент инициализирован - setTimeout(() => { - this._doSetupRealtimeSubscription(); - }, 1000); - } - - _doSetupRealtimeSubscription() { - try { - const supabase = getSupabaseClient(); - - if (!supabase) { - return; - } - - // Подписываемся на изменения в таблице messages - const subscription = supabase - .channel('messages_changes') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'messages' - }, - async (payload) => { - try { - const newMessage = payload.new; - if (!newMessage) { - return; - } - - if (!newMessage.chat_id) { - return; - } - - // Получаем профиль пользователя - const { data: userProfile, error: profileError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', newMessage.user_id) - .single(); - - // Объединяем сообщение с профилем - const messageWithProfile = { - ...newMessage, - user_profiles: userProfile || null - }; - - // Проверяем, есть ли участники в чате - const chatRoomParticipants = this.chatRooms.get(newMessage.chat_id); - - // Отправляем сообщение через Socket.IO всем участникам чата - this.io.to(newMessage.chat_id).emit('new_message', { - message: messageWithProfile, - timestamp: new Date() - }); - } catch (callbackError) { - // Ignore error - } - } - ) - .subscribe(); - - // Сохраняем ссылку на подписку для возможности отписки - this.realtimeSubscription = subscription; - - } catch (error) { - // Ignore error - } - } -} - -// Функция инициализации Socket.IO для чатов -function initializeChatSocket(io) { - const chatHandler = new ChatSocketHandler(io); - - return chatHandler; -} - -module.exports = { - ChatSocketHandler, - initializeChatSocket -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 938cc18..0568afa 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -3,12 +3,30 @@ const { createClient } = require('@supabase/supabase-js'); const { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey } = require('./get-constants'); let supabase = null; +let initializationPromise = null; async function initSupabaseClient() { - const supabaseUrl = await getSupabaseUrl(); - const supabaseAnonKey = await getSupabaseKey(); - const supabaseServiceRoleKey = await getSupabaseServiceKey(); - supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + console.log('🔄 [Supabase Client] Начинаем инициализацию...'); + + try { + console.log('🔄 [Supabase Client] Получаем конфигурацию...'); + const supabaseUrl = await getSupabaseUrl(); + const supabaseAnonKey = await getSupabaseKey(); + const supabaseServiceRoleKey = await getSupabaseServiceKey(); + + + if (!supabaseUrl || !supabaseServiceRoleKey) { + throw new Error('Missing required Supabase configuration'); + } + + supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + + return supabase; + + } catch (error) { + console.error('❌ [Supabase Client] Ошибка инициализации:', error); + throw error; + } } function getSupabaseClient() { @@ -20,20 +38,49 @@ function getSupabaseClient() { // POST /refresh-supabase-client router.post('/refresh-supabase-client', async (req, res) => { -try { + try { await initSupabaseClient(); res.json({ success: true, message: 'Supabase client refreshed' }); -} catch (error) { + } catch (error) { + console.error('❌ [Supabase Client] Ошибка обновления:', error); res.status(500).json({ error: error.message }); -} + } +}); + +// GET /supabase-client-status +router.get('/supabase-client-status', (req, res) => { + console.log('🔍 [Supabase Client] Проверяем статус клиента...'); + + const isInitialized = !!supabase; + + res.json({ + initialized: isInitialized, + clientExists: !!supabase, + timestamp: new Date().toISOString() + }); }); // Инициализация клиента при старте -(async () => { +initializationPromise = (async () => { + try { await initSupabaseClient(); + } catch (error) { + console.error('❌ [Supabase Client] Ошибка инициализации при старте:', error); + // Планируем повторную попытку через 5 секунд + setTimeout(async () => { + try { + await initSupabaseClient(); + } catch (retryError) { + console.error('❌ [Supabase Client] Повторная инициализация неудачна:', retryError); + } + }, 5000); + } })(); module.exports = { getSupabaseClient, - supabaseRouter: router + initSupabaseClient, + supabaseRouter: router, + // Экспортируем промис инициализации для возможности ожидания + initializationPromise }; \ No newline at end of file From 45cafbee91a97187946da83aac15b58fa3542738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Sat, 14 Jun 2025 13:37:26 +0300 Subject: [PATCH 059/147] add requirements --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 0205535..7313728 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "gigachat": "^0.0.14", "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", + "langchain": "^0.3.7", "langchain-gigachat": "^0.0.11", "mongodb": "^6.12.0", "mongoose": "^8.9.2", From 07d35c45168e9614573a9d53e53952d164ef1da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Sat, 14 Jun 2025 14:45:31 +0300 Subject: [PATCH 060/147] add test endpoint --- server/routers/kfu-m-24-1/sber_mobile/cameras.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/routers/kfu-m-24-1/sber_mobile/cameras.js b/server/routers/kfu-m-24-1/sber_mobile/cameras.js index 1425b9f..a1a1e8e 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/cameras.js +++ b/server/routers/kfu-m-24-1/sber_mobile/cameras.js @@ -11,6 +11,10 @@ router.get('/cameras', async (req, res) => { res.json(data); }); +router.get('/creds', async (req, res) => { + res.json({data: process.env.GIGA_AUTH}); +}); + // Получить все камеры по квартире (через building_id) router.get('/cameras/by-apartment', async (req, res) => { const supabase = getSupabaseClient(); From bd0b11dc4a63b5fc2ec0090da2c184bbed461efc Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 14 Jun 2025 16:12:03 +0300 Subject: [PATCH 061/147] add chat moderation --- .../chat-ai-agent/chat-moderation.ts | 91 ++++++ .../chat-ai-agent/moderation-config.js | 18 ++ .../routers/kfu-m-24-1/sber_mobile/index.js | 24 ++ .../kfu-m-24-1/sber_mobile/messages.js | 285 +++++++++++++++++- .../kfu-m-24-1/sber_mobile/moderation.js | 118 ++++++++ .../kfu-m-24-1/sber_mobile/polling-chat.js | 212 +++++++++++++ 6 files changed, 743 insertions(+), 5 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js create mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderation.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts new file mode 100644 index 0000000..ce269b7 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts @@ -0,0 +1,91 @@ +import { Agent } from 'node:https'; +import { GigaChat } from "langchain-gigachat"; +import { z } from "zod"; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +const llm = new GigaChat({ + credentials: "MGIzODY1N2MtYzMwMS00N2I4LWI1YzQtM2U4NzAxZGI5NmMzOjJmNzcyYzBmLWU0NjUtNGNmZC1iMDM2LTRjNmY0N2JhNDdiOA==", + temperature: 0.2, + model: 'GigaChat-2', + httpsAgent, +}); + +// возвращаю комментарий + булево значение (удалять или нет) + финальный текст сообщения +const moderationLlm = llm.withStructuredOutput(z.object({ + comment: z.string(), + isApproved: z.boolean(), +}) as any) + +export const moderationText = async (title: string, body: string): Promise<[string, boolean, string]> => { + const startTime = Date.now(); + const messagePreview = body.length > 50 ? body.substring(0, 50) + '...' : body; + + console.log(`🤖 [AI Agent] Начинаем анализ сообщения: "${messagePreview}"`); + console.log(`🤖 [AI Agent] Длина сообщения: ${body.length} символов`); + console.log(`🤖 [AI Agent] Модель: GigaChat-2, Temperature: 0.2`); + + const prompt = ` + Ты модерируешь сообщения в чате. Твоя задача - проверить сообщение на нецензурную лексику, брань и неприемлемый контент. + + Сообщение: ${body} + + Твои задачи: + 1. Проверь сообщение на наличие нецензурной лексики, мата, ругательств и брани. + 2. Проверь на оскорбления, угрозы и агрессивное поведение. + 3. Проверь на спам и рекламу. + 4. Проверь на неприемлемый контент (дискриминация, экстремизм и т.д.). + + - Если сообщение не содержит запрещенного контента, оно одобряется (isApproved: true). + - Если сообщение содержит запрещенный контент, оно отклоняется (isApproved: false). + + Правила написания комментария: + - Если сообщение одобряется, оставь поле comment пустым. + - Если сообщение отклоняется, пиши комментарий со следующей формулировкой: + "Сообщение удалено. Причина: (укажи конкретную причину: нецензурная лексика, оскорбления, спам и т.д.)" + + ` + + try { + console.log(`🤖 [AI Agent] Отправляем запрос к GigaChat...`); + + const result = await moderationLlm.invoke(prompt); + + const processingTime = Date.now() - startTime; + console.log(`🤖 [AI Agent] Получен ответ от GigaChat за ${processingTime}мс`); + console.log(`🤖 [AI Agent] Результат анализа:`, { + isApproved: result.isApproved, + comment: result.comment || 'нет комментария', + hasComment: !!result.comment + }); + + // Дополнительная проверка + if(!result.isApproved && result.comment.trim() === '') { + console.log(`⚠️ [AI Agent] Сообщение отклонено, но комментарий пустой. Добавляем стандартный комментарий.`); + result.comment = 'Сообщение удалено. Причина: нарушение правил чата.' + } + + // Определяем итоговый текст сообщения + let finalMessage = body; + if (!result.isApproved) { + finalMessage = '[Удалено модератором]'; + console.log(`🚫 [AI Agent] Сообщение будет заменено на: "${finalMessage}"`); + } else { + console.log(`✅ [AI Agent] Сообщение одобрено, остается без изменений`); + } + + console.log(`🤖 [AI Agent] Анализ завершен. Общее время: ${Date.now() - startTime}мс`); + + return [result.comment, result.isApproved, finalMessage]; + + } catch (error) { + const processingTime = Date.now() - startTime; + console.error(`❌ [AI Agent] Ошибка при анализе сообщения (${processingTime}мс):`, error); + console.error(`❌ [AI Agent] Сообщение будет одобрено из-за ошибки модерации`); + + // В случае ошибки одобряем сообщение + return ['', true, body]; + } +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js new file mode 100644 index 0000000..eeab24b --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js @@ -0,0 +1,18 @@ +// Конфигурация системы модерации +const MODERATION_CONFIG = { + // Задержка перед запуском модерации (в миллисекундах) + MODERATION_DELAY: 3000, // 3 секунды + + // Включена ли система модерации + MODERATION_ENABLED: true, + + // Текст для замены заблокированных сообщений + BLOCKED_MESSAGE_TEXT: '[Удалено модератором]', + + // Логировать ли процесс модерации + ENABLE_MODERATION_LOGS: true +}; + +console.log(`⚙️ [Moderation Config] Конфигурация модерации загружена:`, MODERATION_CONFIG); + +module.exports = MODERATION_CONFIG; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 2fdc6bd..5e393c8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,4 +1,8 @@ const router = require('express').Router(); + +console.log(`🏗️ [Main Router] Загружаем основной роутер sber_mobile...`); +console.log(`🏗️ [Main Router] Время: ${new Date().toISOString()}`); + const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); const profileRouter = require('./profile'); @@ -8,7 +12,15 @@ const additionalServicesRouter = require('./additional_services'); const chatsRouter = require('./chats'); const camerasRouter = require('./cameras'); const ticketsRouter = require('./tickets'); + +console.log(`🏗️ [Main Router] Загружаем messagesRouter...`); const messagesRouter = require('./messages'); +console.log(`✅ [Main Router] messagesRouter загружен успешно`); + +console.log(`🏗️ [Main Router] Загружаем moderationRouter...`); +const moderationRouter = require('./moderation'); +console.log(`✅ [Main Router] moderationRouter загружен успешно`); + const utilityPaymentsRouter = require('./utility_payments'); const apartmentsRouter = require('./apartments'); const buildingsRouter = require('./buildings'); @@ -19,6 +31,8 @@ const supportRouter = require('./supportApi'); module.exports = router; +console.log(`🔗 [Main Router] Подключаем роутеры...`); + router.use('/auth', authRouter); router.use('/supabase', supabaseRouter); router.use('', profileRouter); @@ -28,7 +42,15 @@ router.use('', additionalServicesRouter); router.use('', chatsRouter); router.use('', camerasRouter); router.use('', ticketsRouter); + +console.log(`🔗 [Main Router] Подключаем messagesRouter...`); router.use('', messagesRouter); +console.log(`✅ [Main Router] messagesRouter подключен`); + +console.log(`🔗 [Main Router] Подключаем moderationRouter...`); +router.use('', moderationRouter); +console.log(`✅ [Main Router] moderationRouter подключен`); + router.use('', utilityPaymentsRouter); router.use('', apartmentsRouter); router.use('', buildingsRouter); @@ -36,5 +58,7 @@ router.use('', userApartmentsRouter); router.use('', avatarRouter); router.use('', supportRouter); +console.log(`🏗️ [Main Router] Все роутеры подключены успешно!`); + diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index f729679..25eb9fb 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -2,6 +2,89 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); const { getIo } = require('../../../io'); // Импортируем Socket.IO +console.log(`📦 [Messages Module] Загружаем модуль messages.js...`); +console.log(`📦 [Messages Module] Время загрузки: ${new Date().toISOString()}`); + +try { + const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации + console.log(`✅ [Messages Module] Функция модерации загружена успешно`); +} catch (error) { + console.error(`❌ [Messages Module] Ошибка загрузки функции модерации:`, error); +} + +try { + const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации + console.log(`✅ [Messages Module] Конфигурация модерации загружена:`, MODERATION_CONFIG); +} catch (error) { + console.error(`❌ [Messages Module] Ошибка загрузки конфигурации модерации:`, error); +} + +const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации +const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации + +console.log(`📦 [Messages Module] Модуль messages.js загружен полностью`); + +// Добавляем middleware для логирования всех запросов к messages роутеру + +// Тестовый эндпоинт для проверки работы роутера +router.get('/messages/test', (req, res) => { + console.log(`🧪 [Messages Test] Тестовый эндпоинт вызван`); + console.log(`🧪 [Messages Test] Время: ${new Date().toISOString()}`); + res.json({ + status: 'OK', + message: 'Messages router работает', + timestamp: new Date().toISOString(), + moderation_config: MODERATION_CONFIG + }); +}); + +// Тестовый эндпоинт для немедленной проверки AI агента +router.post('/messages/test-ai', async (req, res) => { + console.log(`🧪 [AI Test] === ТЕСТИРОВАНИЕ AI АГЕНТА ===`); + console.log(`🧪 [AI Test] Время: ${new Date().toISOString()}`); + + const { text } = req.body; + if (!text) { + return res.status(400).json({ error: 'text is required' }); + } + + console.log(`🧪 [AI Test] Тестируем текст: "${text}"`); + console.log(`🧪 [AI Test] Функция moderationText доступна: ${typeof moderationText}`); + + try { + console.log(`🧪 [AI Test] Вызываем AI агент напрямую...`); + const startTime = Date.now(); + const result = await moderationText('', text); + const endTime = Date.now(); + + console.log(`🧪 [AI Test] Результат от AI агента:`, result); + console.log(`🧪 [AI Test] Время выполнения: ${endTime - startTime}мс`); + + const [comment, isApproved, finalMessage] = result; + + res.json({ + success: true, + text: text, + comment: comment, + isApproved: isApproved, + finalMessage: finalMessage, + processingTime: endTime - startTime, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error(`❌ [AI Test] Ошибка тестирования AI агента:`, error); + console.error(`❌ [AI Test] Stack:`, error.stack); + + res.status(500).json({ + success: false, + error: error.message, + stack: error.stack, + timestamp: new Date().toISOString() + }); + } +}); + // Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { try { @@ -59,15 +142,40 @@ router.get('/messages', async (req, res) => { // Создать новое сообщение router.post('/messages', async (req, res) => { - const supabase = getSupabaseClient(); + console.log(`🚀 [Message Send] === ВХОД В POST /messages ЭНДПОИНТ ===`); + console.log(`🚀 [Message Send] Время входа: ${new Date().toISOString()}`); + console.log(`🚀 [Message Send] Request method: ${req.method}`); + console.log(`🚀 [Message Send] Request URL: ${req.originalUrl || req.url}`); + console.log(`🚀 [Message Send] Request body:`, JSON.stringify(req.body, null, 2)); + + console.log(`🔌 [Message Send] Получаем Supabase клиент...`); + let supabase; + try { + supabase = getSupabaseClient(); + console.log(`✅ [Message Send] Supabase клиент получен успешно`); + } catch (error) { + console.error(`❌ [Message Send] Ошибка получения Supabase клиента:`, error); + return res.status(500).json({ error: 'Database connection error' }); + } + const { chat_id, user_id, text } = req.body; + console.log(`📤 [Message Send] Получен запрос на отправку сообщения:`); + console.log(`📤 [Message Send] Chat ID: ${chat_id}`); + console.log(`📤 [Message Send] User ID: ${user_id}`); + console.log(`📤 [Message Send] Text length: ${text ? text.length : 0} символов`); + console.log(`📤 [Message Send] Text preview: "${text ? (text.length > 100 ? text.substring(0, 100) + '...' : text) : 'empty'}"`); + if (!chat_id || !user_id || !text) { + console.log(`❌ [Message Send] Отклонен: отсутствуют обязательные поля`); + console.log(`❌ [Message Send] chat_id: ${chat_id}, user_id: ${user_id}, text: ${text}`); return res.status(400).json({ error: 'chat_id, user_id, and text are required' }); } + console.log(`💾 [Message Send] Сохраняем сообщение в Supabase...`); + // Создаем сообщение const { data: newMessage, error } = await supabase .from('messages') @@ -75,14 +183,28 @@ router.post('/messages', async (req, res) => { .select('*') .single(); - if (error) return res.status(400).json({ error: error.message }); + if (error) { + console.error(`❌ [Message Send] Ошибка сохранения в Supabase:`, error); + return res.status(400).json({ error: error.message }); + } + + console.log(`✅ [Message Send] Сообщение сохранено. ID: ${newMessage.id}`); + console.log(`📅 [Message Send] Время создания: ${newMessage.created_at}`); + + console.log(`👤 [Message Send] Получаем профиль пользователя ${user_id}...`); // Получаем профиль пользователя - const { data: userProfile } = await supabase + const { data: userProfile, error: profileError } = await supabase .from('user_profiles') .select('id, full_name, avatar_url') .eq('id', user_id) .single(); + + if (profileError) { + console.log(`⚠️ [Message Send] Профиль пользователя не найден:`, profileError); + } else { + console.log(`✅ [Message Send] Профиль пользователя получен: ${userProfile.full_name || 'No name'}`); + } // Объединяем сообщение с профилем const data = { @@ -90,12 +212,167 @@ router.post('/messages', async (req, res) => { user_profiles: userProfile || null }; + console.log(`📊 [Message Send] Итоговые данные сообщения подготовлены`); + + // === МОДЕРАЦИЯ ЧЕРЕЗ SUPABASE REAL-TIME === + console.log(`🔄 [Message Send] Модерация будет выполняться через Supabase Real-time подписку`); + console.log(`🔄 [Message Send] Статус модерации: ${MODERATION_CONFIG.MODERATION_ENABLED ? 'включена' : 'отключена'}`); + console.log(`🔄 [Message Send] Задержка модерации: ${MODERATION_CONFIG.MODERATION_DELAY}мс`); + console.log(`🔄 [Message Send] После создания сообщения в БД сработает Supabase Real-time подписка в polling-chat.js`); + // Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку // Это предотвращает дублирование сообщений + + console.log(`✅ [Message Send] Сообщение успешно отправлено. Возвращаем ответ клиенту`); + console.log(`📤 [Message Send] === Процесс отправки сообщения завершен ===`); res.json(data); }); +// Функция отложенной модерации сообщения +async function moderateMessage(messageId, messageText, chatId) { + const moderationStartTime = Date.now(); + + try { + console.log(`🔍 [Moderation] === НАЧАЛО МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.log(`🔍 [Moderation] Chat ID: ${chatId}`); + console.log(`🔍 [Moderation] Длина текста: ${messageText.length} символов`); + console.log(`🔍 [Moderation] Превью текста: "${messageText.length > 100 ? messageText.substring(0, 100) + '...' : messageText}"`); + console.log(`🔍 [Moderation] Время запуска: ${new Date().toISOString()}`); + + // Вызываем функцию модерации + console.log(`🔍 [Moderation] Передаем сообщение AI агенту для анализа...`); + console.log(`🔍 [Moderation] Функция moderationText доступна: ${typeof moderationText}`); + console.log(`🔍 [Moderation] Тип сообщения: ${typeof messageText}`); + console.log(`🔍 [Moderation] Текст сообщения: "${messageText}"`); + + let comment, isApproved, finalMessage; + try { + const result = await moderationText('', messageText); + console.log(`🔍 [Moderation] Результат от AI агента получен:`, result); + [comment, isApproved, finalMessage] = result; + console.log(`🔍 [Moderation] Распакованные значения: comment="${comment}", isApproved=${isApproved}, finalMessage="${finalMessage}"`); + } catch (moderationError) { + console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError); + console.error(`❌ [Moderation] Stack trace:`, moderationError.stack); + // В случае ошибки одобряем сообщение + comment = ''; + isApproved = true; + finalMessage = messageText; + console.log(`⚠️ [Moderation] Используем fallback значения из-за ошибки`); + } + + const moderationTime = Date.now() - moderationStartTime; + console.log(`📝 [Moderation] === РЕЗУЛЬТАТ МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.log(`📝 [Moderation] Время модерации: ${moderationTime}мс`); + console.log(`📝 [Moderation] Решение: ${isApproved ? '✅ ОДОБРЕНО' : '❌ ОТКЛОНЕНО'}`); + console.log(`📝 [Moderation] Комментарий: "${comment || 'отсутствует'}"`); + console.log(`📝 [Moderation] Финальный текст: "${finalMessage}"`); + + if (isApproved) { + console.log(`📝 [Moderation] Действие: сообщение остается без изменений`); + } else { + console.log(`📝 [Moderation] Действие: сообщение будет заменено в базе данных`); + } + + // Если сообщение не прошло модерацию, обновляем его в базе данных + if (!isApproved) { + console.log(`💾 [Moderation] Начинаем обновление сообщения в базе данных...`); + + const supabase = getSupabaseClient(); + + // Сначала получаем информацию о сообщении для получения chat_id + console.log(`💾 [Moderation] Получаем данные сообщения из базы...`); + const { data: messageData, error: fetchError } = await supabase + .from('messages') + .select('chat_id, user_id') + .eq('id', messageId) + .single(); + + if (fetchError) { + console.error(`❌ [Moderation] Ошибка получения данных сообщения ${messageId}:`, fetchError); + return; + } + + console.log(`💾 [Moderation] Данные получены. Chat ID: ${messageData.chat_id}, User ID: ${messageData.user_id}`); + + // Обновляем текст сообщения + console.log(`💾 [Moderation] Обновляем текст сообщения на: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}"`); + const { data: updatedMessage, error } = await supabase + .from('messages') + .update({ text: MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT }) + .eq('id', messageId) + .select('*') + .single(); + + if (error) { + console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error); + console.error(`❌ [Moderation] Детали ошибки:`, error); + } else { + console.log(`✅ [Moderation] Сообщение ${messageId} успешно обновлено в базе данных`); + console.log(`✅ [Moderation] Старый текст заменен на: "${updatedMessage.text}"`); + console.log(`✅ [Moderation] Время обновления: ${updatedMessage.updated_at || 'не указано'}`); + + // Отправляем обновление через Socket.IO всем клиентам в чате + console.log(`📡 [Moderation] Начинаем отправку обновления через Socket.IO...`); + try { + const io = getIo(); + if (io) { + console.log(`📡 [Moderation] Socket.IO подключение активно`); + + // Получаем профиль пользователя для полной информации + console.log(`📡 [Moderation] Получаем профиль пользователя для обновления...`); + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', messageData.user_id) + .single(); + + if (profileError) { + console.log(`⚠️ [Moderation] Ошибка получения профиля пользователя:`, profileError); + } else { + console.log(`✅ [Moderation] Профиль пользователя получен: ${userProfile.full_name || 'No name'}`); + } + + const messageWithProfile = { + ...updatedMessage, + user_profiles: userProfile || null + }; + + console.log(`📡 [Moderation] Отправляем обновление в комнату chat_${messageData.chat_id}...`); + io.to(`chat_${messageData.chat_id}`).emit('message_updated', messageWithProfile); + console.log(`📤 [Moderation] ✅ Обновление сообщения отправлено в чат ${messageData.chat_id}`); + console.log(`📤 [Moderation] Событие: message_updated`); + console.log(`📤 [Moderation] Получатели: все участники чата ${messageData.chat_id}`); + } else { + console.log(`⚠️ [Moderation] Socket.IO подключение недоступно`); + } + } catch (socketError) { + console.error(`❌ [Moderation] Ошибка отправки через Socket.IO:`, socketError); + console.error(`❌ [Moderation] Детали ошибки Socket.IO:`, socketError.message); + } + } + } else { + console.log(`✅ [Moderation] Сообщение ${messageId} прошло модерацию - никаких действий не требуется`); + } + + const totalTime = Date.now() - moderationStartTime; + console.log(`🔍 [Moderation] === МОДЕРАЦИЯ СООБЩЕНИЯ ${messageId} ЗАВЕРШЕНА ===`); + console.log(`🔍 [Moderation] Общее время процесса: ${totalTime}мс`); + console.log(`🔍 [Moderation] Время завершения: ${new Date().toISOString()}`); + + } catch (error) { + const totalTime = Date.now() - moderationStartTime; + console.error(`❌ [Moderation] === ОШИБКА МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.error(`❌ [Moderation] Время до ошибки: ${totalTime}мс`); + console.error(`❌ [Moderation] Тип ошибки: ${error.name || 'Unknown'}`); + console.error(`❌ [Moderation] Сообщение ошибки: ${error.message || 'Unknown error'}`); + console.error(`❌ [Moderation] Полная ошибка:`, error); + console.error(`❌ [Moderation] Стек ошибки:`, error.stack); + console.error(`❌ [Moderation] === КОНЕЦ ОБРАБОТКИ ОШИБКИ ===`); + } +} + // Получить конкретное сообщение router.get('/messages/:message_id', async (req, res) => { const supabase = getSupabaseClient(); @@ -203,6 +480,4 @@ router.delete('/messages/:message_id', async (req, res) => { res.json({ success: true, message: 'Message deleted successfully' }); }); - - module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/moderation.js new file mode 100644 index 0000000..a3be652 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderation.js @@ -0,0 +1,118 @@ +const router = require('express').Router(); +const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); +const { moderationText } = require('./chat-ai-agent/chat-moderation'); + +// Получить текущие настройки модерации +router.get('/moderation/config', (req, res) => { + console.log(`⚙️ [Config] Запрос на получение конфигурации модерации`); + console.log(`⚙️ [Config] Текущие настройки:`, MODERATION_CONFIG); + res.json(MODERATION_CONFIG); +}); + +// Обновить настройки модерации +router.post('/moderation/config', (req, res) => { + console.log(`⚙️ [Config] === ОБНОВЛЕНИЕ КОНФИГУРАЦИИ МОДЕРАЦИИ ===`); + console.log(`⚙️ [Config] Время запроса: ${new Date().toISOString()}`); + console.log(`⚙️ [Config] Полученные данные:`, req.body); + + const oldConfig = { ...MODERATION_CONFIG }; + const { MODERATION_DELAY, MODERATION_ENABLED, BLOCKED_MESSAGE_TEXT, ENABLE_MODERATION_LOGS } = req.body; + + const changes = []; + + if (MODERATION_DELAY !== undefined) { + const newValue = parseInt(MODERATION_DELAY); + console.log(`⚙️ [Config] Обновляем MODERATION_DELAY: ${MODERATION_CONFIG.MODERATION_DELAY} -> ${newValue}`); + MODERATION_CONFIG.MODERATION_DELAY = newValue; + changes.push(`MODERATION_DELAY: ${oldConfig.MODERATION_DELAY} -> ${newValue}`); + } + if (MODERATION_ENABLED !== undefined) { + const newValue = Boolean(MODERATION_ENABLED); + console.log(`⚙️ [Config] Обновляем MODERATION_ENABLED: ${MODERATION_CONFIG.MODERATION_ENABLED} -> ${newValue}`); + MODERATION_CONFIG.MODERATION_ENABLED = newValue; + changes.push(`MODERATION_ENABLED: ${oldConfig.MODERATION_ENABLED} -> ${newValue}`); + } + if (BLOCKED_MESSAGE_TEXT !== undefined) { + const newValue = String(BLOCKED_MESSAGE_TEXT); + console.log(`⚙️ [Config] Обновляем BLOCKED_MESSAGE_TEXT: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`); + MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT = newValue; + changes.push(`BLOCKED_MESSAGE_TEXT: "${oldConfig.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`); + } + if (ENABLE_MODERATION_LOGS !== undefined) { + const newValue = Boolean(ENABLE_MODERATION_LOGS); + console.log(`⚙️ [Config] Обновляем ENABLE_MODERATION_LOGS: ${MODERATION_CONFIG.ENABLE_MODERATION_LOGS} -> ${newValue}`); + MODERATION_CONFIG.ENABLE_MODERATION_LOGS = newValue; + changes.push(`ENABLE_MODERATION_LOGS: ${oldConfig.ENABLE_MODERATION_LOGS} -> ${newValue}`); + } + + console.log(`⚙️ [Config] === ИТОГИ ОБНОВЛЕНИЯ ===`); + console.log(`⚙️ [Config] Количество изменений: ${changes.length}`); + if (changes.length > 0) { + changes.forEach((change, index) => { + console.log(`⚙️ [Config] ${index + 1}. ${change}`); + }); + } else { + console.log(`⚙️ [Config] Изменений не было внесено`); + } + console.log(`⚙️ [Config] Новая конфигурация:`, MODERATION_CONFIG); + console.log(`⚙️ [Config] === ОБНОВЛЕНИЕ ЗАВЕРШЕНО ===`); + + res.json({ + success: true, + message: 'Настройки модерации обновлены', + changes: changes, + config: MODERATION_CONFIG + }); +}); + +// Тестовый эндпоинт для проверки модерации +router.post('/moderation/test', async (req, res) => { + const testStartTime = Date.now(); + + try { + const { text } = req.body; + + console.log(`🧪 [Moderation Test] === НАЧАЛО ТЕСТИРОВАНИЯ МОДЕРАЦИИ ===`); + console.log(`🧪 [Moderation Test] Время запуска: ${new Date().toISOString()}`); + console.log(`🧪 [Moderation Test] Текст для тестирования: "${text ? (text.length > 100 ? text.substring(0, 100) + '...' : text) : 'не указан'}"`); + console.log(`🧪 [Moderation Test] Длина текста: ${text ? text.length : 0} символов`); + + if (!text) { + console.log(`❌ [Moderation Test] Отклонен: текст не предоставлен`); + return res.status(400).json({ error: 'text is required' }); + } + + console.log(`🧪 [Moderation Test] Отправляем текст на модерацию...`); + const [comment, isApproved, finalMessage] = await moderationText('', text); + + const testTime = Date.now() - testStartTime; + console.log(`🧪 [Moderation Test] === РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ ===`); + console.log(`🧪 [Moderation Test] Время тестирования: ${testTime}мс`); + console.log(`🧪 [Moderation Test] Результат: ${isApproved ? '✅ ОДОБРЕНО' : '❌ ОТКЛОНЕНО'}`); + console.log(`🧪 [Moderation Test] Комментарий: "${comment || 'отсутствует'}"`); + console.log(`🧪 [Moderation Test] Финальное сообщение: "${finalMessage}"`); + console.log(`🧪 [Moderation Test] === ТЕСТИРОВАНИЕ ЗАВЕРШЕНО ===`); + + res.json({ + original_text: text, + is_approved: isApproved, + comment: comment, + final_message: finalMessage, + processing_time_ms: testTime + }); + } catch (error) { + const testTime = Date.now() - testStartTime; + console.error(`❌ [Moderation Test] === ОШИБКА ТЕСТИРОВАНИЯ ===`); + console.error(`❌ [Moderation Test] Время до ошибки: ${testTime}мс`); + console.error(`❌ [Moderation Test] Ошибка:`, error); + console.error(`❌ [Moderation Test] === КОНЕЦ ОБРАБОТКИ ОШИБКИ ===`); + + res.status(500).json({ + error: 'Moderation test failed', + details: error.message, + processing_time_ms: testTime + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js index db528ca..eb7c4df 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js +++ b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js @@ -1,4 +1,7 @@ const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); +const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); +const { moderationText } = require('./chat-ai-agent/chat-moderation'); +const { getIo } = require('../../../io'); class ChatPollingHandler { constructor() { @@ -146,6 +149,16 @@ class ChatPollingHandler { const lastEventId = parseInt(last_event_id) || 0; const newEvents = eventQueue.filter(event => event.id > lastEventId); + // Логируем отправку событий клиенту + if (newEvents.length > 0) { + console.log(`📨 [Polling Server] Отправляем ${newEvents.length} событий клиенту ${user_id}`); + newEvents.forEach(event => { + if (event.event === 'message_updated') { + console.log(`📨 [Polling Server] → Событие: ${event.event}, Сообщение ID: ${event.data?.message?.id}, Текст: "${event.data?.message?.text?.substring(0, 50)}${(event.data?.message?.text?.length || 0) > 50 ? '...' : ''}"`); + } + }); + } + res.json({ success: true, events: newEvents, @@ -605,6 +618,13 @@ class ChatPollingHandler { return; } + console.log(`📡 [Supabase Real-time] === ПОЛУЧЕНО НОВОЕ СООБЩЕНИЕ ===`); + console.log(`📡 [Supabase Real-time] ID сообщения: ${newMessage.id}`); + console.log(`📡 [Supabase Real-time] Чат ID: ${newMessage.chat_id}`); + console.log(`📡 [Supabase Real-time] Пользователь ID: ${newMessage.user_id}`); + console.log(`📡 [Supabase Real-time] Текст: "${newMessage.text?.substring(0, 100)}${(newMessage.text?.length || 0) > 100 ? '...' : ''}"`); + console.log(`📡 [Supabase Real-time] Время: ${new Date().toISOString()}`); + // Получаем профиль пользователя const { data: userProfile, error: profileError } = await supabase .from('user_profiles') @@ -614,6 +634,8 @@ class ChatPollingHandler { if (profileError) { console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); + } else { + console.log(`✅ [Supabase Real-time] Профиль пользователя получен: ${userProfile.full_name || 'No name'}`); } // Объединяем сообщение с профилем @@ -623,13 +645,100 @@ class ChatPollingHandler { }; // Отправляем сообщение всем участникам чата + console.log(`📤 [Supabase Real-time] Отправляем сообщение участникам чата ${newMessage.chat_id}...`); this.broadcastToChat(newMessage.chat_id, 'new_message', { message: messageWithProfile, timestamp: new Date() }); + console.log(`✅ [Supabase Real-time] Сообщение отправлено участникам чата`); + + // === ЗАПУСК МОДЕРАЦИИ === + if (MODERATION_CONFIG.MODERATION_ENABLED) { + console.log(`🛡️ [Supabase Real-time] Модерация включена - планируем проверку сообщения`); + console.log(`🛡️ [Supabase Real-time] Задержка модерации: ${MODERATION_CONFIG.MODERATION_DELAY}мс`); + + if (MODERATION_CONFIG.MODERATION_DELAY === 0) { + console.log(`⚡ [Supabase Real-time] Мгновенная модерация - запускаем setImmediate`); + setImmediate(() => { + console.log(`⚡ [Supabase Real-time] setImmediate: запускаем модерацию сообщения ${newMessage.id}`); + this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); + }); + } else { + console.log(`⏰ [Supabase Real-time] Отложенная модерация - устанавливаем setTimeout`); + const timeoutId = setTimeout(() => { + console.log(`⏰ [Supabase Real-time] setTimeout: время пришло, запускаем модерацию сообщения ${newMessage.id}`); + console.log(`⏰ [Supabase Real-time] Фактическое время: ${new Date().toISOString()}`); + this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); + }, MODERATION_CONFIG.MODERATION_DELAY); + + console.log(`⏰ [Supabase Real-time] Timeout ID: ${timeoutId}`); + console.log(`⏰ [Supabase Real-time] Ожидаемое время срабатывания: ${new Date(Date.now() + MODERATION_CONFIG.MODERATION_DELAY).toISOString()}`); + } + + console.log(`🛡️ [Supabase Real-time] Модерация запланирована для сообщения ${newMessage.id}`); + } else { + console.log(`🔓 [Supabase Real-time] Модерация отключена - сообщение не будет проверяться`); + } } catch (callbackError) { console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError); + console.error('❌ [Supabase] Stack trace:', callbackError.stack); + } + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const updatedMessage = payload.new; + if (!updatedMessage) { + return; + } + + if (!updatedMessage.chat_id) { + return; + } + + console.log(`🔄 [Supabase Real-time] === ПОЛУЧЕНО ОБНОВЛЕНИЕ СООБЩЕНИЯ ===`); + console.log(`🔄 [Supabase Real-time] ID сообщения: ${updatedMessage.id}`); + console.log(`🔄 [Supabase Real-time] Чат ID: ${updatedMessage.chat_id}`); + console.log(`🔄 [Supabase Real-time] Пользователь ID: ${updatedMessage.user_id}`); + console.log(`🔄 [Supabase Real-time] Обновленный текст: "${updatedMessage.text?.substring(0, 100)}${(updatedMessage.text?.length || 0) > 100 ? '...' : ''}"`); + console.log(`🔄 [Supabase Real-time] Время обновления: ${new Date().toISOString()}`); + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', updatedMessage.user_id) + .single(); + + if (profileError) { + console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); + } + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...updatedMessage, + user_profiles: userProfile || null + }; + + // Отправляем обновление всем участникам чата + console.log(`📤 [Supabase Real-time] Отправляем обновление участникам чата ${updatedMessage.chat_id}...`); + this.broadcastToChat(updatedMessage.chat_id, 'message_updated', { + message: messageWithProfile, + timestamp: new Date() + }); + console.log(`✅ [Supabase Real-time] Обновление отправлено участникам чата`); + console.log(`📊 [Supabase Real-time] Событие: message_updated`); + + } catch (callbackError) { + console.error('❌ [Supabase] Ошибка в обработчике обновления сообщения:', callbackError); } } ) @@ -654,6 +763,109 @@ class ChatPollingHandler { } } + // Функция отложенной модерации сообщения + async moderateMessage(messageId, messageText, chatId) { + const moderationStartTime = Date.now(); + + try { + console.log(`🔍 [Moderation] === НАЧАЛО МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.log(`🔍 [Moderation] Chat ID: ${chatId}`); + console.log(`🔍 [Moderation] Длина текста: ${messageText.length} символов`); + console.log(`🔍 [Moderation] Превью текста: "${messageText.length > 100 ? messageText.substring(0, 100) + '...' : messageText}"`); + console.log(`🔍 [Moderation] Время запуска: ${new Date().toISOString()}`); + + // Вызываем функцию модерации + console.log(`🔍 [Moderation] Передаем сообщение AI агенту для анализа...`); + console.log(`🔍 [Moderation] Функция moderationText доступна: ${typeof moderationText}`); + console.log(`🔍 [Moderation] Тип сообщения: ${typeof messageText}`); + console.log(`🔍 [Moderation] Текст сообщения: "${messageText}"`); + + let comment, isApproved, finalMessage; + try { + const result = await moderationText('', messageText); + console.log(`🔍 [Moderation] Результат от AI агента получен:`, result); + [comment, isApproved, finalMessage] = result; + console.log(`🔍 [Moderation] Распакованные значения: comment="${comment}", isApproved=${isApproved}, finalMessage="${finalMessage}"`); + } catch (moderationError) { + console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError); + console.error(`❌ [Moderation] Stack trace:`, moderationError.stack); + // В случае ошибки одобряем сообщение + comment = ''; + isApproved = true; + finalMessage = messageText; + console.log(`⚠️ [Moderation] Используем fallback значения из-за ошибки`); + } + + const moderationTime = Date.now() - moderationStartTime; + console.log(`📝 [Moderation] === РЕЗУЛЬТАТ МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.log(`📝 [Moderation] Время модерации: ${moderationTime}мс`); + console.log(`📝 [Moderation] Решение: ${isApproved ? '✅ ОДОБРЕНО' : '❌ ОТКЛОНЕНО'}`); + console.log(`📝 [Moderation] Комментарий: "${comment || 'отсутствует'}"`); + console.log(`📝 [Moderation] Финальный текст: "${finalMessage}"`); + + if (isApproved) { + console.log(`📝 [Moderation] Действие: сообщение остается без изменений`); + } else { + console.log(`📝 [Moderation] Действие: сообщение будет заменено в базе данных`); + } + + // Если сообщение не прошло модерацию, обновляем его в базе данных + if (!isApproved) { + console.log(`💾 [Moderation] Начинаем обновление сообщения в базе данных...`); + + const supabase = getSupabaseClient(); + + // Сначала получаем информацию о сообщении для получения chat_id + console.log(`💾 [Moderation] Получаем данные сообщения из базы...`); + const { data: messageData, error: fetchError } = await supabase + .from('messages') + .select('chat_id, user_id') + .eq('id', messageId) + .single(); + + if (fetchError) { + console.error(`❌ [Moderation] Ошибка получения данных сообщения ${messageId}:`, fetchError); + return; + } + + console.log(`💾 [Moderation] Данные получены. Chat ID: ${messageData.chat_id}, User ID: ${messageData.user_id}`); + + // Обновляем текст сообщения + console.log(`💾 [Moderation] Обновляем текст сообщения на: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}"`); + const { data: updatedMessage, error } = await supabase + .from('messages') + .update({ text: MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT }) + .eq('id', messageId) + .select('*') + .single(); + + if (error) { + console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error); + console.error(`❌ [Moderation] Детали ошибки:`, error); + } else { + console.log(`✅ [Moderation] Сообщение ${messageId} успешно обновлено в базе данных`); + console.log(`✅ [Moderation] Старый текст заменен на: "${updatedMessage.text}"`); + console.log(`✅ [Moderation] Время обновления: ${updatedMessage.updated_at || 'не указано'}`); + } + } else { + console.log(`✅ [Moderation] Сообщение ${messageId} прошло модерацию - никаких действий не требуется`); + } + + const totalTime = Date.now() - moderationStartTime; + console.log(`🔍 [Moderation] === МОДЕРАЦИЯ СООБЩЕНИЯ ${messageId} ЗАВЕРШЕНА ===`); + console.log(`🔍 [Moderation] Общее время процесса: ${totalTime}мс`); + console.log(`🔍 [Moderation] Время завершения: ${new Date().toISOString()}`); + + } catch (error) { + const totalTime = Date.now() - moderationStartTime; + console.error(`❌ [Moderation] === ОШИБКА МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); + console.error(`❌ [Moderation] Время до ошибки: ${totalTime}мс`); + console.error(`❌ [Moderation] Тип ошибки: ${error.name || 'Unknown'}`); + console.error(`❌ [Moderation] Сообщение ошибки: ${error.message || 'Unknown error'}`); + console.error(`❌ [Moderation] Stack trace:`, error.stack); + } + } + // Получение статистики подключений getConnectionStats() { return { From f37f34d8035fb4fd243de2f12c8c70e7eb6c736e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Sat, 14 Jun 2025 18:26:13 +0300 Subject: [PATCH 062/147] fix getting giga token --- .../kfu-m-24-1/sber_mobile/get-constants.js | 3 ++- .../sber_mobile/support-ai-agent/gigachat.ts | 18 ++++++++++-------- .../support-ai-agent/support-agent.ts | 3 ++- .../kfu-m-24-1/sber_mobile/supportApi.js | 9 ++++++--- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js index 0d7c355..47f34e8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js +++ b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js @@ -67,7 +67,8 @@ const getRagSupabaseUrl = async () => { module.exports = { getSupabaseUrl, getSupabaseKey, - getSupabaseServiceKey + getSupabaseServiceKey, + getGigaAuth }; // IIFE для установки переменных окружения diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts index e46c067..ab98ebb 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts @@ -1,18 +1,20 @@ import { Agent } from 'node:https'; import { GigaChat } from 'langchain-gigachat'; +import { getGigaAuth } from '../get-constants'; const httpsAgent = new Agent({ rejectUnauthorized: false, }); // Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js) -export const gigachat = new GigaChat({ - model: 'GigaChat-2', - temperature: 0.7, - scope: 'GIGACHAT_API_PERS', - streaming: false, - credentials: process.env.GIGA_AUTH, - httpsAgent -}); +export const gigachat = (GIGA_AUTH) => + new GigaChat({ + model: 'GigaChat-2', + temperature: 0.7, + scope: 'GIGACHAT_API_PERS', + streaming: false, + credentials: GIGA_AUTH, + httpsAgent + }); export default gigachat; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts index 93816e6..e8c5c68 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts @@ -10,6 +10,7 @@ import { CreateTicketTool } from './create-ticket-tool'; export interface SupportAgentConfig { temperature?: number; threadId?: string; + GIGA_AUTH?: string; } export interface SupportResponse { @@ -34,7 +35,7 @@ export class SupportAgent { this.memorySaver = new MemorySaver(); this.isFirstMessage = true; - this.llm = gigachat; + this.llm = gigachat(config.GIGA_AUTH); if (config.temperature !== undefined) { this.llm.temperature = config.temperature; } diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js index 1babe43..afb4ea8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js @@ -1,5 +1,6 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); +const { getGigaAuth } = require('./get-constants'); const { SupportAgent } = require('./support-ai-agent/support-agent'); // Хранилище агентов для разных пользователей @@ -8,11 +9,13 @@ const userAgents = new Map(); /** * Получить или создать агента для пользователя */ -function getUserAgent(userId) { +async function getUserAgent(userId) { if (!userAgents.has(userId)) { + const GIGA_AUTH = await getGigaAuth(); const config = { threadId: userId, - temperature: 0.7 + temperature: 0.7, + GIGA_AUTH }; userAgents.set(userId, new SupportAgent(config)); } @@ -74,7 +77,7 @@ router.post('/support', async (req, res) => { } // Получаем агента для пользователя - const agent = getUserAgent(user_id); + const agent = await getUserAgent(user_id); // Получаем ответ от AI-агента, передавая apartment_id const aiResponse = await agent.processMessage(message, apartment_id); From 825d7f1dd21337f8c7f2f89ccb9a0b954f5aca72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Sat, 14 Jun 2025 18:29:20 +0300 Subject: [PATCH 063/147] remove test api --- server/routers/kfu-m-24-1/sber_mobile/cameras.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/cameras.js b/server/routers/kfu-m-24-1/sber_mobile/cameras.js index a1a1e8e..1425b9f 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/cameras.js +++ b/server/routers/kfu-m-24-1/sber_mobile/cameras.js @@ -11,10 +11,6 @@ router.get('/cameras', async (req, res) => { res.json(data); }); -router.get('/creds', async (req, res) => { - res.json({data: process.env.GIGA_AUTH}); -}); - // Получить все камеры по квартире (через building_id) router.get('/cameras/by-apartment', async (req, res) => { const supabase = getSupabaseClient(); From 6e59e801b0246eaba21474802661528c226e9865 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 14 Jun 2025 19:29:48 +0300 Subject: [PATCH 064/147] add tickets data --- .../routers/kfu-m-24-1/sber_mobile/tickets.js | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/tickets.js b/server/routers/kfu-m-24-1/sber_mobile/tickets.js index 5ff082d..bfeceb8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/tickets.js +++ b/server/routers/kfu-m-24-1/sber_mobile/tickets.js @@ -1,14 +1,31 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить все тикеты по дому +// Получить заявки пользователя по квартире router.get('/tickets', async (req, res) => { const supabase = getSupabaseClient(); - const { building_id } = req.query; - if (!building_id) return res.status(400).json({ error: 'building_id required' }); - const { data, error } = await supabase.from('tickets').select('*').eq('building_id', building_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + const { user_id, apartment_id } = req.query; + + if (!user_id || !apartment_id) { + return res.status(400).json({ error: 'Требуется user_id и apartment_id' }); + } + + try { + const { data, error } = await supabase + .from('tickets') + .select('*') + .eq('user_id', user_id) + .eq('apartment_id', apartment_id) + .order('created_at', { ascending: false }); + + if (error) { + return res.status(400).json({ error: error.message }); + } + + res.json(data || []); + } catch (err) { + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } }); module.exports = router; \ No newline at end of file From 5665c4bf1e20bdbe5db0c1964929afc1a8a668ea Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Sat, 14 Jun 2025 23:35:48 +0300 Subject: [PATCH 065/147] code refactoring and agent improvement --- .../chat-ai-agent/chat-moderation.ts | 157 +++++------ .../sber_mobile/chat-ai-agent/gigachat.ts | 18 ++ .../chat-ai-agent/moderation-config.js | 4 +- .../kfu-m-24-1/sber_mobile/get-constants.js | 3 +- .../routers/kfu-m-24-1/sber_mobile/index.js | 12 - .../kfu-m-24-1/sber_mobile/messages.js | 248 ------------------ .../kfu-m-24-1/sber_mobile/moderation.js | 67 +---- .../kfu-m-24-1/sber_mobile/polling-chat.js | 86 ++---- .../kfu-m-24-1/sber_mobile/supabaseClient.js | 3 - 9 files changed, 111 insertions(+), 487 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts index ce269b7..b89f8d5 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts @@ -1,91 +1,78 @@ -import { Agent } from 'node:https'; -import { GigaChat } from "langchain-gigachat"; import { z } from "zod"; +import gigachat from './gigachat'; -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); +export interface ModerationResult { + comment: string; + isApproved: boolean; + success: boolean; + error?: string; +} -const llm = new GigaChat({ - credentials: "MGIzODY1N2MtYzMwMS00N2I4LWI1YzQtM2U4NzAxZGI5NmMzOjJmNzcyYzBmLWU0NjUtNGNmZC1iMDM2LTRjNmY0N2JhNDdiOA==", - temperature: 0.2, - model: 'GigaChat-2', - httpsAgent, -}); +export class ChatModerationAgent { + private moderationLlm: any; -// возвращаю комментарий + булево значение (удалять или нет) + финальный текст сообщения -const moderationLlm = llm.withStructuredOutput(z.object({ - comment: z.string(), - isApproved: z.boolean(), -}) as any) - -export const moderationText = async (title: string, body: string): Promise<[string, boolean, string]> => { - const startTime = Date.now(); - const messagePreview = body.length > 50 ? body.substring(0, 50) + '...' : body; - - console.log(`🤖 [AI Agent] Начинаем анализ сообщения: "${messagePreview}"`); - console.log(`🤖 [AI Agent] Длина сообщения: ${body.length} символов`); - console.log(`🤖 [AI Agent] Модель: GigaChat-2, Temperature: 0.2`); - - const prompt = ` - Ты модерируешь сообщения в чате. Твоя задача - проверить сообщение на нецензурную лексику, брань и неприемлемый контент. - - Сообщение: ${body} - - Твои задачи: - 1. Проверь сообщение на наличие нецензурной лексики, мата, ругательств и брани. - 2. Проверь на оскорбления, угрозы и агрессивное поведение. - 3. Проверь на спам и рекламу. - 4. Проверь на неприемлемый контент (дискриминация, экстремизм и т.д.). - - - Если сообщение не содержит запрещенного контента, оно одобряется (isApproved: true). - - Если сообщение содержит запрещенный контент, оно отклоняется (isApproved: false). - - Правила написания комментария: - - Если сообщение одобряется, оставь поле comment пустым. - - Если сообщение отклоняется, пиши комментарий со следующей формулировкой: - "Сообщение удалено. Причина: (укажи конкретную причину: нецензурная лексика, оскорбления, спам и т.д.)" - - ` - - try { - console.log(`🤖 [AI Agent] Отправляем запрос к GigaChat...`); - - const result = await moderationLlm.invoke(prompt); - - const processingTime = Date.now() - startTime; - console.log(`🤖 [AI Agent] Получен ответ от GigaChat за ${processingTime}мс`); - console.log(`🤖 [AI Agent] Результат анализа:`, { - isApproved: result.isApproved, - comment: result.comment || 'нет комментария', - hasComment: !!result.comment - }); - - // Дополнительная проверка - if(!result.isApproved && result.comment.trim() === '') { - console.log(`⚠️ [AI Agent] Сообщение отклонено, но комментарий пустой. Добавляем стандартный комментарий.`); - result.comment = 'Сообщение удалено. Причина: нарушение правил чата.' - } - - // Определяем итоговый текст сообщения - let finalMessage = body; - if (!result.isApproved) { - finalMessage = '[Удалено модератором]'; - console.log(`🚫 [AI Agent] Сообщение будет заменено на: "${finalMessage}"`); - } else { - console.log(`✅ [AI Agent] Сообщение одобрено, остается без изменений`); - } - - console.log(`🤖 [AI Agent] Анализ завершен. Общее время: ${Date.now() - startTime}мс`); - - return [result.comment, result.isApproved, finalMessage]; - - } catch (error) { - const processingTime = Date.now() - startTime; - console.error(`❌ [AI Agent] Ошибка при анализе сообщения (${processingTime}мс):`, error); - console.error(`❌ [AI Agent] Сообщение будет одобрено из-за ошибки модерации`); - - // В случае ошибки одобряем сообщение - return ['', true, body]; + constructor(GIGA_AUTH) { + // Создаем структурированный вывод для модерации + this.moderationLlm = gigachat(GIGA_AUTH).withStructuredOutput(z.object({ + comment: z.string(), + isApproved: z.boolean(), + }) as any); } + + private getSystemPrompt(): string { + return `Ты модерируешь сообщения в чате. Твоя задача - проверить сообщение на нецензурную лексику, брань и неприемлемый контент. + +Твои задачи: +1. Проверь сообщение на наличие нецензурной лексики, мата, ругательств и брани. +2. Проверь на оскорбления, угрозы и агрессивное поведение. +3. Проверь на спам и рекламу. +4. Проверь на неприемлемый контент (дискриминация, экстремизм и т.д.). + +- Если сообщение не содержит запрещенного контента, оно одобряется (isApproved: true). +- Если сообщение содержит запрещенный контент, оно отклоняется (isApproved: false). + +Правила написания комментария: +- Если сообщение одобряется, оставь поле comment пустым. +- Если сообщение отклоняется, пиши комментарий со следующей формулировкой: + "Сообщение удалено. Причина: (укажи конкретную причину: нецензурная лексика, оскорбления, спам и т.д.)"`; + } + + public async moderateMessage(message: string): Promise { + try { + const prompt = `${this.getSystemPrompt()} + +Сообщение: ${message}`; + + const result = await this.moderationLlm.invoke(prompt); + + // Дополнительная проверка + if (!result.isApproved && result.comment.trim() === '') { + result.comment = 'Сообщение удалено. Причина: нарушение правил чата.'; + } + + return { + comment: result.comment, + isApproved: result.isApproved, + success: true + }; + + } catch (error) { + console.error('❌ [Chat Moderation] Ошибка при модерации:', error); + + // В случае ошибки одобряем сообщение + return { + comment: '', + isApproved: true, + success: false, + error: error instanceof Error ? error.message : 'Неизвестная ошибка' + }; + } + } +} + +// Экспортируем функцию для обратной совместимости +export const moderationText = async (title: string, body: string, GIGA_AUTH): Promise<[string, boolean, string]> => { + const agent = new ChatModerationAgent(GIGA_AUTH); + const result = await agent.moderateMessage(body); + return [result.comment, result.isApproved, body]; }; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts new file mode 100644 index 0000000..609020a --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts @@ -0,0 +1,18 @@ +import { Agent } from 'node:https'; +import { GigaChat } from 'langchain-gigachat'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js) +export const gigachat = (GIGA_AUTH) => new + GigaChat({ + model: 'GigaChat-2', + scope: 'GIGACHAT_API_PERS', + streaming: false, + credentials: GIGA_AUTH, + httpsAgent +}); + +export default gigachat; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js index eeab24b..c4efec6 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js @@ -1,7 +1,7 @@ // Конфигурация системы модерации const MODERATION_CONFIG = { // Задержка перед запуском модерации (в миллисекундах) - MODERATION_DELAY: 3000, // 3 секунды + MODERATION_DELAY: 1500, // 1.5 секунды // Включена ли система модерации MODERATION_ENABLED: true, @@ -13,6 +13,4 @@ const MODERATION_CONFIG = { ENABLE_MODERATION_LOGS: true }; -console.log(`⚙️ [Moderation Config] Конфигурация модерации загружена:`, MODERATION_CONFIG); - module.exports = MODERATION_CONFIG; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js index 0d7c355..47f34e8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js +++ b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js @@ -67,7 +67,8 @@ const getRagSupabaseUrl = async () => { module.exports = { getSupabaseUrl, getSupabaseKey, - getSupabaseServiceKey + getSupabaseServiceKey, + getGigaAuth }; // IIFE для установки переменных окружения diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 5e393c8..653579f 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,7 +1,5 @@ const router = require('express').Router(); -console.log(`🏗️ [Main Router] Загружаем основной роутер sber_mobile...`); -console.log(`🏗️ [Main Router] Время: ${new Date().toISOString()}`); const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); @@ -13,13 +11,9 @@ const chatsRouter = require('./chats'); const camerasRouter = require('./cameras'); const ticketsRouter = require('./tickets'); -console.log(`🏗️ [Main Router] Загружаем messagesRouter...`); const messagesRouter = require('./messages'); -console.log(`✅ [Main Router] messagesRouter загружен успешно`); -console.log(`🏗️ [Main Router] Загружаем moderationRouter...`); const moderationRouter = require('./moderation'); -console.log(`✅ [Main Router] moderationRouter загружен успешно`); const utilityPaymentsRouter = require('./utility_payments'); const apartmentsRouter = require('./apartments'); @@ -31,7 +25,6 @@ const supportRouter = require('./supportApi'); module.exports = router; -console.log(`🔗 [Main Router] Подключаем роутеры...`); router.use('/auth', authRouter); router.use('/supabase', supabaseRouter); @@ -43,13 +36,9 @@ router.use('', chatsRouter); router.use('', camerasRouter); router.use('', ticketsRouter); -console.log(`🔗 [Main Router] Подключаем messagesRouter...`); router.use('', messagesRouter); -console.log(`✅ [Main Router] messagesRouter подключен`); -console.log(`🔗 [Main Router] Подключаем moderationRouter...`); router.use('', moderationRouter); -console.log(`✅ [Main Router] moderationRouter подключен`); router.use('', utilityPaymentsRouter); router.use('', apartmentsRouter); @@ -58,7 +47,6 @@ router.use('', userApartmentsRouter); router.use('', avatarRouter); router.use('', supportRouter); -console.log(`🏗️ [Main Router] Все роутеры подключены успешно!`); diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index 25eb9fb..a495e33 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,35 +1,13 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -const { getIo } = require('../../../io'); // Импортируем Socket.IO - -console.log(`📦 [Messages Module] Загружаем модуль messages.js...`); -console.log(`📦 [Messages Module] Время загрузки: ${new Date().toISOString()}`); - -try { - const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации - console.log(`✅ [Messages Module] Функция модерации загружена успешно`); -} catch (error) { - console.error(`❌ [Messages Module] Ошибка загрузки функции модерации:`, error); -} - -try { - const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации - console.log(`✅ [Messages Module] Конфигурация модерации загружена:`, MODERATION_CONFIG); -} catch (error) { - console.error(`❌ [Messages Module] Ошибка загрузки конфигурации модерации:`, error); -} - const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации -console.log(`📦 [Messages Module] Модуль messages.js загружен полностью`); // Добавляем middleware для логирования всех запросов к messages роутеру // Тестовый эндпоинт для проверки работы роутера router.get('/messages/test', (req, res) => { - console.log(`🧪 [Messages Test] Тестовый эндпоинт вызван`); - console.log(`🧪 [Messages Test] Время: ${new Date().toISOString()}`); res.json({ status: 'OK', message: 'Messages router работает', @@ -38,53 +16,6 @@ router.get('/messages/test', (req, res) => { }); }); -// Тестовый эндпоинт для немедленной проверки AI агента -router.post('/messages/test-ai', async (req, res) => { - console.log(`🧪 [AI Test] === ТЕСТИРОВАНИЕ AI АГЕНТА ===`); - console.log(`🧪 [AI Test] Время: ${new Date().toISOString()}`); - - const { text } = req.body; - if (!text) { - return res.status(400).json({ error: 'text is required' }); - } - - console.log(`🧪 [AI Test] Тестируем текст: "${text}"`); - console.log(`🧪 [AI Test] Функция moderationText доступна: ${typeof moderationText}`); - - try { - console.log(`🧪 [AI Test] Вызываем AI агент напрямую...`); - const startTime = Date.now(); - const result = await moderationText('', text); - const endTime = Date.now(); - - console.log(`🧪 [AI Test] Результат от AI агента:`, result); - console.log(`🧪 [AI Test] Время выполнения: ${endTime - startTime}мс`); - - const [comment, isApproved, finalMessage] = result; - - res.json({ - success: true, - text: text, - comment: comment, - isApproved: isApproved, - finalMessage: finalMessage, - processingTime: endTime - startTime, - timestamp: new Date().toISOString() - }); - - } catch (error) { - console.error(`❌ [AI Test] Ошибка тестирования AI агента:`, error); - console.error(`❌ [AI Test] Stack:`, error.stack); - - res.status(500).json({ - success: false, - error: error.message, - stack: error.stack, - timestamp: new Date().toISOString() - }); - } -}); - // Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { try { @@ -142,17 +73,10 @@ router.get('/messages', async (req, res) => { // Создать новое сообщение router.post('/messages', async (req, res) => { - console.log(`🚀 [Message Send] === ВХОД В POST /messages ЭНДПОИНТ ===`); - console.log(`🚀 [Message Send] Время входа: ${new Date().toISOString()}`); - console.log(`🚀 [Message Send] Request method: ${req.method}`); - console.log(`🚀 [Message Send] Request URL: ${req.originalUrl || req.url}`); - console.log(`🚀 [Message Send] Request body:`, JSON.stringify(req.body, null, 2)); - console.log(`🔌 [Message Send] Получаем Supabase клиент...`); let supabase; try { supabase = getSupabaseClient(); - console.log(`✅ [Message Send] Supabase клиент получен успешно`); } catch (error) { console.error(`❌ [Message Send] Ошибка получения Supabase клиента:`, error); return res.status(500).json({ error: 'Database connection error' }); @@ -160,11 +84,6 @@ router.post('/messages', async (req, res) => { const { chat_id, user_id, text } = req.body; - console.log(`📤 [Message Send] Получен запрос на отправку сообщения:`); - console.log(`📤 [Message Send] Chat ID: ${chat_id}`); - console.log(`📤 [Message Send] User ID: ${user_id}`); - console.log(`📤 [Message Send] Text length: ${text ? text.length : 0} символов`); - console.log(`📤 [Message Send] Text preview: "${text ? (text.length > 100 ? text.substring(0, 100) + '...' : text) : 'empty'}"`); if (!chat_id || !user_id || !text) { console.log(`❌ [Message Send] Отклонен: отсутствуют обязательные поля`); @@ -174,8 +93,6 @@ router.post('/messages', async (req, res) => { }); } - console.log(`💾 [Message Send] Сохраняем сообщение в Supabase...`); - // Создаем сообщение const { data: newMessage, error } = await supabase .from('messages') @@ -188,11 +105,6 @@ router.post('/messages', async (req, res) => { return res.status(400).json({ error: error.message }); } - console.log(`✅ [Message Send] Сообщение сохранено. ID: ${newMessage.id}`); - console.log(`📅 [Message Send] Время создания: ${newMessage.created_at}`); - - console.log(`👤 [Message Send] Получаем профиль пользователя ${user_id}...`); - // Получаем профиль пользователя const { data: userProfile, error: profileError } = await supabase .from('user_profiles') @@ -202,8 +114,6 @@ router.post('/messages', async (req, res) => { if (profileError) { console.log(`⚠️ [Message Send] Профиль пользователя не найден:`, profileError); - } else { - console.log(`✅ [Message Send] Профиль пользователя получен: ${userProfile.full_name || 'No name'}`); } // Объединяем сообщение с профилем @@ -211,168 +121,10 @@ router.post('/messages', async (req, res) => { ...newMessage, user_profiles: userProfile || null }; - - console.log(`📊 [Message Send] Итоговые данные сообщения подготовлены`); - - // === МОДЕРАЦИЯ ЧЕРЕЗ SUPABASE REAL-TIME === - console.log(`🔄 [Message Send] Модерация будет выполняться через Supabase Real-time подписку`); - console.log(`🔄 [Message Send] Статус модерации: ${MODERATION_CONFIG.MODERATION_ENABLED ? 'включена' : 'отключена'}`); - console.log(`🔄 [Message Send] Задержка модерации: ${MODERATION_CONFIG.MODERATION_DELAY}мс`); - console.log(`🔄 [Message Send] После создания сообщения в БД сработает Supabase Real-time подписка в polling-chat.js`); - - // Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку - // Это предотвращает дублирование сообщений - - console.log(`✅ [Message Send] Сообщение успешно отправлено. Возвращаем ответ клиенту`); - console.log(`📤 [Message Send] === Процесс отправки сообщения завершен ===`); res.json(data); }); -// Функция отложенной модерации сообщения -async function moderateMessage(messageId, messageText, chatId) { - const moderationStartTime = Date.now(); - - try { - console.log(`🔍 [Moderation] === НАЧАЛО МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); - console.log(`🔍 [Moderation] Chat ID: ${chatId}`); - console.log(`🔍 [Moderation] Длина текста: ${messageText.length} символов`); - console.log(`🔍 [Moderation] Превью текста: "${messageText.length > 100 ? messageText.substring(0, 100) + '...' : messageText}"`); - console.log(`🔍 [Moderation] Время запуска: ${new Date().toISOString()}`); - - // Вызываем функцию модерации - console.log(`🔍 [Moderation] Передаем сообщение AI агенту для анализа...`); - console.log(`🔍 [Moderation] Функция moderationText доступна: ${typeof moderationText}`); - console.log(`🔍 [Moderation] Тип сообщения: ${typeof messageText}`); - console.log(`🔍 [Moderation] Текст сообщения: "${messageText}"`); - - let comment, isApproved, finalMessage; - try { - const result = await moderationText('', messageText); - console.log(`🔍 [Moderation] Результат от AI агента получен:`, result); - [comment, isApproved, finalMessage] = result; - console.log(`🔍 [Moderation] Распакованные значения: comment="${comment}", isApproved=${isApproved}, finalMessage="${finalMessage}"`); - } catch (moderationError) { - console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError); - console.error(`❌ [Moderation] Stack trace:`, moderationError.stack); - // В случае ошибки одобряем сообщение - comment = ''; - isApproved = true; - finalMessage = messageText; - console.log(`⚠️ [Moderation] Используем fallback значения из-за ошибки`); - } - - const moderationTime = Date.now() - moderationStartTime; - console.log(`📝 [Moderation] === РЕЗУЛЬТАТ МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); - console.log(`📝 [Moderation] Время модерации: ${moderationTime}мс`); - console.log(`📝 [Moderation] Решение: ${isApproved ? '✅ ОДОБРЕНО' : '❌ ОТКЛОНЕНО'}`); - console.log(`📝 [Moderation] Комментарий: "${comment || 'отсутствует'}"`); - console.log(`📝 [Moderation] Финальный текст: "${finalMessage}"`); - - if (isApproved) { - console.log(`📝 [Moderation] Действие: сообщение остается без изменений`); - } else { - console.log(`📝 [Moderation] Действие: сообщение будет заменено в базе данных`); - } - - // Если сообщение не прошло модерацию, обновляем его в базе данных - if (!isApproved) { - console.log(`💾 [Moderation] Начинаем обновление сообщения в базе данных...`); - - const supabase = getSupabaseClient(); - - // Сначала получаем информацию о сообщении для получения chat_id - console.log(`💾 [Moderation] Получаем данные сообщения из базы...`); - const { data: messageData, error: fetchError } = await supabase - .from('messages') - .select('chat_id, user_id') - .eq('id', messageId) - .single(); - - if (fetchError) { - console.error(`❌ [Moderation] Ошибка получения данных сообщения ${messageId}:`, fetchError); - return; - } - - console.log(`💾 [Moderation] Данные получены. Chat ID: ${messageData.chat_id}, User ID: ${messageData.user_id}`); - - // Обновляем текст сообщения - console.log(`💾 [Moderation] Обновляем текст сообщения на: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}"`); - const { data: updatedMessage, error } = await supabase - .from('messages') - .update({ text: MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT }) - .eq('id', messageId) - .select('*') - .single(); - - if (error) { - console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error); - console.error(`❌ [Moderation] Детали ошибки:`, error); - } else { - console.log(`✅ [Moderation] Сообщение ${messageId} успешно обновлено в базе данных`); - console.log(`✅ [Moderation] Старый текст заменен на: "${updatedMessage.text}"`); - console.log(`✅ [Moderation] Время обновления: ${updatedMessage.updated_at || 'не указано'}`); - - // Отправляем обновление через Socket.IO всем клиентам в чате - console.log(`📡 [Moderation] Начинаем отправку обновления через Socket.IO...`); - try { - const io = getIo(); - if (io) { - console.log(`📡 [Moderation] Socket.IO подключение активно`); - - // Получаем профиль пользователя для полной информации - console.log(`📡 [Moderation] Получаем профиль пользователя для обновления...`); - const { data: userProfile, error: profileError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', messageData.user_id) - .single(); - - if (profileError) { - console.log(`⚠️ [Moderation] Ошибка получения профиля пользователя:`, profileError); - } else { - console.log(`✅ [Moderation] Профиль пользователя получен: ${userProfile.full_name || 'No name'}`); - } - - const messageWithProfile = { - ...updatedMessage, - user_profiles: userProfile || null - }; - - console.log(`📡 [Moderation] Отправляем обновление в комнату chat_${messageData.chat_id}...`); - io.to(`chat_${messageData.chat_id}`).emit('message_updated', messageWithProfile); - console.log(`📤 [Moderation] ✅ Обновление сообщения отправлено в чат ${messageData.chat_id}`); - console.log(`📤 [Moderation] Событие: message_updated`); - console.log(`📤 [Moderation] Получатели: все участники чата ${messageData.chat_id}`); - } else { - console.log(`⚠️ [Moderation] Socket.IO подключение недоступно`); - } - } catch (socketError) { - console.error(`❌ [Moderation] Ошибка отправки через Socket.IO:`, socketError); - console.error(`❌ [Moderation] Детали ошибки Socket.IO:`, socketError.message); - } - } - } else { - console.log(`✅ [Moderation] Сообщение ${messageId} прошло модерацию - никаких действий не требуется`); - } - - const totalTime = Date.now() - moderationStartTime; - console.log(`🔍 [Moderation] === МОДЕРАЦИЯ СООБЩЕНИЯ ${messageId} ЗАВЕРШЕНА ===`); - console.log(`🔍 [Moderation] Общее время процесса: ${totalTime}мс`); - console.log(`🔍 [Moderation] Время завершения: ${new Date().toISOString()}`); - - } catch (error) { - const totalTime = Date.now() - moderationStartTime; - console.error(`❌ [Moderation] === ОШИБКА МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); - console.error(`❌ [Moderation] Время до ошибки: ${totalTime}мс`); - console.error(`❌ [Moderation] Тип ошибки: ${error.name || 'Unknown'}`); - console.error(`❌ [Moderation] Сообщение ошибки: ${error.message || 'Unknown error'}`); - console.error(`❌ [Moderation] Полная ошибка:`, error); - console.error(`❌ [Moderation] Стек ошибки:`, error.stack); - console.error(`❌ [Moderation] === КОНЕЦ ОБРАБОТКИ ОШИБКИ ===`); - } -} - // Получить конкретное сообщение router.get('/messages/:message_id', async (req, res) => { const supabase = getSupabaseClient(); diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/moderation.js index a3be652..7f54af9 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderation.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderation.js @@ -4,16 +4,11 @@ const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Получить текущие настройки модерации router.get('/moderation/config', (req, res) => { - console.log(`⚙️ [Config] Запрос на получение конфигурации модерации`); - console.log(`⚙️ [Config] Текущие настройки:`, MODERATION_CONFIG); res.json(MODERATION_CONFIG); }); // Обновить настройки модерации router.post('/moderation/config', (req, res) => { - console.log(`⚙️ [Config] === ОБНОВЛЕНИЕ КОНФИГУРАЦИИ МОДЕРАЦИИ ===`); - console.log(`⚙️ [Config] Время запроса: ${new Date().toISOString()}`); - console.log(`⚙️ [Config] Полученные данные:`, req.body); const oldConfig = { ...MODERATION_CONFIG }; const { MODERATION_DELAY, MODERATION_ENABLED, BLOCKED_MESSAGE_TEXT, ENABLE_MODERATION_LOGS } = req.body; @@ -22,40 +17,30 @@ router.post('/moderation/config', (req, res) => { if (MODERATION_DELAY !== undefined) { const newValue = parseInt(MODERATION_DELAY); - console.log(`⚙️ [Config] Обновляем MODERATION_DELAY: ${MODERATION_CONFIG.MODERATION_DELAY} -> ${newValue}`); MODERATION_CONFIG.MODERATION_DELAY = newValue; changes.push(`MODERATION_DELAY: ${oldConfig.MODERATION_DELAY} -> ${newValue}`); } if (MODERATION_ENABLED !== undefined) { const newValue = Boolean(MODERATION_ENABLED); - console.log(`⚙️ [Config] Обновляем MODERATION_ENABLED: ${MODERATION_CONFIG.MODERATION_ENABLED} -> ${newValue}`); MODERATION_CONFIG.MODERATION_ENABLED = newValue; changes.push(`MODERATION_ENABLED: ${oldConfig.MODERATION_ENABLED} -> ${newValue}`); } if (BLOCKED_MESSAGE_TEXT !== undefined) { const newValue = String(BLOCKED_MESSAGE_TEXT); - console.log(`⚙️ [Config] Обновляем BLOCKED_MESSAGE_TEXT: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`); MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT = newValue; changes.push(`BLOCKED_MESSAGE_TEXT: "${oldConfig.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`); } if (ENABLE_MODERATION_LOGS !== undefined) { - const newValue = Boolean(ENABLE_MODERATION_LOGS); - console.log(`⚙️ [Config] Обновляем ENABLE_MODERATION_LOGS: ${MODERATION_CONFIG.ENABLE_MODERATION_LOGS} -> ${newValue}`); + const newValue = Boolean(ENABLE_MODERATION_LOGS) MODERATION_CONFIG.ENABLE_MODERATION_LOGS = newValue; changes.push(`ENABLE_MODERATION_LOGS: ${oldConfig.ENABLE_MODERATION_LOGS} -> ${newValue}`); } - console.log(`⚙️ [Config] === ИТОГИ ОБНОВЛЕНИЯ ===`); - console.log(`⚙️ [Config] Количество изменений: ${changes.length}`); if (changes.length > 0) { changes.forEach((change, index) => { - console.log(`⚙️ [Config] ${index + 1}. ${change}`); }); } else { - console.log(`⚙️ [Config] Изменений не было внесено`); } - console.log(`⚙️ [Config] Новая конфигурация:`, MODERATION_CONFIG); - console.log(`⚙️ [Config] === ОБНОВЛЕНИЕ ЗАВЕРШЕНО ===`); res.json({ success: true, @@ -65,54 +50,4 @@ router.post('/moderation/config', (req, res) => { }); }); -// Тестовый эндпоинт для проверки модерации -router.post('/moderation/test', async (req, res) => { - const testStartTime = Date.now(); - - try { - const { text } = req.body; - - console.log(`🧪 [Moderation Test] === НАЧАЛО ТЕСТИРОВАНИЯ МОДЕРАЦИИ ===`); - console.log(`🧪 [Moderation Test] Время запуска: ${new Date().toISOString()}`); - console.log(`🧪 [Moderation Test] Текст для тестирования: "${text ? (text.length > 100 ? text.substring(0, 100) + '...' : text) : 'не указан'}"`); - console.log(`🧪 [Moderation Test] Длина текста: ${text ? text.length : 0} символов`); - - if (!text) { - console.log(`❌ [Moderation Test] Отклонен: текст не предоставлен`); - return res.status(400).json({ error: 'text is required' }); - } - - console.log(`🧪 [Moderation Test] Отправляем текст на модерацию...`); - const [comment, isApproved, finalMessage] = await moderationText('', text); - - const testTime = Date.now() - testStartTime; - console.log(`🧪 [Moderation Test] === РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ ===`); - console.log(`🧪 [Moderation Test] Время тестирования: ${testTime}мс`); - console.log(`🧪 [Moderation Test] Результат: ${isApproved ? '✅ ОДОБРЕНО' : '❌ ОТКЛОНЕНО'}`); - console.log(`🧪 [Moderation Test] Комментарий: "${comment || 'отсутствует'}"`); - console.log(`🧪 [Moderation Test] Финальное сообщение: "${finalMessage}"`); - console.log(`🧪 [Moderation Test] === ТЕСТИРОВАНИЕ ЗАВЕРШЕНО ===`); - - res.json({ - original_text: text, - is_approved: isApproved, - comment: comment, - final_message: finalMessage, - processing_time_ms: testTime - }); - } catch (error) { - const testTime = Date.now() - testStartTime; - console.error(`❌ [Moderation Test] === ОШИБКА ТЕСТИРОВАНИЯ ===`); - console.error(`❌ [Moderation Test] Время до ошибки: ${testTime}мс`); - console.error(`❌ [Moderation Test] Ошибка:`, error); - console.error(`❌ [Moderation Test] === КОНЕЦ ОБРАБОТКИ ОШИБКИ ===`); - - res.status(500).json({ - error: 'Moderation test failed', - details: error.message, - processing_time_ms: testTime - }); - } -}); - module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js index eb7c4df..d33ead2 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js +++ b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js @@ -1,7 +1,12 @@ const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); +const { getGigaAuth } = require('./get-constants'); const { moderationText } = require('./chat-ai-agent/chat-moderation'); -const { getIo } = require('../../../io'); + +async function getGigaKey() { + const GIGA_AUTH = await getGigaAuth(); + return GIGA_AUTH; +} class ChatPollingHandler { constructor() { @@ -520,8 +525,8 @@ class ChatPollingHandler { // Очистка старых событий cleanupOldEvents() { const now = new Date(); - const MAX_EVENT_AGE = 24 * 60 * 60 * 1000; // 24 часа - const INACTIVE_USER_THRESHOLD = 60 * 60 * 1000; // 1 час + const MAX_EVENT_AGE = 1 * 60 * 60 * 1000; // 1 час + const INACTIVE_USER_THRESHOLD = 30 * 60 * 1000; // 30 минут // Очищаем старые события this.userEventQueues.forEach((eventQueue, user_id) => { @@ -618,13 +623,6 @@ class ChatPollingHandler { return; } - console.log(`📡 [Supabase Real-time] === ПОЛУЧЕНО НОВОЕ СООБЩЕНИЕ ===`); - console.log(`📡 [Supabase Real-time] ID сообщения: ${newMessage.id}`); - console.log(`📡 [Supabase Real-time] Чат ID: ${newMessage.chat_id}`); - console.log(`📡 [Supabase Real-time] Пользователь ID: ${newMessage.user_id}`); - console.log(`📡 [Supabase Real-time] Текст: "${newMessage.text?.substring(0, 100)}${(newMessage.text?.length || 0) > 100 ? '...' : ''}"`); - console.log(`📡 [Supabase Real-time] Время: ${new Date().toISOString()}`); - // Получаем профиль пользователя const { data: userProfile, error: profileError } = await supabase .from('user_profiles') @@ -634,9 +632,7 @@ class ChatPollingHandler { if (profileError) { console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); - } else { - console.log(`✅ [Supabase Real-time] Профиль пользователя получен: ${userProfile.full_name || 'No name'}`); - } + } // Объединяем сообщение с профилем const messageWithProfile = { @@ -644,40 +640,26 @@ class ChatPollingHandler { user_profiles: userProfile || null }; - // Отправляем сообщение всем участникам чата - console.log(`📤 [Supabase Real-time] Отправляем сообщение участникам чата ${newMessage.chat_id}...`); + // Отправляем сообщение всем участникам чат this.broadcastToChat(newMessage.chat_id, 'new_message', { message: messageWithProfile, timestamp: new Date() }); - console.log(`✅ [Supabase Real-time] Сообщение отправлено участникам чата`); // === ЗАПУСК МОДЕРАЦИИ === if (MODERATION_CONFIG.MODERATION_ENABLED) { - console.log(`🛡️ [Supabase Real-time] Модерация включена - планируем проверку сообщения`); - console.log(`🛡️ [Supabase Real-time] Задержка модерации: ${MODERATION_CONFIG.MODERATION_DELAY}мс`); if (MODERATION_CONFIG.MODERATION_DELAY === 0) { - console.log(`⚡ [Supabase Real-time] Мгновенная модерация - запускаем setImmediate`); setImmediate(() => { - console.log(`⚡ [Supabase Real-time] setImmediate: запускаем модерацию сообщения ${newMessage.id}`); this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); }); } else { - console.log(`⏰ [Supabase Real-time] Отложенная модерация - устанавливаем setTimeout`); const timeoutId = setTimeout(() => { - console.log(`⏰ [Supabase Real-time] setTimeout: время пришло, запускаем модерацию сообщения ${newMessage.id}`); - console.log(`⏰ [Supabase Real-time] Фактическое время: ${new Date().toISOString()}`); this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); }, MODERATION_CONFIG.MODERATION_DELAY); - console.log(`⏰ [Supabase Real-time] Timeout ID: ${timeoutId}`); - console.log(`⏰ [Supabase Real-time] Ожидаемое время срабатывания: ${new Date(Date.now() + MODERATION_CONFIG.MODERATION_DELAY).toISOString()}`); } - console.log(`🛡️ [Supabase Real-time] Модерация запланирована для сообщения ${newMessage.id}`); - } else { - console.log(`🔓 [Supabase Real-time] Модерация отключена - сообщение не будет проверяться`); } } catch (callbackError) { @@ -704,13 +686,6 @@ class ChatPollingHandler { return; } - console.log(`🔄 [Supabase Real-time] === ПОЛУЧЕНО ОБНОВЛЕНИЕ СООБЩЕНИЯ ===`); - console.log(`🔄 [Supabase Real-time] ID сообщения: ${updatedMessage.id}`); - console.log(`🔄 [Supabase Real-time] Чат ID: ${updatedMessage.chat_id}`); - console.log(`🔄 [Supabase Real-time] Пользователь ID: ${updatedMessage.user_id}`); - console.log(`🔄 [Supabase Real-time] Обновленный текст: "${updatedMessage.text?.substring(0, 100)}${(updatedMessage.text?.length || 0) > 100 ? '...' : ''}"`); - console.log(`🔄 [Supabase Real-time] Время обновления: ${new Date().toISOString()}`); - // Получаем профиль пользователя const { data: userProfile, error: profileError } = await supabase .from('user_profiles') @@ -728,14 +703,11 @@ class ChatPollingHandler { user_profiles: userProfile || null }; - // Отправляем обновление всем участникам чата - console.log(`📤 [Supabase Real-time] Отправляем обновление участникам чата ${updatedMessage.chat_id}...`); + // Отправляем обновление всем участникам чат this.broadcastToChat(updatedMessage.chat_id, 'message_updated', { message: messageWithProfile, timestamp: new Date() }); - console.log(`✅ [Supabase Real-time] Обновление отправлено участникам чата`); - console.log(`📊 [Supabase Real-time] Событие: message_updated`); } catch (callbackError) { console.error('❌ [Supabase] Ошибка в обработчике обновления сообщения:', callbackError); @@ -768,24 +740,15 @@ class ChatPollingHandler { const moderationStartTime = Date.now(); try { - console.log(`🔍 [Moderation] === НАЧАЛО МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); - console.log(`🔍 [Moderation] Chat ID: ${chatId}`); - console.log(`🔍 [Moderation] Длина текста: ${messageText.length} символов`); - console.log(`🔍 [Moderation] Превью текста: "${messageText.length > 100 ? messageText.substring(0, 100) + '...' : messageText}"`); - console.log(`🔍 [Moderation] Время запуска: ${new Date().toISOString()}`); // Вызываем функцию модерации - console.log(`🔍 [Moderation] Передаем сообщение AI агенту для анализа...`); - console.log(`🔍 [Moderation] Функция moderationText доступна: ${typeof moderationText}`); - console.log(`🔍 [Moderation] Тип сообщения: ${typeof messageText}`); - console.log(`🔍 [Moderation] Текст сообщения: "${messageText}"`); let comment, isApproved, finalMessage; + const GIGA_AUTH = await getGigaKey(); + console.log(GIGA_AUTH) try { - const result = await moderationText('', messageText); - console.log(`🔍 [Moderation] Результат от AI агента получен:`, result); + const result = await moderationText('', messageText, GIGA_AUTH); [comment, isApproved, finalMessage] = result; - console.log(`🔍 [Moderation] Распакованные значения: comment="${comment}", isApproved=${isApproved}, finalMessage="${finalMessage}"`); } catch (moderationError) { console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError); console.error(`❌ [Moderation] Stack trace:`, moderationError.stack); @@ -797,11 +760,6 @@ class ChatPollingHandler { } const moderationTime = Date.now() - moderationStartTime; - console.log(`📝 [Moderation] === РЕЗУЛЬТАТ МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); - console.log(`📝 [Moderation] Время модерации: ${moderationTime}мс`); - console.log(`📝 [Moderation] Решение: ${isApproved ? '✅ ОДОБРЕНО' : '❌ ОТКЛОНЕНО'}`); - console.log(`📝 [Moderation] Комментарий: "${comment || 'отсутствует'}"`); - console.log(`📝 [Moderation] Финальный текст: "${finalMessage}"`); if (isApproved) { console.log(`📝 [Moderation] Действие: сообщение остается без изменений`); @@ -842,19 +800,9 @@ class ChatPollingHandler { if (error) { console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error); console.error(`❌ [Moderation] Детали ошибки:`, error); - } else { - console.log(`✅ [Moderation] Сообщение ${messageId} успешно обновлено в базе данных`); - console.log(`✅ [Moderation] Старый текст заменен на: "${updatedMessage.text}"`); - console.log(`✅ [Moderation] Время обновления: ${updatedMessage.updated_at || 'не указано'}`); - } - } else { - console.log(`✅ [Moderation] Сообщение ${messageId} прошло модерацию - никаких действий не требуется`); - } - - const totalTime = Date.now() - moderationStartTime; - console.log(`🔍 [Moderation] === МОДЕРАЦИЯ СООБЩЕНИЯ ${messageId} ЗАВЕРШЕНА ===`); - console.log(`🔍 [Moderation] Общее время процесса: ${totalTime}мс`); - console.log(`🔍 [Moderation] Время завершения: ${new Date().toISOString()}`); + } + } + } catch (error) { const totalTime = Date.now() - moderationStartTime; diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 0568afa..2ddeb2f 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -6,10 +6,8 @@ let supabase = null; let initializationPromise = null; async function initSupabaseClient() { - console.log('🔄 [Supabase Client] Начинаем инициализацию...'); try { - console.log('🔄 [Supabase Client] Получаем конфигурацию...'); const supabaseUrl = await getSupabaseUrl(); const supabaseAnonKey = await getSupabaseKey(); const supabaseServiceRoleKey = await getSupabaseServiceKey(); @@ -49,7 +47,6 @@ router.post('/refresh-supabase-client', async (req, res) => { // GET /supabase-client-status router.get('/supabase-client-status', (req, res) => { - console.log('🔍 [Supabase Client] Проверяем статус клиента...'); const isInitialized = !!supabase; From 37238a13858664c7ffe34adb1f7afe6e507fc56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Sun, 15 Jun 2025 16:13:57 +0300 Subject: [PATCH 066/147] change moderate and initiatives --- .../sber_mobile/initiatives-ai-agents/llm.ts | 22 +++ .../initiatives-ai-agents/moderation.ts | 33 ++--- .../initiatives-ai-agents/picture.ts | 21 +-- .../kfu-m-24-1/sber_mobile/initiatives.js | 4 +- .../kfu-m-24-1/sber_mobile/moderate.js | 128 ++++++++++++------ 5 files changed, 125 insertions(+), 83 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts new file mode 100644 index 0000000..a5a0e23 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts @@ -0,0 +1,22 @@ +import { GigaChat as GigaChatLang} from 'langchain-gigachat'; +import { GigaChat } from 'gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +export const llm_mod = (GIGA_AUTH) => + new GigaChatLang({ + credentials: GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2-Max', + httpsAgent, +}); + +export const llm_gen = (GIGA_AUTH) => + new GigaChat({ + credentials: GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts index 7a34fdb..dc2022c 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -1,32 +1,23 @@ -import { Agent } from 'node:https'; -import { GigaChat } from "langchain-gigachat"; +import { llm_mod } from './llm' import { z } from "zod"; -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -const llm = new GigaChat({ - credentials: process.env.GIGA_AUTH, - temperature: 0.2, - model: 'GigaChat-2', - httpsAgent, -}); // возвращаю комментарий + исправленное предложение + булево значение -const moderationLlm = llm.withStructuredOutput(z.object({ - comment: z.string(), - fixedText: z.string().optional(), - isApproved: z.boolean(), -}) as any) -export const moderationText = async (title: string, body: string): Promise<[string, string | undefined, boolean]> => { +export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => { + + const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({ + comment: z.string(), + fixedText: z.string().optional(), + isApproved: z.boolean(), + }) as any) + const prompt = ` Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, не имеющая отношения к Управляющей компании). Заголовок: ${title} - Основной текст: ${body} + Основной текст: ${description} Твои задачи: 1. Проверь предложение и заголовок на спам. @@ -58,9 +49,9 @@ export const moderationText = async (title: string, body: string): Promise<[stri const result = await moderationLlm.invoke(prompt); console.log(result) // Дополнительная проверка - if(!result.isApproved && result.comment.trim() === '' && result.fixedText.trim() === '') { + if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', - result.fixedText = body + result.fixedText = description } return [result.comment, result.fixedText, result.isApproved]; diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts index 2544dd3..d216c5d 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -1,19 +1,8 @@ -import { GigaChat, detectImage } from 'gigachat'; -import { Agent } from 'node:https'; +import { llm_gen } from './llm' +import { detectImage } from 'gigachat'; -const httpsAgent = new Agent({ - rejectUnauthorized: false, - timeout: 60000 -}); - -export const llm = new GigaChat({ - credentials: process.env.GIGA_AUTH, - model: 'GigaChat-2', - httpsAgent, -}); - -export const generatePicture = async (prompt: string) => { - const resp = await llm.chat({ +export const generatePicture = async (prompt: string, GIGA_AUTH) => { + const resp = await llm_gen(GIGA_AUTH).chat({ messages: [ { "role": "system", @@ -36,7 +25,7 @@ export const generatePicture = async (prompt: string) => { throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); } - const image = await llm.getImage(detectedImage.uuid); + const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid); // Возвращаем содержимое изображения, убеждаясь что это Buffer if (Buffer.isBuffer(image.content)) { diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js index 3ca0562..42f82e4 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js @@ -38,9 +38,9 @@ router.get('/initiatives/:id', async (req, res) => { // Создать инициативу router.post('/initiatives', async (req, res) => { const supabase = getSupabaseClient(); - const { building_id, creator_id, title, description, status, target_amount, image_url } = req.body; + const { building_id, creator_id, title, description, status, target_amount, current_amount, image_url } = req.body; const { data, error } = await supabase.from('initiatives').insert([ - { building_id, creator_id, title, description, status, target_amount, image_url } + { building_id, creator_id, title, description, status, target_amount, current_amount: current_amount || 0, image_url } ]).select().single(); if (error) return res.status(400).json({ error: error.message }); res.json(data); diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js index 25b5e3a..0e8b3f8 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -2,63 +2,76 @@ const router = require('express').Router(); const { moderationText } = require('./initiatives-ai-agents/moderation.ts'); const { generatePicture } = require('./initiatives-ai-agents/picture.ts'); const { getSupabaseClient } = require('./supabaseClient'); +const { getGigaAuth } = require('./get-constants'); -// Обработчик для модерации текста +async function getGigaKey() { + const GIGA_AUTH = await getGigaAuth(); + return GIGA_AUTH; + } + +// Обработчик для модерации и создания инициативы router.post('/moderate', async (req, res) => { + + const GIGA_AUTH = await getGigaKey(); + try { - const { title, body } = req.body; - if (!title || !body) { - res.status(400).json({ error: 'Заголовок и текст обязательны' }); + const { title, description, building_id, creator_id, target_amount, status } = req.body; + + if (!title || !description) { + res.status(400).json({ error: 'Заголовок и описание обязательны' }); return; } - console.log('Запрос на модерацию:', { title: title.substring(0, 50), body: body.substring(0, 100) }); + if (!building_id || !creator_id) { + res.status(400).json({ error: 'ID дома и создателя обязательны' }); + return; + } - const [comment, fixedText, isApproved] = await moderationText(title, body); + // Валидация статуса, если передан + const validStatuses = ['moderation', 'review', 'fundraising', 'approved', 'rejected']; + if (status && !validStatuses.includes(status)) { + res.status(400).json({ error: `Недопустимый статус. Допустимые значения: ${validStatuses.join(', ')}` }); + return; + } + + console.log('Запрос на модерацию:', { title: title.substring(0, 50), description: description.substring(0, 100) }); + + // Модерация текста (передаем title и description как body) + const [comment, fixedText, isApproved] = await moderationText(title, description, GIGA_AUTH); console.log('Результат модерации получен:', { comment, fixedText: fixedText?.substring(0, 100), isApproved }); - // Дополнительная проверка на стороне сервера - if (!isApproved && (!comment || comment.trim() === '')) { - console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); - } - - res.json({ - comment, - fixedText, - isApproved - }); - } catch (error) { - console.error('Error in moderation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -// Обработчик для генерации изображений -router.post('/generate-image', async (req, res) => { - try { - const { prompt, userId } = req.body; - if (!prompt) { - res.status(400).json({ error: 'Необходимо указать запрос для генерации' }); + // Если модерация не прошла, возвращаем undefined + if (!isApproved) { + if (!comment || comment.trim() === '') { + console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); + } + + res.json({ + comment, + fixedText, + isApproved, + initiative: undefined + }); return; } - - // Генерируем изображение - const imageBuffer = await generatePicture(prompt); - //console.log('Изображение получено, размер буфера:', imageBuffer?.length || 0, 'байт'); + // Модерация прошла, генерируем изображение используя заголовок как промпт + console.log('Модерация прошла, генерируем изображение с промптом:', title); + + const imageBuffer = await generatePicture(title, GIGA_AUTH); + if (!imageBuffer || imageBuffer.length === 0) { res.status(500).json({ error: 'Получен пустой буфер изображения' }); return; } - //console.log('Начинаем загрузку в Supabase Storage...'); - // Получаем Supabase клиент и создаем имя файла const supabase = getSupabaseClient(); const timestamp = Date.now(); - const filename = `image_${userId || 'user'}_${timestamp}.jpg`; + const filename = `image_${creator_id}_${timestamp}.jpg`; + // Загружаем изображение в Supabase Storage let uploadResult; let retries = 0; const maxRetries = 5; @@ -76,7 +89,6 @@ router.post('/generate-image', async (req, res) => { break; // Успешная загрузка } - //console.warn(`Попытка загрузки ${retries + 1} неудачна:`, uploadResult.error); retries++; if (retries < maxRetries) { @@ -84,7 +96,7 @@ router.post('/generate-image', async (req, res) => { await new Promise(resolve => setTimeout(resolve, 1000 * retries)); } } catch (error) { - //console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); + console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); retries++; if (retries < maxRetries) { @@ -97,26 +109,54 @@ router.post('/generate-image', async (req, res) => { } if (uploadResult?.error) { - //console.error('Supabase storage error after all retries:', uploadResult.error); + console.error('Supabase storage error after all retries:', uploadResult.error); res.status(500).json({ error: 'Ошибка при сохранении изображения после нескольких попыток' }); return; } - //console.log('Изображение успешно загружено в Supabase Storage:', filename); + console.log('Изображение успешно загружено в Supabase Storage:', filename); // Получаем публичный URL const { data: urlData } = supabase.storage .from('images') .getPublicUrl(filename); - + + // Определяем статус: если передан в запросе, используем его, иначе 'review' + const finalStatus = status || 'review'; + + // Создаем инициативу в базе данных + const { data: initiative, error: initiativeError } = await supabase + .from('initiatives') + .insert([{ + building_id, + creator_id, + title: fixedText || title, + description, + status: finalStatus, + target_amount: target_amount || null, + current_amount: 0, + image_url: urlData.publicUrl + }]) + .select() + .single(); + + if (initiativeError) { + console.error('Ошибка создания инициативы:', initiativeError); + res.status(500).json({ error: 'Ошибка при создании инициативы', details: initiativeError.message }); + return; + } + + console.log('Инициатива успешно создана:', initiative.id); + res.json({ - success: true, - imageUrl: urlData.publicUrl, - imagePath: filename + comment, + fixedText, + isApproved, + initiative }); } catch (error) { - //console.error('Error in image generation:', error); + console.error('Error in moderation and initiative creation:', error); res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); } }); From 12f8e63390ec07078b97a1391ae85b7e1fe54929 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Jun 2025 17:36:41 +0300 Subject: [PATCH 067/147] fix api method --- .../routers/kfu-m-24-1/sber_mobile/additional_services.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/additional_services.js b/server/routers/kfu-m-24-1/sber_mobile/additional_services.js index 1861a60..70236e4 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/additional_services.js +++ b/server/routers/kfu-m-24-1/sber_mobile/additional_services.js @@ -1,13 +1,10 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить все дополнительные сервисы (по УК) +// Получить все дополнительные сервисы router.get('/additional-services', async (req, res) => { const supabase = getSupabaseClient(); - const { management_company_id } = req.query; - let query = supabase.from('additional_services').select('*'); - if (management_company_id) query = query.eq('management_company_id', management_company_id); - const { data, error } = await query; + const { data, error } = await supabase.from('additional_services').select('*').order('created_at', { ascending: false }); if (error) return res.status(400).json({ error: error.message }); res.json(data); }); From 3739fc844912c51ae937db183f79891ebb7068c2 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 15 Jun 2025 18:28:53 +0300 Subject: [PATCH 068/147] add avatars --- .../routers/kfu-m-24-1/sber_mobile/profile.js | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/profile.js b/server/routers/kfu-m-24-1/sber_mobile/profile.js index 982a17e..529e057 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/profile.js +++ b/server/routers/kfu-m-24-1/sber_mobile/profile.js @@ -18,10 +18,26 @@ router.get('/profile', async (req, res) => { if (profileError) return res.status(400).json({ error: profileError.message }); + // Получаем аватарку из бакета + let avatarUrl = null; + const avatarPath = `avatars/${user_id}.jpg`; + const { data: avatarData } = await supabase.storage.from('sber.mobile').getPublicUrl(avatarPath); + + if (avatarData) { + // Проверяем, существует ли файл + const { data: fileData, error: fileError } = await supabase.storage.from('sber.mobile').list('avatars', { + search: `${user_id}.jpg` + }); + + if (!fileError && fileData && fileData.length > 0) { + avatarUrl = avatarData.publicUrl; + } + } + res.json({ id: profileData.id, username: profileData.full_name, - avatar_url: profileData.avatar_url, + avatar_url: avatarUrl || profileData.avatar_url, phone: userData.user.phone, updated_at: profileData.updated_at }); @@ -39,15 +55,51 @@ router.post('/profile', async (req, res) => { if (userError) return res.status(400).json({ error: userError.message }); + let avatarUrl = data.avatar_url; + + // Если передана аватарка в base64, сохраняем в бакет + if (data.avatarBase64) { + try { + // Удаляем старую аватарку + const oldAvatarPath = `avatars/${user_id}.jpg`; + await supabase.storage.from('sber.mobile').remove([oldAvatarPath]); + + // Конвертируем base64 в buffer + const base64Data = data.avatarBase64.replace(/^data:image\/[a-z]+;base64,/, ''); + const buffer = Buffer.from(base64Data, 'base64'); + + // Загружаем новую аватарку + const avatarPath = `avatars/${user_id}.jpg`; + const { error: uploadError } = await supabase.storage + .from('sber.mobile') + .upload(avatarPath, buffer, { + contentType: 'image/jpeg', + upsert: true + }); + + if (uploadError) { + console.error('Ошибка загрузки аватарки:', uploadError); + } else { + // Получаем публичный URL + const { data: urlData } = await supabase.storage + .from('sber.mobile') + .getPublicUrl(avatarPath); + avatarUrl = urlData.publicUrl; + } + } catch (error) { + console.error('Ошибка обработки аватарки:', error); + } + } + let { error: profileError } = await supabase.from('user_profiles').update({ full_name: data.username, - avatar_url: data.avatar_url, + avatar_url: avatarUrl, // apartment: data.apartment }).eq('id', user_id).single(); if (profileError) return res.status(400).json({ error: profileError.message }); - res.json({ success: true }); + res.json({ success: true, avatar_url: avatarUrl }); }); // Получить управляющую компанию по квартире From cc2a66367dff2fc803aade9003c1aa1acee7a37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BD=D0=B8=D1=8F?= Date: Sun, 15 Jun 2025 20:39:27 +0300 Subject: [PATCH 069/147] votes --- .../routers/kfu-m-24-1/sber_mobile/votes.js | 95 +++++++++++++++---- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/votes.js b/server/routers/kfu-m-24-1/sber_mobile/votes.js index d46df22..9a861da 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/votes.js +++ b/server/routers/kfu-m-24-1/sber_mobile/votes.js @@ -6,39 +6,100 @@ router.get('/votes/:initiative_id', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id } = req.params; const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) return res.status(400).json({ error: error.message }); + if (error) + return res.status(400).json({ error: error.message }); res.json(data); }); // Получить голос пользователя по инициативе -router.get('/votes/:initiative_id/:user_id', async (req, res) => { +router.get('/votes/:initiative_id/user/:user_id', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id, user_id } = req.params; const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id).eq('user_id', user_id).single(); - if (error) return res.status(400).json({ error: error.message }); + if (error) { + console.log(error, '/votes/:initiative_id/:user_id') + console.log(initiative_id, user_id) + return res.status(400).json({ error: error.message }); + } res.json(data); }); -// Получить все голоса по инициативе (через query) -router.get('/votes', async (req, res) => { +// Получить статистику голосов по инициативе +router.get('/votes/stats/:initiative_id', async (req, res) => { const supabase = getSupabaseClient(); - const { initiative_id } = req.query; - if (!initiative_id) return res.status(400).json({ error: 'initiative_id required' }); - const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + const { initiative_id } = req.params; + + const { data, error } = await supabase + .from('votes') + .select('vote_type') + .eq('initiative_id', initiative_id); + console.log(data, error) + if (error) { + console.log('/votes/:initiative_id/stats') + res.status(400).json({ error: error.message }); + } + const stats = { + for: data.filter(vote => vote.vote_type === 'for').length, + against: data.filter(vote => vote.vote_type === 'against').length, + total: data.length + }; + + res.json(stats); }); -// Проголосовать (создать или обновить голос) +// Проголосовать (создать, обновить или удалить голос) router.post('/votes', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id, user_id, vote_type } = req.body; - // upsert: если голос уже есть, обновить, иначе создать - const { data, error } = await supabase.from('votes').upsert([ - { initiative_id, user_id, vote_type } - ], { onConflict: ['initiative_id', 'user_id'] }).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + + // Проверяем существующий голос + const { data: existingVote, error: checkError } = await supabase + .from('votes') + .select('*') + .eq('initiative_id', initiative_id) + .eq('user_id', user_id) + .single(); + + if (checkError && checkError.code !== 'PGRST116') { + console.log('1/votes') + return res.status(400).json({ error: checkError.message }); + } + + if (existingVote) { + if (existingVote.vote_type === vote_type) { + // Если нажали тот же тип голоса - УДАЛЯЕМ (отменяем голос) + const { error: deleteError } = await supabase + .from('votes') + .delete() + .eq('initiative_id', initiative_id) + .eq('user_id', user_id); + + if (deleteError) return res.status(400).json({ error: deleteError.message }); + res.json({ message: 'Vote removed', action: 'removed', previous_vote: existingVote.vote_type }); + } else { + // Если нажали другой тип голоса - ОБНОВЛЯЕМ + const { data, error } = await supabase + .from('votes') + .update({ vote_type }) + .eq('initiative_id', initiative_id) + .eq('user_id', user_id) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ ...data, action: 'updated', previous_vote: existingVote.vote_type }); + } + } else { + // Если голоса нет - СОЗДАЕМ новый + const { data, error } = await supabase + .from('votes') + .insert([{ initiative_id, user_id, vote_type }]) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ ...data, action: 'created' }); + } }); module.exports = router; \ No newline at end of file From 409a315a2586aa3d0a4937be791a057c8a1c9c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Sun, 15 Jun 2025 22:51:10 +0300 Subject: [PATCH 070/147] refactoring --- server/routers/kfu-m-24-1/sber_mobile/index.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index 625b375..9ef7bc9 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -1,6 +1,4 @@ const router = require('express').Router(); - - const authRouter = require('./auth'); const { supabaseRouter } = require('./supabaseClient'); const profileRouter = require('./profile'); @@ -10,11 +8,8 @@ const additionalServicesRouter = require('./additional_services'); const chatsRouter = require('./chats'); const camerasRouter = require('./cameras'); const ticketsRouter = require('./tickets'); - const messagesRouter = require('./messages'); - const moderationRouter = require('./moderation'); - const utilityPaymentsRouter = require('./utility_payments'); const apartmentsRouter = require('./apartments'); const buildingsRouter = require('./buildings'); @@ -26,7 +21,6 @@ const moderateRouter = require('./moderate.js'); module.exports = router; - router.use('/auth', authRouter); router.use('/supabase', supabaseRouter); router.use('', profileRouter); @@ -36,17 +30,12 @@ router.use('', additionalServicesRouter); router.use('', chatsRouter); router.use('', camerasRouter); router.use('', ticketsRouter); - router.use('', messagesRouter); - router.use('', moderationRouter); - router.use('', utilityPaymentsRouter); router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); router.use('', avatarRouter); router.use('', supportRouter); -router.use('', moderateRouter); - - +router.use('', moderateRouter); \ No newline at end of file From 9d68ee735a898e085d5896274f770173e5074947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Sun, 15 Jun 2025 23:07:17 +0300 Subject: [PATCH 071/147] remove console logs --- server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 2ddeb2f..460d723 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -22,7 +22,6 @@ async function initSupabaseClient() { return supabase; } catch (error) { - console.error('❌ [Supabase Client] Ошибка инициализации:', error); throw error; } } @@ -40,7 +39,6 @@ router.post('/refresh-supabase-client', async (req, res) => { await initSupabaseClient(); res.json({ success: true, message: 'Supabase client refreshed' }); } catch (error) { - console.error('❌ [Supabase Client] Ошибка обновления:', error); res.status(500).json({ error: error.message }); } }); @@ -62,7 +60,6 @@ initializationPromise = (async () => { try { await initSupabaseClient(); } catch (error) { - console.error('❌ [Supabase Client] Ошибка инициализации при старте:', error); // Планируем повторную попытку через 5 секунд setTimeout(async () => { try { @@ -78,6 +75,5 @@ module.exports = { getSupabaseClient, initSupabaseClient, supabaseRouter: router, - // Экспортируем промис инициализации для возможности ожидания initializationPromise }; \ No newline at end of file From 4fe16e5aa8f6cddcf1186fd1e9a2fe25f40c43d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Mon, 16 Jun 2025 12:36:41 +0300 Subject: [PATCH 072/147] remove console log --- .../kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts index dc2022c..48162c0 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -47,8 +47,6 @@ export const moderationText = async (title: string, description: string, GIGA_AU ` const result = await moderationLlm.invoke(prompt); - console.log(result) - // Дополнительная проверка if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', result.fixedText = description From 8090de8031bd4bcf8ec0cb24bd99813d9b0810bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Mon, 16 Jun 2025 14:02:19 +0300 Subject: [PATCH 073/147] remove initiatives folder --- .../sber_mobile/initiatives-ai-agents/llm.ts | 22 -------- .../initiatives-ai-agents/moderation.ts | 56 ------------------- .../initiatives-ai-agents/picture.ts | 38 ------------- 3 files changed, 116 deletions(-) delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts deleted file mode 100644 index a5a0e23..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GigaChat as GigaChatLang} from 'langchain-gigachat'; -import { GigaChat } from 'gigachat'; -import { Agent } from 'node:https'; - -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -export const llm_mod = (GIGA_AUTH) => - new GigaChatLang({ - credentials: GIGA_AUTH, - temperature: 0.2, - model: 'GigaChat-2-Max', - httpsAgent, -}); - -export const llm_gen = (GIGA_AUTH) => - new GigaChat({ - credentials: GIGA_AUTH, - model: 'GigaChat-2', - httpsAgent, -}); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts deleted file mode 100644 index 48162c0..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { llm_mod } from './llm' -import { z } from "zod"; - - -// возвращаю комментарий + исправленное предложение + булево значение - -export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => { - - const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({ - comment: z.string(), - fixedText: z.string().optional(), - isApproved: z.boolean(), - }) as any) - - const prompt = ` - Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, - не имеющая отношения к Управляющей компании). - - Заголовок: ${title} - Основной текст: ${description} - - Твои задачи: - 1. Проверь предложение и заголовок на спам. - 2. Проверь, чтобы заголовок и текст были на одну тему. - 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. - 4. Проверь грамматику. - 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. - 6. Не должно быть рекламы, ссылок и т.д. - 7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы. - 8. Предложение должно быть в вежливой форме. - - - Если все правила соблюдены, то предложение принимается! - - - Если предложение отклонено, всегда пиши комментарий и fixedText! - - Правила написания комментария: - - Если предложение отклоняется, пиши комментарий со следующей формулировкой: - "Предложение отклонено. Причина: (укажи проблему)" - - Правила написания fixedText: - - Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам. - - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, - которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. - - Если текст не представляет никакой ценности, возврати в поле "fixedText" правило, - по которому оно не прошло. - -Если предложение принимается, то ничего не возвращай в поле fixedText. - ` - - const result = await moderationLlm.invoke(prompt); - if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { - result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', - result.fixedText = description - } - - return [result.comment, result.fixedText, result.isApproved]; -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts deleted file mode 100644 index d216c5d..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { llm_gen } from './llm' -import { detectImage } from 'gigachat'; - -export const generatePicture = async (prompt: string, GIGA_AUTH) => { - const resp = await llm_gen(GIGA_AUTH).chat({ - messages: [ - { - "role": "system", - "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" - }, - { - role: "user", - content: `Старайся передать атмосферу уюта и безопасности. - Нарисуй картинку подходящую для такого события: ${prompt} - В картинке не должно быть текста, только изображение.`, - }, - ], - function_call: 'auto', - }); - - // Получение изображения по идентификатору - const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); - - if (!detectedImage?.uuid) { - throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); - } - - const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid); - - // Возвращаем содержимое изображения, убеждаясь что это Buffer - if (Buffer.isBuffer(image.content)) { - return image.content; - } else if (typeof image.content === 'string') { - return Buffer.from(image.content, 'binary'); - } else { - throw new Error('Unexpected image content type: ' + typeof image.content); - } -} From f66114b22f5905db35f5e8e4f50f6af27a5bff19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Mon, 16 Jun 2025 14:03:13 +0300 Subject: [PATCH 074/147] add initiatives folder --- .../sber_mobile/initiatives-ai-agents/llm.ts | 22 ++++++++ .../initiatives-ai-agents/moderation.ts | 56 +++++++++++++++++++ .../initiatives-ai-agents/picture.ts | 38 +++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts create mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts new file mode 100644 index 0000000..a5a0e23 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts @@ -0,0 +1,22 @@ +import { GigaChat as GigaChatLang} from 'langchain-gigachat'; +import { GigaChat } from 'gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +export const llm_mod = (GIGA_AUTH) => + new GigaChatLang({ + credentials: GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2-Max', + httpsAgent, +}); + +export const llm_gen = (GIGA_AUTH) => + new GigaChat({ + credentials: GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts new file mode 100644 index 0000000..48162c0 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -0,0 +1,56 @@ +import { llm_mod } from './llm' +import { z } from "zod"; + + +// возвращаю комментарий + исправленное предложение + булево значение + +export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => { + + const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({ + comment: z.string(), + fixedText: z.string().optional(), + isApproved: z.boolean(), + }) as any) + + const prompt = ` + Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, + не имеющая отношения к Управляющей компании). + + Заголовок: ${title} + Основной текст: ${description} + + Твои задачи: + 1. Проверь предложение и заголовок на спам. + 2. Проверь, чтобы заголовок и текст были на одну тему. + 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. + 4. Проверь грамматику. + 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. + 6. Не должно быть рекламы, ссылок и т.д. + 7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы. + 8. Предложение должно быть в вежливой форме. + + - Если все правила соблюдены, то предложение принимается! + + - Если предложение отклонено, всегда пиши комментарий и fixedText! + + Правила написания комментария: + - Если предложение отклоняется, пиши комментарий со следующей формулировкой: + "Предложение отклонено. Причина: (укажи проблему)" + + Правила написания fixedText: + - Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам. + - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, + которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. + - Если текст не представляет никакой ценности, возврати в поле "fixedText" правило, + по которому оно не прошло. + -Если предложение принимается, то ничего не возвращай в поле fixedText. + ` + + const result = await moderationLlm.invoke(prompt); + if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { + result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', + result.fixedText = description + } + + return [result.comment, result.fixedText, result.isApproved]; +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts new file mode 100644 index 0000000..d216c5d --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -0,0 +1,38 @@ +import { llm_gen } from './llm' +import { detectImage } from 'gigachat'; + +export const generatePicture = async (prompt: string, GIGA_AUTH) => { + const resp = await llm_gen(GIGA_AUTH).chat({ + messages: [ + { + "role": "system", + "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" + }, + { + role: "user", + content: `Старайся передать атмосферу уюта и безопасности. + Нарисуй картинку подходящую для такого события: ${prompt} + В картинке не должно быть текста, только изображение.`, + }, + ], + function_call: 'auto', + }); + + // Получение изображения по идентификатору + const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); + + if (!detectedImage?.uuid) { + throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); + } + + const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid); + + // Возвращаем содержимое изображения, убеждаясь что это Buffer + if (Buffer.isBuffer(image.content)) { + return image.content; + } else if (typeof image.content === 'string') { + return Buffer.from(image.content, 'binary'); + } else { + throw new Error('Unexpected image content type: ' + typeof image.content); + } +} From 3639524fc7717c8a4259149bf355c04c50e8a4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Mon, 16 Jun 2025 14:04:48 +0300 Subject: [PATCH 075/147] remove console log --- server/routers/kfu-m-24-1/sber_mobile/moderate.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js index 0e8b3f8..96c9926 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -39,8 +39,6 @@ router.post('/moderate', async (req, res) => { // Модерация текста (передаем title и description как body) const [comment, fixedText, isApproved] = await moderationText(title, description, GIGA_AUTH); - console.log('Результат модерации получен:', { comment, fixedText: fixedText?.substring(0, 100), isApproved }); - // Если модерация не прошла, возвращаем undefined if (!isApproved) { if (!comment || comment.trim() === '') { From ac5f3eee96d8bcfeb9615f66201dd2652d06b76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B5=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A1=D0=B5=D1=80=D0=B3=D0=B5?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Mon, 16 Jun 2025 14:12:52 +0300 Subject: [PATCH 076/147] fix moderate.js --- server/routers/kfu-m-24-1/sber_mobile/moderate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js index 96c9926..61c0692 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -1,6 +1,6 @@ const router = require('express').Router(); -const { moderationText } = require('./initiatives-ai-agents/moderation.ts'); -const { generatePicture } = require('./initiatives-ai-agents/picture.ts'); +const { moderationText } = require('./initiatives-ai-agents/moderation'); +const { generatePicture } = require('./initiatives-ai-agents/picture'); const { getSupabaseClient } = require('./supabaseClient'); const { getGigaAuth } = require('./get-constants'); From db6665736a8783048142406688933591f288c0af Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Sun, 29 Jun 2025 10:50:45 +0000 Subject: [PATCH 077/147] create server/routers/project-monday --- server/routers/project-monday | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 server/routers/project-monday diff --git a/server/routers/project-monday b/server/routers/project-monday new file mode 100644 index 0000000..e69de29 From 80b9d9c8c8cab9a04e077eafdd89ff63cbbf6119 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Sun, 29 Jun 2025 11:07:36 +0000 Subject: [PATCH 078/147] delete server/routers/project-monday --- server/routers/project-monday | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 server/routers/project-monday diff --git a/server/routers/project-monday b/server/routers/project-monday deleted file mode 100644 index e69de29..0000000 From b1a9ee14036c79ac1dda1bc94fa95520d91ad1cd Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Sun, 29 Jun 2025 22:31:00 +0000 Subject: [PATCH 079/147] upload files --- server/routers/back-new/.gitignore | 2 + server/routers/back-new/README.md | 21 + server/routers/back-new/app.js | 24 + server/routers/back-new/features.config.js | 5 + server/routers/back-new/package-lock.json | 1024 ++++++++++++++++++++ server/routers/back-new/package.json | 17 + server/routers/back-new/server.js | 5 + 7 files changed, 1098 insertions(+) create mode 100644 server/routers/back-new/.gitignore create mode 100644 server/routers/back-new/README.md create mode 100644 server/routers/back-new/app.js create mode 100644 server/routers/back-new/features.config.js create mode 100644 server/routers/back-new/package-lock.json create mode 100644 server/routers/back-new/package.json create mode 100644 server/routers/back-new/server.js diff --git a/server/routers/back-new/.gitignore b/server/routers/back-new/.gitignore new file mode 100644 index 0000000..9439df7 --- /dev/null +++ b/server/routers/back-new/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env \ No newline at end of file diff --git a/server/routers/back-new/README.md b/server/routers/back-new/README.md new file mode 100644 index 0000000..97811d1 --- /dev/null +++ b/server/routers/back-new/README.md @@ -0,0 +1,21 @@ +# back-new + +非Python实现的后端(Node.js + Express) + +## 启动方法 + +1. 安装依赖: + ```bash + npm install + ``` +2. 启动服务: + ```bash + npm start + ``` + +默认端口:`3002` + +## 支持接口 +- POST `/api/auth/login` 用户登录 +- POST `/api/auth/register` 用户注册 +- GET `/gigachat/prompt?prompt=xxx` 生成图片(返回模拟图片链接) \ No newline at end of file diff --git a/server/routers/back-new/app.js b/server/routers/back-new/app.js new file mode 100644 index 0000000..e87fbc8 --- /dev/null +++ b/server/routers/back-new/app.js @@ -0,0 +1,24 @@ +const express = require('express'); +const cors = require('cors'); +const featuresConfig = require('./features.config'); +const imageRoutes = require('./features/image/image.routes'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +if (featuresConfig.auth) { + app.use('/api/auth', require('./features/auth/auth.routes')); +} +if (featuresConfig.user) { + app.use('/api/user', require('./features/user/user.routes')); +} +if (featuresConfig.image) { + app.use('/gigachat', imageRoutes); +} + +app.get('/api/', (req, res) => { + res.json({ message: 'API root' }); +}); + +module.exports = app; \ No newline at end of file diff --git a/server/routers/back-new/features.config.js b/server/routers/back-new/features.config.js new file mode 100644 index 0000000..7a69e24 --- /dev/null +++ b/server/routers/back-new/features.config.js @@ -0,0 +1,5 @@ +module.exports = { + auth: true, + user: true, + image: true, // 关闭为 false +}; \ No newline at end of file diff --git a/server/routers/back-new/package-lock.json b/server/routers/back-new/package-lock.json new file mode 100644 index 0000000..c4d644d --- /dev/null +++ b/server/routers/back-new/package-lock.json @@ -0,0 +1,1024 @@ +{ + "name": "back-new", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "back-new", + "version": "1.0.0", + "dependencies": { + "axios": "^1.10.0", + "cors": "^2.8.5", + "dotenv": "^17.0.0", + "express": "^4.21.2", + "qs": "^6.14.0", + "uuid": "^11.1.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmmirror.com/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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "17.0.0", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.0.0.tgz", + "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/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.3", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/server/routers/back-new/package.json b/server/routers/back-new/package.json new file mode 100644 index 0000000..8663f06 --- /dev/null +++ b/server/routers/back-new/package.json @@ -0,0 +1,17 @@ +{ + "name": "back-new", + "version": "1.0.0", + "description": "非Python实现的后端,兼容前端接口", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "axios": "^1.10.0", + "cors": "^2.8.5", + "dotenv": "^17.0.0", + "express": "^4.21.2", + "qs": "^6.14.0", + "uuid": "^11.1.0" + } +} diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js new file mode 100644 index 0000000..093eb01 --- /dev/null +++ b/server/routers/back-new/server.js @@ -0,0 +1,5 @@ +const app = require('./app'); +const PORT = process.env.PORT || 3002; +app.listen(PORT, () => { + console.log(`Mock backend running on http://localhost:${PORT}`); +}); \ No newline at end of file From 8450cc2d4d858e005253b30d91eaa78613512845 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Sun, 29 Jun 2025 22:31:53 +0000 Subject: [PATCH 080/147] upload files --- .../back-new/features/auth/auth.controller.js | 95 +++++++++++++++++++ .../back-new/features/auth/auth.routes.js | 10 ++ .../features/image/image.controller.js | 81 ++++++++++++++++ .../back-new/features/image/image.routes.js | 7 ++ .../back-new/features/user/user.controller.js | 12 +++ .../back-new/features/user/user.routes.js | 7 ++ server/routers/back-new/shared/hateoas.js | 8 ++ server/routers/back-new/shared/usersDb.js | 20 ++++ 8 files changed, 240 insertions(+) create mode 100644 server/routers/back-new/features/auth/auth.controller.js create mode 100644 server/routers/back-new/features/auth/auth.routes.js create mode 100644 server/routers/back-new/features/image/image.controller.js create mode 100644 server/routers/back-new/features/image/image.routes.js create mode 100644 server/routers/back-new/features/user/user.controller.js create mode 100644 server/routers/back-new/features/user/user.routes.js create mode 100644 server/routers/back-new/shared/hateoas.js create mode 100644 server/routers/back-new/shared/usersDb.js diff --git a/server/routers/back-new/features/auth/auth.controller.js b/server/routers/back-new/features/auth/auth.controller.js new file mode 100644 index 0000000..21c400f --- /dev/null +++ b/server/routers/back-new/features/auth/auth.controller.js @@ -0,0 +1,95 @@ +const usersDb = require('../../shared/usersDb'); +const makeLinks = require('../../shared/hateoas'); + +exports.login = (req, res) => { + const { username, password, email } = req.body; + const user = usersDb.findUser(username, email, password); + if (user) { + res.json({ + data: { + user: { + id: user.id, + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName + }, + token: 'token-' + user.id, + message: 'Login successful' + }, + _links: makeLinks('/api/auth', { + self: '/login', + profile: '/profile/', + logout: '/logout' + }), + _meta: {} + }); + } else { + res.status(401).json({ error: 'Invalid credentials' }); + } +}; + +exports.register = (req, res) => { + const { username, password, email, firstName, lastName } = req.body; + if (usersDb.exists(username, email)) { + return res.status(409).json({ error: 'User already exists' }); + } + const newUser = usersDb.addUser({ username, password, email, firstName, lastName }); + res.json({ + data: { + user: { + id: newUser.id, + username, + email, + firstName, + lastName + }, + token: 'token-' + newUser.id, + message: 'Register successful' + }, + _links: makeLinks('/api/auth', { + self: '/register', + login: '/login', + profile: '/profile/' + }), + _meta: {} + }); +}; + +exports.profile = (req, res) => { + const auth = req.headers.authorization; + if (!auth || !auth.startsWith('Bearer ')) { + return res.status(401).json({ error: 'No token provided' }); + } + const token = auth.replace('Bearer ', ''); + const id = parseInt(token.replace('token-', '')); + const user = usersDb.findById(id); + if (!user) { + return res.status(401).json({ error: 'Invalid token' }); + } + res.json({ + data: { + id: user.id, + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName + }, + _links: makeLinks('/api/auth', { + self: '/profile/', + logout: '/logout' + }), + _meta: {} + }); +}; + +exports.logout = (req, res) => { + res.json({ + message: 'Logout successful', + _links: makeLinks('/api/auth', { + self: '/logout', + login: '/login' + }), + _meta: {} + }); +}; \ No newline at end of file diff --git a/server/routers/back-new/features/auth/auth.routes.js b/server/routers/back-new/features/auth/auth.routes.js new file mode 100644 index 0000000..d983771 --- /dev/null +++ b/server/routers/back-new/features/auth/auth.routes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('./auth.controller'); + +router.post('/login', ctrl.login); +router.post('/register', ctrl.register); +router.get('/profile/', ctrl.profile); +router.post('/logout', ctrl.logout); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js new file mode 100644 index 0000000..49b4489 --- /dev/null +++ b/server/routers/back-new/features/image/image.controller.js @@ -0,0 +1,81 @@ +const axios = require('axios'); +const makeLinks = require('../../shared/hateoas'); +const path = require('path'); +const qs = require('qs'); +const { v4: uuidv4 } = require('uuid'); +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +exports.generate = async (req, res) => { + const { prompt } = req.query; + if (!prompt) { + return res.status(400).json({ error: 'Prompt parameter is required' }); + } + try { + const apiKey = process.env.GIGACHAT_API_KEY; + const tokenResp = await axios.post( + 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', + { + 'scope':' GIGACHAT_API_PERS', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': `Basic ${apiKey}`, + 'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e' + }, + } + ); + const accessToken = tokenResp.data.access_token; + const chatResp = await axios.post( + 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions', + { + model: "GigaChat", + messages: [ + { role: "system", content: "Ты — Василий Кандинский" }, + { role: "user", content: prompt } + ], + stream: false, + function_call: 'auto' + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'RqUID': uuidv4(), + } + } + ); + const content = chatResp.data.choices[0].message.content; + const match = content.match(/ { + res.json({ + data: usersDb.getAll(), + _links: makeLinks('/api/user', { + self: '/list', + }), + _meta: {} + }); +}; \ No newline at end of file diff --git a/server/routers/back-new/features/user/user.routes.js b/server/routers/back-new/features/user/user.routes.js new file mode 100644 index 0000000..f444cad --- /dev/null +++ b/server/routers/back-new/features/user/user.routes.js @@ -0,0 +1,7 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('./user.controller'); + +router.get('/list', ctrl.list); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/back-new/shared/hateoas.js b/server/routers/back-new/shared/hateoas.js new file mode 100644 index 0000000..b12ec23 --- /dev/null +++ b/server/routers/back-new/shared/hateoas.js @@ -0,0 +1,8 @@ +function makeLinks(base, links) { + const result = {}; + for (const [rel, path] of Object.entries(links)) { + result[rel] = { href: base + path }; + } + return result; +} +module.exports = makeLinks; \ No newline at end of file diff --git a/server/routers/back-new/shared/usersDb.js b/server/routers/back-new/shared/usersDb.js new file mode 100644 index 0000000..0b888ef --- /dev/null +++ b/server/routers/back-new/shared/usersDb.js @@ -0,0 +1,20 @@ +let users = [ + { id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User' } +]; +let nextId = 2; + +exports.findUser = (username, email, password) => + users.find(u => (u.username === username || u.email === email) && u.password === password); + +exports.findById = (id) => users.find(u => u.id === id); + +exports.addUser = ({ username, password, email, firstName, lastName }) => { + const newUser = { id: nextId++, username, password, email, firstName, lastName }; + users.push(newUser); + return newUser; +}; + +exports.exists = (username, email) => + users.some(u => u.username === username || u.email === email); + +exports.getAll = () => users; \ No newline at end of file From c11bcd5d26066882510924ac23f817f4107e3968 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Mon, 30 Jun 2025 16:23:09 +0000 Subject: [PATCH 081/147] upload files --- server/routers/kfu-m-24-1/back-new/.gitignore | 2 + server/routers/kfu-m-24-1/back-new/README.md | 21 + server/routers/kfu-m-24-1/back-new/app.js | 24 + .../kfu-m-24-1/back-new/features.config.js | 5 + .../kfu-m-24-1/back-new/package-lock.json | 5455 +++++++++++++++++ .../routers/kfu-m-24-1/back-new/package.json | 21 + server/routers/kfu-m-24-1/back-new/server.js | 5 + 7 files changed, 5533 insertions(+) create mode 100644 server/routers/kfu-m-24-1/back-new/.gitignore create mode 100644 server/routers/kfu-m-24-1/back-new/README.md create mode 100644 server/routers/kfu-m-24-1/back-new/app.js create mode 100644 server/routers/kfu-m-24-1/back-new/features.config.js create mode 100644 server/routers/kfu-m-24-1/back-new/package-lock.json create mode 100644 server/routers/kfu-m-24-1/back-new/package.json create mode 100644 server/routers/kfu-m-24-1/back-new/server.js diff --git a/server/routers/kfu-m-24-1/back-new/.gitignore b/server/routers/kfu-m-24-1/back-new/.gitignore new file mode 100644 index 0000000..9439df7 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/README.md b/server/routers/kfu-m-24-1/back-new/README.md new file mode 100644 index 0000000..97811d1 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/README.md @@ -0,0 +1,21 @@ +# back-new + +非Python实现的后端(Node.js + Express) + +## 启动方法 + +1. 安装依赖: + ```bash + npm install + ``` +2. 启动服务: + ```bash + npm start + ``` + +默认端口:`3002` + +## 支持接口 +- POST `/api/auth/login` 用户登录 +- POST `/api/auth/register` 用户注册 +- GET `/gigachat/prompt?prompt=xxx` 生成图片(返回模拟图片链接) \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/app.js b/server/routers/kfu-m-24-1/back-new/app.js new file mode 100644 index 0000000..e87fbc8 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/app.js @@ -0,0 +1,24 @@ +const express = require('express'); +const cors = require('cors'); +const featuresConfig = require('./features.config'); +const imageRoutes = require('./features/image/image.routes'); + +const app = express(); +app.use(cors()); +app.use(express.json()); + +if (featuresConfig.auth) { + app.use('/api/auth', require('./features/auth/auth.routes')); +} +if (featuresConfig.user) { + app.use('/api/user', require('./features/user/user.routes')); +} +if (featuresConfig.image) { + app.use('/gigachat', imageRoutes); +} + +app.get('/api/', (req, res) => { + res.json({ message: 'API root' }); +}); + +module.exports = app; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/features.config.js b/server/routers/kfu-m-24-1/back-new/features.config.js new file mode 100644 index 0000000..7a69e24 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/features.config.js @@ -0,0 +1,5 @@ +module.exports = { + auth: true, + user: true, + image: true, // 关闭为 false +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/package-lock.json b/server/routers/kfu-m-24-1/back-new/package-lock.json new file mode 100644 index 0000000..f8e2608 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/package-lock.json @@ -0,0 +1,5455 @@ +{ + "name": "back-new", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "back-new", + "version": "1.0.0", + "dependencies": { + "axios": "^1.10.0", + "cors": "^2.8.5", + "dotenv": "^17.0.0", + "express": "^4.21.2", + "qs": "^6.14.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "jest": "^30.0.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@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.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.27.7.tgz", + "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "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/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@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.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "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.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.7" + }, + "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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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.npmmirror.com/@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.npmmirror.com/@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.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.27.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/console/-/console-30.0.2.tgz", + "integrity": "sha512-krGElPU0FipAqpVZ/BRZOy0MZh/ARdJ0Nj+PiH1ykFY1+VpBlYNLjdjVA5CFKxnKR6PFqFutO4Z7cdK9BlGiDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/@jest/core/-/core-30.0.3.tgz", + "integrity": "sha512-Mgs1N+NSHD3Fusl7bOq1jyxv1JDAUwjy+0DhVR93Q6xcBP9/bAQ+oZhXb5TTnP5sQzAHgb7ROCKQ2SnovtxYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.2", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.2", + "jest-config": "30.0.3", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-resolve-dependencies": "30.0.3", + "jest-runner": "30.0.3", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "jest-watcher": "30.0.2", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/environment/-/environment-30.0.2.tgz", + "integrity": "sha512-hRLhZRJNxBiOhxIKSq2UkrlhMt3/zVFQOAi5lvS8T9I03+kxsbflwHJEF+eXEYXCrRGRhHwECT7CDk6DyngsRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/@jest/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-73BVLqfCeWjYWPEQoYjiRZ4xuQRhQZU0WdgvbyXGRHItKQqg5e6mt2y1kVhzLSuZpmUnccZHbGynoaL7IcLU3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.0.3", + "jest-snapshot": "30.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/@jest/expect-utils/-/expect-utils-30.0.3.tgz", + "integrity": "sha512-SMtBvf2sfX2agcT0dA9pXwcUrKvOSDqBY4e4iRfT+Hya33XzV35YVg+98YQFErVGA/VR1Gto5Y2+A6G9LSQ3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/fake-timers/-/fake-timers-30.0.2.tgz", + "integrity": "sha512-jfx0Xg7l0gmphTY9UKm5RtH12BlLYj/2Plj6wXjVW5Era4FZKfXeIvwC67WX+4q8UCFxYS20IgnMcFBcEU0DtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/@jest/globals/-/globals-30.0.3.tgz", + "integrity": "sha512-fIduqNyYpMeeSr5iEAiMn15KxCzvrmxl7X7VwLDRGj7t5CoHtbF+7K3EvKk32mOUIJ4kIvFRlaixClMH2h/Vaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/types": "30.0.1", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/reporters/-/reporters-30.0.2.tgz", + "integrity": "sha512-l4QzS/oKf57F8WtPZK+vvF4Io6ukplc6XgNFu4Hd/QxaLEO9f+8dSFzUua62Oe0HKlCUjKHpltKErAgDiMJKsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/snapshot-utils/-/snapshot-utils-30.0.1.tgz", + "integrity": "sha512-6Dpv7vdtoRiISEFwYF8/c7LIvqXD7xDXtLPNzC2xqAfBznKip0MQM+rkseKwUPUpv2PJ7KW/YsnwWXrIL2xF+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/test-result/-/test-result-30.0.2.tgz", + "integrity": "sha512-KKMuBKkkZYP/GfHMhI+cH2/P3+taMZS3qnqqiPC1UXZTJskkCS+YU/ILCtw5anw1+YsTulDHFpDo70mmCedW8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.2", + "@jest/types": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/test-sequencer/-/test-sequencer-30.0.2.tgz", + "integrity": "sha512-fbyU5HPka0rkalZ3MXVvq0hwZY8dx3Y6SCqR64zRmh+xXlDeFl0IdL4l9e7vp4gxEXTYHbwLFA1D+WW5CucaSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/@jest/transform/-/transform-30.0.2.tgz", + "integrity": "sha512-kJIuhLMTxRF7sc0gPzPtCDib/V9KwW3I2U25b+lYCYMVqHHSrcZopS8J8H+znx9yixuFv+Iozl8raLt/4MoxrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/@jest/types/-/types-30.0.1.tgz", + "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.10", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.10.tgz", + "integrity": "sha512-HM2F4B9N4cA0RH2KQiIZOHAZqtP4xGS4IZ+SFe1SIbO4dyjf9MTY2Bo3vHYnm0hglWfXqBrzUBSa+cJfl3Xvrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.2.tgz", + "integrity": "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.27", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.27.tgz", + "integrity": "sha512-VO95AxtSFMelbg3ouljAYnfvTEwSWVt/2YLf+U5Ejd8iT5mXE2Sa/1LGyvySMne2CGsepGLI7KpF3EzE3Aq9Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.37", + "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@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": "13.0.5", + "resolved": "https://registry.npmmirror.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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/node": { + "version": "24.0.7", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.0.7.tgz", + "integrity": "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.2.tgz", + "integrity": "sha512-tS+lqTU3N0kkthU+rYp0spAYq15DU8ld9kXkaKg9sbQqJNF+WPMuNHZQGCgdxrUOEO0j22RKMwRVhF1HTl+X8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.2.tgz", + "integrity": "sha512-MffGiZULa/KmkNjHeuuflLVqfhqLv1vZLm8lWIyeADvlElJ/GLSOkoUX+5jf4/EGtfwrNFcEaB8BRas03KT0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.2.tgz", + "integrity": "sha512-dzJYK5rohS1sYl1DHdJ3mwfwClJj5BClQnQSyAgEfggbUwA9RlROQSSbKBLqrGfsiC/VyrDPtbO8hh56fnkbsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.2.tgz", + "integrity": "sha512-gaIMWK+CWtXcg9gUyznkdV54LzQ90S3X3dn8zlh+QR5Xy7Y+Efqw4Rs4im61K1juy4YNb67vmJsCDAGOnIeffQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.2.tgz", + "integrity": "sha512-S7QpkMbVoVJb0xwHFwujnwCAEDe/596xqY603rpi/ioTn9VDgBHnCCxh+UFrr5yxuMH+dliHfjwCZJXOPJGPnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.2.tgz", + "integrity": "sha512-+XPUMCuCCI80I46nCDFbGum0ZODP5NWGiwS3Pj8fOgsG5/ctz+/zzuBlq/WmGa+EjWZdue6CF0aWWNv84sE1uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.2.tgz", + "integrity": "sha512-sqvUyAd1JUpwbz33Ce2tuTLJKM+ucSsYpPGl2vuFwZnEIg0CmdxiZ01MHQ3j6ExuRqEDUCy8yvkDKvjYFPb8Zg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.2.tgz", + "integrity": "sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.2.tgz", + "integrity": "sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.2.tgz", + "integrity": "sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.2.tgz", + "integrity": "sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.2.tgz", + "integrity": "sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.2.tgz", + "integrity": "sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.2.tgz", + "integrity": "sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.2.tgz", + "integrity": "sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.2.tgz", + "integrity": "sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.2.tgz", + "integrity": "sha512-EdFbGn7o1SxGmN6aZw9wAkehZJetFPao0VGZ9OMBwKx6TkvDuj6cNeLimF/Psi6ts9lMOe+Dt6z19fZQ9Ye2fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.2.tgz", + "integrity": "sha512-JY9hi1p7AG+5c/dMU8o2kWemM8I6VZxfGwn1GCtf3c5i+IKcMo2NQ8OjZ4Z3/itvY/Si3K10jOBQn7qsD/whUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.2.tgz", + "integrity": "sha512-ryoo+EB19lMxAd80ln9BVf8pdOAxLb97amrQ3SFN9OCRn/5M5wvwDgAe4i8ZjhpbiHoDeP8yavcTEnpKBo7lZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "4.3.0", + "resolved": "https://registry.npmmirror.com/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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/babel-jest/-/babel-jest-30.0.2.tgz", + "integrity": "sha512-A5kqR1/EUTidM2YC2YMEUDP2+19ppgOwK0IAd9Swc3q2KqFb5f9PtRUXVeZcngu0z5mDMyZ9zH2huJZSOMLiTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.0.2", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "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.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/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": "30.0.1", + "resolved": "https://registry.npmmirror.com/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/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.25.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "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.npmmirror.com/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-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.30001726", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "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.npmmirror.com/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/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-4.2.0.tgz", + "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/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/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmmirror.com/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "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.npmmirror.com/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.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.0.0", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.0.0.tgz", + "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.177", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.177.tgz", + "integrity": "sha512-7EH2G59nLsEMj97fpDuvVcYi6lwTcM1xuWw3PssD8xzboAW7zj7iB3COEEEATUfjLHrs5uKBLQT03V/8URx06g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/expect/-/expect-30.0.3.tgz", + "integrity": "sha512-HXg6NvK35/cSYZCUKAtmlgCFyqKM4frEPbzrav5hRqb0GMz0E0lS5hfzYjSaiaE5ysnp/qI2aeZkeyeIAOeXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "1.3.1", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/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/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.3", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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": "4.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/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-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest/-/jest-30.0.3.tgz", + "integrity": "sha512-Uy8xfeE/WpT2ZLGDXQmaYNzw2v8NUKuYeKGtkS6sDxwsdQihdgYCXaKIYnph1h95DN5H35ubFDm0dfmsQnjn4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.3", + "@jest/types": "30.0.1", + "import-local": "^3.2.0", + "jest-cli": "30.0.3" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-changed-files/-/jest-changed-files-30.0.2.tgz", + "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.2", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-circus/-/jest-circus-30.0.3.tgz", + "integrity": "sha512-rD9qq2V28OASJHJWDRVdhoBdRs6k3u3EmBzDYcyuMby8XCO3Ll1uq9kyqM41ZcC4fMiPulMVh3qMw0cBvDbnyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/expect": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.2", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-runtime": "30.0.3", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "p-limit": "^3.1.0", + "pretty-format": "30.0.2", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-cli/-/jest-cli-30.0.3.tgz", + "integrity": "sha512-UWDSj0ayhumEAxpYRlqQLrssEi29kdQ+kddP94AuHhZknrE+mT0cR0J+zMHKFe9XPfX3dKQOc2TfWki3WhFTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.3", + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-config/-/jest-config-30.0.3.tgz", + "integrity": "sha512-j0L4oRCtJwNyZktXIqwzEiDVQXBbQ4dqXuLD/TZdn++hXIcIfZmjHgrViEy5s/+j4HvITmAXbexVZpQ/jnr0bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.2", + "@jest/types": "30.0.1", + "babel-jest": "30.0.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-runner": "30.0.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-diff/-/jest-diff-30.0.3.tgz", + "integrity": "sha512-Q1TAV0cUcBTic57SVnk/mug0/ASyAqtSIOkr7RAlxx97llRYsM74+E8N5WdGJUlwCKwgxPAkVjKh653h1+HA9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmmirror.com/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-each/-/jest-each-30.0.2.tgz", + "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-environment-node/-/jest-environment-node-30.0.2.tgz", + "integrity": "sha512-XsGtZ0H+a70RsxAQkKuIh0D3ZlASXdZdhpOSBq9WRPq6lhe0IoQHGW0w9ZUaPiZQ/CpkIdprvlfV1QcXcvIQLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2", + "jest-util": "30.0.2", + "jest-validate": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-haste-map/-/jest-haste-map-30.0.2.tgz", + "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", + "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-matcher-utils/-/jest-matcher-utils-30.0.3.tgz", + "integrity": "sha512-hMpVFGFOhYmIIRGJ0HgM9htC5qUiJ00famcc9sRFchJJiLZbbVKrAztcgE6VnXLRxA3XZ0bvNA7hQWh3oHXo/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.3", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-message-util/-/jest-message-util-30.0.2.tgz", + "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-mock/-/jest-mock-30.0.2.tgz", + "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/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": "30.0.1", + "resolved": "https://registry.npmmirror.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-resolve/-/jest-resolve-30.0.2.tgz", + "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.3.tgz", + "integrity": "sha512-FlL6u7LiHbF0Oe27k7DHYMq2T2aNpPhxnNo75F7lEtu4A6sSw+TKkNNUGNcVckdFoL0RCWREJsC1HsKDwKRZzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-runner/-/jest-runner-30.0.3.tgz", + "integrity": "sha512-CxYBzu9WStOBBXAKkLXGoUtNOWsiS1RRmUQb6SsdUdTcqVncOau7m8AJ4cW3Mz+YL1O9pOGPSYLyvl8HBdFmkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.2", + "@jest/environment": "30.0.2", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.2", + "jest-haste-map": "30.0.2", + "jest-leak-detector": "30.0.2", + "jest-message-util": "30.0.2", + "jest-resolve": "30.0.2", + "jest-runtime": "30.0.3", + "jest-util": "30.0.2", + "jest-watcher": "30.0.2", + "jest-worker": "30.0.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-runtime/-/jest-runtime-30.0.3.tgz", + "integrity": "sha512-Xjosq0C48G9XEQOtmgrjXJwPaUPaq3sPJwHDRaiC+5wi4ZWxO6Lx6jNkizK/0JmTulVNuxP8iYwt77LGnfg3/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.2", + "@jest/fake-timers": "30.0.2", + "@jest/globals": "30.0.3", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.2", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-snapshot": "30.0.3", + "jest-util": "30.0.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.0.3", + "resolved": "https://registry.npmmirror.com/jest-snapshot/-/jest-snapshot-30.0.3.tgz", + "integrity": "sha512-F05JCohd3OA1N9+5aEPXA6I0qOfZDGIx0zTq5Z4yMBg2i1p5ELfBusjYAWwTkC12c7dHcbyth4QAfQbS7cRjow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.3", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.1", + "@jest/transform": "30.0.2", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.3", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.3", + "jest-matcher-utils": "30.0.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-util/-/jest-util-30.0.2.tgz", + "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/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/jest-validate": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-validate/-/jest-validate-30.0.2.tgz", + "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/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": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-watcher/-/jest-watcher-30.0.2.tgz", + "integrity": "sha512-vYO5+E7jJuF+XmONr6CrbXdlYrgvZqtkn6pdkgjt/dU64UAdc0v1cAVaAeWtAfUUMScxNmnUjKPUMdCpNVASwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.2", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.2", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-30.0.2.tgz", + "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/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-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/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/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/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": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/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.5", + "resolved": "https://registry.npmmirror.com/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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.2.5.tgz", + "integrity": "sha512-kmsgUvCRIJohHjbZ3V8avP0I1Pekw329MVAMDzVxsrkjgdnqiwvMX5XwR+hWV66vsAtZ+iM+fVnq8RTQawUmCQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/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-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/pretty-format": { + "version": "30.0.2", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-30.0.2.tgz", + "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.1", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/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/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "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.npmmirror.com/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.npmmirror.com/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": "2.5.2", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/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-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/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.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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.npmmirror.com/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.npmmirror.com/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/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unrs-resolver": { + "version": "1.9.2", + "resolved": "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.9.2.tgz", + "integrity": "sha512-VUyWiTNQD7itdiMuJy+EuLEErLj3uwX/EpHQF8EOf33Dq3Ju6VW1GXm+swk6+1h7a49uv9fKZ+dft9jU7esdLA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.2.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.9.2", + "@unrs/resolver-binding-android-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-arm64": "1.9.2", + "@unrs/resolver-binding-darwin-x64": "1.9.2", + "@unrs/resolver-binding-freebsd-x64": "1.9.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.2", + "@unrs/resolver-binding-linux-x64-musl": "1.9.2", + "@unrs/resolver-binding-wasm32-wasi": "1.9.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/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/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/routers/kfu-m-24-1/back-new/package.json b/server/routers/kfu-m-24-1/back-new/package.json new file mode 100644 index 0000000..df84ae0 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/package.json @@ -0,0 +1,21 @@ +{ + "name": "back-new", + "version": "1.0.0", + "description": "非Python实现的后端,兼容前端接口", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test": "jest" + }, + "dependencies": { + "axios": "^1.10.0", + "cors": "^2.8.5", + "dotenv": "^17.0.0", + "express": "^4.21.2", + "qs": "^6.14.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "jest": "^30.0.3" + } +} diff --git a/server/routers/kfu-m-24-1/back-new/server.js b/server/routers/kfu-m-24-1/back-new/server.js new file mode 100644 index 0000000..8bdfa2d --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/server.js @@ -0,0 +1,5 @@ +const app = require('./app'); +const PORT = process.env.PORT || 3002; +app.listen(PORT, () => { + console.log(`Mock backend running on https://dev.bro.js.ru/ms/back-new/${PORT}`); +}); \ No newline at end of file From 36558dfb85cf54c38651683cd6a0a19f5370e7f6 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Mon, 30 Jun 2025 16:23:34 +0000 Subject: [PATCH 082/147] upload files --- .../back-new/features/auth/auth.controller.js | 95 +++++++++++++++++++ .../back-new/features/auth/auth.routes.js | 10 ++ .../features/image/image.controller.js | 81 ++++++++++++++++ .../back-new/features/image/image.routes.js | 7 ++ .../back-new/features/user/user.controller.js | 12 +++ .../back-new/features/user/user.routes.js | 7 ++ .../kfu-m-24-1/back-new/shared/hateoas.js | 8 ++ .../kfu-m-24-1/back-new/shared/usersDb.js | 20 ++++ 8 files changed, 240 insertions(+) create mode 100644 server/routers/kfu-m-24-1/back-new/features/auth/auth.controller.js create mode 100644 server/routers/kfu-m-24-1/back-new/features/auth/auth.routes.js create mode 100644 server/routers/kfu-m-24-1/back-new/features/image/image.controller.js create mode 100644 server/routers/kfu-m-24-1/back-new/features/image/image.routes.js create mode 100644 server/routers/kfu-m-24-1/back-new/features/user/user.controller.js create mode 100644 server/routers/kfu-m-24-1/back-new/features/user/user.routes.js create mode 100644 server/routers/kfu-m-24-1/back-new/shared/hateoas.js create mode 100644 server/routers/kfu-m-24-1/back-new/shared/usersDb.js diff --git a/server/routers/kfu-m-24-1/back-new/features/auth/auth.controller.js b/server/routers/kfu-m-24-1/back-new/features/auth/auth.controller.js new file mode 100644 index 0000000..21c400f --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/features/auth/auth.controller.js @@ -0,0 +1,95 @@ +const usersDb = require('../../shared/usersDb'); +const makeLinks = require('../../shared/hateoas'); + +exports.login = (req, res) => { + const { username, password, email } = req.body; + const user = usersDb.findUser(username, email, password); + if (user) { + res.json({ + data: { + user: { + id: user.id, + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName + }, + token: 'token-' + user.id, + message: 'Login successful' + }, + _links: makeLinks('/api/auth', { + self: '/login', + profile: '/profile/', + logout: '/logout' + }), + _meta: {} + }); + } else { + res.status(401).json({ error: 'Invalid credentials' }); + } +}; + +exports.register = (req, res) => { + const { username, password, email, firstName, lastName } = req.body; + if (usersDb.exists(username, email)) { + return res.status(409).json({ error: 'User already exists' }); + } + const newUser = usersDb.addUser({ username, password, email, firstName, lastName }); + res.json({ + data: { + user: { + id: newUser.id, + username, + email, + firstName, + lastName + }, + token: 'token-' + newUser.id, + message: 'Register successful' + }, + _links: makeLinks('/api/auth', { + self: '/register', + login: '/login', + profile: '/profile/' + }), + _meta: {} + }); +}; + +exports.profile = (req, res) => { + const auth = req.headers.authorization; + if (!auth || !auth.startsWith('Bearer ')) { + return res.status(401).json({ error: 'No token provided' }); + } + const token = auth.replace('Bearer ', ''); + const id = parseInt(token.replace('token-', '')); + const user = usersDb.findById(id); + if (!user) { + return res.status(401).json({ error: 'Invalid token' }); + } + res.json({ + data: { + id: user.id, + username: user.username, + email: user.email, + firstName: user.firstName, + lastName: user.lastName + }, + _links: makeLinks('/api/auth', { + self: '/profile/', + logout: '/logout' + }), + _meta: {} + }); +}; + +exports.logout = (req, res) => { + res.json({ + message: 'Logout successful', + _links: makeLinks('/api/auth', { + self: '/logout', + login: '/login' + }), + _meta: {} + }); +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/features/auth/auth.routes.js b/server/routers/kfu-m-24-1/back-new/features/auth/auth.routes.js new file mode 100644 index 0000000..d983771 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/features/auth/auth.routes.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('./auth.controller'); + +router.post('/login', ctrl.login); +router.post('/register', ctrl.register); +router.get('/profile/', ctrl.profile); +router.post('/logout', ctrl.logout); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js b/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js new file mode 100644 index 0000000..49b4489 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js @@ -0,0 +1,81 @@ +const axios = require('axios'); +const makeLinks = require('../../shared/hateoas'); +const path = require('path'); +const qs = require('qs'); +const { v4: uuidv4 } = require('uuid'); +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +exports.generate = async (req, res) => { + const { prompt } = req.query; + if (!prompt) { + return res.status(400).json({ error: 'Prompt parameter is required' }); + } + try { + const apiKey = process.env.GIGACHAT_API_KEY; + const tokenResp = await axios.post( + 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', + { + 'scope':' GIGACHAT_API_PERS', + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': `Basic ${apiKey}`, + 'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e' + }, + } + ); + const accessToken = tokenResp.data.access_token; + const chatResp = await axios.post( + 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions', + { + model: "GigaChat", + messages: [ + { role: "system", content: "Ты — Василий Кандинский" }, + { role: "user", content: prompt } + ], + stream: false, + function_call: 'auto' + }, + { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'RqUID': uuidv4(), + } + } + ); + const content = chatResp.data.choices[0].message.content; + const match = content.match(/ { + res.json({ + data: usersDb.getAll(), + _links: makeLinks('/api/user', { + self: '/list', + }), + _meta: {} + }); +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/features/user/user.routes.js b/server/routers/kfu-m-24-1/back-new/features/user/user.routes.js new file mode 100644 index 0000000..f444cad --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/features/user/user.routes.js @@ -0,0 +1,7 @@ +const express = require('express'); +const router = express.Router(); +const ctrl = require('./user.controller'); + +router.get('/list', ctrl.list); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/shared/hateoas.js b/server/routers/kfu-m-24-1/back-new/shared/hateoas.js new file mode 100644 index 0000000..b12ec23 --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/shared/hateoas.js @@ -0,0 +1,8 @@ +function makeLinks(base, links) { + const result = {}; + for (const [rel, path] of Object.entries(links)) { + result[rel] = { href: base + path }; + } + return result; +} +module.exports = makeLinks; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/back-new/shared/usersDb.js b/server/routers/kfu-m-24-1/back-new/shared/usersDb.js new file mode 100644 index 0000000..0b888ef --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/shared/usersDb.js @@ -0,0 +1,20 @@ +let users = [ + { id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User' } +]; +let nextId = 2; + +exports.findUser = (username, email, password) => + users.find(u => (u.username === username || u.email === email) && u.password === password); + +exports.findById = (id) => users.find(u => u.id === id); + +exports.addUser = ({ username, password, email, firstName, lastName }) => { + const newUser = { id: nextId++, username, password, email, firstName, lastName }; + users.push(newUser); + return newUser; +}; + +exports.exists = (username, email) => + users.some(u => u.username === username || u.email === email); + +exports.getAll = () => users; \ No newline at end of file From 800b60fb6dbc806d25d94ceb2d89af0894cc8237 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Mon, 30 Jun 2025 21:36:40 +0000 Subject: [PATCH 083/147] delete server/routers/back-new/server.js --- server/routers/back-new/server.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 server/routers/back-new/server.js diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js deleted file mode 100644 index 093eb01..0000000 --- a/server/routers/back-new/server.js +++ /dev/null @@ -1,5 +0,0 @@ -const app = require('./app'); -const PORT = process.env.PORT || 3002; -app.listen(PORT, () => { - console.log(`Mock backend running on http://localhost:${PORT}`); -}); \ No newline at end of file From f25bae1a08e4315a6804d8f30def01ce13f085ef Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Mon, 30 Jun 2025 21:37:18 +0000 Subject: [PATCH 084/147] update server/routers/back-new/app.js --- server/routers/back-new/app.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/routers/back-new/app.js b/server/routers/back-new/app.js index e87fbc8..d9743a8 100644 --- a/server/routers/back-new/app.js +++ b/server/routers/back-new/app.js @@ -1,24 +1,22 @@ const express = require('express'); -const cors = require('cors'); const featuresConfig = require('./features.config'); const imageRoutes = require('./features/image/image.routes'); -const app = express(); -app.use(cors()); -app.use(express.json()); +const router = express.Router(); +// 动态加载路由 if (featuresConfig.auth) { - app.use('/api/auth', require('./features/auth/auth.routes')); + router.use('/auth', require('./features/auth/auth.routes')); } if (featuresConfig.user) { - app.use('/api/user', require('./features/user/user.routes')); + router.use('/user', require('./features/user/user.routes')); } if (featuresConfig.image) { - app.use('/gigachat', imageRoutes); + router.use('/image', imageRoutes); } -app.get('/api/', (req, res) => { +router.get('/', (req, res) => { res.json({ message: 'API root' }); }); -module.exports = app; \ No newline at end of file +module.exports = router; \ No newline at end of file From d09dbcb69772a80f50eb3937eadbe9c091bfa33d Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Mon, 30 Jun 2025 21:42:11 +0000 Subject: [PATCH 085/147] update server/index.ts --- server/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/index.ts b/server/index.ts index 7f1d913..40861ae 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,6 +20,7 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' +import backNewRouter from './routers/back-new/app' import { setIo } from './io' const { createChatPollingRouter } = require('./routers/kfu-m-24-1/sber_mobile/polling-chat') @@ -113,6 +114,7 @@ const initServer = async () => { app.use("/esc", escRouter) app.use('/connectme', connectmeRouter) app.use('/questioneer', questioneerRouter) + app.use('/back-new', backNewRouter) app.use(errorHandler) From f442544912f2594c741c198196eafcc7a739cb69 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 09:59:29 +0000 Subject: [PATCH 086/147] create server/routers/back-new/server.js --- server/routers/back-new/server.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 server/routers/back-new/server.js diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js new file mode 100644 index 0000000..81f30ba --- /dev/null +++ b/server/routers/back-new/server.js @@ -0,0 +1,18 @@ +const express = require('express'); +const cors = require('cors'); +const dotenv = require('dotenv'); +dotenv.config(); + +const app = express(); +const router = require('./app'); + +app.use(cors()); +app.use(express.json()); + +// 路由前缀要和前端请求一致 +app.use('/ms/back-new', router); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); \ No newline at end of file From de101348fcf1398d852bc81224babcb45bfb6834 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 10:04:03 +0000 Subject: [PATCH 087/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index 81f30ba..876ba5f 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -10,7 +10,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('/ms/back-new', router); +app.use('/ms/back-new/api', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From 5785e50cc51f296dfc973cac4e1e3f628679d15a Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 10:32:36 +0000 Subject: [PATCH 088/147] create server/routers/back-new/.env --- server/routers/back-new/.env | 1 + 1 file changed, 1 insertion(+) create mode 100644 server/routers/back-new/.env diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env new file mode 100644 index 0000000..c7e941b --- /dev/null +++ b/server/routers/back-new/.env @@ -0,0 +1 @@ +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw== From 82e8b785c43d7a9aac19d39afd2ffecd452ec60e Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 11:10:56 +0000 Subject: [PATCH 089/147] update server/routers/back-new/.env --- server/routers/back-new/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env index c7e941b..124b4de 100644 --- a/server/routers/back-new/.env +++ b/server/routers/back-new/.env @@ -1 +1 @@ -GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw== +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjRiOGQxZTEwLWM2ZDAtNDc2Mi1iMjI5LTBjOGJhNzVlYzQzYQ== From d4cc85f644422fd5ce1b531a992c4662fad35514 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 12:17:29 +0000 Subject: [PATCH 090/147] create server/routers/kfu-m-24-1/back-new/.env --- server/routers/kfu-m-24-1/back-new/.env | 1 + 1 file changed, 1 insertion(+) create mode 100644 server/routers/kfu-m-24-1/back-new/.env diff --git a/server/routers/kfu-m-24-1/back-new/.env b/server/routers/kfu-m-24-1/back-new/.env new file mode 100644 index 0000000..c7e941b --- /dev/null +++ b/server/routers/kfu-m-24-1/back-new/.env @@ -0,0 +1 @@ +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw== From 6154932d9e26eb298c598ac39911047c5ba41fa3 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 12:46:40 +0000 Subject: [PATCH 091/147] update server/routers/back-new/.env --- server/routers/back-new/.env | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env index 124b4de..cb06909 100644 --- a/server/routers/back-new/.env +++ b/server/routers/back-new/.env @@ -1 +1,2 @@ -GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjRiOGQxZTEwLWM2ZDAtNDc2Mi1iMjI5LTBjOGJhNzVlYzQzYQ== +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjNlY2I4MmRiLTFmODMtNDJkNS1iOTI4LTQxZDEzYTczNGE2YQ== + From b4858efa738e4b7bea0b7789982e24b073e41c5e Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 12:47:42 +0000 Subject: [PATCH 092/147] update server/routers/back-new/features/image/image.controller.js --- server/routers/back-new/features/image/image.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index 49b4489..3ad1d7c 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -23,7 +23,7 @@ exports.generate = async (req, res) => { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': `Basic ${apiKey}`, - 'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e' + 'RqUID': uuidv4() }, } ); From c2f8d6ecee8b09f89d9f10d1372f2b34db636c2f Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 14:42:09 +0000 Subject: [PATCH 093/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index 876ba5f..81f30ba 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -10,7 +10,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('/ms/back-new/api', router); +app.use('/ms/back-new', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From 279c4fc86df01d154dc3df082d0c99e7d79bf4e9 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 14:50:12 +0000 Subject: [PATCH 094/147] update server/routers/back-new/.env --- server/routers/back-new/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env index cb06909..8a37fc1 100644 --- a/server/routers/back-new/.env +++ b/server/routers/back-new/.env @@ -1,2 +1,2 @@ -GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjNlY2I4MmRiLTFmODMtNDJkNS1iOTI4LTQxZDEzYTczNGE2YQ== +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw== From cede47157ebced1aacedd75a99497d7d50af950d Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 14:53:42 +0000 Subject: [PATCH 095/147] update server/routers/back-new/features/image/image.controller.js --- server/routers/back-new/features/image/image.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index 3ad1d7c..49b4489 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -23,7 +23,7 @@ exports.generate = async (req, res) => { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': `Basic ${apiKey}`, - 'RqUID': uuidv4() + 'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e' }, } ); From ba923b9f91c54266c46bd08cdb4016b0440a6357 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 15:15:32 +0000 Subject: [PATCH 096/147] update server/routers/back-new/.env --- server/routers/back-new/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env index 8a37fc1..b8c46bf 100644 --- a/server/routers/back-new/.env +++ b/server/routers/back-new/.env @@ -1,2 +1,2 @@ -GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw== +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjhjMTBiN2QwLTgxMzctNGNhYi1iMDdhLWU5YWU5ODc4NGFjMQ== From cc41fa73cd171adfbf5d8dfaca06aac4fc36bc54 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 15:30:24 +0000 Subject: [PATCH 097/147] update server/routers/back-new/.env --- server/routers/back-new/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env index b8c46bf..92abb4e 100644 --- a/server/routers/back-new/.env +++ b/server/routers/back-new/.env @@ -1,2 +1,2 @@ -GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjhjMTBiN2QwLTgxMzctNGNhYi1iMDdhLWU5YWU5ODc4NGFjMQ== +GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjUxY2VkNDU5LTMzOWMtNGE3ZS1iMGQ2LWE2ZDcyNjlhMmFiNA== From 87a9b8b02d2811982ffde73a71c00feb0bbb1d57 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 15:59:39 +0000 Subject: [PATCH 098/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index 81f30ba..bf69ef6 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -10,7 +10,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('/ms/back-new', router); +app.use('back-new', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From baba20c028e9992524e0fc72a1b48e2d98de4e57 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 16:03:09 +0000 Subject: [PATCH 099/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index bf69ef6..6bd79ab 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -10,7 +10,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('back-new', router); +app.use('/back-new', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From 7d3b5637593b373f25ec7342785de87f3a17455a Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Tue, 1 Jul 2025 16:06:02 +0000 Subject: [PATCH 100/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index 6bd79ab..81f30ba 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -10,7 +10,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('/back-new', router); +app.use('/ms/back-new', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From 63b25928ffa0fa68957269bd3513931c223dbfc6 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 09:17:24 +0000 Subject: [PATCH 101/147] update server/routers/back-new/features/image/image.controller.js --- .../features/image/image.controller.js | 89 ++++++++++++++----- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index 49b4489..2f8dd6a 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -1,33 +1,41 @@ const axios = require('axios'); const makeLinks = require('../../shared/hateoas'); -const path = require('path'); -const qs = require('qs'); const { v4: uuidv4 } = require('uuid'); -require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -exports.generate = async (req, res) => { - const { prompt } = req.query; - if (!prompt) { - return res.status(400).json({ error: 'Prompt parameter is required' }); - } +// 获取access_token +async function fetchAccessToken(apiKey) { try { - const apiKey = process.env.GIGACHAT_API_KEY; const tokenResp = await axios.post( 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', - { - 'scope':' GIGACHAT_API_PERS', - }, + { scope: 'GIGACHAT_API_PERS' }, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Authorization': `Basic ${apiKey}`, - 'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e' + 'RqUID': uuidv4() }, } ); - const accessToken = tokenResp.data.access_token; + return tokenResp.data.access_token; + } catch (err) { + console.error('AI生成图片出错: 获取access_token失败'); + if (err.response) { + console.error('status:', err.response.status); + console.error('headers:', err.response.headers); + console.error('data:', err.response.data); + console.error('config:', err.config); + } else { + console.error('AI生成图片出错:', err.message); + } + throw new Error('获取access_token失败: ' + err.message); + } +} + +// 调用chat生成图片描述 +async function fetchChatContent(accessToken, prompt) { + try { const chatResp = await axios.post( 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions', { @@ -48,11 +56,25 @@ exports.generate = async (req, res) => { } ); const content = chatResp.data.choices[0].message.content; - const match = content.match(/ { responseType: 'arraybuffer' } ); - res.set('Content-Type', 'image/jpeg'); - res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' }))); - res.send(imageResp.data); + return imageResp.data; } catch (err) { + console.error('AI生成图片出错: 获取图片内容失败'); if (err.response) { - console.error('AI生成图片出错:'); console.error('status:', err.response.status); console.error('headers:', err.response.headers); console.error('data:', err.response.data); @@ -76,6 +96,31 @@ exports.generate = async (req, res) => { } else { console.error('AI生成图片出错:', err.message); } + throw new Error('获取图片内容失败: ' + err.message); + } +} + +exports.generate = async (req, res) => { + const { prompt } = req.query; + if (!prompt) { + return res.status(400).json({ error: 'Prompt parameter is required' }); + } + try { + const apiKey = process.env.GIGACHAT_API_KEY; + const accessToken = await fetchAccessToken(apiKey); + const content = await fetchChatContent(accessToken, prompt); + const match = content.match(/ Date: Wed, 2 Jul 2025 09:27:12 +0000 Subject: [PATCH 102/147] update server/routers/back-new/features/image/image.controller.js --- .../back-new/features/image/image.controller.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index 2f8dd6a..b3f1c6b 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -1,14 +1,25 @@ const axios = require('axios'); const makeLinks = require('../../shared/hateoas'); const { v4: uuidv4 } = require('uuid'); +const qs = require('qs'); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // 获取access_token async function fetchAccessToken(apiKey) { try { + console.log('请求token参数:', { + url: 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', + data: qs.stringify({ scope: 'GIGACHAT_API_PERS' }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + 'Authorization': `Basic ${apiKey}`, + 'RqUID': uuidv4() + } + }); const tokenResp = await axios.post( 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', - { scope: 'GIGACHAT_API_PERS' }, + qs.stringify({ scope: 'GIGACHAT_API_PERS' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', From 5ac9559b8f7bae8bab9d300b21b9487c3130fb7f Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 09:45:02 +0000 Subject: [PATCH 103/147] update server/routers/back-new/features/image/image.controller.js --- .../features/image/image.controller.js | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index b3f1c6b..93733f9 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -2,45 +2,44 @@ const axios = require('axios'); const makeLinks = require('../../shared/hateoas'); const { v4: uuidv4 } = require('uuid'); const qs = require('qs'); +require('dotenv').config(); process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -// 获取access_token -async function fetchAccessToken(apiKey) { +// 获取GigaChat access_token,严格按官方文档 +async function getGigaChatToken() { + const apiKey = process.env.GIGACHAT_API_KEY; + const scope = process.env.GIGACHAT_SCOPE || 'GIGACHAT_API_PERS'; + if (!apiKey) throw new Error('GIGACHAT_API_KEY 未配置'); + + const rqUID = uuidv4(); + const auth = Buffer.from(apiKey.trim()).toString('base64'); + try { - console.log('请求token参数:', { - url: 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', - data: qs.stringify({ scope: 'GIGACHAT_API_PERS' }), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json', - 'Authorization': `Basic ${apiKey}`, - 'RqUID': uuidv4() - } - }); - const tokenResp = await axios.post( + const resp = await axios.post( 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', - qs.stringify({ scope: 'GIGACHAT_API_PERS' }), + new URLSearchParams({ scope }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', - 'Authorization': `Basic ${apiKey}`, - 'RqUID': uuidv4() + 'RqUID': rqUID, + 'Authorization': `Basic ${auth}`, }, + timeout: 10000, } ); - return tokenResp.data.access_token; - } catch (err) { - console.error('AI生成图片出错: 获取access_token失败'); - if (err.response) { - console.error('status:', err.response.status); - console.error('headers:', err.response.headers); - console.error('data:', err.response.data); - console.error('config:', err.config); - } else { - console.error('AI生成图片出错:', err.message); + if (!resp.data.access_token) { + console.error('GigaChat token响应异常:', resp.data); + throw new Error('GigaChat token响应异常'); } - throw new Error('获取access_token失败: ' + err.message); + return resp.data.access_token; + } catch (err) { + if (err.response) { + console.error('获取access_token失败:', err.response.status, err.response.data); + } else { + console.error('获取access_token异常:', err.message); + } + throw new Error('获取access_token失败'); } } @@ -116,9 +115,13 @@ exports.generate = async (req, res) => { if (!prompt) { return res.status(400).json({ error: 'Prompt parameter is required' }); } + let accessToken; + try { + accessToken = await getGigaChatToken(); + } catch (e) { + return res.status(500).json({ error: e.message }); + } try { - const apiKey = process.env.GIGACHAT_API_KEY; - const accessToken = await fetchAccessToken(apiKey); const content = await fetchChatContent(accessToken, prompt); const match = content.match(/ Date: Wed, 2 Jul 2025 09:46:19 +0000 Subject: [PATCH 104/147] update server/routers/back-new/.env --- server/routers/back-new/.env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env index 92abb4e..2261982 100644 --- a/server/routers/back-new/.env +++ b/server/routers/back-new/.env @@ -1,2 +1,2 @@ -GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjUxY2VkNDU5LTMzOWMtNGE3ZS1iMGQ2LWE2ZDcyNjlhMmFiNA== - +GIGACHAT_API_KEY=78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974 +GIGACHAT_SCOPE=GIGACHAT_API_PERS \ No newline at end of file From ca01d1c53858cb7a0dd193da2c6dca9298d2c072 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 09:53:14 +0000 Subject: [PATCH 105/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index 81f30ba..f52e9d1 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -1,7 +1,6 @@ +require('dotenv').config(); const express = require('express'); const cors = require('cors'); -const dotenv = require('dotenv'); -dotenv.config(); const app = express(); const router = require('./app'); @@ -10,7 +9,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('/ms/back-new', router); +app.use('back-new', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From 1383e360a1f7e716ac18eef73668cb2e579c6a2f Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 10:02:36 +0000 Subject: [PATCH 106/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index f52e9d1..c24c2e2 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -1,4 +1,5 @@ require('dotenv').config(); +console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY); const express = require('express'); const cors = require('cors'); @@ -9,7 +10,7 @@ app.use(cors()); app.use(express.json()); // 路由前缀要和前端请求一致 -app.use('back-new', router); +app.use('/ms/back-new', router); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { From 63a825f153d1086ad0e13dbbb7bdf204125ca810 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 10:28:05 +0000 Subject: [PATCH 107/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index c24c2e2..2368161 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -1,3 +1,5 @@ +process.env.GIGACHAT_API_KEY = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974'; +process.env.GIGACHAT_SCOPE = 'GIGACHAT_API_PERS'; require('dotenv').config(); console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY); const express = require('express'); From 1500486cd827c90f6c7dd69e5d3560be3be028ba Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 18:03:43 +0000 Subject: [PATCH 108/147] update server/routers/back-new/features/image/image.controller.js --- server/routers/back-new/features/image/image.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index 93733f9..0ac2e00 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -7,7 +7,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // 获取GigaChat access_token,严格按官方文档 async function getGigaChatToken() { - const apiKey = process.env.GIGACHAT_API_KEY; + const apiKey = process.env.GIGACHAT_API_KEY || 'KEYVALUE'; const scope = process.env.GIGACHAT_SCOPE || 'GIGACHAT_API_PERS'; if (!apiKey) throw new Error('GIGACHAT_API_KEY 未配置'); From 256de78e64fa6e4fe03abf5ed18590067ea3c869 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Wed, 2 Jul 2025 18:24:58 +0000 Subject: [PATCH 109/147] update server/routers/back-new/features/image/image.controller.js --- server/routers/back-new/features/image/image.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index 0ac2e00..b0ccd7d 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -7,7 +7,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // 获取GigaChat access_token,严格按官方文档 async function getGigaChatToken() { - const apiKey = process.env.GIGACHAT_API_KEY || 'KEYVALUE'; + const apiKey = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974'; const scope = process.env.GIGACHAT_SCOPE || 'GIGACHAT_API_PERS'; if (!apiKey) throw new Error('GIGACHAT_API_KEY 未配置'); From 659f9fd684e8b03fa367e3ed438fae16a42eb6bb Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Thu, 3 Jul 2025 11:12:28 +0000 Subject: [PATCH 110/147] update server/routers/back-new/features/auth/auth.routes.js --- server/routers/back-new/features/auth/auth.routes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/routers/back-new/features/auth/auth.routes.js b/server/routers/back-new/features/auth/auth.routes.js index d983771..0262123 100644 --- a/server/routers/back-new/features/auth/auth.routes.js +++ b/server/routers/back-new/features/auth/auth.routes.js @@ -6,5 +6,6 @@ router.post('/login', ctrl.login); router.post('/register', ctrl.register); router.get('/profile/', ctrl.profile); router.post('/logout', ctrl.logout); +router.put('/profile/', ctrl.updateProfile); module.exports = router; \ No newline at end of file From f5faae79070043194e1af52fe8e23b0e42764752 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Thu, 3 Jul 2025 11:13:17 +0000 Subject: [PATCH 111/147] update server/routers/back-new/features/auth/auth.controller.js --- server/routers/back-new/features/auth/auth.controller.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/routers/back-new/features/auth/auth.controller.js b/server/routers/back-new/features/auth/auth.controller.js index 21c400f..f97b846 100644 --- a/server/routers/back-new/features/auth/auth.controller.js +++ b/server/routers/back-new/features/auth/auth.controller.js @@ -92,4 +92,13 @@ exports.logout = (req, res) => { }), _meta: {} }); +}; + +exports.updateProfile = (req, res) => { + const userId = req.user?.id || req.body.id; // 这里假设有用户认证中间件,否则用body.id + if (!userId) return res.status(401).json({ error: 'Unauthorized' }); + const { firstName, lastName, bio, location, website } = req.body; + const updated = require('../../shared/usersDb').updateUser(userId, { firstName, lastName, bio, location, website }); + if (!updated) return res.status(404).json({ error: 'User not found' }); + res.json({ success: true, user: updated }); }; \ No newline at end of file From 00386cc13584b49dccb101560082bfc63940f926 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Thu, 3 Jul 2025 11:13:50 +0000 Subject: [PATCH 112/147] update server/routers/back-new/shared/usersDb.js --- server/routers/back-new/shared/usersDb.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/server/routers/back-new/shared/usersDb.js b/server/routers/back-new/shared/usersDb.js index 0b888ef..fcc21db 100644 --- a/server/routers/back-new/shared/usersDb.js +++ b/server/routers/back-new/shared/usersDb.js @@ -1,5 +1,5 @@ let users = [ - { id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User' } + { id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User', bio: '', location: '', website: '' } ]; let nextId = 2; @@ -8,8 +8,8 @@ exports.findUser = (username, email, password) => exports.findById = (id) => users.find(u => u.id === id); -exports.addUser = ({ username, password, email, firstName, lastName }) => { - const newUser = { id: nextId++, username, password, email, firstName, lastName }; +exports.addUser = ({ username, password, email, firstName, lastName, bio = '', location = '', website = '' }) => { + const newUser = { id: nextId++, username, password, email, firstName, lastName, bio, location, website }; users.push(newUser); return newUser; }; @@ -17,4 +17,12 @@ exports.addUser = ({ username, password, email, firstName, lastName }) => { exports.exists = (username, email) => users.some(u => u.username === username || u.email === email); -exports.getAll = () => users; \ No newline at end of file +exports.getAll = () => users; + +// 新增:更新用户信息 +exports.updateUser = (id, update) => { + const user = users.find(u => u.id === id); + if (!user) return null; + Object.assign(user, update); + return user; +}; \ No newline at end of file From 80498a0ff0d70ff1c6d491f368a99cfa7daa84d6 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Thu, 3 Jul 2025 13:15:52 +0000 Subject: [PATCH 113/147] update server/routers/back-new/features/auth/auth.controller.js --- server/routers/back-new/features/auth/auth.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/back-new/features/auth/auth.controller.js b/server/routers/back-new/features/auth/auth.controller.js index f97b846..8f57b3a 100644 --- a/server/routers/back-new/features/auth/auth.controller.js +++ b/server/routers/back-new/features/auth/auth.controller.js @@ -97,8 +97,8 @@ exports.logout = (req, res) => { exports.updateProfile = (req, res) => { const userId = req.user?.id || req.body.id; // 这里假设有用户认证中间件,否则用body.id if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - const { firstName, lastName, bio, location, website } = req.body; - const updated = require('../../shared/usersDb').updateUser(userId, { firstName, lastName, bio, location, website }); + const { firstName, lastName, bio, location, website, email, username, password } = req.body; + const updated = require('../../shared/usersDb').updateUser(userId, { firstName, lastName, bio, location, website, email, username, password }); if (!updated) return res.status(404).json({ error: 'User not found' }); res.json({ success: true, user: updated }); }; \ No newline at end of file From 4ef4dd3c1b210d84e34471ade1f368370dc7cfe7 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Thu, 3 Jul 2025 13:28:29 +0000 Subject: [PATCH 114/147] upload files --- server/routers/back-new/middleware/auth.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 server/routers/back-new/middleware/auth.js diff --git a/server/routers/back-new/middleware/auth.js b/server/routers/back-new/middleware/auth.js new file mode 100644 index 0000000..d1cc427 --- /dev/null +++ b/server/routers/back-new/middleware/auth.js @@ -0,0 +1,11 @@ +// 简单token认证中间件,支持token-3格式 +module.exports = function (req, res, next) { + const auth = req.headers.authorization; + if (auth && auth.startsWith('Bearer token-')) { + const id = parseInt(auth.replace('Bearer token-', '')); + if (!isNaN(id)) { + req.user = { id }; + } + } + next(); +}; \ No newline at end of file From 34163788f3656078183f1a43ed719df4987c7b68 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Thu, 3 Jul 2025 13:29:35 +0000 Subject: [PATCH 115/147] update server/routers/back-new/server.js --- server/routers/back-new/server.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js index 2368161..977f24d 100644 --- a/server/routers/back-new/server.js +++ b/server/routers/back-new/server.js @@ -8,8 +8,29 @@ const cors = require('cors'); const app = express(); const router = require('./app'); +app.use(cors()); +app.use(express.json());process.env.GIGACHAT_API_KEY = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974'; +process.env.GIGACHAT_SCOPE = 'GIGACHAT_API_PERS'; +require('dotenv').config(); +console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY); +const express = require('express'); +const cors = require('cors'); +const authMiddleware = require('./middleware/auth'); + +const app = express(); +const router = require('./app'); + app.use(cors()); app.use(express.json()); +app.use(authMiddleware); + +// 路由前缀要和前端请求一致 +app.use('/ms/back-new', router); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); // 路由前缀要和前端请求一致 app.use('/ms/back-new', router); From 351ea750728d347e2b35834fa46ee504f5b87ea2 Mon Sep 17 00:00:00 2001 From: "xingzhe.ru" Date: Fri, 4 Jul 2025 00:21:13 +0000 Subject: [PATCH 116/147] update server/routers/back-new/features/image/image.controller.js --- .../features/image/image.controller.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js index b0ccd7d..ada3233 100644 --- a/server/routers/back-new/features/image/image.controller.js +++ b/server/routers/back-new/features/image/image.controller.js @@ -110,6 +110,20 @@ async function fetchImageContent(accessToken, imageId) { } } +// 工具函数:异步重试 +async function retryAsync(fn, times = 3, delay = 800) { + let lastErr; + for (let i = 0; i < times; i++) { + try { + return await fn(); + } catch (err) { + lastErr = err; + if (i < times - 1 && delay) await new Promise(r => setTimeout(r, delay)); + } + } + throw lastErr; +} + exports.generate = async (req, res) => { const { prompt } = req.query; if (!prompt) { @@ -122,14 +136,17 @@ exports.generate = async (req, res) => { return res.status(500).json({ error: e.message }); } try { - const content = await fetchChatContent(accessToken, prompt); - const match = content.match(/ fetchChatContent(accessToken, prompt), 3, 800); + // 升级正则,兼容更多图片标签格式 + const match = content.match(/]+src=['"]([^'"]+)['"]/); if (!match) { console.error('AI生成图片出错: GigaChat未返回图片标签'); return res.status(500).json({ error: 'No image generated' }); } const imageId = match[1]; - const imageData = await fetchImageContent(accessToken, imageId); + // 2. 重试获取图片内容 + const imageData = await retryAsync(() => fetchImageContent(accessToken, imageId), 3, 800); res.set('Content-Type', 'image/jpeg'); res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' }))); res.send(imageData); From d049c29f930d1a02656ef6dc9d27c3166ffceea5 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Tue, 23 Sep 2025 14:23:52 +0300 Subject: [PATCH 117/147] =?UTF-8?q?=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 222 ++-- package.json | 19 +- server/index.ts | 10 - server/routers/back-new/.env | 2 - server/routers/back-new/.gitignore | 2 - server/routers/back-new/README.md | 21 - server/routers/back-new/app.js | 22 - server/routers/back-new/features.config.js | 5 - .../back-new/features/auth/auth.controller.js | 104 -- .../back-new/features/auth/auth.routes.js | 11 - .../features/image/image.controller.js | 157 --- .../back-new/features/image/image.routes.js | 7 - .../back-new/features/user/user.controller.js | 12 - .../back-new/features/user/user.routes.js | 7 - server/routers/back-new/middleware/auth.js | 11 - server/routers/back-new/package-lock.json | 1024 ----------------- server/routers/back-new/package.json | 17 - server/routers/back-new/server.js | 41 - server/routers/back-new/shared/hateoas.js | 8 - server/routers/back-new/shared/usersDb.js | 28 - server/routers/kfu-m-24-1/index.js | 1 - .../kfu-m-24-1/sber_mobile/DB_Scheme.txt | 231 ---- .../sber_mobile/additional_services.js | 53 - .../kfu-m-24-1/sber_mobile/apartments.js | 45 - server/routers/kfu-m-24-1/sber_mobile/auth.js | 51 - .../kfu-m-24-1/sber_mobile/buildings.js | 14 - .../routers/kfu-m-24-1/sber_mobile/cameras.js | 28 - .../chat-ai-agent/chat-moderation.ts | 78 -- .../sber_mobile/chat-ai-agent/gigachat.ts | 18 - .../chat-ai-agent/moderation-config.js | 16 - .../routers/kfu-m-24-1/sber_mobile/chats.js | 218 ---- .../kfu-m-24-1/sber_mobile/get-constants.js | 90 -- .../routers/kfu-m-24-1/sber_mobile/index.js | 41 - .../sber_mobile/initiatives-ai-agents/llm.ts | 22 - .../initiatives-ai-agents/moderation.ts | 56 - .../initiatives-ai-agents/picture.ts | 38 - .../kfu-m-24-1/sber_mobile/initiatives.js | 101 -- .../routers/kfu-m-24-1/sber_mobile/media.js | 15 - .../kfu-m-24-1/sber_mobile/messages.js | 235 ---- .../kfu-m-24-1/sber_mobile/moderate.js | 162 --- .../kfu-m-24-1/sber_mobile/moderation.js | 53 - .../kfu-m-24-1/sber_mobile/polling-chat.js | 982 ---------------- .../routers/kfu-m-24-1/sber_mobile/profile.js | 119 -- .../kfu-m-24-1/sber_mobile/supabaseClient.js | 79 -- .../support-ai-agent/create-ticket-tool.ts | 66 -- .../sber_mobile/support-ai-agent/gigachat.ts | 20 - .../support-ai-agent/knowledge-base-tool.ts | 41 - .../support-ai-agent/support-agent.ts | 167 --- .../support-ai-agent/support-context-tool.ts | 56 - .../support-ai-agent/vector-store.ts | 33 - .../kfu-m-24-1/sber_mobile/supportApi.js | 151 --- .../routers/kfu-m-24-1/sber_mobile/tickets.js | 31 - .../kfu-m-24-1/sber_mobile/user_apartments.js | 18 - .../sber_mobile/utility_payments.js | 50 - .../routers/kfu-m-24-1/sber_mobile/votes.js | 105 -- 55 files changed, 144 insertions(+), 5070 deletions(-) delete mode 100644 server/routers/back-new/.env delete mode 100644 server/routers/back-new/.gitignore delete mode 100644 server/routers/back-new/README.md delete mode 100644 server/routers/back-new/app.js delete mode 100644 server/routers/back-new/features.config.js delete mode 100644 server/routers/back-new/features/auth/auth.controller.js delete mode 100644 server/routers/back-new/features/auth/auth.routes.js delete mode 100644 server/routers/back-new/features/image/image.controller.js delete mode 100644 server/routers/back-new/features/image/image.routes.js delete mode 100644 server/routers/back-new/features/user/user.controller.js delete mode 100644 server/routers/back-new/features/user/user.routes.js delete mode 100644 server/routers/back-new/middleware/auth.js delete mode 100644 server/routers/back-new/package-lock.json delete mode 100644 server/routers/back-new/package.json delete mode 100644 server/routers/back-new/server.js delete mode 100644 server/routers/back-new/shared/hateoas.js delete mode 100644 server/routers/back-new/shared/usersDb.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/additional_services.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/apartments.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/auth.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/buildings.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/cameras.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/chats.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/get-constants.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/index.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/initiatives.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/media.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/messages.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderate.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/moderation.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/polling-chat.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/profile.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/supportApi.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/tickets.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/user_apartments.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/utility_payments.js delete mode 100644 server/routers/kfu-m-24-1/sber_mobile/votes.js diff --git a/package-lock.json b/package-lock.json index 7313728..540a4cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,9 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@langchain/community": "^0.3.41", - "@langchain/core": "^0.3.46", - "@langchain/langgraph": "^0.2.65", - "@supabase/supabase-js": "^2.49.4", + "@langchain/community": "^0.3.56", + "@langchain/core": "^0.3.77", + "@langchain/langgraph": "^0.4.9", "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", @@ -25,15 +24,15 @@ "express": "5.0.1", "express-jwt": "^8.5.1", "express-session": "^1.18.1", - "gigachat": "^0.0.14", + "gigachat": "^0.0.16", "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", - "langchain": "^0.3.7", - "langchain-gigachat": "^0.0.11", - "mongodb": "^6.12.0", - "mongoose": "^8.9.2", + "langchain": "^0.3.34", + "langchain-gigachat": "^0.0.14", + "mongodb": "^6.20.0", + "mongoose": "^8.18.2", "mongoose-sequence": "^6.0.1", - "morgan": "^1.10.0", + "morgan": "^1.10.1", "multer": "^1.4.5-lts.1", "pbkdf2-password": "^1.2.1", "rotating-file-stream": "^3.2.5", @@ -1928,19 +1927,19 @@ } }, "node_modules/@langchain/community": { - "version": "0.3.46", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.46.tgz", - "integrity": "sha512-loix9LkoNcn1gQlVCopmrJW9TmgZb+YpZw7nkFzXT6ozR8ZDh1XlFq1ymR5gTFtdNzF0neK2oJtE9iEl1lm7Dw==", + "version": "0.3.56", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.56.tgz", + "integrity": "sha512-lDjUnRfHAX7aMXyEB2EWbe5qOmdQdz8n+0CNQ4ExpLy3NOFQhEVkWclhsucaX04zh0r/VH5Pkk9djpnhPBDH7g==", "license": "MIT", "dependencies": { - "@langchain/openai": ">=0.2.0 <0.6.0", + "@langchain/openai": ">=0.2.0 <0.7.0", "@langchain/weaviate": "^0.2.0", "binary-extensions": "^2.2.0", "expr-eval": "^2.0.2", "flat": "^5.0.2", "js-yaml": "^4.1.0", "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", - "langsmith": "^0.3.29", + "langsmith": "^0.3.67", "uuid": "^10.0.0", "zod": "^3.25.32" }, @@ -1975,10 +1974,10 @@ "@google-ai/generativelanguage": "*", "@google-cloud/storage": "^6.10.1 || ^7.7.0", "@gradientai/nodejs-sdk": "^1.2.0", - "@huggingface/inference": "^2.6.4", - "@huggingface/transformers": "^3.2.3", + "@huggingface/inference": "^4.0.5", + "@huggingface/transformers": "^3.5.2", "@ibm-cloud/watsonx-ai": "*", - "@lancedb/lancedb": "^0.12.0", + "@lancedb/lancedb": "^0.19.1", "@langchain/core": ">=0.3.58 <0.4.0", "@layerup/layerup-security": "^1.5.12", "@libsql/client": "^0.14.0", @@ -1991,7 +1990,7 @@ "@pinecone-database/pinecone": "*", "@planetscale/database": "^1.8.0", "@premai/prem-sdk": "^0.3.25", - "@qdrant/js-client-rest": "^1.8.2", + "@qdrant/js-client-rest": "^1.15.0", "@raycast/api": "^1.55.2", "@rockset/client": "^0.9.1", "@smithy/eventstream-codec": "^2.0.5", @@ -2027,11 +2026,10 @@ "crypto-js": "^4.2.0", "d3-dsv": "^2.0.0", "discord.js": "^14.14.1", - "dria": "^0.0.3", "duck-duck-scrape": "^2.2.5", "epub2": "^3.0.1", "fast-xml-parser": "*", - "firebase-admin": "^11.9.0 || ^12.0.0", + "firebase-admin": "^11.9.0 || ^12.0.0 || ^13.0.0", "google-auth-library": "*", "googleapis": "*", "hnswlib-node": "^3.0.0", @@ -2049,7 +2047,7 @@ "mammoth": "^1.6.0", "mariadb": "^3.4.0", "mem0ai": "^2.1.8", - "mongodb": ">=5.2.0", + "mongodb": "^6.17.0", "mysql2": "^3.9.8", "neo4j-driver": "*", "notion-to-md": "^3.1.0", @@ -2309,9 +2307,6 @@ "discord.js": { "optional": true }, - "dria": { - "optional": true - }, "duck-duck-scrape": { "optional": true }, @@ -2466,9 +2461,9 @@ } }, "node_modules/@langchain/core": { - "version": "0.3.58", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.58.tgz", - "integrity": "sha512-HLkOtVofgBHefaUae/+2fLNkpMLzEjHSavTmUF0YC7bDa5NPIZGlP80CGrSFXAeJ+WCPd8rIK8K/p6AW94inUQ==", + "version": "0.3.77", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.77.tgz", + "integrity": "sha512-aqXHea9xfpVn6VoCq9pjujwFqrh3vw3Fgm9KFUZJ1cF7Bx5HI62DvQPw8LlRB3NB4dhwBBA1ldAVkkkd1du8nA==", "license": "MIT", "dependencies": { "@cfworker/json-schema": "^4.0.2", @@ -2476,7 +2471,7 @@ "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.3.29", + "langsmith": "^0.3.67", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -2526,21 +2521,21 @@ } }, "node_modules/@langchain/langgraph": { - "version": "0.2.74", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.74.tgz", - "integrity": "sha512-oHpEi5sTZTPaeZX1UnzfM2OAJ21QGQrwReTV6+QnX7h8nDCBzhtipAw1cK616S+X8zpcVOjgOtJuaJhXa4mN8w==", + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.4.9.tgz", + "integrity": "sha512-+rcdTGi4Ium4X/VtIX3Zw4RhxEkYWpwUyz806V6rffjHOAMamg6/WZDxpJbrP33RV/wJG1GH12Z29oX3Pqq3Aw==", "license": "MIT", "dependencies": { - "@langchain/langgraph-checkpoint": "~0.0.17", - "@langchain/langgraph-sdk": "~0.0.32", + "@langchain/langgraph-checkpoint": "^0.1.1", + "@langchain/langgraph-sdk": "~0.1.0", "uuid": "^10.0.0", - "zod": "^3.23.8" + "zod": "^3.25.32" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@langchain/core": ">=0.2.36 <0.3.0 || >=0.3.40 < 0.4.0", + "@langchain/core": ">=0.3.58 < 0.4.0", "zod-to-json-schema": "^3.x" }, "peerDependenciesMeta": { @@ -2550,9 +2545,9 @@ } }, "node_modules/@langchain/langgraph-checkpoint": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.0.18.tgz", - "integrity": "sha512-IS7zJj36VgY+4pf8ZjsVuUWef7oTwt1y9ylvwu0aLuOn1d0fg05Om9DLm3v2GZ2Df6bhLV1kfWAM0IAl9O5rQQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-0.1.1.tgz", + "integrity": "sha512-h2bP0RUikQZu0Um1ZUPErQLXyhzroJqKRbRcxYRTAh49oNlsfeq4A3K4YEDRbGGuyPZI/Jiqwhks1wZwY73AZw==", "license": "MIT", "dependencies": { "uuid": "^10.0.0" @@ -2561,7 +2556,7 @@ "node": ">=18" }, "peerDependencies": { - "@langchain/core": ">=0.2.31 <0.4.0" + "@langchain/core": ">=0.2.31 <0.4.0 || ^1.0.0-alpha" } }, "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { @@ -2578,9 +2573,9 @@ } }, "node_modules/@langchain/langgraph-sdk": { - "version": "0.0.84", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.84.tgz", - "integrity": "sha512-l0PFQyJ+6m6aclORNPPWlcRwgKcXVXsPaJCbCUYFABR3yf4cOpsjhUNR0cJ7+2cS400oieHjGRdGGyO/hbSjhg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.1.6.tgz", + "integrity": "sha512-PeXxfo4ls8yql6YdW8qjnZgp1giy7oqJiGjy4j2OSJ7lpkir8n62YpvADDByEh9sPzGLJYh92ZUAh0GNfQ18vA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.15", @@ -2589,8 +2584,9 @@ "uuid": "^9.0.0" }, "peerDependencies": { - "@langchain/core": ">=0.2.31 <0.4.0", - "react": "^18 || ^19" + "@langchain/core": ">=0.2.31 <0.4.0 || ^1.0.0-alpha", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" }, "peerDependenciesMeta": { "@langchain/core": { @@ -2598,6 +2594,9 @@ }, "react": { "optional": true + }, + "react-dom": { + "optional": true } } }, @@ -2737,9 +2736,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -2871,6 +2870,8 @@ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz", "integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -2880,6 +2881,8 @@ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -2889,6 +2892,8 @@ "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -2901,6 +2906,8 @@ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz", "integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -2910,6 +2917,8 @@ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14", "@types/phoenix": "^1.5.4", @@ -2922,6 +2931,8 @@ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@supabase/node-fetch": "^2.6.14" } @@ -2931,6 +2942,8 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz", "integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@supabase/auth-js": "2.69.1", "@supabase/functions-js": "2.4.4", @@ -3133,7 +3146,9 @@ "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/retry": { "version": "0.12.0", @@ -3195,6 +3210,8 @@ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -3802,9 +3819,9 @@ } }, "node_modules/bson": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz", - "integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", "license": "Apache-2.0", "engines": { "node": ">=16.20.1" @@ -5760,9 +5777,9 @@ } }, "node_modules/gigachat": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.14.tgz", - "integrity": "sha512-BwXDecDxF6aKJT+juuoATrBnFLDBg5Vho1dxYRsgM18zgZ55q5SwNiOgC05/J7rhGY66Pj6Wsnvk3FC6K4IMQw==", + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.16.tgz", + "integrity": "sha512-37MFTFltKGDE1EDW6y87BxzU5orIU3fpLDqAMHCNdV8JUL2oNbHMe6CACWWqUh7HLaztwkysRP8nJxBYBms1gg==", "license": "ISC", "dependencies": { "axios": "^1.8.2", @@ -7389,17 +7406,17 @@ } }, "node_modules/langchain": { - "version": "0.3.28", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.28.tgz", - "integrity": "sha512-h4GGlBJNGU/Sj2PipW9kL+ewj7To3c+SnnNKH3HZaVHEqGPMHVB96T1lLjtCLcZCyUfabMr/zFIkLNI4War+Xg==", + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.34.tgz", + "integrity": "sha512-OADHLQYRX+36EqQBxIoryCdMKfHex32cJBSWveadIIeRhygqivacIIDNwVjX51Y++c80JIdR0jaQHWn2r3H1iA==", "license": "MIT", "dependencies": { - "@langchain/openai": ">=0.1.0 <0.6.0", + "@langchain/openai": ">=0.1.0 <0.7.0", "@langchain/textsplitters": ">=0.0.0 <0.2.0", "js-tiktoken": "^1.0.12", "js-yaml": "^4.1.0", "jsonpointer": "^5.0.1", - "langsmith": "^0.3.29", + "langsmith": "^0.3.67", "openapi-types": "^12.1.3", "p-retry": "4", "uuid": "^10.0.0", @@ -7484,12 +7501,12 @@ } }, "node_modules/langchain-gigachat": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/langchain-gigachat/-/langchain-gigachat-0.0.11.tgz", - "integrity": "sha512-2hYES1Dt0U/p/h+F+/1lDfmaYTWQyuHG5KAAIQGYygursAUGDDoyKQlGywbJ4JgmENy4u5fv7keVC9+k0X8tbQ==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/langchain-gigachat/-/langchain-gigachat-0.0.14.tgz", + "integrity": "sha512-8jnHMZI1QqAs98iTdldouT1chiFRTtEnxXHFiQl8th7u/B6Eot0OJfMT5iviCFO6/pMNxYgq0Fzzr29ndaJyEQ==", "license": "MIT", "dependencies": { - "gigachat": "^0.0.14", + "gigachat": "^0.0.15", "uuid": "^11.0.5", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.5" @@ -7501,6 +7518,16 @@ "@langchain/core": ">=0.2.21 <0.4.0" } }, + "node_modules/langchain-gigachat/node_modules/gigachat": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/gigachat/-/gigachat-0.0.15.tgz", + "integrity": "sha512-4hAf/obnzwW4xp+AOP6Zv81F3Dr9QcsEjVOGTdY4aRWphzgV8YVZ134huqQfA/LQCuoD9UMmlt3nfix6exgjYg==", + "license": "ISC", + "dependencies": { + "axios": "^1.8.2", + "uuid": "^11.0.3" + } + }, "node_modules/langchain/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -7515,9 +7542,9 @@ } }, "node_modules/langsmith": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.31.tgz", - "integrity": "sha512-9lwuLZuN3tXFYQ6eMg0rmbBw7oxQo4bu1NYeylbjz27bOdG1XB9XNoxaiIArkK4ciLdOIOhPMBXP4bkvZOgHRw==", + "version": "0.3.69", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.69.tgz", + "integrity": "sha512-YKzu92YAP2o+d+1VmR38xqFX0RIRLKYj1IqdflVEY83X0FoiVlrWO3xDLXgnu7vhZ2N2M6jx8VO9fVF8yy9gHA==", "license": "MIT", "dependencies": { "@types/uuid": "^10.0.0", @@ -7529,9 +7556,21 @@ "uuid": "^10.0.0" }, "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, "openai": { "optional": true } @@ -7862,14 +7901,14 @@ } }, "node_modules/mongodb": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz", - "integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", - "mongodb-connection-string-url": "^3.0.0" + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" @@ -7880,7 +7919,7 @@ "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", + "snappy": "^7.3.2", "socks": "^2.7.1" }, "peerDependenciesMeta": { @@ -7952,14 +7991,14 @@ } }, "node_modules/mongoose": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz", - "integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.2.tgz", + "integrity": "sha512-gA6GFlshOHUdNyw9OQTmMLSGzVOPbcbjaSZ1dvR5iMp668N2UUznTuzgTY6V6Q41VtBc4kmL/qqML1RNgXB5Fg==", "license": "MIT", "dependencies": { - "bson": "^6.10.1", + "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "~6.12.0", + "mongodb": "~6.18.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -7987,13 +8026,13 @@ } }, "node_modules/mongoose/node_modules/mongodb": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", - "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.1", + "bson": "^6.10.4", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -8039,16 +8078,16 @@ "license": "MIT" }, "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", - "on-headers": "~1.0.2" + "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" @@ -8066,6 +8105,15 @@ "node": ">= 0.8" } }, + "node_modules/morgan/node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", diff --git a/package.json b/package.json index a43b5aa..948f3ea 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,9 @@ "license": "MIT", "homepage": "https://bitbucket.org/online-mentor/multi-stub#readme", "dependencies": { - "@supabase/supabase-js": "^2.49.4", - "@langchain/community": "^0.3.41", - "@langchain/core": "^0.3.46", - "@langchain/langgraph": "^0.2.65", + "@langchain/community": "^0.3.56", + "@langchain/core": "^0.3.77", + "@langchain/langgraph": "^0.4.9", "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", @@ -37,15 +36,15 @@ "express": "5.0.1", "express-jwt": "^8.5.1", "express-session": "^1.18.1", - "gigachat": "^0.0.14", + "gigachat": "^0.0.16", "jsdom": "^25.0.1", "jsonwebtoken": "^9.0.2", - "langchain": "^0.3.7", - "langchain-gigachat": "^0.0.11", - "mongodb": "^6.12.0", - "mongoose": "^8.9.2", + "langchain": "^0.3.34", + "langchain-gigachat": "^0.0.14", + "mongodb": "^6.20.0", + "mongoose": "^8.18.2", "mongoose-sequence": "^6.0.1", - "morgan": "^1.10.0", + "morgan": "^1.10.1", "multer": "^1.4.5-lts.1", "pbkdf2-password": "^1.2.1", "rotating-file-stream": "^3.2.5", diff --git a/server/index.ts b/server/index.ts index 40861ae..fe23c60 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,9 +20,7 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' -import backNewRouter from './routers/back-new/app' import { setIo } from './io' -const { createChatPollingRouter } = require('./routers/kfu-m-24-1/sber_mobile/polling-chat') export const app = express() @@ -90,18 +88,11 @@ const initServer = async () => { ) app.use(root) - // Инициализация Polling для чата (после настройки middleware) - const { router: chatPollingRouter, chatHandler } = createChatPollingRouter(express) - /** * Добавляйте сюда свои routers. */ app.use("/kfu-m-24-1", kfuM241Router) - - // Добавляем Polling роутер для чата - app.use("/kfu-m-24-1/sber_mobile", chatPollingRouter) - app.use("/epja-2024-1", epja20241Router) app.use("/v1/todo", todoRouter) app.use("/dogsitters-finder", dogsittersFinderRouter) @@ -114,7 +105,6 @@ const initServer = async () => { app.use("/esc", escRouter) app.use('/connectme', connectmeRouter) app.use('/questioneer', questioneerRouter) - app.use('/back-new', backNewRouter) app.use(errorHandler) diff --git a/server/routers/back-new/.env b/server/routers/back-new/.env deleted file mode 100644 index 2261982..0000000 --- a/server/routers/back-new/.env +++ /dev/null @@ -1,2 +0,0 @@ -GIGACHAT_API_KEY=78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974 -GIGACHAT_SCOPE=GIGACHAT_API_PERS \ No newline at end of file diff --git a/server/routers/back-new/.gitignore b/server/routers/back-new/.gitignore deleted file mode 100644 index 9439df7..0000000 --- a/server/routers/back-new/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -.env \ No newline at end of file diff --git a/server/routers/back-new/README.md b/server/routers/back-new/README.md deleted file mode 100644 index 97811d1..0000000 --- a/server/routers/back-new/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# back-new - -非Python实现的后端(Node.js + Express) - -## 启动方法 - -1. 安装依赖: - ```bash - npm install - ``` -2. 启动服务: - ```bash - npm start - ``` - -默认端口:`3002` - -## 支持接口 -- POST `/api/auth/login` 用户登录 -- POST `/api/auth/register` 用户注册 -- GET `/gigachat/prompt?prompt=xxx` 生成图片(返回模拟图片链接) \ No newline at end of file diff --git a/server/routers/back-new/app.js b/server/routers/back-new/app.js deleted file mode 100644 index d9743a8..0000000 --- a/server/routers/back-new/app.js +++ /dev/null @@ -1,22 +0,0 @@ -const express = require('express'); -const featuresConfig = require('./features.config'); -const imageRoutes = require('./features/image/image.routes'); - -const router = express.Router(); - -// 动态加载路由 -if (featuresConfig.auth) { - router.use('/auth', require('./features/auth/auth.routes')); -} -if (featuresConfig.user) { - router.use('/user', require('./features/user/user.routes')); -} -if (featuresConfig.image) { - router.use('/image', imageRoutes); -} - -router.get('/', (req, res) => { - res.json({ message: 'API root' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/back-new/features.config.js b/server/routers/back-new/features.config.js deleted file mode 100644 index 7a69e24..0000000 --- a/server/routers/back-new/features.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - auth: true, - user: true, - image: true, // 关闭为 false -}; \ No newline at end of file diff --git a/server/routers/back-new/features/auth/auth.controller.js b/server/routers/back-new/features/auth/auth.controller.js deleted file mode 100644 index 8f57b3a..0000000 --- a/server/routers/back-new/features/auth/auth.controller.js +++ /dev/null @@ -1,104 +0,0 @@ -const usersDb = require('../../shared/usersDb'); -const makeLinks = require('../../shared/hateoas'); - -exports.login = (req, res) => { - const { username, password, email } = req.body; - const user = usersDb.findUser(username, email, password); - if (user) { - res.json({ - data: { - user: { - id: user.id, - username: user.username, - email: user.email, - firstName: user.firstName, - lastName: user.lastName - }, - token: 'token-' + user.id, - message: 'Login successful' - }, - _links: makeLinks('/api/auth', { - self: '/login', - profile: '/profile/', - logout: '/logout' - }), - _meta: {} - }); - } else { - res.status(401).json({ error: 'Invalid credentials' }); - } -}; - -exports.register = (req, res) => { - const { username, password, email, firstName, lastName } = req.body; - if (usersDb.exists(username, email)) { - return res.status(409).json({ error: 'User already exists' }); - } - const newUser = usersDb.addUser({ username, password, email, firstName, lastName }); - res.json({ - data: { - user: { - id: newUser.id, - username, - email, - firstName, - lastName - }, - token: 'token-' + newUser.id, - message: 'Register successful' - }, - _links: makeLinks('/api/auth', { - self: '/register', - login: '/login', - profile: '/profile/' - }), - _meta: {} - }); -}; - -exports.profile = (req, res) => { - const auth = req.headers.authorization; - if (!auth || !auth.startsWith('Bearer ')) { - return res.status(401).json({ error: 'No token provided' }); - } - const token = auth.replace('Bearer ', ''); - const id = parseInt(token.replace('token-', '')); - const user = usersDb.findById(id); - if (!user) { - return res.status(401).json({ error: 'Invalid token' }); - } - res.json({ - data: { - id: user.id, - username: user.username, - email: user.email, - firstName: user.firstName, - lastName: user.lastName - }, - _links: makeLinks('/api/auth', { - self: '/profile/', - logout: '/logout' - }), - _meta: {} - }); -}; - -exports.logout = (req, res) => { - res.json({ - message: 'Logout successful', - _links: makeLinks('/api/auth', { - self: '/logout', - login: '/login' - }), - _meta: {} - }); -}; - -exports.updateProfile = (req, res) => { - const userId = req.user?.id || req.body.id; // 这里假设有用户认证中间件,否则用body.id - if (!userId) return res.status(401).json({ error: 'Unauthorized' }); - const { firstName, lastName, bio, location, website, email, username, password } = req.body; - const updated = require('../../shared/usersDb').updateUser(userId, { firstName, lastName, bio, location, website, email, username, password }); - if (!updated) return res.status(404).json({ error: 'User not found' }); - res.json({ success: true, user: updated }); -}; \ No newline at end of file diff --git a/server/routers/back-new/features/auth/auth.routes.js b/server/routers/back-new/features/auth/auth.routes.js deleted file mode 100644 index 0262123..0000000 --- a/server/routers/back-new/features/auth/auth.routes.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const ctrl = require('./auth.controller'); - -router.post('/login', ctrl.login); -router.post('/register', ctrl.register); -router.get('/profile/', ctrl.profile); -router.post('/logout', ctrl.logout); -router.put('/profile/', ctrl.updateProfile); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/back-new/features/image/image.controller.js b/server/routers/back-new/features/image/image.controller.js deleted file mode 100644 index ada3233..0000000 --- a/server/routers/back-new/features/image/image.controller.js +++ /dev/null @@ -1,157 +0,0 @@ -const axios = require('axios'); -const makeLinks = require('../../shared/hateoas'); -const { v4: uuidv4 } = require('uuid'); -const qs = require('qs'); -require('dotenv').config(); -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; - -// 获取GigaChat access_token,严格按官方文档 -async function getGigaChatToken() { - const apiKey = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974'; - const scope = process.env.GIGACHAT_SCOPE || 'GIGACHAT_API_PERS'; - if (!apiKey) throw new Error('GIGACHAT_API_KEY 未配置'); - - const rqUID = uuidv4(); - const auth = Buffer.from(apiKey.trim()).toString('base64'); - - try { - const resp = await axios.post( - 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth', - new URLSearchParams({ scope }), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Accept': 'application/json', - 'RqUID': rqUID, - 'Authorization': `Basic ${auth}`, - }, - timeout: 10000, - } - ); - if (!resp.data.access_token) { - console.error('GigaChat token响应异常:', resp.data); - throw new Error('GigaChat token响应异常'); - } - return resp.data.access_token; - } catch (err) { - if (err.response) { - console.error('获取access_token失败:', err.response.status, err.response.data); - } else { - console.error('获取access_token异常:', err.message); - } - throw new Error('获取access_token失败'); - } -} - -// 调用chat生成图片描述 -async function fetchChatContent(accessToken, prompt) { - try { - const chatResp = await axios.post( - 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions', - { - model: "GigaChat", - messages: [ - { role: "system", content: "Ты — Василий Кандинский" }, - { role: "user", content: prompt } - ], - stream: false, - function_call: 'auto' - }, - { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'RqUID': uuidv4(), - } - } - ); - const content = chatResp.data.choices[0].message.content; - console.log('GigaChat返回内容:', content); - return content; - } catch (err) { - console.error('AI生成图片出错: chat接口失败'); - if (err.response) { - console.error('status:', err.response.status); - console.error('headers:', err.response.headers); - console.error('data:', err.response.data); - console.error('config:', err.config); - } else { - console.error('AI生成图片出错:', err.message); - } - throw new Error('chat接口失败: ' + err.message); - } -} - -// 获取图片内容 -async function fetchImageContent(accessToken, imageId) { - try { - const imageResp = await axios.get( - `https://gigachat.devices.sberbank.ru/api/v1/files/${imageId}/content`, - { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'RqUID': uuidv4(), - }, - responseType: 'arraybuffer' - } - ); - return imageResp.data; - } catch (err) { - console.error('AI生成图片出错: 获取图片内容失败'); - if (err.response) { - console.error('status:', err.response.status); - console.error('headers:', err.response.headers); - console.error('data:', err.response.data); - console.error('config:', err.config); - } else { - console.error('AI生成图片出错:', err.message); - } - throw new Error('获取图片内容失败: ' + err.message); - } -} - -// 工具函数:异步重试 -async function retryAsync(fn, times = 3, delay = 800) { - let lastErr; - for (let i = 0; i < times; i++) { - try { - return await fn(); - } catch (err) { - lastErr = err; - if (i < times - 1 && delay) await new Promise(r => setTimeout(r, delay)); - } - } - throw lastErr; -} - -exports.generate = async (req, res) => { - const { prompt } = req.query; - if (!prompt) { - return res.status(400).json({ error: 'Prompt parameter is required' }); - } - let accessToken; - try { - accessToken = await getGigaChatToken(); - } catch (e) { - return res.status(500).json({ error: e.message }); - } - try { - // 1. 重试获取图片描述内容 - const content = await retryAsync(() => fetchChatContent(accessToken, prompt), 3, 800); - // 升级正则,兼容更多图片标签格式 - const match = content.match(/]+src=['"]([^'"]+)['"]/); - if (!match) { - console.error('AI生成图片出错: GigaChat未返回图片标签'); - return res.status(500).json({ error: 'No image generated' }); - } - const imageId = match[1]; - // 2. 重试获取图片内容 - const imageData = await retryAsync(() => fetchImageContent(accessToken, imageId), 3, 800); - res.set('Content-Type', 'image/jpeg'); - res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' }))); - res.send(imageData); - } catch (err) { - console.error('AI生成图片出错: 未知错误', err); - res.status(500).json({ error: err.message }); - } -}; \ No newline at end of file diff --git a/server/routers/back-new/features/image/image.routes.js b/server/routers/back-new/features/image/image.routes.js deleted file mode 100644 index 7dbebf8..0000000 --- a/server/routers/back-new/features/image/image.routes.js +++ /dev/null @@ -1,7 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const ctrl = require('./image.controller'); - -router.get('/prompt', ctrl.generate); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/back-new/features/user/user.controller.js b/server/routers/back-new/features/user/user.controller.js deleted file mode 100644 index 313ff47..0000000 --- a/server/routers/back-new/features/user/user.controller.js +++ /dev/null @@ -1,12 +0,0 @@ -const usersDb = require('../../shared/usersDb'); -const makeLinks = require('../../shared/hateoas'); - -exports.list = (req, res) => { - res.json({ - data: usersDb.getAll(), - _links: makeLinks('/api/user', { - self: '/list', - }), - _meta: {} - }); -}; \ No newline at end of file diff --git a/server/routers/back-new/features/user/user.routes.js b/server/routers/back-new/features/user/user.routes.js deleted file mode 100644 index f444cad..0000000 --- a/server/routers/back-new/features/user/user.routes.js +++ /dev/null @@ -1,7 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const ctrl = require('./user.controller'); - -router.get('/list', ctrl.list); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/back-new/middleware/auth.js b/server/routers/back-new/middleware/auth.js deleted file mode 100644 index d1cc427..0000000 --- a/server/routers/back-new/middleware/auth.js +++ /dev/null @@ -1,11 +0,0 @@ -// 简单token认证中间件,支持token-3格式 -module.exports = function (req, res, next) { - const auth = req.headers.authorization; - if (auth && auth.startsWith('Bearer token-')) { - const id = parseInt(auth.replace('Bearer token-', '')); - if (!isNaN(id)) { - req.user = { id }; - } - } - next(); -}; \ No newline at end of file diff --git a/server/routers/back-new/package-lock.json b/server/routers/back-new/package-lock.json deleted file mode 100644 index c4d644d..0000000 --- a/server/routers/back-new/package-lock.json +++ /dev/null @@ -1,1024 +0,0 @@ -{ - "name": "back-new", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "back-new", - "version": "1.0.0", - "dependencies": { - "axios": "^1.10.0", - "cors": "^2.8.5", - "dotenv": "^17.0.0", - "express": "^4.21.2", - "qs": "^6.14.0", - "uuid": "^11.1.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/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/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmmirror.com/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/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "17.0.0", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.0.0.tgz", - "integrity": "sha512-A0BJ5lrpJVSfnMMXjmeO0xUnoxqsBHWCoqqTnGwGYVdnctqXXUEhJOO7LxmgxJon9tEZFGpe0xPRX0h2v3AANQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/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/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmmirror.com/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.3", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/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-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/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/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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": "2.5.2", - "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - } - } -} diff --git a/server/routers/back-new/package.json b/server/routers/back-new/package.json deleted file mode 100644 index 8663f06..0000000 --- a/server/routers/back-new/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "back-new", - "version": "1.0.0", - "description": "非Python实现的后端,兼容前端接口", - "main": "server.js", - "scripts": { - "start": "node server.js" - }, - "dependencies": { - "axios": "^1.10.0", - "cors": "^2.8.5", - "dotenv": "^17.0.0", - "express": "^4.21.2", - "qs": "^6.14.0", - "uuid": "^11.1.0" - } -} diff --git a/server/routers/back-new/server.js b/server/routers/back-new/server.js deleted file mode 100644 index 977f24d..0000000 --- a/server/routers/back-new/server.js +++ /dev/null @@ -1,41 +0,0 @@ -process.env.GIGACHAT_API_KEY = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974'; -process.env.GIGACHAT_SCOPE = 'GIGACHAT_API_PERS'; -require('dotenv').config(); -console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY); -const express = require('express'); -const cors = require('cors'); - -const app = express(); -const router = require('./app'); - -app.use(cors()); -app.use(express.json());process.env.GIGACHAT_API_KEY = '78359123-4447-481a-9028-861f53b24ed1:04a4f1e9-1349-4a84-85f9-0c6c687c0974'; -process.env.GIGACHAT_SCOPE = 'GIGACHAT_API_PERS'; -require('dotenv').config(); -console.log('GIGACHAT_API_KEY:', process.env.GIGACHAT_API_KEY); -const express = require('express'); -const cors = require('cors'); -const authMiddleware = require('./middleware/auth'); - -const app = express(); -const router = require('./app'); - -app.use(cors()); -app.use(express.json()); -app.use(authMiddleware); - -// 路由前缀要和前端请求一致 -app.use('/ms/back-new', router); - -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); - -// 路由前缀要和前端请求一致 -app.use('/ms/back-new', router); - -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`Server running on port ${PORT}`); -}); \ No newline at end of file diff --git a/server/routers/back-new/shared/hateoas.js b/server/routers/back-new/shared/hateoas.js deleted file mode 100644 index b12ec23..0000000 --- a/server/routers/back-new/shared/hateoas.js +++ /dev/null @@ -1,8 +0,0 @@ -function makeLinks(base, links) { - const result = {}; - for (const [rel, path] of Object.entries(links)) { - result[rel] = { href: base + path }; - } - return result; -} -module.exports = makeLinks; \ No newline at end of file diff --git a/server/routers/back-new/shared/usersDb.js b/server/routers/back-new/shared/usersDb.js deleted file mode 100644 index fcc21db..0000000 --- a/server/routers/back-new/shared/usersDb.js +++ /dev/null @@ -1,28 +0,0 @@ -let users = [ - { id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User', bio: '', location: '', website: '' } -]; -let nextId = 2; - -exports.findUser = (username, email, password) => - users.find(u => (u.username === username || u.email === email) && u.password === password); - -exports.findById = (id) => users.find(u => u.id === id); - -exports.addUser = ({ username, password, email, firstName, lastName, bio = '', location = '', website = '' }) => { - const newUser = { id: nextId++, username, password, email, firstName, lastName, bio, location, website }; - users.push(newUser); - return newUser; -}; - -exports.exists = (username, email) => - users.some(u => u.username === username || u.email === email); - -exports.getAll = () => users; - -// 新增:更新用户信息 -exports.updateUser = (id, update) => { - const user = users.find(u => u.id === id); - if (!user) return null; - Object.assign(user, update); - return user; -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/index.js b/server/routers/kfu-m-24-1/index.js index c65cfc8..609da3e 100644 --- a/server/routers/kfu-m-24-1/index.js +++ b/server/routers/kfu-m-24-1/index.js @@ -4,7 +4,6 @@ const router = Router() router.use('/eng-it-lean', require('./eng-it-lean/index')) router.use('/sberhubproject', require('./sberhubproject/index')) router.use('/sber_web', require('./sber_web/index')) -router.use('/sber_mobile', require('./sber_mobile/index')) module.exports = router diff --git a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt b/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt deleted file mode 100644 index 367f42a..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt +++ /dev/null @@ -1,231 +0,0 @@ --- Расширение для генерации UUID -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- 1. Управляющие компании -CREATE TABLE management_companies ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - logo_url TEXT, - contact_phone TEXT NOT NULL, - email TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 2. Жилые дома -CREATE TABLE buildings ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - management_company_id UUID NOT NULL REFERENCES management_companies(id), - name TEXT, - address TEXT NOT NULL, - floors INTEGER, - entrances INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 3. Профили пользователей -CREATE TABLE user_profiles ( - id UUID PRIMARY KEY REFERENCES auth.users(id), - full_name TEXT, - avatar_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 4. Квартиры -CREATE TABLE apartments ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - number TEXT NOT NULL, - area DECIMAL(10, 2), - floor INTEGER, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 5. Связь пользователей с квартирами -CREATE TABLE apartment_residents ( - apartment_id UUID NOT NULL REFERENCES apartments(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - is_owner BOOLEAN DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (apartment_id, user_id) -); - --- 6. Сервисы УК -CREATE TABLE management_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - management_company_id UUID NOT NULL REFERENCES management_companies(id), - title TEXT NOT NULL, - description TEXT, - category TEXT NOT NULL, - base_price DECIMAL(10, 2), - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 7. Связь сервисов УК с домами -CREATE TABLE building_management_services ( - building_id UUID NOT NULL REFERENCES buildings(id), - service_id UUID NOT NULL REFERENCES management_services(id), - custom_price DECIMAL(10, 2), - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (building_id, service_id) -); - --- 9. Дополнительные сервисы -CREATE TABLE additional_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title TEXT NOT NULL, - description TEXT, - category TEXT NOT NULL, - price DECIMAL(10, 2), - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 10. Инициативы -CREATE TABLE initiatives ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - creator_id UUID NOT NULL REFERENCES auth.users(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - status TEXT NOT NULL CHECK ( - status IN ('moderation', 'review', 'fundraising', 'approved', 'rejected') - ), - target_amount DECIMAL(10, 2), - current_amount DECIMAL(10, 2) DEFAULT 0, - image_url TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 11. Голосования -CREATE TABLE votes ( - initiative_id UUID NOT NULL REFERENCES initiatives(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - vote_type TEXT NOT NULL CHECK (vote_type IN ('for', 'against')), - created_at TIMESTAMPTZ DEFAULT NOW(), - PRIMARY KEY (initiative_id, user_id) -); - --- 12. Чат -CREATE TABLE chats ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - name TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 13. Сообщения -CREATE TABLE messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - chat_id UUID NOT NULL REFERENCES chats(id), - user_id UUID NOT NULL REFERENCES auth.users(id), - text TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- 14. Камеры -CREATE TABLE cameras ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - building_id UUID NOT NULL REFERENCES buildings(id), - location TEXT NOT NULL, - stream_url TEXT NOT NULL, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 15. Платежки по квартире (ЖКХ, Интернет и т.д.) -CREATE TABLE payment_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - apartment_id UUID NOT NULL REFERENCES apartments(id), - name TEXT NOT NULL, -- Например, "ЖКХ", "Интернет" - icon TEXT, -- Можно хранить название иконки или url - amount DECIMAL(10, 2) NOT NULL, -- Общая сумма по платежке - is_paid BOOLEAN DEFAULT FALSE, -- Оплачен ли весь агрегатор - payment_method TEXT CHECK (payment_method IN ('card', 'sber')), - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 16. Детализация по платежке (например, отопление, вода и т.д.) -CREATE TABLE payment_service_details ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - payment_service_id UUID NOT NULL REFERENCES payment_services(id) ON DELETE CASCADE, - name TEXT NOT NULL, -- Например, "Отопление" - amount DECIMAL(10, 2) NOT NULL, -- Сумма по детализации - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 17. Заявки -CREATE TABLE tickets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id), - apartment_id UUID NOT NULL REFERENCES apartments(id), - title TEXT NOT NULL, - description TEXT NOT NULL, - status TEXT NOT NULL CHECK (status IN ('open', 'in_progress', 'resolved')), - category TEXT NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- 18. Сообщения в службу поддержки -CREATE TABLE support ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES auth.users(id), - message TEXT NOT NULL, - is_from_user BOOLEAN NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- Индексы -CREATE INDEX idx_buildings_management_company ON buildings(management_company_id); -CREATE INDEX idx_management_services_company ON management_services(management_company_id); -CREATE INDEX idx_building_services_building ON building_management_services(building_id); -CREATE INDEX idx_initiatives_building ON initiatives(building_id); -CREATE INDEX idx_votes_initiative ON votes(initiative_id); -CREATE INDEX idx_messages_chat ON messages(chat_id); -CREATE INDEX idx_cameras_building ON cameras(building_id); -CREATE INDEX idx_tickets_user ON tickets(user_id); -CREATE INDEX idx_tickets_apartment ON tickets(apartment_id); -CREATE INDEX idx_apartments_building ON apartments(building_id); -CREATE INDEX idx_apartment_residents_apartment ON apartment_residents(apartment_id); -CREATE INDEX idx_apartment_residents_user ON apartment_residents(user_id); - --- Триггеры для обновления updated_at -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Применяем триггеры ко всем таблицам с updated_at -DO $$ -DECLARE - t record; -BEGIN - FOR t IN - SELECT table_name - FROM information_schema.columns - WHERE column_name = 'updated_at' - AND table_schema = 'public' - LOOP - EXECUTE format('CREATE TRIGGER trigger_%s_updated_at - BEFORE UPDATE ON %I - FOR EACH ROW EXECUTE FUNCTION update_updated_at()', - t.table_name, t.table_name); - END LOOP; -END; -$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/additional_services.js b/server/routers/kfu-m-24-1/sber_mobile/additional_services.js deleted file mode 100644 index 70236e4..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/additional_services.js +++ /dev/null @@ -1,53 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все дополнительные сервисы -router.get('/additional-services', async (req, res) => { - const supabase = getSupabaseClient(); - const { data, error } = await supabase.from('additional_services').select('*').order('created_at', { ascending: false }); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить сервис по id -router.get('/additional-services/:id', async (req, res) => { - const supabase = getSupabaseClient(); - const { id } = req.params; - const { data, error } = await supabase.from('additional_services').select('*').eq('id', id).single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Создать сервис -router.post('/additional-services', async (req, res) => { - const supabase = getSupabaseClient(); - const { title, description, category, price, image_url } = req.body; - const { data, error } = await supabase.from('additional_services').insert([ - { title, description, category, price, image_url } - ]).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Обновить сервис -router.put('/additional-services/:id', async (req, res) => { - const supabase = getSupabaseClient(); - const { id } = req.params; - const { title, description, category, price, image_url } = req.body; - const { data, error } = await supabase.from('additional_services').update({ - title, description, category, price, image_url - }).eq('id', id).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Удалить сервис -router.delete('/additional-services/:id', async (req, res) => { - const supabase = getSupabaseClient(); - const { id } = req.params; - const { error } = await supabase.from('additional_services').delete().eq('id', id); - if (error) return res.status(400).json({ error: error.message }); - res.json({ success: true }); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/apartments.js b/server/routers/kfu-m-24-1/sber_mobile/apartments.js deleted file mode 100644 index fd360ef..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/apartments.js +++ /dev/null @@ -1,45 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все квартиры по дому -router.get('/apartments', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id } = req.query; - if (!building_id) return res.status(400).json({ error: 'building_id required' }); - const { data, error } = await supabase.from('apartments').select('*').eq('building_id', building_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить адрес квартиры и название дома по id квартиры -router.get('/apartment-info', async (req, res) => { - const supabase = getSupabaseClient(); - const { apartment_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); - - // Получаем квартиру с building_id и номером - const { data: apartment, error: err1 } = await supabase - .from('apartments') - .select('id, number, building_id') - .eq('id', apartment_id) - .single(); - if (err1) return res.status(400).json({ error: err1.message }); - - // Получаем дом по building_id - const { data: building, error: err2 } = await supabase - .from('buildings') - .select('id, name, address') - .eq('id', apartment.building_id) - .single(); - if (err2) return res.status(400).json({ error: err2.message }); - - res.json({ - apartment_id: apartment.id, - apartment_number: apartment.number, - building_id: building.id, - building_name: building.name, - building_address: building.address - }); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/auth.js b/server/routers/kfu-m-24-1/sber_mobile/auth.js deleted file mode 100644 index 39ef7bd..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/auth.js +++ /dev/null @@ -1,51 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// POST /sign-in -router.post('/sign-in', async (req, res) => { - const { email, password } = req.body; - const supabase = getSupabaseClient(); - const { data, error } = await supabase.auth.signInWithPassword({ email, password }); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// POST /sign-up -router.post('/sign-up', async (req, res) => { - const { email, password } = req.body; - const supabase = getSupabaseClient(); - const { data, error } = await supabase.auth.signUp({ email, password }); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// POST /sign-out -router.post('/sign-out', async (req, res) => { - const { access_token } = req.body; - const supabase = getSupabaseClient(); - supabase.auth.setSession({ access_token, refresh_token: '' }); - const { error } = await supabase.auth.signOut(); - if (error) return res.status(400).json({ error: error.message }); - res.json({ success: true }); -}); - -// POST /reset-password -router.post('/reset-password', async (req, res) => { - const { email } = req.body; - const supabase = getSupabaseClient(); - const { data, error } = await supabase.auth.resetPasswordForEmail(email); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// POST /update-password -router.post('/update-password', async (req, res) => { - const { access_token, newPassword } = req.body; - const supabase = getSupabaseClient(); - supabase.auth.setSession({ access_token, refresh_token: '' }); - const { data, error } = await supabase.auth.updateUser({ password: newPassword }); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/buildings.js b/server/routers/kfu-m-24-1/sber_mobile/buildings.js deleted file mode 100644 index 8230923..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/buildings.js +++ /dev/null @@ -1,14 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все дома по УК -router.get('/buildings', async (req, res) => { - const supabase = getSupabaseClient(); - const { management_company_id } = req.query; - if (!management_company_id) return res.status(400).json({ error: 'management_company_id required' }); - const { data, error } = await supabase.from('buildings').select('*').eq('management_company_id', management_company_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/cameras.js b/server/routers/kfu-m-24-1/sber_mobile/cameras.js deleted file mode 100644 index 1425b9f..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/cameras.js +++ /dev/null @@ -1,28 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все камеры по дому -router.get('/cameras', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id } = req.query; - if (!building_id) return res.status(400).json({ error: 'building_id required' }); - const { data, error } = await supabase.from('cameras').select('*').eq('building_id', building_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить все камеры по квартире (через building_id) -router.get('/cameras/by-apartment', async (req, res) => { - const supabase = getSupabaseClient(); - const { apartment_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); - // Получаем building_id квартиры и сразу камеры этого дома - const { data, error } = await supabase - .from('cameras') - .select('*, apartments!inner(id, building_id)') - .eq('apartments.id', apartment_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts deleted file mode 100644 index b89f8d5..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/chat-moderation.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { z } from "zod"; -import gigachat from './gigachat'; - -export interface ModerationResult { - comment: string; - isApproved: boolean; - success: boolean; - error?: string; -} - -export class ChatModerationAgent { - private moderationLlm: any; - - constructor(GIGA_AUTH) { - // Создаем структурированный вывод для модерации - this.moderationLlm = gigachat(GIGA_AUTH).withStructuredOutput(z.object({ - comment: z.string(), - isApproved: z.boolean(), - }) as any); - } - - private getSystemPrompt(): string { - return `Ты модерируешь сообщения в чате. Твоя задача - проверить сообщение на нецензурную лексику, брань и неприемлемый контент. - -Твои задачи: -1. Проверь сообщение на наличие нецензурной лексики, мата, ругательств и брани. -2. Проверь на оскорбления, угрозы и агрессивное поведение. -3. Проверь на спам и рекламу. -4. Проверь на неприемлемый контент (дискриминация, экстремизм и т.д.). - -- Если сообщение не содержит запрещенного контента, оно одобряется (isApproved: true). -- Если сообщение содержит запрещенный контент, оно отклоняется (isApproved: false). - -Правила написания комментария: -- Если сообщение одобряется, оставь поле comment пустым. -- Если сообщение отклоняется, пиши комментарий со следующей формулировкой: - "Сообщение удалено. Причина: (укажи конкретную причину: нецензурная лексика, оскорбления, спам и т.д.)"`; - } - - public async moderateMessage(message: string): Promise { - try { - const prompt = `${this.getSystemPrompt()} - -Сообщение: ${message}`; - - const result = await this.moderationLlm.invoke(prompt); - - // Дополнительная проверка - if (!result.isApproved && result.comment.trim() === '') { - result.comment = 'Сообщение удалено. Причина: нарушение правил чата.'; - } - - return { - comment: result.comment, - isApproved: result.isApproved, - success: true - }; - - } catch (error) { - console.error('❌ [Chat Moderation] Ошибка при модерации:', error); - - // В случае ошибки одобряем сообщение - return { - comment: '', - isApproved: true, - success: false, - error: error instanceof Error ? error.message : 'Неизвестная ошибка' - }; - } - } -} - -// Экспортируем функцию для обратной совместимости -export const moderationText = async (title: string, body: string, GIGA_AUTH): Promise<[string, boolean, string]> => { - const agent = new ChatModerationAgent(GIGA_AUTH); - const result = await agent.moderateMessage(body); - return [result.comment, result.isApproved, body]; -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts deleted file mode 100644 index 609020a..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/gigachat.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Agent } from 'node:https'; -import { GigaChat } from 'langchain-gigachat'; - -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js) -export const gigachat = (GIGA_AUTH) => new - GigaChat({ - model: 'GigaChat-2', - scope: 'GIGACHAT_API_PERS', - streaming: false, - credentials: GIGA_AUTH, - httpsAgent -}); - -export default gigachat; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js b/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js deleted file mode 100644 index c4efec6..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/chat-ai-agent/moderation-config.js +++ /dev/null @@ -1,16 +0,0 @@ -// Конфигурация системы модерации -const MODERATION_CONFIG = { - // Задержка перед запуском модерации (в миллисекундах) - MODERATION_DELAY: 1500, // 1.5 секунды - - // Включена ли система модерации - MODERATION_ENABLED: true, - - // Текст для замены заблокированных сообщений - BLOCKED_MESSAGE_TEXT: '[Удалено модератором]', - - // Логировать ли процесс модерации - ENABLE_MODERATION_LOGS: true -}; - -module.exports = MODERATION_CONFIG; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js deleted file mode 100644 index 047ac37..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ /dev/null @@ -1,218 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все чаты по дому -router.get('/chats', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id } = req.query; - - if (!building_id) { - return res.status(400).json({ error: 'building_id required' }); - } - - try { - const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); - - if (error) { - return res.status(400).json({ error: error.message }); - } - - res.json(data || []); - } catch (err) { - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Получить все чаты по квартире (через building_id) -router.get('/chats/by-apartment', async (req, res) => { - const supabase = getSupabaseClient(); - const { apartment_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); - // Получаем building_id квартиры и сразу чаты этого дома - const { data, error } = await supabase - .from('chats') - .select('*, apartments!inner(id, building_id)') - .eq('apartments.id', apartment_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Создать новый чат -router.post('/chats', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id, name } = req.body; - - if (!building_id) { - return res.status(400).json({ error: 'building_id is required' }); - } - - const { data, error } = await supabase - .from('chats') - .insert({ building_id, name }) - .select() - .single(); - - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить конкретный чат по ID -router.get('/chats/:chat_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { chat_id } = req.params; - - const { data, error } = await supabase - .from('chats') - .select('*') - .eq('id', chat_id) - .single(); - - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Обновить чат -router.put('/chats/:chat_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { chat_id } = req.params; - const { name } = req.body; - - const { data, error } = await supabase - .from('chats') - .update({ name }) - .eq('id', chat_id) - .select() - .single(); - - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Удалить чат -router.delete('/chats/:chat_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { chat_id } = req.params; - - const { error } = await supabase - .from('chats') - .delete() - .eq('id', chat_id); - - if (error) return res.status(400).json({ error: error.message }); - res.json({ success: true, message: 'Chat deleted successfully' }); -}); - -// Получить статистику чата (количество сообщений, участников и т.д.) -router.get('/chats/:chat_id/stats', async (req, res) => { - const supabase = getSupabaseClient(); - const { chat_id } = req.params; - - try { - // Получаем количество сообщений - const { count: messageCount, error: messageError } = await supabase - .from('messages') - .select('*', { count: 'exact', head: true }) - .eq('chat_id', chat_id); - - if (messageError) throw messageError; - - // Получаем информацию о чате с домом - const { data: chatInfo, error: chatError } = await supabase - .from('chats') - .select(` - *, - buildings ( - id, - name, - address, - apartments ( - apartment_residents ( - user_id - ) - ) - ) - `) - .eq('id', chat_id) - .single(); - - if (chatError) throw chatError; - - // Собираем уникальные user_id жителей дома - const userIds = new Set(); - chatInfo.buildings.apartments.forEach(apartment => { - apartment.apartment_residents.forEach(resident => { - userIds.add(resident.user_id); - }); - }); - - // Получаем профили всех жителей - let uniqueResidents = []; - if (userIds.size > 0) { - const { data: profiles } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .in('id', Array.from(userIds)); - - uniqueResidents = profiles || []; - } - - res.json({ - chat_id, - chat_name: chatInfo.name, - building: { - id: chatInfo.buildings.id, - name: chatInfo.buildings.name, - address: chatInfo.buildings.address - }, - message_count: messageCount || 0, - total_residents: uniqueResidents.length, - residents: uniqueResidents - }); - } catch (error) { - res.status(400).json({ error: error.message }); - } -}); - -// Получить последнее сообщение в чате -router.get('/chats/:chat_id/last-message', async (req, res) => { - const supabase = getSupabaseClient(); - const { chat_id } = req.params; - - try { - // Получаем последнее сообщение - const { data: lastMessage, error } = await supabase - .from('messages') - .select('*') - .eq('chat_id', chat_id) - .order('created_at', { ascending: false }) - .limit(1) - .single(); - - let data = null; - - if (error && error.code === 'PGRST116') { - data = null; - } else if (error) { - return res.status(400).json({ error: error.message }); - } else if (lastMessage) { - // Получаем профиль пользователя для сообщения - const { data: userProfile } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', lastMessage.user_id) - .single(); - - // Объединяем сообщение с профилем - data = { - ...lastMessage, - user_profiles: userProfile || null - }; - } - - res.json(data); - } catch (err) { - res.status(500).json({ error: 'Internal server error' }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js b/server/routers/kfu-m-24-1/sber_mobile/get-constants.js deleted file mode 100644 index 47f34e8..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/get-constants.js +++ /dev/null @@ -1,90 +0,0 @@ -const getSupabaseUrl = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].SUPABASE_URL.value; -}; - -const getSupabaseKey = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].SUPABASE_KEY.value; -}; - -const getSupabaseServiceKey = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].SUPABASE_SERVICE_KEY.value; -}; - -const getGigaAuth = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].GIGA_AUTH.value; -}; - -const getLangsmithApiKey = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].LANGSMITH_API_KEY.value; -}; - -const getLangsmithEndpoint = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].LANGSMITH_ENDPOINT.value; -}; - -const getLangsmithTracing = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].LANGSMITH_TRACING.value; -}; - -const getLangsmithProject = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].LANGSMITH_PROJECT.value; -}; - -const getTavilyApiKey = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].TAVILY_API_KEY.value; -}; - -const getRagSupabaseServiceRoleKey = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].RAG_SUPABASE_SERVICE_ROLE_KEY.value; -}; - -const getRagSupabaseUrl = async () => { - const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev'); - const data = await response.json(); - return data.features['sber_mobile'].RAG_SUPABASE_URL.value; -}; - -module.exports = { - getSupabaseUrl, - getSupabaseKey, - getSupabaseServiceKey, - getGigaAuth -}; - -// IIFE для установки переменных окружения -(async () => { - try { - process.env.GIGA_AUTH = await getGigaAuth(); - process.env.LANGSMITH_API_KEY = await getLangsmithApiKey(); - process.env.LANGSMITH_ENDPOINT = await getLangsmithEndpoint(); - process.env.LANGSMITH_TRACING = await getLangsmithTracing(); - process.env.LANGSMITH_PROJECT = await getLangsmithProject(); - process.env.TAVILY_API_KEY = await getTavilyApiKey(); - process.env.RAG_SUPABASE_SERVICE_ROLE_KEY = await getRagSupabaseServiceRoleKey(); - process.env.RAG_SUPABASE_URL = await getRagSupabaseUrl(); - - console.log('Environment variables loaded successfully'); - } catch (error) { - console.error('Error loading environment variables:', error); - } -})(); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js deleted file mode 100644 index 9ef7bc9..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ /dev/null @@ -1,41 +0,0 @@ -const router = require('express').Router(); -const authRouter = require('./auth'); -const { supabaseRouter } = require('./supabaseClient'); -const profileRouter = require('./profile'); -const initiativesRouter = require('./initiatives'); -const votesRouter = require('./votes'); -const additionalServicesRouter = require('./additional_services'); -const chatsRouter = require('./chats'); -const camerasRouter = require('./cameras'); -const ticketsRouter = require('./tickets'); -const messagesRouter = require('./messages'); -const moderationRouter = require('./moderation'); -const utilityPaymentsRouter = require('./utility_payments'); -const apartmentsRouter = require('./apartments'); -const buildingsRouter = require('./buildings'); -const userApartmentsRouter = require('./user_apartments'); -const avatarRouter = require('./media'); -const supportRouter = require('./supportApi'); -const moderateRouter = require('./moderate.js'); - - -module.exports = router; - -router.use('/auth', authRouter); -router.use('/supabase', supabaseRouter); -router.use('', profileRouter); -router.use('', initiativesRouter); -router.use('', votesRouter); -router.use('', additionalServicesRouter); -router.use('', chatsRouter); -router.use('', camerasRouter); -router.use('', ticketsRouter); -router.use('', messagesRouter); -router.use('', moderationRouter); -router.use('', utilityPaymentsRouter); -router.use('', apartmentsRouter); -router.use('', buildingsRouter); -router.use('', userApartmentsRouter); -router.use('', avatarRouter); -router.use('', supportRouter); -router.use('', moderateRouter); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts deleted file mode 100644 index a5a0e23..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GigaChat as GigaChatLang} from 'langchain-gigachat'; -import { GigaChat } from 'gigachat'; -import { Agent } from 'node:https'; - -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -export const llm_mod = (GIGA_AUTH) => - new GigaChatLang({ - credentials: GIGA_AUTH, - temperature: 0.2, - model: 'GigaChat-2-Max', - httpsAgent, -}); - -export const llm_gen = (GIGA_AUTH) => - new GigaChat({ - credentials: GIGA_AUTH, - model: 'GigaChat-2', - httpsAgent, -}); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts deleted file mode 100644 index 48162c0..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { llm_mod } from './llm' -import { z } from "zod"; - - -// возвращаю комментарий + исправленное предложение + булево значение - -export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => { - - const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({ - comment: z.string(), - fixedText: z.string().optional(), - isApproved: z.boolean(), - }) as any) - - const prompt = ` - Представь, что ты модерируешь предложения от жильцов многоквартирного дома (это личная инициатива по улучшения, - не имеющая отношения к Управляющей компании). - - Заголовок: ${title} - Основной текст: ${description} - - Твои задачи: - 1. Проверь предложение и заголовок на спам. - 2. Проверь, чтобы заголовок и текст были на одну тему. - 3. Проверь само предложение пользователя на отсутствие грубой лексики и пошлостей. - 4. Проверь грамматику. - 5. Проверь на бессмысленность предложения. Оно не должно содержать только случайные символы. - 6. Не должно быть рекламы, ссылок и т.д. - 7. Проверь предложение на информативность, предложение не может быть коротким, оно должно ясно отражжать суть инициативы. - 8. Предложение должно быть в вежливой форме. - - - Если все правила соблюдены, то предложение принимается! - - - Если предложение отклонено, всегда пиши комментарий и fixedText! - - Правила написания комментария: - - Если предложение отклоняется, пиши комментарий со следующей формулировкой: - "Предложение отклонено. Причина: (укажи проблему)" - - Правила написания fixedText: - - Если предложение отклонено, то верни в поле "fixedText" измененный текст, который будет соответствовать правилам. - - Если предложение отклонено и содержит запрещённый контент (рекламу, личные данные), удали всю информацию, - которая противоречит правилам, и верни в только подходящий фрагмент, сохраняя общий смысл. - - Если текст не представляет никакой ценности, возврати в поле "fixedText" правило, - по которому оно не прошло. - -Если предложение принимается, то ничего не возвращай в поле fixedText. - ` - - const result = await moderationLlm.invoke(prompt); - if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { - result.comment = 'Предложение отклонено. Причина: несоблюдение требований к оформлению или содержанию.', - result.fixedText = description - } - - return [result.comment, result.fixedText, result.isApproved]; -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts deleted file mode 100644 index d216c5d..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { llm_gen } from './llm' -import { detectImage } from 'gigachat'; - -export const generatePicture = async (prompt: string, GIGA_AUTH) => { - const resp = await llm_gen(GIGA_AUTH).chat({ - messages: [ - { - "role": "system", - "content": "Ты — Василий Кандинский для жильцов многоквартирного дома" - }, - { - role: "user", - content: `Старайся передать атмосферу уюта и безопасности. - Нарисуй картинку подходящую для такого события: ${prompt} - В картинке не должно быть текста, только изображение.`, - }, - ], - function_call: 'auto', - }); - - // Получение изображения по идентификатору - const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); - - if (!detectedImage?.uuid) { - throw new Error('Не удалось получить UUID изображения из ответа GigaChat'); - } - - const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid); - - // Возвращаем содержимое изображения, убеждаясь что это Buffer - if (Buffer.isBuffer(image.content)) { - return image.content; - } else if (typeof image.content === 'string') { - return Buffer.from(image.content, 'binary'); - } else { - throw new Error('Unexpected image content type: ' + typeof image.content); - } -} diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js deleted file mode 100644 index 42f82e4..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js +++ /dev/null @@ -1,101 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все предложения, инициативы status=review (по дому) -router.get('/initiatives-review', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id } = req.query; - let query = supabase.from('initiatives').select('*').eq('status', 'review'); - if (building_id) query = query.eq('building_id', building_id); - const { data, error } = await query; - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить все сборы, инициативы status=fundraising (по дому) -router.get('/initiatives-fundraising', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id } = req.query; - let query = supabase.from('initiatives').select('*').eq('status', 'fundraising'); - if (building_id) query = query.eq('building_id', building_id); - const { data, error } = await query; - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить инициативу по id (и optionally building_id) -router.get('/initiatives/:id', async (req, res) => { - const supabase = getSupabaseClient(); - const { id } = req.params; - const { building_id } = req.query; - let query = supabase.from('initiatives').select('*').eq('id', id); - if (building_id) query = query.eq('building_id', building_id); - const { data, error } = await query.single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Создать инициативу -router.post('/initiatives', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id, creator_id, title, description, status, target_amount, current_amount, image_url } = req.body; - const { data, error } = await supabase.from('initiatives').insert([ - { building_id, creator_id, title, description, status, target_amount, current_amount: current_amount || 0, image_url } - ]).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Обновить инициативу -router.put('/initiatives/:id', async (req, res) => { - const supabase = getSupabaseClient(); - const { id } = req.params; - const { title, description, status, target_amount, current_amount, image_url } = req.body; - const { data, error } = await supabase.from('initiatives').update({ - title, description, status, target_amount, current_amount, image_url - }).eq('id', id).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Удалить инициативу -router.delete('/initiatives/:id', async (req, res) => { - const supabase = getSupabaseClient(); - const { id } = req.params; - const { error } = await supabase.from('initiatives').delete().eq('id', id); - if (error) return res.status(400).json({ error: error.message }); - res.json({ success: true }); -}); - -// Получить все инициативы по квартире с голосами пользователя -router.get('/initiatives/by-apartment', async (req, res) => { - const supabase = getSupabaseClient(); - const { apartment_id, user_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); - // Получаем building_id квартиры - const { data: apartments, error: err1 } = await supabase - .from('apartments') - .select('building_id') - .eq('id', apartment_id) - .single(); - if (err1) return res.status(400).json({ error: err1.message }); - const building_id = apartments.building_id; - // Получаем инициативы этого дома с голосами пользователя (если user_id передан) - let selectStr = '*, votes:initiatives(id, votes!left(user_id, vote_type))'; - if (!user_id) selectStr = '*'; - const { data, error } = await supabase - .from('initiatives') - .select(selectStr) - .eq('building_id', building_id); - if (error) return res.status(400).json({ error: error.message }); - // Если user_id передан, фильтруем только голос текущего пользователя - if (user_id && data) { - data.forEach(initiative => { - initiative.user_vote = (initiative.votes || []).find(v => v.user_id === user_id) || null; - delete initiative.votes; - }); - } - res.json(data); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/media.js b/server/routers/kfu-m-24-1/sber_mobile/media.js deleted file mode 100644 index e2a8df9..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/media.js +++ /dev/null @@ -1,15 +0,0 @@ -const router = require('express').Router(); -const { supabaseRouter } = require('./supabaseClient'); - - -// GET /avatar -router.get('/avatar', async (req, res) => { - const supabase = getSupabaseClient(); - const { user_id } = req.query; - if (!user_id) return res.status(400).json({ error: 'user_id required' }); - const { data, error } = await supabase.storage.from('avatars').download(`avatar_${user_id}.png`); - if (error) return res.status(400).json({ error: error.message }); - res.blob(data); - }); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js deleted file mode 100644 index a495e33..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ /dev/null @@ -1,235 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); -const { moderationText } = require('./chat-ai-agent/chat-moderation'); // Импортируем функцию модерации -const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); // Импортируем конфигурацию модерации - - -// Добавляем middleware для логирования всех запросов к messages роутеру - -// Тестовый эндпоинт для проверки работы роутера -router.get('/messages/test', (req, res) => { - res.json({ - status: 'OK', - message: 'Messages router работает', - timestamp: new Date().toISOString(), - moderation_config: MODERATION_CONFIG - }); -}); - -// Получить все сообщения в чате с информацией о пользователе -router.get('/messages', async (req, res) => { - try { - const { chat_id, limit = 50, offset = 0 } = req.query; - - if (!chat_id) { - return res.status(400).json({ error: 'chat_id is required' }); - } - - const supabase = getSupabaseClient(); - - const { data, error } = await supabase - .from('messages') - .select(` - *, - user_profiles ( - id, - full_name, - avatar_url - ) - `) - .eq('chat_id', chat_id) - .order('created_at', { ascending: true }) - .range(offset, offset + limit - 1); - - if (error) { - return res.status(500).json({ error: 'Failed to fetch messages' }); - } - - // Получаем уникальные ID пользователей из сообщений, у которых нет профиля - const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles); - const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))]; - - if (userIds.length > 0) { - const { data: profiles, error: profilesError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .in('id', userIds); - - if (!profilesError && profiles) { - // Добавляем профили к сообщениям - data.forEach(message => { - if (!message.user_profiles) { - message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null; - } - }); - } - } - - res.json(data); - } catch (err) { - res.status(500).json({ error: 'Unexpected error occurred' }); - } -}); - -// Создать новое сообщение -router.post('/messages', async (req, res) => { - - let supabase; - try { - supabase = getSupabaseClient(); - } catch (error) { - console.error(`❌ [Message Send] Ошибка получения Supabase клиента:`, error); - return res.status(500).json({ error: 'Database connection error' }); - } - - const { chat_id, user_id, text } = req.body; - - - if (!chat_id || !user_id || !text) { - console.log(`❌ [Message Send] Отклонен: отсутствуют обязательные поля`); - console.log(`❌ [Message Send] chat_id: ${chat_id}, user_id: ${user_id}, text: ${text}`); - return res.status(400).json({ - error: 'chat_id, user_id, and text are required' - }); - } - - // Создаем сообщение - const { data: newMessage, error } = await supabase - .from('messages') - .insert({ chat_id, user_id, text }) - .select('*') - .single(); - - if (error) { - console.error(`❌ [Message Send] Ошибка сохранения в Supabase:`, error); - return res.status(400).json({ error: error.message }); - } - - // Получаем профиль пользователя - const { data: userProfile, error: profileError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', user_id) - .single(); - - if (profileError) { - console.log(`⚠️ [Message Send] Профиль пользователя не найден:`, profileError); - } - - // Объединяем сообщение с профилем - const data = { - ...newMessage, - user_profiles: userProfile || null - }; - - res.json(data); -}); - -// Получить конкретное сообщение -router.get('/messages/:message_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { message_id } = req.params; - - // Получаем сообщение - const { data: message, error } = await supabase - .from('messages') - .select('*') - .eq('id', message_id) - .single(); - - if (error) return res.status(400).json({ error: error.message }); - - // Получаем профиль пользователя - const { data: userProfile } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', message.user_id) - .single(); - - // Объединяем сообщение с профилем - const data = { - ...message, - user_profiles: userProfile || null - }; - - res.json(data); -}); - -// Получить последние сообщения для каждого чата (для списка чатов) -router.get('/chats/last-messages', async (req, res) => { - const supabase = getSupabaseClient(); - const { building_id } = req.query; - - if (!building_id) { - return res.status(400).json({ error: 'building_id required' }); - } - - // Получаем чаты и их последние сообщения через обычные запросы - const { data: chats, error: chatsError } = await supabase - .from('chats') - .select('*') - .eq('building_id', building_id); - - if (chatsError) return res.status(400).json({ error: chatsError.message }); - - // Для каждого чата получаем последнее сообщение - const chatsWithMessages = await Promise.all( - chats.map(async (chat) => { - const { data: lastMessage } = await supabase - .from('messages') - .select(` - *, - user_profiles:user_id ( - id, - full_name, - avatar_url - ) - `) - .eq('chat_id', chat.id) - .order('created_at', { ascending: false }) - .limit(1) - .single(); - - return { - ...chat, - last_message: lastMessage || null - }; - }) - ); - - res.json(chatsWithMessages); -}); - -// Удалить сообщение (только для автора) -router.delete('/messages/:message_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { message_id } = req.params; - const { user_id } = req.body; - - if (!user_id) { - return res.status(400).json({ error: 'user_id required' }); - } - - // Проверяем, что пользователь является автором сообщения - const { data: message, error: fetchError } = await supabase - .from('messages') - .select('user_id') - .eq('id', message_id) - .single(); - - if (fetchError) return res.status(400).json({ error: fetchError.message }); - - if (message.user_id !== user_id) { - return res.status(403).json({ error: 'You can only delete your own messages' }); - } - - const { error } = await supabase - .from('messages') - .delete() - .eq('id', message_id); - - if (error) return res.status(400).json({ error: error.message }); - res.json({ success: true, message: 'Message deleted successfully' }); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js deleted file mode 100644 index 61c0692..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/moderate.js +++ /dev/null @@ -1,162 +0,0 @@ -const router = require('express').Router(); -const { moderationText } = require('./initiatives-ai-agents/moderation'); -const { generatePicture } = require('./initiatives-ai-agents/picture'); -const { getSupabaseClient } = require('./supabaseClient'); -const { getGigaAuth } = require('./get-constants'); - -async function getGigaKey() { - const GIGA_AUTH = await getGigaAuth(); - return GIGA_AUTH; - } - -// Обработчик для модерации и создания инициативы -router.post('/moderate', async (req, res) => { - - const GIGA_AUTH = await getGigaKey(); - - try { - const { title, description, building_id, creator_id, target_amount, status } = req.body; - - if (!title || !description) { - res.status(400).json({ error: 'Заголовок и описание обязательны' }); - return; - } - - if (!building_id || !creator_id) { - res.status(400).json({ error: 'ID дома и создателя обязательны' }); - return; - } - - // Валидация статуса, если передан - const validStatuses = ['moderation', 'review', 'fundraising', 'approved', 'rejected']; - if (status && !validStatuses.includes(status)) { - res.status(400).json({ error: `Недопустимый статус. Допустимые значения: ${validStatuses.join(', ')}` }); - return; - } - - console.log('Запрос на модерацию:', { title: title.substring(0, 50), description: description.substring(0, 100) }); - - // Модерация текста (передаем title и description как body) - const [comment, fixedText, isApproved] = await moderationText(title, description, GIGA_AUTH); - - // Если модерация не прошла, возвращаем undefined - if (!isApproved) { - if (!comment || comment.trim() === '') { - console.warn('Обнаружен некорректный результат модерации - пустой комментарий при отклонении'); - } - - res.json({ - comment, - fixedText, - isApproved, - initiative: undefined - }); - return; - } - - // Модерация прошла, генерируем изображение используя заголовок как промпт - console.log('Модерация прошла, генерируем изображение с промптом:', title); - - const imageBuffer = await generatePicture(title, GIGA_AUTH); - - if (!imageBuffer || imageBuffer.length === 0) { - res.status(500).json({ error: 'Получен пустой буфер изображения' }); - return; - } - - // Получаем Supabase клиент и создаем имя файла - const supabase = getSupabaseClient(); - const timestamp = Date.now(); - const filename = `image_${creator_id}_${timestamp}.jpg`; - - // Загружаем изображение в Supabase Storage - let uploadResult; - let retries = 0; - const maxRetries = 5; - - while (retries < maxRetries) { - try { - uploadResult = await supabase.storage - .from('images') - .upload(filename, imageBuffer, { - contentType: 'image/jpeg', - upsert: true - }); - - if (!uploadResult.error) { - break; // Успешная загрузка - } - - retries++; - - if (retries < maxRetries) { - // Ждем перед повторной попыткой - await new Promise(resolve => setTimeout(resolve, 1000 * retries)); - } - } catch (error) { - console.warn(`Попытка загрузки ${retries + 1} неудачна (исключение):`, error.message); - retries++; - - if (retries < maxRetries) { - // Ждем перед повторной попыткой - await new Promise(resolve => setTimeout(resolve, 1000 * retries)); - } else { - throw error; // Перебрасываем ошибку после всех попыток - } - } - } - - if (uploadResult?.error) { - console.error('Supabase storage error after all retries:', uploadResult.error); - res.status(500).json({ error: 'Ошибка при сохранении изображения после нескольких попыток' }); - return; - } - - console.log('Изображение успешно загружено в Supabase Storage:', filename); - - // Получаем публичный URL - const { data: urlData } = supabase.storage - .from('images') - .getPublicUrl(filename); - - // Определяем статус: если передан в запросе, используем его, иначе 'review' - const finalStatus = status || 'review'; - - // Создаем инициативу в базе данных - const { data: initiative, error: initiativeError } = await supabase - .from('initiatives') - .insert([{ - building_id, - creator_id, - title: fixedText || title, - description, - status: finalStatus, - target_amount: target_amount || null, - current_amount: 0, - image_url: urlData.publicUrl - }]) - .select() - .single(); - - if (initiativeError) { - console.error('Ошибка создания инициативы:', initiativeError); - res.status(500).json({ error: 'Ошибка при создании инициативы', details: initiativeError.message }); - return; - } - - console.log('Инициатива успешно создана:', initiative.id); - - res.json({ - comment, - fixedText, - isApproved, - initiative - }); - - } catch (error) { - console.error('Error in moderation and initiative creation:', error); - res.status(500).json({ error: 'Внутренняя ошибка сервера', details: error.message }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderation.js b/server/routers/kfu-m-24-1/sber_mobile/moderation.js deleted file mode 100644 index 7f54af9..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/moderation.js +++ /dev/null @@ -1,53 +0,0 @@ -const router = require('express').Router(); -const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); -const { moderationText } = require('./chat-ai-agent/chat-moderation'); - -// Получить текущие настройки модерации -router.get('/moderation/config', (req, res) => { - res.json(MODERATION_CONFIG); -}); - -// Обновить настройки модерации -router.post('/moderation/config', (req, res) => { - - const oldConfig = { ...MODERATION_CONFIG }; - const { MODERATION_DELAY, MODERATION_ENABLED, BLOCKED_MESSAGE_TEXT, ENABLE_MODERATION_LOGS } = req.body; - - const changes = []; - - if (MODERATION_DELAY !== undefined) { - const newValue = parseInt(MODERATION_DELAY); - MODERATION_CONFIG.MODERATION_DELAY = newValue; - changes.push(`MODERATION_DELAY: ${oldConfig.MODERATION_DELAY} -> ${newValue}`); - } - if (MODERATION_ENABLED !== undefined) { - const newValue = Boolean(MODERATION_ENABLED); - MODERATION_CONFIG.MODERATION_ENABLED = newValue; - changes.push(`MODERATION_ENABLED: ${oldConfig.MODERATION_ENABLED} -> ${newValue}`); - } - if (BLOCKED_MESSAGE_TEXT !== undefined) { - const newValue = String(BLOCKED_MESSAGE_TEXT); - MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT = newValue; - changes.push(`BLOCKED_MESSAGE_TEXT: "${oldConfig.BLOCKED_MESSAGE_TEXT}" -> "${newValue}"`); - } - if (ENABLE_MODERATION_LOGS !== undefined) { - const newValue = Boolean(ENABLE_MODERATION_LOGS) - MODERATION_CONFIG.ENABLE_MODERATION_LOGS = newValue; - changes.push(`ENABLE_MODERATION_LOGS: ${oldConfig.ENABLE_MODERATION_LOGS} -> ${newValue}`); - } - - if (changes.length > 0) { - changes.forEach((change, index) => { - }); - } else { - } - - res.json({ - success: true, - message: 'Настройки модерации обновлены', - changes: changes, - config: MODERATION_CONFIG - }); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js deleted file mode 100644 index d33ead2..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js +++ /dev/null @@ -1,982 +0,0 @@ -const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); -const MODERATION_CONFIG = require('./chat-ai-agent/moderation-config'); -const { getGigaAuth } = require('./get-constants'); -const { moderationText } = require('./chat-ai-agent/chat-moderation'); - -async function getGigaKey() { - const GIGA_AUTH = await getGigaAuth(); - return GIGA_AUTH; -} - -class ChatPollingHandler { - constructor() { - this.connectedClients = new Map(); // user_id -> { user_info, chats: Set(), lastActivity: Date } - this.chatParticipants = new Map(); // chat_id -> Set(user_id) - this.userEventQueues = new Map(); // user_id -> [{id, event, data, timestamp}] - this.eventIdCounter = 0; - this.realtimeSubscription = null; - - // Инициализируем Supabase подписку с задержкой и проверками - this.initializeWithRetry(); - - // Очистка старых событий каждые 5 минут - setInterval(() => { - this.cleanupOldEvents(); - }, 5 * 60 * 1000); - } - - // Инициализация с повторными попытками - async initializeWithRetry() { - try { - // Сначала ждем завершения основной инициализации - await initializationPromise; - - this.setupRealtimeSubscription(); - this.testRealtimeConnection(); - return; - - } catch (error) { - console.log('❌ [Supabase] Основная инициализация неудачна, пробуем альтернативный подход'); - } - - // Если основная инициализация не удалась, используем повторные попытки - let attempts = 0; - const maxAttempts = 10; - const baseDelay = 2000; // 2 секунды - - while (attempts < maxAttempts) { - try { - attempts++; - - // Ждем перед попыткой - await new Promise(resolve => setTimeout(resolve, baseDelay * attempts)); - - // Проверяем готовность Supabase клиента - const supabase = getSupabaseClient(); - if (supabase) { - this.setupRealtimeSubscription(); - this.testRealtimeConnection(); - return; // Успех, выходим - } - } catch (error) { - console.log(`❌ [Supabase] Попытка #${attempts} неудачна:`, error.message); - - if (attempts === maxAttempts) { - console.error('❌ [Supabase] Все попытки инициализации исчерпаны'); - console.error('❌ [Supabase] Realtime подписка будет недоступна'); - return; - } - } - } - } - - // Аутентификация пользователя - async handleAuthentication(req, res) { - const { user_id, token } = req.body; - - if (!user_id) { - res.status(400).json({ error: 'user_id is required' }); - return; - } - - try { - // Проверяем пользователя в базе данных - const supabase = getSupabaseClient(); - const { data: userProfile, error } = await supabase - .from('user_profiles') - .select('*') - .eq('id', user_id) - .single(); - - if (error) { - console.log('❌ [Polling Server] Пользователь не найден:', error); - res.status(401).json({ error: 'User not found' }); - return; - } - - // Регистрируем пользователя - this.connectedClients.set(user_id, { - user_info: { - user_id, - profile: userProfile, - last_seen: new Date() - }, - chats: new Set(), - lastActivity: new Date() - }); - - // Создаем очередь событий для пользователя - if (!this.userEventQueues.has(user_id)) { - this.userEventQueues.set(user_id, []); - } - - // Добавляем событие аутентификации в очередь - this.addEventToQueue(user_id, 'authenticated', { - message: 'Successfully authenticated', - user: userProfile - }); - - res.json({ - success: true, - message: 'Successfully authenticated', - user: userProfile - }); - - } catch (error) { - console.error('❌ [Polling Server] Ошибка аутентификации:', error); - res.status(500).json({ error: 'Authentication failed' }); - } - } - - // Эндпоинт для получения событий (polling) - async handleGetEvents(req, res) { - try { - const { user_id, last_event_id } = req.query; - - if (!user_id) { - res.status(400).json({ error: 'user_id is required' }); - return; - } - - const client = this.connectedClients.get(user_id); - if (!client) { - res.status(401).json({ error: 'Not authenticated' }); - return; - } - - // Обновляем время последней активности - client.lastActivity = new Date(); - - // Получаем очередь событий пользователя - const eventQueue = this.userEventQueues.get(user_id) || []; - - // Фильтруем события после last_event_id - const lastEventId = parseInt(last_event_id) || 0; - const newEvents = eventQueue.filter(event => event.id > lastEventId); - - // Логируем отправку событий клиенту - if (newEvents.length > 0) { - console.log(`📨 [Polling Server] Отправляем ${newEvents.length} событий клиенту ${user_id}`); - newEvents.forEach(event => { - if (event.event === 'message_updated') { - console.log(`📨 [Polling Server] → Событие: ${event.event}, Сообщение ID: ${event.data?.message?.id}, Текст: "${event.data?.message?.text?.substring(0, 50)}${(event.data?.message?.text?.length || 0) > 50 ? '...' : ''}"`); - } - }); - } - - res.json({ - success: true, - events: newEvents, - last_event_id: eventQueue.length > 0 ? Math.max(...eventQueue.map(e => e.id)) : lastEventId - }); - - } catch (error) { - console.error('❌ [Polling Server] Ошибка получения событий:', error); - res.status(500).json({ error: 'Failed to get events' }); - } - } - - // HTTP эндпоинт для присоединения к чату - async handleJoinChat(req, res) { - try { - const { user_id, chat_id } = req.body; - - if (!user_id || !chat_id) { - res.status(400).json({ error: 'user_id and chat_id are required' }); - return; - } - - const client = this.connectedClients.get(user_id); - if (!client) { - res.status(401).json({ error: 'Not authenticated' }); - return; - } - - // Проверяем, что чат существует и пользователь имеет доступ к нему - const supabase = getSupabaseClient(); - const { data: chat, error } = await supabase - .from('chats') - .select(` - *, - buildings ( - management_company_id, - apartments ( - apartment_residents ( - user_id - ) - ) - ) - `) - .eq('id', chat_id) - .single(); - - if (error || !chat) { - res.status(404).json({ error: 'Chat not found' }); - return; - } - - // Проверяем доступ пользователя к чату через квартиры в доме - const hasAccess = chat.buildings.apartments.some(apartment => - apartment.apartment_residents.some(resident => - resident.user_id === user_id - ) - ); - - if (!hasAccess) { - res.status(403).json({ error: 'Access denied to this chat' }); - return; - } - - // Добавляем пользователя в чат - client.chats.add(chat_id); - - if (!this.chatParticipants.has(chat_id)) { - this.chatParticipants.set(chat_id, new Set()); - } - this.chatParticipants.get(chat_id).add(user_id); - - // Добавляем событие присоединения в очередь пользователя - this.addEventToQueue(user_id, 'joined_chat', { - chat_id, - chat: chat, - message: 'Successfully joined chat' - }); - - // Уведомляем других участников о подключении - this.broadcastToChatExcludeUser(chat_id, user_id, 'user_joined', { - chat_id, - user: client.user_info.profile, - timestamp: new Date() - }); - - res.json({ success: true, message: 'Joined chat successfully' }); - - } catch (error) { - res.status(500).json({ error: 'Failed to join chat' }); - } - } - - // HTTP эндпоинт для покидания чата - async handleLeaveChat(req, res) { - try { - const { user_id, chat_id } = req.body; - - if (!user_id || !chat_id) { - res.status(400).json({ error: 'user_id and chat_id are required' }); - return; - } - - const client = this.connectedClients.get(user_id); - if (!client) { - res.status(401).json({ error: 'Not authenticated' }); - return; - } - - // Удаляем пользователя из чата - client.chats.delete(chat_id); - - if (this.chatParticipants.has(chat_id)) { - this.chatParticipants.get(chat_id).delete(user_id); - - // Если чат пуст, удаляем его - if (this.chatParticipants.get(chat_id).size === 0) { - this.chatParticipants.delete(chat_id); - } - } - - // Уведомляем других участников об отключении - this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { - chat_id, - user: client.user_info.profile, - timestamp: new Date() - }); - - res.json({ success: true, message: 'Left chat successfully' }); - - } catch (error) { - res.status(500).json({ error: 'Failed to leave chat' }); - } - } - - // HTTP эндпоинт для отправки сообщения - async handleSendMessage(req, res) { - try { - const { user_id, chat_id, text } = req.body; - - if (!user_id || !chat_id || !text) { - res.status(400).json({ error: 'user_id, chat_id and text are required' }); - return; - } - - const client = this.connectedClients.get(user_id); - if (!client) { - res.status(401).json({ error: 'Not authenticated' }); - return; - } - - if (!client.chats.has(chat_id)) { - res.status(403).json({ error: 'Not joined to this chat' }); - return; - } - - // Сохраняем сообщение в базу данных - const supabase = getSupabaseClient(); - const { data: message, error } = await supabase - .from('messages') - .insert({ - chat_id, - user_id, - text - }) - .select(` - *, - user_profiles ( - id, - full_name, - avatar_url - ) - `) - .single(); - - if (error) { - res.status(500).json({ error: 'Failed to save message' }); - return; - } - - // Отправляем сообщение всем участникам чата - this.broadcastToChat(chat_id, 'new_message', { - message, - timestamp: new Date() - }); - - res.json({ success: true, message: 'Message sent successfully' }); - - } catch (error) { - res.status(500).json({ error: 'Failed to send message' }); - } - } - - // HTTP эндпоинт для индикации печатания - async handleTypingStart(req, res) { - try { - const { user_id, chat_id } = req.body; - - if (!user_id || !chat_id) { - res.status(400).json({ error: 'user_id and chat_id are required' }); - return; - } - - const client = this.connectedClients.get(user_id); - if (!client) { - res.status(401).json({ error: 'Not authenticated' }); - return; - } - - if (!client.chats.has(chat_id)) { - res.status(403).json({ error: 'Not joined to this chat' }); - return; - } - - this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_start', { - chat_id, - user: client.user_info.profile, - timestamp: new Date() - }); - - res.json({ success: true }); - - } catch (error) { - res.status(500).json({ error: 'Failed to send typing indicator' }); - } - } - - // HTTP эндпоинт для остановки индикации печатания - async handleTypingStop(req, res) { - try { - const { user_id, chat_id } = req.body; - - if (!user_id || !chat_id) { - res.status(400).json({ error: 'user_id and chat_id are required' }); - return; - } - - const client = this.connectedClients.get(user_id); - if (!client) { - res.status(401).json({ error: 'Not authenticated' }); - return; - } - - if (!client.chats.has(chat_id)) { - res.status(403).json({ error: 'Not joined to this chat' }); - return; - } - - this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_stop', { - chat_id, - user: client.user_info.profile, - timestamp: new Date() - }); - - res.json({ success: true }); - - } catch (error) { - res.status(500).json({ error: 'Failed to send typing indicator' }); - } - } - - // Обработка отключения клиента - handleClientDisconnect(user_id) { - const client = this.connectedClients.get(user_id); - if (!client) return; - - // Удаляем пользователя из всех чатов - client.chats.forEach(chat_id => { - if (this.chatParticipants.has(chat_id)) { - this.chatParticipants.get(chat_id).delete(user_id); - - // Уведомляем других участников об отключении - this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { - chat_id, - user: client.user_info.profile, - timestamp: new Date() - }); - - // Если чат пуст, удаляем его - if (this.chatParticipants.get(chat_id).size === 0) { - this.chatParticipants.delete(chat_id); - } - } - }); - - // Удаляем клиента - this.connectedClients.delete(user_id); - } - - // Добавление события в очередь пользователя - addEventToQueue(user_id, event, data) { - if (!this.userEventQueues.has(user_id)) { - this.userEventQueues.set(user_id, []); - } - - const eventQueue = this.userEventQueues.get(user_id); - const eventId = ++this.eventIdCounter; - - eventQueue.push({ - id: eventId, - event, - data, - timestamp: new Date() - }); - - // Ограничиваем размер очереди (последние 100 событий) - if (eventQueue.length > 100) { - eventQueue.splice(0, eventQueue.length - 100); - } - } - - // Рассылка события всем участникам чата - broadcastToChat(chat_id, event, data) { - const participants = this.chatParticipants.get(chat_id); - if (!participants) return; - - participants.forEach(user_id => { - this.addEventToQueue(user_id, event, data); - }); - } - - // Рассылка события всем участникам чата кроме отправителя - broadcastToChatExcludeUser(chat_id, exclude_user_id, event, data) { - const participants = this.chatParticipants.get(chat_id); - if (!participants) return; - - participants.forEach(user_id => { - if (user_id !== exclude_user_id) { - this.addEventToQueue(user_id, event, data); - } - }); - } - - // Получение списка онлайн пользователей в чате - getOnlineUsersInChat(chat_id) { - const participants = this.chatParticipants.get(chat_id) || new Set(); - const onlineUsers = []; - const now = new Date(); - const ONLINE_THRESHOLD = 2 * 60 * 1000; // 2 минуты - - participants.forEach(user_id => { - const client = this.connectedClients.get(user_id); - if (client && (now - client.lastActivity) < ONLINE_THRESHOLD) { - onlineUsers.push(client.user_info.profile); - } - }); - - return onlineUsers; - } - - // Отправка системного сообщения в чат - async sendSystemMessage(chat_id, text) { - this.broadcastToChat(chat_id, 'system_message', { - chat_id, - text, - timestamp: new Date() - }); - } - - // Очистка старых событий - cleanupOldEvents() { - const now = new Date(); - const MAX_EVENT_AGE = 1 * 60 * 60 * 1000; // 1 час - const INACTIVE_USER_THRESHOLD = 30 * 60 * 1000; // 30 минут - - // Очищаем старые события - this.userEventQueues.forEach((eventQueue, user_id) => { - const filteredEvents = eventQueue.filter(event => - (now - event.timestamp) < MAX_EVENT_AGE - ); - - if (filteredEvents.length !== eventQueue.length) { - this.userEventQueues.set(user_id, filteredEvents); - } - }); - - // Удаляем неактивных пользователей - this.connectedClients.forEach((client, user_id) => { - if ((now - client.lastActivity) > INACTIVE_USER_THRESHOLD) { - this.handleClientDisconnect(user_id); - this.userEventQueues.delete(user_id); - } - }); - } - - // Тестирование Real-time подписки - async testRealtimeConnection() { - try { - const supabase = getSupabaseClient(); - if (!supabase) { - return false; - } - - // Создаем тестовый канал для проверки подключения - const testChannel = supabase - .channel('test_connection') - .subscribe((status, error) => { - if (error) { - console.error('❌ [Supabase] Тестовый канал - ошибка:', error); - } - - if (status === 'SUBSCRIBED') { - // Отписываемся от тестового канала - setTimeout(() => { - testChannel.unsubscribe(); - }, 2000); - } - }); - - return true; - } catch (error) { - console.error('❌ [Supabase] Ошибка тестирования Realtime:', error); - return false; - } - } - - // Проверка статуса подписки - checkSubscriptionStatus() { - if (this.realtimeSubscription) { - return true; - } else { - return false; - } - } - - setupRealtimeSubscription() { - // Убираем setTimeout, вызываем сразу - this._doSetupRealtimeSubscription(); - } - - _doSetupRealtimeSubscription() { - try { - const supabase = getSupabaseClient(); - - if (!supabase) { - console.log('❌ [Supabase] Supabase клиент не найден'); - throw new Error('Supabase client not available'); - } - - // Подписываемся на изменения в таблице messages - const subscription = supabase - .channel('messages_changes') - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'messages' - }, - async (payload) => { - try { - const newMessage = payload.new; - if (!newMessage) { - return; - } - - if (!newMessage.chat_id) { - return; - } - - // Получаем профиль пользователя - const { data: userProfile, error: profileError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', newMessage.user_id) - .single(); - - if (profileError) { - console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); - } - - // Объединяем сообщение с профилем - const messageWithProfile = { - ...newMessage, - user_profiles: userProfile || null - }; - - // Отправляем сообщение всем участникам чат - this.broadcastToChat(newMessage.chat_id, 'new_message', { - message: messageWithProfile, - timestamp: new Date() - }); - - // === ЗАПУСК МОДЕРАЦИИ === - if (MODERATION_CONFIG.MODERATION_ENABLED) { - - if (MODERATION_CONFIG.MODERATION_DELAY === 0) { - setImmediate(() => { - this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); - }); - } else { - const timeoutId = setTimeout(() => { - this.moderateMessage(newMessage.id, newMessage.text, newMessage.chat_id); - }, MODERATION_CONFIG.MODERATION_DELAY); - - } - - } - - } catch (callbackError) { - console.error('❌ [Supabase] Ошибка в обработчике сообщения:', callbackError); - console.error('❌ [Supabase] Stack trace:', callbackError.stack); - } - } - ) - .on( - 'postgres_changes', - { - event: 'UPDATE', - schema: 'public', - table: 'messages' - }, - async (payload) => { - try { - const updatedMessage = payload.new; - if (!updatedMessage) { - return; - } - - if (!updatedMessage.chat_id) { - return; - } - - // Получаем профиль пользователя - const { data: userProfile, error: profileError } = await supabase - .from('user_profiles') - .select('id, full_name, avatar_url') - .eq('id', updatedMessage.user_id) - .single(); - - if (profileError) { - console.error('❌ [Supabase] Ошибка получения профиля пользователя:', profileError); - } - - // Объединяем сообщение с профилем - const messageWithProfile = { - ...updatedMessage, - user_profiles: userProfile || null - }; - - // Отправляем обновление всем участникам чат - this.broadcastToChat(updatedMessage.chat_id, 'message_updated', { - message: messageWithProfile, - timestamp: new Date() - }); - - } catch (callbackError) { - console.error('❌ [Supabase] Ошибка в обработчике обновления сообщения:', callbackError); - } - } - ) - .subscribe((status, error) => { - if (error) { - console.error('❌ [Supabase] Ошибка подписки:', error); - } - - if (status === 'CHANNEL_ERROR') { - console.error('❌ [Supabase] Ошибка канала'); - } else if (status === 'TIMED_OUT') { - console.error('❌ [Supabase] Таймаут подписки'); - } - }); - - // Сохраняем ссылку на подписку для возможности отписки - this.realtimeSubscription = subscription; - - } catch (error) { - console.error('❌ [Supabase] Критическая ошибка при настройке подписки:', error); - throw error; // Пробрасываем ошибку для обработки в initializeWithRetry - } - } - - // Функция отложенной модерации сообщения - async moderateMessage(messageId, messageText, chatId) { - const moderationStartTime = Date.now(); - - try { - - // Вызываем функцию модерации - - let comment, isApproved, finalMessage; - const GIGA_AUTH = await getGigaKey(); - console.log(GIGA_AUTH) - try { - const result = await moderationText('', messageText, GIGA_AUTH); - [comment, isApproved, finalMessage] = result; - } catch (moderationError) { - console.error(`❌ [Moderation] Ошибка при вызове AI агента:`, moderationError); - console.error(`❌ [Moderation] Stack trace:`, moderationError.stack); - // В случае ошибки одобряем сообщение - comment = ''; - isApproved = true; - finalMessage = messageText; - console.log(`⚠️ [Moderation] Используем fallback значения из-за ошибки`); - } - - const moderationTime = Date.now() - moderationStartTime; - - if (isApproved) { - console.log(`📝 [Moderation] Действие: сообщение остается без изменений`); - } else { - console.log(`📝 [Moderation] Действие: сообщение будет заменено в базе данных`); - } - - // Если сообщение не прошло модерацию, обновляем его в базе данных - if (!isApproved) { - console.log(`💾 [Moderation] Начинаем обновление сообщения в базе данных...`); - - const supabase = getSupabaseClient(); - - // Сначала получаем информацию о сообщении для получения chat_id - console.log(`💾 [Moderation] Получаем данные сообщения из базы...`); - const { data: messageData, error: fetchError } = await supabase - .from('messages') - .select('chat_id, user_id') - .eq('id', messageId) - .single(); - - if (fetchError) { - console.error(`❌ [Moderation] Ошибка получения данных сообщения ${messageId}:`, fetchError); - return; - } - - console.log(`💾 [Moderation] Данные получены. Chat ID: ${messageData.chat_id}, User ID: ${messageData.user_id}`); - - // Обновляем текст сообщения - console.log(`💾 [Moderation] Обновляем текст сообщения на: "${MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT}"`); - const { data: updatedMessage, error } = await supabase - .from('messages') - .update({ text: MODERATION_CONFIG.BLOCKED_MESSAGE_TEXT }) - .eq('id', messageId) - .select('*') - .single(); - - if (error) { - console.error(`❌ [Moderation] Ошибка обновления сообщения ${messageId}:`, error); - console.error(`❌ [Moderation] Детали ошибки:`, error); - } - } - - - } catch (error) { - const totalTime = Date.now() - moderationStartTime; - console.error(`❌ [Moderation] === ОШИБКА МОДЕРАЦИИ СООБЩЕНИЯ ${messageId} ===`); - console.error(`❌ [Moderation] Время до ошибки: ${totalTime}мс`); - console.error(`❌ [Moderation] Тип ошибки: ${error.name || 'Unknown'}`); - console.error(`❌ [Moderation] Сообщение ошибки: ${error.message || 'Unknown error'}`); - console.error(`❌ [Moderation] Stack trace:`, error.stack); - } - } - - // Получение статистики подключений - getConnectionStats() { - return { - connectedClients: this.connectedClients.size, - activeChats: this.chatParticipants.size, - totalChatParticipants: Array.from(this.chatParticipants.values()) - .reduce((total, participants) => total + participants.size, 0), - totalEventQueues: this.userEventQueues.size, - totalEvents: Array.from(this.userEventQueues.values()) - .reduce((total, queue) => total + queue.length, 0) - }; - } -} - -// Функция для создания роутера с polling эндпоинтами -function createChatPollingRouter(express) { - const router = express.Router(); - const chatHandler = new ChatPollingHandler(); - - // CORS middleware для всех запросов - router.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Authorization'); - res.header('Access-Control-Allow-Credentials', 'true'); - - // Обрабатываем OPTIONS запросы - if (req.method === 'OPTIONS') { - res.status(200).end(); - return; - } - - next(); - }); - - // Эндпоинт для аутентификации - router.post('/auth', (req, res) => { - chatHandler.handleAuthentication(req, res); - }); - - // Эндпоинт для получения событий (polling) - router.get('/events', (req, res) => { - chatHandler.handleGetEvents(req, res); - }); - - // HTTP эндпоинты для действий - router.post('/join-chat', (req, res) => { - chatHandler.handleJoinChat(req, res); - }); - - router.post('/leave-chat', (req, res) => { - chatHandler.handleLeaveChat(req, res); - }); - - router.post('/send-message', (req, res) => { - chatHandler.handleSendMessage(req, res); - }); - - router.post('/typing-start', (req, res) => { - chatHandler.handleTypingStart(req, res); - }); - - router.post('/typing-stop', (req, res) => { - chatHandler.handleTypingStop(req, res); - }); - - // Эндпоинт для получения онлайн пользователей в чате - router.get('/online-users/:chat_id', (req, res) => { - const { chat_id } = req.params; - const onlineUsers = chatHandler.getOnlineUsersInChat(chat_id); - res.json({ onlineUsers }); - }); - - // Эндпоинт для получения статистики - router.get('/stats', (req, res) => { - const stats = chatHandler.getConnectionStats(); - res.json(stats); - }); - - // Эндпоинт для проверки статуса Supabase подписки - router.get('/supabase-status', (req, res) => { - const isConnected = chatHandler.checkSubscriptionStatus(); - res.json({ - supabaseSubscriptionActive: isConnected, - subscriptionExists: !!chatHandler.realtimeSubscription, - subscriptionInfo: chatHandler.realtimeSubscription ? { - channel: chatHandler.realtimeSubscription.topic, - state: chatHandler.realtimeSubscription.state - } : null - }); - }); - - // Эндпоинт для принудительного переподключения к Supabase - router.post('/reconnect-supabase', (req, res) => { - try { - // Отписываемся от текущей подписки - if (chatHandler.realtimeSubscription) { - chatHandler.realtimeSubscription.unsubscribe(); - chatHandler.realtimeSubscription = null; - } - - // Создаем новую подписку - chatHandler.setupRealtimeSubscription(); - - res.json({ - success: true, - message: 'Reconnection initiated' - }); - } catch (error) { - console.error('❌ [Polling Server] Ошибка переподключения:', error); - res.status(500).json({ - success: false, - error: 'Reconnection failed', - details: error.message - }); - } - }); - - // Тестовый эндпоинт для создания сообщения в обход API - router.post('/test-message', async (req, res) => { - const { chat_id, user_id, text } = req.body; - - if (!chat_id || !user_id || !text) { - res.status(400).json({ error: 'chat_id, user_id и text обязательны' }); - return; - } - - try { - // Создаем тестовое событие напрямую - chatHandler.broadcastToChat(chat_id, 'new_message', { - message: { - id: `test_${Date.now()}`, - chat_id, - user_id, - text, - created_at: new Date().toISOString(), - user_profiles: { - id: user_id, - full_name: 'Test User', - avatar_url: null - } - }, - timestamp: new Date() - }); - - res.json({ - success: true, - message: 'Test message sent to polling clients' - }); - } catch (error) { - console.error('❌ [Polling Server] Ошибка отправки тестового сообщения:', error); - res.status(500).json({ - success: false, - error: 'Failed to send test message', - details: error.message - }); - } - }); - - return { router, chatHandler }; -} - -module.exports = { - ChatPollingHandler, - createChatPollingRouter -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/profile.js b/server/routers/kfu-m-24-1/sber_mobile/profile.js deleted file mode 100644 index 529e057..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/profile.js +++ /dev/null @@ -1,119 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// GET /profile -router.get('/profile', async (req, res) => { - const { user_id } = req.query; - const supabase = getSupabaseClient(); - let { data: userData, error: userError } = await supabase.auth.admin.getUserById(user_id); - - if (userError) return res.status(400).json({ error: userError.message }); - - let { data: profileData, error: profileError } = await supabase.from('user_profiles').select(` - id, - full_name, - avatar_url, - updated_at - `).eq('id', user_id).single(); - - if (profileError) return res.status(400).json({ error: profileError.message }); - - // Получаем аватарку из бакета - let avatarUrl = null; - const avatarPath = `avatars/${user_id}.jpg`; - const { data: avatarData } = await supabase.storage.from('sber.mobile').getPublicUrl(avatarPath); - - if (avatarData) { - // Проверяем, существует ли файл - const { data: fileData, error: fileError } = await supabase.storage.from('sber.mobile').list('avatars', { - search: `${user_id}.jpg` - }); - - if (!fileError && fileData && fileData.length > 0) { - avatarUrl = avatarData.publicUrl; - } - } - - res.json({ - id: profileData.id, - username: profileData.full_name, - avatar_url: avatarUrl || profileData.avatar_url, - phone: userData.user.phone, - updated_at: profileData.updated_at - }); -}); - -// POST /profile -router.post('/profile', async (req, res) => { - const { user_id, data } = req.body; - const supabase = getSupabaseClient(); - - const { data: userData, error: userError } = await supabase.auth.admin.updateUserById( - user_id, - { phone: data.phone } - ) - - if (userError) return res.status(400).json({ error: userError.message }); - - let avatarUrl = data.avatar_url; - - // Если передана аватарка в base64, сохраняем в бакет - if (data.avatarBase64) { - try { - // Удаляем старую аватарку - const oldAvatarPath = `avatars/${user_id}.jpg`; - await supabase.storage.from('sber.mobile').remove([oldAvatarPath]); - - // Конвертируем base64 в buffer - const base64Data = data.avatarBase64.replace(/^data:image\/[a-z]+;base64,/, ''); - const buffer = Buffer.from(base64Data, 'base64'); - - // Загружаем новую аватарку - const avatarPath = `avatars/${user_id}.jpg`; - const { error: uploadError } = await supabase.storage - .from('sber.mobile') - .upload(avatarPath, buffer, { - contentType: 'image/jpeg', - upsert: true - }); - - if (uploadError) { - console.error('Ошибка загрузки аватарки:', uploadError); - } else { - // Получаем публичный URL - const { data: urlData } = await supabase.storage - .from('sber.mobile') - .getPublicUrl(avatarPath); - avatarUrl = urlData.publicUrl; - } - } catch (error) { - console.error('Ошибка обработки аватарки:', error); - } - } - - let { error: profileError } = await supabase.from('user_profiles').update({ - full_name: data.username, - avatar_url: avatarUrl, - // apartment: data.apartment - }).eq('id', user_id).single(); - - if (profileError) return res.status(400).json({ error: profileError.message }); - - res.json({ success: true, avatar_url: avatarUrl }); -}); - -// Получить управляющую компанию по квартире -router.get('/management-company', async (req, res) => { - const supabase = getSupabaseClient(); - const { apartment_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id required' }); - const { data: apartment, error: err1 } = await supabase.from('apartments').select('building_id').eq('id', apartment_id).single(); - if (err1) return res.status(400).json({ error: err1.message }); - const { data: building, error: err2 } = await supabase.from('buildings').select('management_company_id').eq('id', apartment.building_id).single(); - if (err2) return res.status(400).json({ error: err2.message }); - const { data: company, error: err3 } = await supabase.from('management_companies').select('*').eq('id', building.management_company_id).single(); - if (err3) return res.status(400).json({ error: err3.message }); - res.json(company); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js deleted file mode 100644 index 460d723..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ /dev/null @@ -1,79 +0,0 @@ -const router = require('express').Router(); -const { createClient } = require('@supabase/supabase-js'); -const { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey } = require('./get-constants'); - -let supabase = null; -let initializationPromise = null; - -async function initSupabaseClient() { - - try { - const supabaseUrl = await getSupabaseUrl(); - const supabaseAnonKey = await getSupabaseKey(); - const supabaseServiceRoleKey = await getSupabaseServiceKey(); - - - if (!supabaseUrl || !supabaseServiceRoleKey) { - throw new Error('Missing required Supabase configuration'); - } - - supabase = createClient(supabaseUrl, supabaseServiceRoleKey); - - return supabase; - - } catch (error) { - throw error; - } -} - -function getSupabaseClient() { - if (!supabase) { - throw new Error('Supabase client is not initialized. Call initSupabaseClient first.'); - } - return supabase; -} - -// POST /refresh-supabase-client -router.post('/refresh-supabase-client', async (req, res) => { - try { - await initSupabaseClient(); - res.json({ success: true, message: 'Supabase client refreshed' }); - } catch (error) { - res.status(500).json({ error: error.message }); - } -}); - -// GET /supabase-client-status -router.get('/supabase-client-status', (req, res) => { - - const isInitialized = !!supabase; - - res.json({ - initialized: isInitialized, - clientExists: !!supabase, - timestamp: new Date().toISOString() - }); -}); - -// Инициализация клиента при старте -initializationPromise = (async () => { - try { - await initSupabaseClient(); - } catch (error) { - // Планируем повторную попытку через 5 секунд - setTimeout(async () => { - try { - await initSupabaseClient(); - } catch (retryError) { - console.error('❌ [Supabase Client] Повторная инициализация неудачна:', retryError); - } - }, 5000); - } -})(); - -module.exports = { - getSupabaseClient, - initSupabaseClient, - supabaseRouter: router, - initializationPromise -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts deleted file mode 100644 index ff88787..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/create-ticket-tool.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools'; -import { z } from 'zod'; -import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; -import { getSupabaseClient } from '../supabaseClient'; - -export class CreateTicketTool extends StructuredTool { - name = 'create_ticket'; - description = 'Создает заявку в системе. ВАЖНО: используй этот инструмент ТОЛЬКО после получения явного согласия пользователя на создание заявки с конкретным текстом.'; - - schema = z.object({ - title: z.string().describe('Заголовок заявки'), - description: z.string().describe('Подробное описание проблемы'), - category: z.string().describe('Категория заявки (например: ремонт, уборка, техническая_поддержка, жалоба)'), - }); - - private userId: string; - private apartmentId: string; - - constructor(userId: string, apartmentId: string) { - super(); - this.userId = userId; - this.apartmentId = apartmentId; - } - - protected async _call( - arg: z.infer, - runManager?: CallbackManagerForToolRun, - parentConfig?: ToolRunnableConfig> - ): Promise { - try { - if (!this.apartmentId) { - return 'Не удалось определить вашу квартиру. Обратитесь к администратору для создания заявки.'; - } - - const supabase = getSupabaseClient(); - - const { data: ticket, error } = await supabase - .from('tickets') - .insert({ - user_id: this.userId, - apartment_id: this.apartmentId, - title: arg.title, - description: arg.description, - category: arg.category, - status: 'open' - }) - .select() - .single(); - - if (error) { - return 'Произошла ошибка при создании заявки. Попробуйте позже или обратитесь к администратору.'; - } - - return `Заявка успешно создана! -Номер заявки: ${ticket.id} -Заголовок: ${ticket.title} -Статус: Открыта -Дата создания: ${new Date(ticket.created_at).toLocaleString('ru-RU')} - -Ваша заявка принята в работу. Мы свяжемся с вами в ближайшее время.`; - - } catch (error) { - return 'Произошла техническая ошибка при создании заявки. Пожалуйста, попробуйте позже.'; - } - } -} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts deleted file mode 100644 index ab98ebb..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/gigachat.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Agent } from 'node:https'; -import { GigaChat } from 'langchain-gigachat'; -import { getGigaAuth } from '../get-constants'; - -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -// Получаем GIGA_AUTH из переменной окружения (устанавливается в get-constants.js) -export const gigachat = (GIGA_AUTH) => - new GigaChat({ - model: 'GigaChat-2', - temperature: 0.7, - scope: 'GIGACHAT_API_PERS', - streaming: false, - credentials: GIGA_AUTH, - httpsAgent - }); - -export default gigachat; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts deleted file mode 100644 index 84a681d..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/knowledge-base-tool.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools'; -import { z } from 'zod'; -import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; -import { getVectorStore } from './vector-store'; - -export class KnowledgeBaseTool extends StructuredTool { - name = 'search_knowledge_base'; - description = 'Ищет информацию в базе знаний компании о процессах, оплатах, подаче заявок, правилах и документах УК. Используй этот инструмент для вопросов, требующих специфических знаний о компании.'; - - schema = z.object({ - query: z.string().describe('Поисковый запрос для поиска в базе знаний'), - }); - - protected async _call( - arg: z.infer, - runManager?: CallbackManagerForToolRun, - parentConfig?: ToolRunnableConfig> - ): Promise { - try { - const vectorStore = getVectorStore(); - const retriever = vectorStore.asRetriever({ - k: 5 - }); - - const relevantDocs = await retriever.getRelevantDocuments(arg.query); - - if (!relevantDocs || relevantDocs.length === 0) { - return 'В базе знаний не найдено информации по данному запросу. Возможно, стоит переформулировать вопрос или обратиться к специалисту.'; - } - - const formattedDocs = relevantDocs.map((doc, index) => { - return `Документ ${index + 1}:\n${doc.pageContent}\n`; - }).join('\n---\n'); - - return `Найдена следующая информация в базе знаний компании:\n\n${formattedDocs}\n\nИспользуй эту информацию для ответа на вопрос пользователя.`; - - } catch (error) { - return 'Произошла ошибка при поиске в базе знаний. Попробуйте переформулировать запрос.'; - } - } -} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts deleted file mode 100644 index e8c5c68..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-agent.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from '@langchain/core/messages'; -import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; -import { MemorySaver } from '@langchain/langgraph'; -import gigachat from './gigachat'; -import { SupportContextTool } from './support-context-tool'; -import { KnowledgeBaseTool } from './knowledge-base-tool'; -import { CreateTicketTool } from './create-ticket-tool'; - -export interface SupportAgentConfig { - temperature?: number; - threadId?: string; - GIGA_AUTH?: string; -} - -export interface SupportResponse { - content: string; - success: boolean; - error?: string; -} - -export class SupportAgent { - private llm: any; - private memorySaver: MemorySaver; - private agent: any; - private systemPrompt: string; - private threadId: string; - private isFirstMessage: boolean; - private userId: string; - - constructor(config: SupportAgentConfig = {}) { - this.systemPrompt = this.getDefaultSystemPrompt(); - this.threadId = config.threadId || 'default'; - this.userId = this.threadId; - this.memorySaver = new MemorySaver(); - this.isFirstMessage = true; - - this.llm = gigachat(config.GIGA_AUTH); - if (config.temperature !== undefined) { - this.llm.temperature = config.temperature; - } - - const tools = [ - new SupportContextTool(this.userId), - new KnowledgeBaseTool() - ]; - - this.agent = createReactAgent({ - llm: this.llm, - tools: tools, - checkpointSaver: this.memorySaver - }); - } - - private getDefaultSystemPrompt(): string { - return `Ты - профессиональный агент службы поддержки управляющей компании. - -ОСНОВНЫЕ ПРИНЦИПЫ: -- Помогай только с реальными проблемами и вопросами, связанными с ЖКХ, управляющей компанией и приложением -- Будь вежливым, профессиональным и по существу -- Если вопрос неуместен, не связан с твоими обязанностями или является развлекательным - вежливо откажись и перенаправь к основным темам - -ДОСТУПНЫЕ ИНСТРУМЕНТЫ: - -1. get_support_context - получает историю сообщений пользователя - ВСЕГДА используй ПЕРВЫМ при каждом новом сообщении - -2. search_knowledge_base - поиск в базе знаний компании - Используй ТОЛЬКО для серьезных вопросов о: - - Процессах оплаты ЖКХ и тарифах - - Подаче заявок и документообороте - - Правилах и регламентах УК - - Технических вопросах приложения - - Процедурах и инструкциях компании - -3. create_ticket - создание заявки в системе - Используй ТОЛЬКО когда: - - Пользователь сообщает о реальной проблеме (поломка, неисправность, жалоба) - - Проблема требует вмешательства УК или технических служб - - ОБЯЗАТЕЛЬНО сначала покажи пользователю полный текст заявки - - Получи ЯВНОЕ согласие пользователя перед созданием - - НЕ создавай заявки для консультационных вопросов - -ПРАВИЛА ИСПОЛЬЗОВАНИЯ ИНСТРУМЕНТОВ: -- НЕ используй search_knowledge_base и create_ticket для: - * Общих вопросов и болтовни - * Развлекательных запросов - * Вопросов не по теме ЖКХ/УК - * Простых консультаций, которые можно решить обычным ответом - -АЛГОРИТМ РАБОТЫ: -1. Получи контекст истории сообщений -2. Определи, является ли вопрос уместным и серьезным -3. Если нужна специфическая информация - найди в базе знаний -4. Если нужно создать заявку - покажи текст и получи согласие -5. Дай полный и полезный ответ - -Всегда отвечай на русском языке и фокусируйся на помощи с реальными проблемами ЖКХ.`; - } - - public async processMessage(userMessage: string, apartmentId?: string): Promise { - try { - const messages: BaseMessage[] = []; - - if (this.isFirstMessage) { - messages.push(new SystemMessage(this.systemPrompt)); - this.isFirstMessage = false; - } - - messages.push(new HumanMessage(userMessage)); - - // Создаем инструменты с актуальным apartmentId - const tools = [ - new SupportContextTool(this.userId), - new KnowledgeBaseTool(), - new CreateTicketTool(this.userId, apartmentId || '') - ]; - - // Пересоздаем агента с обновленными инструментами - const tempAgent = createReactAgent({ - llm: this.llm, - tools: tools, - checkpointSaver: this.memorySaver - }); - - const response = await tempAgent.invoke({ - messages: messages - }, { - configurable: { - thread_id: this.threadId - } - }); - - const lastMessage = response.messages[response.messages.length - 1]; - - return { - content: typeof lastMessage.content === 'string' ? lastMessage.content : 'Извините, не удалось сформировать ответ.', - success: true - }; - - } catch (error) { - console.error('Ошибка при обработке сообщения:', error); - return { - content: 'Извините, произошла ошибка при обработке вашего запроса. Попробуйте позже.', - success: false, - error: error instanceof Error ? error.message : 'Неизвестная ошибка' - }; - } - } - - public async clearHistory(): Promise { - this.memorySaver = new MemorySaver(); - - const tools = [ - new SupportContextTool(this.userId), - new KnowledgeBaseTool() - ]; - - this.agent = createReactAgent({ - llm: this.llm, - tools: tools, - checkpointSaver: this.memorySaver - }); - - this.isFirstMessage = true; - } -} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts deleted file mode 100644 index 94a573c..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/support-context-tool.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { StructuredTool, ToolRunnableConfig } from '@langchain/core/tools'; -import { z } from 'zod'; -import { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager'; -import { getSupabaseClient } from '../supabaseClient'; - -export class SupportContextTool extends StructuredTool { - name = 'get_support_context'; - description = 'Получает последние 10 сообщений из истории поддержки для понимания контекста разговора. Используй этот инструмент в начале разговора.'; - - schema = z.object({}); - - private userId: string; - - constructor(userId: string) { - super(); - this.userId = userId; - } - - protected async _call( - arg: z.infer, - runManager?: CallbackManagerForToolRun, - parentConfig?: ToolRunnableConfig> - ): Promise { - try { - const supabase = getSupabaseClient(); - - const { data: messages, error } = await supabase - .from('support') - .select('message, is_from_user, created_at') - .eq('user_id', this.userId) - .order('created_at', { ascending: false }) - .limit(10); - - if (error) { - return 'Не удалось получить историю сообщений.'; - } - - if (!messages || messages.length === 0) { - return 'История сообщений поддержки пуста. Это первое обращение пользователя.'; - } - - const chronologicalMessages = messages.reverse(); - - const contextMessages = chronologicalMessages.map((msg, index) => { - const role = msg.is_from_user ? 'Пользователь' : 'Агент поддержки'; - const time = new Date(msg.created_at).toLocaleString('ru-RU'); - return `${index + 1}. [${time}] ${role}: ${msg.message}`; - }).join('\n'); - - return `Последние сообщения из истории поддержки (${messages.length} сообщений):\n\n${contextMessages}\n\nИспользуй этот контекст для понимания предыдущих обращений пользователя и предоставления более точных ответов.`; - - } catch (error) { - return 'Произошла ошибка при получении истории сообщений.'; - } - } -} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts b/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts deleted file mode 100644 index 3841672..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/support-ai-agent/vector-store.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; -import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase'; -import { GigaChatEmbeddings } from 'langchain-gigachat'; -import { Agent } from 'node:https'; - -const httpsAgent = new Agent({ - rejectUnauthorized: false, -}); - -let vectorStoreInstance: SupabaseVectorStore | null = null; - -export function getVectorStore(): SupabaseVectorStore { - if (!vectorStoreInstance) { - const client = createClient( - process.env.RAG_SUPABASE_URL!, - process.env.RAG_SUPABASE_SERVICE_ROLE_KEY!, - ); - - vectorStoreInstance = new SupabaseVectorStore( - new GigaChatEmbeddings({ - credentials: process.env.GIGA_AUTH, - httpsAgent, - }), - { - client, - tableName: 'slon', - queryName: 'match_slon' - } - ); - } - - return vectorStoreInstance; -} \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js b/server/routers/kfu-m-24-1/sber_mobile/supportApi.js deleted file mode 100644 index afb4ea8..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/supportApi.js +++ /dev/null @@ -1,151 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); -const { getGigaAuth } = require('./get-constants'); -const { SupportAgent } = require('./support-ai-agent/support-agent'); - -// Хранилище агентов для разных пользователей -const userAgents = new Map(); - -/** - * Получить или создать агента для пользователя - */ -async function getUserAgent(userId) { - if (!userAgents.has(userId)) { - const GIGA_AUTH = await getGigaAuth(); - const config = { - threadId: userId, - temperature: 0.7, - GIGA_AUTH - }; - userAgents.set(userId, new SupportAgent(config)); - } - return userAgents.get(userId); -} - -// GET /api/support - Получить историю сообщений пользователя -router.get('/support', async (req, res) => { - const supabase = getSupabaseClient(); - const { user_id } = req.query; - - if (!user_id) { - return res.status(400).json({ error: 'user_id обязателен' }); - } - - try { - // Получаем все сообщения пользователя из базы данных - const { data: messages, error } = await supabase - .from('support') - .select('*') - .eq('user_id', user_id) - .order('created_at', { ascending: true }); - - if (error) { - return res.status(400).json({ error: error.message }); - } - - res.json({ - messages: messages || [], - success: true - }); - - } catch (error) { - console.error('Ошибка в GET /support:', error); - res.status(500).json({ - error: 'Внутренняя ошибка сервера', - success: false - }); - } -}); - -// POST /api/support -router.post('/support', async (req, res) => { - const supabase = getSupabaseClient(); - const { user_id, message, apartment_id } = req.body; - - if (!user_id || !message) { - return res.status(400).json({ error: 'user_id и message обязательны' }); - } - - try { - // Сохраняем сообщение пользователя в базу данных - const { error: insertError } = await supabase - .from('support') - .insert({ user_id, message, is_from_user: true }); - - if (insertError) { - return res.status(400).json({ error: insertError.message }); - } - - // Получаем агента для пользователя - const agent = await getUserAgent(user_id); - - // Получаем ответ от AI-агента, передавая apartment_id - const aiResponse = await agent.processMessage(message, apartment_id); - - if (!aiResponse.success) { - console.error('Ошибка AI-агента:', aiResponse.error); - return res.status(500).json({ - error: 'Ошибка при генерации ответа', - reply: 'Извините, произошла ошибка. Попробуйте позже.' - }); - } - - // Сохраняем ответ агента в базу данных - const { error: responseError } = await supabase - .from('support') - .insert({ - user_id, - message: aiResponse.content, - is_from_user: false - }); - - if (responseError) { - console.error('Ошибка сохранения ответа:', responseError); - // Не возвращаем ошибку пользователю, так как ответ уже сгенерирован - } - - // Возвращаем ответ пользователю - res.json({ - reply: aiResponse.content, - success: true - }); - - } catch (error) { - console.error('Ошибка в supportApi:', error); - res.status(500).json({ - error: 'Внутренняя ошибка сервера', - reply: 'Извините, произошла ошибка. Попробуйте позже.' - }); - } -}); - -// DELETE /api/support/history/:userId - Очистка истории диалога -router.delete('/support/history/:userId', async (req, res) => { - const { userId } = req.params; - - try { - if (userAgents.has(userId)) { - const agent = userAgents.get(userId); - await agent.clearHistory(); - - res.json({ - message: 'История диалога очищена', - success: true - }); - } else { - res.json({ - message: 'Агент для данного пользователя не найден', - success: true - }); - } - - } catch (error) { - console.error('Ошибка в /support/history:', error); - res.status(500).json({ - error: 'Внутренняя ошибка сервера', - success: false - }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/tickets.js b/server/routers/kfu-m-24-1/sber_mobile/tickets.js deleted file mode 100644 index bfeceb8..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/tickets.js +++ /dev/null @@ -1,31 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить заявки пользователя по квартире -router.get('/tickets', async (req, res) => { - const supabase = getSupabaseClient(); - const { user_id, apartment_id } = req.query; - - if (!user_id || !apartment_id) { - return res.status(400).json({ error: 'Требуется user_id и apartment_id' }); - } - - try { - const { data, error } = await supabase - .from('tickets') - .select('*') - .eq('user_id', user_id) - .eq('apartment_id', apartment_id) - .order('created_at', { ascending: false }); - - if (error) { - return res.status(400).json({ error: error.message }); - } - - res.json(data || []); - } catch (err) { - res.status(500).json({ error: 'Внутренняя ошибка сервера' }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/user_apartments.js b/server/routers/kfu-m-24-1/sber_mobile/user_apartments.js deleted file mode 100644 index e5421ba..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/user_apartments.js +++ /dev/null @@ -1,18 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все квартиры пользователя -router.get('/user-apartments', async (req, res) => { - const supabase = getSupabaseClient(); - const { user_id } = req.query; - if (!user_id) return res.status(400).json({ error: 'user_id required' }); - const { data: links, error: err1 } = await supabase.from('apartment_residents').select('apartment_id').eq('user_id', user_id); - if (err1) return res.status(400).json({ error: err1.message }); - const apartmentIds = links.map(l => l.apartment_id); - if (!apartmentIds.length) return res.json([]); - const { data, error } = await supabase.from('apartments').select('*').in('id', apartmentIds); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js b/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js deleted file mode 100644 index b5082b6..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/utility_payments.js +++ /dev/null @@ -1,50 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить платежки с деталями для квартиры -router.get('/payment-services', async (req, res) => { - const supabase = getSupabaseClient(); - const { apartment_id } = req.query; - if (!apartment_id) return res.status(400).json({ error: 'apartment_id обязателен' }); - - // Получаем все платежки по квартире - const { data: services, error: servicesError } = await supabase - .from('payment_services') - .select('id, name, icon, amount, is_paid, payment_method') - .eq('apartment_id', apartment_id); - if (servicesError) return res.status(400).json({ error: servicesError.message }); - - // Получаем детализацию по всем платежкам - const serviceIds = services.map(s => s.id); - let details = []; - if (serviceIds.length > 0) { - const { data: detailsData, error: detailsError } = await supabase - .from('payment_service_details') - .select('id, payment_service_id, name, amount') - .in('payment_service_id', serviceIds); - if (detailsError) return res.status(400).json({ error: detailsError.message }); - details = detailsData; - } - - // Формируем структуру для фронта - const result = services.map(service => { - const serviceDetails = details.filter(d => d.payment_service_id === service.id).map(detail => ({ - id: detail.id, - name: detail.name, - amount: detail.amount - })); - return { - id: service.id, - title: service.name, - icon: service.icon, - amount: service.amount, - isPaid: service.is_paid, - paymentMethod: service.payment_method, - details: serviceDetails - }; - }); - - res.json(result); -}); - -module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/votes.js b/server/routers/kfu-m-24-1/sber_mobile/votes.js deleted file mode 100644 index 9a861da..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/votes.js +++ /dev/null @@ -1,105 +0,0 @@ -const router = require('express').Router(); -const { getSupabaseClient } = require('./supabaseClient'); - -// Получить все голоса по инициативе -router.get('/votes/:initiative_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { initiative_id } = req.params; - const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) - return res.status(400).json({ error: error.message }); - res.json(data); -}); - -// Получить голос пользователя по инициативе -router.get('/votes/:initiative_id/user/:user_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { initiative_id, user_id } = req.params; - const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id).eq('user_id', user_id).single(); - if (error) { - console.log(error, '/votes/:initiative_id/:user_id') - console.log(initiative_id, user_id) - return res.status(400).json({ error: error.message }); - } - res.json(data); -}); - -// Получить статистику голосов по инициативе -router.get('/votes/stats/:initiative_id', async (req, res) => { - const supabase = getSupabaseClient(); - const { initiative_id } = req.params; - - const { data, error } = await supabase - .from('votes') - .select('vote_type') - .eq('initiative_id', initiative_id); - console.log(data, error) - if (error) { - console.log('/votes/:initiative_id/stats') - res.status(400).json({ error: error.message }); - } - const stats = { - for: data.filter(vote => vote.vote_type === 'for').length, - against: data.filter(vote => vote.vote_type === 'against').length, - total: data.length - }; - - res.json(stats); -}); - -// Проголосовать (создать, обновить или удалить голос) -router.post('/votes', async (req, res) => { - const supabase = getSupabaseClient(); - const { initiative_id, user_id, vote_type } = req.body; - - // Проверяем существующий голос - const { data: existingVote, error: checkError } = await supabase - .from('votes') - .select('*') - .eq('initiative_id', initiative_id) - .eq('user_id', user_id) - .single(); - - if (checkError && checkError.code !== 'PGRST116') { - console.log('1/votes') - return res.status(400).json({ error: checkError.message }); - } - - if (existingVote) { - if (existingVote.vote_type === vote_type) { - // Если нажали тот же тип голоса - УДАЛЯЕМ (отменяем голос) - const { error: deleteError } = await supabase - .from('votes') - .delete() - .eq('initiative_id', initiative_id) - .eq('user_id', user_id); - - if (deleteError) return res.status(400).json({ error: deleteError.message }); - res.json({ message: 'Vote removed', action: 'removed', previous_vote: existingVote.vote_type }); - } else { - // Если нажали другой тип голоса - ОБНОВЛЯЕМ - const { data, error } = await supabase - .from('votes') - .update({ vote_type }) - .eq('initiative_id', initiative_id) - .eq('user_id', user_id) - .select() - .single(); - - if (error) return res.status(400).json({ error: error.message }); - res.json({ ...data, action: 'updated', previous_vote: existingVote.vote_type }); - } - } else { - // Если голоса нет - СОЗДАЕМ новый - const { data, error } = await supabase - .from('votes') - .insert([{ initiative_id, user_id, vote_type }]) - .select() - .single(); - - if (error) return res.status(400).json({ error: error.message }); - res.json({ ...data, action: 'created' }); - } -}); - -module.exports = router; \ No newline at end of file From f65fd175ca0ab7a0766eb7d1c8fc1d7bc1155b27 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 14 Oct 2025 11:08:29 +0300 Subject: [PATCH 118/147] add /procurement --- server/index.ts | 3 +- server/routers/procurement/index.js | 575 ++++++++++++++++++ server/routers/procurement/mocks/auth.json | 46 ++ .../routers/procurement/mocks/companies.json | 430 +++++++++++++ .../routers/procurement/mocks/products.json | 158 +++++ server/routers/procurement/mocks/search.json | 122 ++++ server/routers/procurement/mocks/user.json | 13 + 7 files changed, 1346 insertions(+), 1 deletion(-) create mode 100644 server/routers/procurement/index.js create mode 100644 server/routers/procurement/mocks/auth.json create mode 100644 server/routers/procurement/mocks/companies.json create mode 100644 server/routers/procurement/mocks/products.json create mode 100644 server/routers/procurement/mocks/search.json create mode 100644 server/routers/procurement/mocks/user.json diff --git a/server/index.ts b/server/index.ts index fe23c60..6ed51c4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,6 +20,7 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' +import procurementRouter from './routers/procurement' import { setIo } from './io' export const app = express() @@ -105,7 +106,7 @@ const initServer = async () => { app.use("/esc", escRouter) app.use('/connectme', connectmeRouter) app.use('/questioneer', questioneerRouter) - + app.use('/procurement', procurementRouter) app.use(errorHandler) // Создаем обычный HTTP сервер diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js new file mode 100644 index 0000000..8425a31 --- /dev/null +++ b/server/routers/procurement/index.js @@ -0,0 +1,575 @@ +const router = require('express').Router(); +const fs = require('fs'); +const path = require('path'); + +const timer = (time = 300) => (req, res, next) => setTimeout(next, time); + +// Настройка кодировки UTF-8 +router.use((req, res, next) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + next(); +}); + +router.use(timer()); + +// Загружаем моки из JSON файлов +const loadMockData = (filename) => { + try { + const filePath = path.join(__dirname, '..', 'mocks', filename); + const data = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(data); + } catch (error) { + console.error(`Ошибка загрузки ${filename}:`, error); + return {}; + } +}; + +// Загружаем все моки +const userMocks = loadMockData('user.json'); +const companyMocks = loadMockData('companies.json'); +const productMocks = loadMockData('products.json'); +const searchMocks = loadMockData('search.json'); +const authMocks = loadMockData('auth.json'); + +// Логируем загруженные данные для отладки +console.log('SearchMocks loaded:', searchMocks); +console.log('Suggestions:', searchMocks.suggestions); + +// Вспомогательные функции для генерации динамических данных +const generateTimestamp = () => Date.now(); +const generateDate = (daysAgo) => new Date(Date.now() - 86400000 * daysAgo).toISOString(); + +// Функция для замены плейсхолдеров в данных +const processMockData = (data) => { + const timestamp = generateTimestamp(); + const processedData = JSON.stringify(data) + .replace(/{{timestamp}}/g, timestamp) + .replace(/{{date-(\d+)-days?}}/g, (match, days) => generateDate(parseInt(days))) + .replace(/{{date-1-day}}/g, generateDate(1)) + .replace(/{{date-2-days}}/g, generateDate(2)) + .replace(/{{date-3-days}}/g, generateDate(3)) + .replace(/{{date-4-days}}/g, generateDate(4)) + .replace(/{{date-5-days}}/g, generateDate(5)) + .replace(/{{date-6-days}}/g, generateDate(6)) + .replace(/{{date-7-days}}/g, generateDate(7)) + .replace(/{{date-8-days}}/g, generateDate(8)) + .replace(/{{date-10-days}}/g, generateDate(10)) + .replace(/{{date-12-days}}/g, generateDate(12)) + .replace(/{{date-15-days}}/g, generateDate(15)) + .replace(/{{date-18-days}}/g, generateDate(18)) + .replace(/{{date-20-days}}/g, generateDate(20)) + .replace(/{{date-21-days}}/g, generateDate(21)) + .replace(/{{date-25-days}}/g, generateDate(25)) + .replace(/{{date-28-days}}/g, generateDate(28)) + .replace(/{{date-30-days}}/g, generateDate(30)) + .replace(/{{date-35-days}}/g, generateDate(35)); + + return JSON.parse(processedData); +}; + +// Auth endpoints +router.post('/auth/login', (req, res) => { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + error: 'Validation failed', + message: authMocks.errorMessages?.validationFailed || 'Email и пароль обязательны' + }); + } + + // Имитация неверных учетных данных + if (password === 'wrong') { + return res.status(401).json({ + error: 'Unauthorized', + message: authMocks.errorMessages?.invalidCredentials || 'Неверный email или пароль' + }); + } + + const authResponse = processMockData(authMocks.mockAuthResponse); + res.status(200).json(authResponse); +}); + +router.post('/auth/register', (req, res) => { + const { email, password, inn, agreeToTerms } = req.body; + + if (!email || !password || !inn) { + return res.status(400).json({ + error: 'Validation failed', + message: authMocks.errorMessages?.validationFailed || 'Заполните все обязательные поля' + }); + } + + if (!agreeToTerms) { + return res.status(400).json({ + error: 'Validation failed', + message: authMocks.errorMessages?.termsRequired || 'Необходимо принять условия использования' + }); + } + + // Создаем нового пользователя с данными из регистрации + const newUser = { + id: 'user-' + generateTimestamp(), + email: email, + firstName: req.body.firstName || 'Иван', + lastName: req.body.lastName || 'Петров', + position: req.body.position || 'Директор' + }; + + const newCompany = { + id: 'company-' + generateTimestamp(), + name: req.body.fullName || companyMocks.mockCompany?.name, + inn: req.body.inn, + ogrn: req.body.ogrn || companyMocks.mockCompany?.ogrn, + fullName: req.body.fullName || companyMocks.mockCompany?.fullName, + shortName: req.body.shortName, + legalForm: req.body.legalForm || 'ООО', + industry: req.body.industry || 'Другое', + companySize: req.body.companySize || '1-10', + website: req.body.website || '', + verified: false, + rating: 0 + }; + + res.status(201).json({ + user: newUser, + company: newCompany, + tokens: { + accessToken: 'mock-access-token-' + generateTimestamp(), + refreshToken: 'mock-refresh-token-' + generateTimestamp() + } + }); +}); + +router.post('/auth/logout', (req, res) => { + res.status(200).json({ + message: authMocks.successMessages?.logoutSuccess || 'Успешный выход' + }); +}); + +router.post('/auth/refresh', (req, res) => { + const { refreshToken } = req.body; + + if (!refreshToken) { + return res.status(401).json({ + error: 'Unauthorized', + message: authMocks.errorMessages?.refreshTokenRequired || 'Refresh token обязателен' + }); + } + + res.status(200).json({ + accessToken: 'mock-access-token-refreshed-' + generateTimestamp(), + refreshToken: 'mock-refresh-token-refreshed-' + generateTimestamp() + }); +}); + +router.get('/auth/verify-email/:token', (req, res) => { + res.status(200).json({ + message: authMocks.successMessages?.emailVerified || 'Email успешно подтвержден' + }); +}); + +router.post('/auth/request-password-reset', (req, res) => { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ + error: 'Validation failed', + message: authMocks.errorMessages?.emailRequired || 'Email обязателен' + }); + } + + res.status(200).json({ + message: authMocks.successMessages?.passwordResetSent || 'Письмо для восстановления пароля отправлено' + }); +}); + +router.post('/auth/reset-password', (req, res) => { + const { token, newPassword } = req.body; + + if (!token || !newPassword) { + return res.status(400).json({ + error: 'Validation failed', + message: authMocks.errorMessages?.validationFailed || 'Token и новый пароль обязательны' + }); + } + + res.status(200).json({ + message: authMocks.successMessages?.passwordResetSuccess || 'Пароль успешно изменен' + }); +}); + +// Companies endpoints +router.get('/companies/my/stats', (req, res) => { + res.status(200).json({ + profileViews: 142, + profileViewsChange: 12, + sentRequests: 8, + sentRequestsChange: 2, + receivedRequests: 15, + receivedRequestsChange: 5, + newMessages: 3, + rating: 4.5 + }); +}); + +router.get('/companies/:id', (req, res) => { + const company = processMockData(companyMocks.mockCompany); + res.status(200).json(company); +}); + +router.patch('/companies/:id', (req, res) => { + const updatedCompany = { + ...processMockData(companyMocks.mockCompany), + ...req.body, + id: req.params.id + }; + + res.status(200).json(updatedCompany); +}); + +router.get('/companies/:id/stats', (req, res) => { + res.status(200).json({ + profileViews: 142, + profileViewsChange: 12, + sentRequests: 8, + sentRequestsChange: 2, + receivedRequests: 15, + receivedRequestsChange: 5, + newMessages: 3, + rating: 4.5 + }); +}); + +router.post('/companies/:id/logo', (req, res) => { + res.status(200).json({ + logoUrl: 'https://via.placeholder.com/200x200/4299E1/FFFFFF?text=Logo' + }); +}); + +router.get('/companies/check-inn/:inn', (req, res) => { + const inn = req.params.inn; + + // Имитация проверки ИНН + if (inn.length !== 10 && inn.length !== 12) { + return res.status(400).json({ + error: 'Validation failed', + message: authMocks.errorMessages?.innValidation || 'ИНН должен содержать 10 или 12 цифр' + }); + } + + const mockINNData = companyMocks.mockINNData || {}; + const companyData = mockINNData[inn] || { + name: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ТЕСТОВАЯ КОМПАНИЯ ' + inn + '"', + ogrn: '10277' + inn, + legal_form: 'ООО' + }; + + res.status(200).json({ data: companyData }); +}); + +// Products endpoints +router.get('/products/my', (req, res) => { + const products = processMockData(productMocks.mockProducts); + res.status(200).json(products); +}); + +router.get('/products', (req, res) => { + const products = processMockData(productMocks.mockProducts); + res.status(200).json({ + items: products, + total: products.length, + page: 1, + pageSize: 20 + }); +}); + +router.post('/products', (req, res) => { + const newProduct = { + id: 'prod-' + generateTimestamp(), + ...req.body, + companyId: 'company-123', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + res.status(201).json(newProduct); +}); + +router.get('/products/:id', (req, res) => { + const products = processMockData(productMocks.mockProducts); + const product = products.find(p => p.id === req.params.id); + + if (product) { + res.status(200).json(product); + } else { + res.status(200).json({ + id: req.params.id, + name: 'Продукт ' + req.params.id, + description: 'Описание продукта', + category: 'Категория', + type: 'sell', + companyId: 'company-123', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }); + } +}); + +router.patch('/products/:id', (req, res) => { + const products = processMockData(productMocks.mockProducts); + const product = products.find(p => p.id === req.params.id); + + const updatedProduct = { + ...(product || {}), + ...req.body, + id: req.params.id, + updatedAt: new Date().toISOString() + }; + + res.status(200).json(updatedProduct); +}); + +router.delete('/products/:id', (req, res) => { + res.status(204).send(); +}); + +// Тестовый endpoint для проверки данных +router.get('/test-data', (req, res) => { + res.status(200).json({ + companiesCount: companyMocks.mockCompanies?.length || 0, + suggestionsCount: searchMocks.suggestions?.length || 0, + firstCompany: companyMocks.mockCompanies?.[0] || null, + firstSuggestion: searchMocks.suggestions?.[0] || null, + allSuggestions: searchMocks.suggestions || [] + }); +}); +router.get('/search', (req, res) => { + const { + query, + industries, + companySize, + geography, + minRating, + type, + sortBy = 'relevance', + sortOrder = 'desc', + page = 1, + limit = 20 + } = req.query; + + console.log('Search query:', query); + console.log('Search params:', req.query); + + const companies = processMockData(companyMocks.mockCompanies); + console.log('Companies loaded:', companies.length); + console.log('First company:', companies[0]); + + let filtered = [...companies]; + + // Поиск по тексту + if (query) { + const q = query.toLowerCase().trim(); + console.log('Searching for:', q); + + filtered = filtered.filter(c => { + const fullName = (c.fullName || '').toLowerCase(); + const shortName = (c.shortName || '').toLowerCase(); + const industry = (c.industry || '').toLowerCase(); + const slogan = (c.slogan || '').toLowerCase(); + const legalAddress = (c.legalAddress || '').toLowerCase(); + + const matches = fullName.includes(q) || + shortName.includes(q) || + industry.includes(q) || + slogan.includes(q) || + legalAddress.includes(q); + + if (matches) { + console.log('Found match:', c.shortName, 'in', { fullName, shortName, industry, slogan, legalAddress }); + } + + return matches; + }); + + console.log('Filtered results:', filtered.length); + } + + // Фильтр по отраслям + if (industries && industries.length > 0) { + const industriesArray = Array.isArray(industries) ? industries : [industries]; + filtered = filtered.filter(c => industriesArray.includes(c.industry)); + } + + // Фильтр по размеру компании + if (companySize && companySize.length > 0) { + const sizeArray = Array.isArray(companySize) ? companySize : [companySize]; + filtered = filtered.filter(c => sizeArray.includes(c.companySize)); + } + + // Фильтр по рейтингу + if (minRating) { + filtered = filtered.filter(c => c.rating >= parseFloat(minRating)); + } + + // Сортировка + filtered.sort((a, b) => { + let comparison = 0; + + switch (sortBy) { + case 'rating': + comparison = a.rating - b.rating; + break; + case 'name': + comparison = (a.shortName || a.fullName).localeCompare(b.shortName || b.fullName); + break; + case 'relevance': + default: + // Для релевантности используем рейтинг как основной критерий + comparison = a.rating - b.rating; + break; + } + + return sortOrder === 'asc' ? comparison : -comparison; + }); + + const total = filtered.length; + const totalPages = Math.ceil(total / limit); + const startIndex = (page - 1) * limit; + const endIndex = startIndex + parseInt(limit); + const paginatedResults = filtered.slice(startIndex, endIndex); + + res.status(200).json({ + companies: paginatedResults, + total, + page: parseInt(page), + totalPages + }); +}); + +router.post('/search/ai', (req, res) => { + const { query } = req.body; + + // Простая логика AI поиска на основе ключевых слов + const companies = processMockData(companyMocks.mockCompanies); + let aiResults = [...companies]; + const q = query.toLowerCase(); + + // Определяем приоритетные отрасли на основе запроса + if (q.includes('строитель') || q.includes('строй') || q.includes('дом') || q.includes('здание')) { + aiResults = aiResults.filter(c => c.industry === 'Строительство'); + } else if (q.includes('металл') || q.includes('сталь') || q.includes('производств') || q.includes('завод')) { + aiResults = aiResults.filter(c => c.industry === 'Производство'); + } else if (q.includes('логистик') || q.includes('доставк') || q.includes('транспорт')) { + aiResults = aiResults.filter(c => c.industry === 'Логистика'); + } else if (q.includes('торговл') || q.includes('продаж') || q.includes('снабжени')) { + aiResults = aiResults.filter(c => c.industry === 'Торговля'); + } else if (q.includes('it') || q.includes('программ') || q.includes('технолог') || q.includes('софт')) { + aiResults = aiResults.filter(c => c.industry === 'IT'); + } else if (q.includes('услуг') || q.includes('консалт') || q.includes('помощь')) { + aiResults = aiResults.filter(c => c.industry === 'Услуги'); + } + + // Сортируем по рейтингу и берем топ-5 + aiResults.sort((a, b) => b.rating - a.rating); + const topResults = aiResults.slice(0, 5); + + // Генерируем AI предложение + let aiSuggestion = `На основе вашего запроса "${query}" мы нашли ${topResults.length} подходящих партнеров. `; + + if (topResults.length > 0) { + const industries = [...new Set(topResults.map(c => c.industry))]; + aiSuggestion += `Рекомендуем обратить внимание на компании в сфере ${industries.join(', ')}. `; + aiSuggestion += `Все предложенные партнеры имеют высокий рейтинг (от ${Math.min(...topResults.map(c => c.rating)).toFixed(1)} до ${Math.max(...topResults.map(c => c.rating)).toFixed(1)}) и подтвержденный статус.`; + } else { + aiSuggestion += 'Попробуйте изменить формулировку запроса или использовать фильтры для более точного поиска.'; + } + + res.status(200).json({ + companies: topResults, + total: topResults.length, + page: 1, + totalPages: 1, + aiSuggestion + }); +}); + +router.get('/search/suggestions', (req, res) => { + const { q } = req.query; + + const suggestions = searchMocks.suggestions || []; + console.log('Suggestions loaded:', suggestions); + console.log('Query:', q); + + const filtered = q + ? suggestions.filter(s => s.toLowerCase().includes(q.toLowerCase())) + : suggestions.slice(0, 10); // Показываем только первые 10 если нет запроса + + console.log('Filtered suggestions:', filtered); + res.status(200).json(filtered); +}); + +router.get('/search/recommendations', (req, res) => { + // Динамически генерируем рекомендации на основе топовых компаний + const companies = processMockData(companyMocks.mockCompanies); + const topCompanies = companies + .filter(c => c.verified && c.rating >= 4.5) + .sort((a, b) => b.rating - a.rating) + .slice(0, 6); + + const recommendations = topCompanies.map(company => ({ + id: company.id, + name: company.shortName || company.fullName, + industry: company.industry, + logo: company.logo, + matchScore: Math.floor(company.rating * 20), // Конвертируем рейтинг в проценты + reason: getRecommendationReason(company, searchMocks.recommendationReasons) + })); + + res.status(200).json(recommendations); +}); + +// Вспомогательная функция для генерации причин рекомендаций +function getRecommendationReason(company, reasons) { + return reasons?.[company.industry] || 'Проверенный партнер с высоким рейтингом'; +} + +router.get('/search/history', (req, res) => { + const history = processMockData(searchMocks.searchHistory); + res.status(200).json(history); +}); + +router.get('/search/saved', (req, res) => { + const savedSearches = processMockData(searchMocks.savedSearches); + res.status(200).json(savedSearches); +}); + +router.post('/search/saved', (req, res) => { + const { name, params } = req.body; + + res.status(201).json({ + id: 'saved-' + generateTimestamp(), + name, + params, + createdAt: new Date().toISOString() + }); +}); + +router.delete('/search/saved/:id', (req, res) => { + res.status(204).send(); +}); + +router.post('/search/favorites/:companyId', (req, res) => { + res.status(200).json({ + message: authMocks.successMessages?.addedToFavorites || 'Добавлено в избранное' + }); +}); + +router.delete('/search/favorites/:companyId', (req, res) => { + res.status(204).send(); +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/procurement/mocks/auth.json b/server/routers/procurement/mocks/auth.json new file mode 100644 index 0000000..eabb81d --- /dev/null +++ b/server/routers/procurement/mocks/auth.json @@ -0,0 +1,46 @@ +{ + "mockAuthResponse": { + "user": { + "id": "user-123", + "email": "test@company.com", + "firstName": "Иван", + "lastName": "Петров", + "position": "Генеральный директор" + }, + "company": { + "id": "company-123", + "name": "ООО \"Тестовая Компания\"", + "inn": "7707083893", + "ogrn": "1027700132195", + "fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"", + "shortName": "ООО \"Тест\"", + "legalForm": "ООО", + "industry": "Производство", + "companySize": "50-100", + "website": "https://test-company.ru", + "verified": true, + "rating": 4.5 + }, + "tokens": { + "accessToken": "mock-access-token-{{timestamp}}", + "refreshToken": "mock-refresh-token-{{timestamp}}" + } + }, + "errorMessages": { + "validationFailed": "Заполните все обязательные поля", + "emailRequired": "Email обязателен", + "passwordRequired": "Пароль обязателен", + "termsRequired": "Необходимо принять условия использования", + "invalidCredentials": "Неверный email или пароль", + "refreshTokenRequired": "Refresh token обязателен", + "innValidation": "ИНН должен содержать 10 или 12 цифр" + }, + "successMessages": { + "logoutSuccess": "Успешный выход", + "emailVerified": "Email успешно подтвержден", + "passwordResetSent": "Письмо для восстановления пароля отправлено", + "passwordResetSuccess": "Пароль успешно изменен", + "logoUploaded": "Логотип успешно загружен", + "addedToFavorites": "Добавлено в избранное" + } +} diff --git a/server/routers/procurement/mocks/companies.json b/server/routers/procurement/mocks/companies.json new file mode 100644 index 0000000..8e3bffe --- /dev/null +++ b/server/routers/procurement/mocks/companies.json @@ -0,0 +1,430 @@ +{ + "mockCompany": { + "id": "company-123", + "name": "ООО \"Тестовая Компания\"", + "inn": "7707083893", + "ogrn": "1027700132195", + "fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"", + "shortName": "ООО \"Тест\"", + "legalForm": "ООО", + "industry": "Производство", + "companySize": "50-100", + "website": "https://test-company.ru", + "verified": true, + "rating": 4.5 + }, + "mockINNData": { + "7707083893": { + "name": "ПУБЛИЧНОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО \"СБЕРБАНК РОССИИ\"", + "ogrn": "1027700132195", + "legal_form": "ПАО" + }, + "7730048036": { + "name": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ \"КОМПАНИЯ\"", + "ogrn": "1047730048036", + "legal_form": "ООО" + } + }, + "mockCompanies": [ + { + "id": "company-1", + "inn": "7707083893", + "ogrn": "1027700132195", + "fullName": "Общество с ограниченной ответственностью \"СтройКомплект\"", + "shortName": "ООО \"СтройКомплект\"", + "legalForm": "ООО", + "industry": "Строительство", + "companySize": "100-250", + "website": "https://stroykomplekt.ru", + "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=SK", + "slogan": "Строим будущее вместе", + "rating": 4.8, + "verified": true, + "phone": "+7 (495) 123-45-67", + "email": "info@stroykomplekt.ru", + "legalAddress": "г. Москва, ул. Строительная, д. 15", + "foundedYear": 2010, + "employeeCount": "150 сотрудников" + }, + { + "id": "company-6", + "inn": "7707083894", + "ogrn": "1027700132196", + "fullName": "Акционерное общество \"Московский Строй\"", + "shortName": "АО \"Московский Строй\"", + "legalForm": "АО", + "industry": "Строительство", + "companySize": "500+", + "website": "https://moscow-stroy.ru", + "logo": "https://via.placeholder.com/100x100/1A365D/FFFFFF?text=MS", + "slogan": "Качество и надежность с 1995 года", + "rating": 4.9, + "verified": true, + "phone": "+7 (495) 987-65-43", + "email": "info@moscow-stroy.ru", + "legalAddress": "г. Москва, пр. Мира, д. 100", + "foundedYear": 1995, + "employeeCount": "800+ сотрудников" + }, + { + "id": "company-7", + "inn": "7707083895", + "ogrn": "1027700132197", + "fullName": "Общество с ограниченной ответственностью \"ДомСтрой\"", + "shortName": "ООО \"ДомСтрой\"", + "legalForm": "ООО", + "industry": "Строительство", + "companySize": "50-100", + "website": "https://domstroy.ru", + "logo": "https://via.placeholder.com/100x100/2D3748/FFFFFF?text=DS", + "slogan": "Строим дома мечты", + "rating": 4.3, + "verified": true, + "phone": "+7 (495) 555-12-34", + "email": "info@domstroy.ru", + "legalAddress": "г. Москва, ул. Жилстроительная, д. 25", + "foundedYear": 2015, + "employeeCount": "75 сотрудников" + }, + { + "id": "company-4", + "inn": "7730048038", + "ogrn": "1047730048038", + "fullName": "Общество с ограниченной ответственностью \"МеталлПром\"", + "shortName": "ООО \"МеталлПром\"", + "legalForm": "ООО", + "industry": "Производство", + "companySize": "250-500", + "website": "https://metallprom.ru", + "logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MP", + "slogan": "Металл высшего качества", + "rating": 4.7, + "verified": true, + "phone": "+7 (495) 456-78-90", + "email": "info@metallprom.ru", + "legalAddress": "г. Москва, ул. Промышленная, д. 50", + "foundedYear": 2008, + "employeeCount": "300 сотрудников" + }, + { + "id": "company-8", + "inn": "7730048040", + "ogrn": "1047730048040", + "fullName": "Общество с ограниченной ответственностью \"СтальМет\"", + "shortName": "ООО \"СтальМет\"", + "legalForm": "ООО", + "industry": "Производство", + "companySize": "100-250", + "website": "https://stalmet.ru", + "logo": "https://via.placeholder.com/100x100/9C4221/FFFFFF?text=SM", + "slogan": "Сталь для промышленности", + "rating": 4.6, + "verified": true, + "phone": "+7 (495) 777-88-99", + "email": "sales@stalmet.ru", + "legalAddress": "г. Москва, ул. Металлургическая, д. 30", + "foundedYear": 2012, + "employeeCount": "180 сотрудников" + }, + { + "id": "company-9", + "inn": "7730048041", + "ogrn": "1047730048041", + "fullName": "Общество с ограниченной ответственностью \"ПластМаш\"", + "shortName": "ООО \"ПластМаш\"", + "legalForm": "ООО", + "industry": "Производство", + "companySize": "50-100", + "website": "https://plastmash.ru", + "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PM", + "slogan": "Пластиковые изделия для всех отраслей", + "rating": 4.4, + "verified": true, + "phone": "+7 (495) 333-44-55", + "email": "info@plastmash.ru", + "legalAddress": "г. Москва, ул. Пластиковая, д. 12", + "foundedYear": 2018, + "employeeCount": "80 сотрудников" + }, + { + "id": "company-2", + "inn": "7730048036", + "ogrn": "1047730048036", + "fullName": "Общество с ограниченной ответственностью \"ТехСнаб\"", + "shortName": "ООО \"ТехСнаб\"", + "legalForm": "ООО", + "industry": "Торговля", + "companySize": "50-100", + "website": "https://techsnab.ru", + "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=TS", + "slogan": "Снабжение для профессионалов", + "rating": 4.5, + "verified": true, + "phone": "+7 (495) 234-56-78", + "email": "sales@techsnab.ru", + "legalAddress": "г. Москва, ул. Торговая, д. 8", + "foundedYear": 2010, + "employeeCount": "90 сотрудников" + }, + { + "id": "company-10", + "inn": "7730048042", + "ogrn": "1047730048042", + "fullName": "Общество с ограниченной ответственностью \"ОптТорг\"", + "shortName": "ООО \"ОптТорг\"", + "legalForm": "ООО", + "industry": "Торговля", + "companySize": "100-250", + "website": "https://opttorg.ru", + "logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=OT", + "slogan": "Оптовые поставки по всей России", + "rating": 4.2, + "verified": true, + "phone": "+7 (495) 111-22-33", + "email": "info@opttorg.ru", + "legalAddress": "г. Москва, ул. Оптовая, д. 45", + "foundedYear": 2005, + "employeeCount": "200 сотрудников" + }, + { + "id": "company-5", + "inn": "7730048039", + "ogrn": "1047730048039", + "fullName": "Общество с ограниченной ответственностью \"ЛогистикПлюс\"", + "shortName": "ООО \"ЛогистикПлюс\"", + "legalForm": "ООО", + "industry": "Логистика", + "companySize": "100-250", + "website": "https://logistikplus.ru", + "logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=LP", + "slogan": "Доставляем быстро и надежно", + "rating": 4.6, + "verified": true, + "phone": "+7 (495) 567-89-01", + "email": "info@logistikplus.ru", + "legalAddress": "г. Москва, ул. Логистическая, д. 20", + "foundedYear": 2013, + "employeeCount": "150 сотрудников" + }, + { + "id": "company-11", + "inn": "7730048043", + "ogrn": "1047730048043", + "fullName": "Общество с ограниченной ответственностью \"ТрансЛогист\"", + "shortName": "ООО \"ТрансЛогист\"", + "legalForm": "ООО", + "industry": "Логистика", + "companySize": "250-500", + "website": "https://translogist.ru", + "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=TL", + "slogan": "Транспортные решения для бизнеса", + "rating": 4.8, + "verified": true, + "phone": "+7 (495) 999-88-77", + "email": "info@translogist.ru", + "legalAddress": "г. Москва, ул. Транспортная, д. 60", + "foundedYear": 2007, + "employeeCount": "350 сотрудников" + }, + { + "id": "company-12", + "inn": "7730048044", + "ogrn": "1047730048044", + "fullName": "Общество с ограниченной ответственностью \"ТехСофт\"", + "shortName": "ООО \"ТехСофт\"", + "legalForm": "ООО", + "industry": "IT", + "companySize": "50-100", + "website": "https://techsoft.ru", + "logo": "https://via.placeholder.com/100x100/3182CE/FFFFFF?text=TS", + "slogan": "IT-решения для бизнеса", + "rating": 4.7, + "verified": true, + "phone": "+7 (495) 444-55-66", + "email": "info@techsoft.ru", + "legalAddress": "г. Москва, ул. Программистов, д. 10", + "foundedYear": 2016, + "employeeCount": "85 сотрудников" + }, + { + "id": "company-13", + "inn": "7730048045", + "ogrn": "1047730048045", + "fullName": "Общество с ограниченной ответственностью \"КиберТех\"", + "shortName": "ООО \"КиберТех\"", + "legalForm": "ООО", + "industry": "IT", + "companySize": "100-250", + "website": "https://cybertech.ru", + "logo": "https://via.placeholder.com/100x100/553C9A/FFFFFF?text=CT", + "slogan": "Кибербезопасность и автоматизация", + "rating": 4.9, + "verified": true, + "phone": "+7 (495) 666-77-88", + "email": "info@cybertech.ru", + "legalAddress": "г. Москва, ул. Кибернетическая, д. 5", + "foundedYear": 2014, + "employeeCount": "120 сотрудников" + }, + { + "id": "company-3", + "inn": "7730048037", + "ogrn": "1047730048037", + "fullName": "Индивидуальный предприниматель Сидоров Петр Иванович", + "shortName": "ИП Сидоров П.И.", + "legalForm": "ИП", + "industry": "Услуги", + "companySize": "1-10", + "website": "https://sidorov-service.ru", + "logo": "https://via.placeholder.com/100x100/D69E2E/FFFFFF?text=SI", + "slogan": "Качественные услуги для малого бизнеса", + "rating": 4.2, + "verified": false, + "phone": "+7 (495) 345-67-89", + "email": "info@sidorov-service.ru", + "legalAddress": "г. Москва, ул. Сервисная, д. 3", + "foundedYear": 2020, + "employeeCount": "5 сотрудников" + }, + { + "id": "company-14", + "inn": "7730048046", + "ogrn": "1047730048046", + "fullName": "Общество с ограниченной ответственностью \"КонсалтПро\"", + "shortName": "ООО \"КонсалтПро\"", + "legalForm": "ООО", + "industry": "Услуги", + "companySize": "10-50", + "website": "https://konsultpro.ru", + "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=KP", + "slogan": "Консалтинг для роста бизнеса", + "rating": 4.5, + "verified": true, + "phone": "+7 (495) 222-33-44", + "email": "info@konsultpro.ru", + "legalAddress": "г. Москва, ул. Консультационная, д. 15", + "foundedYear": 2017, + "employeeCount": "25 сотрудников" + }, + { + "id": "company-15", + "inn": "7730048047", + "ogrn": "1047730048047", + "fullName": "Общество с ограниченной ответственностью \"ПищеПром\"", + "shortName": "ООО \"ПищеПром\"", + "legalForm": "ООО", + "industry": "Пищевая промышленность", + "companySize": "100-250", + "website": "https://pishcheprom.ru", + "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PP", + "slogan": "Качественные продукты питания", + "rating": 4.4, + "verified": true, + "phone": "+7 (495) 888-99-00", + "email": "info@pishcheprom.ru", + "legalAddress": "г. Москва, ул. Пищевая, д. 40", + "foundedYear": 2011, + "employeeCount": "180 сотрудников" + }, + { + "id": "company-16", + "inn": "7730048048", + "ogrn": "1047730048048", + "fullName": "Общество с ограниченной ответственностью \"ЭнергоСервис\"", + "shortName": "ООО \"ЭнергоСервис\"", + "legalForm": "ООО", + "industry": "Энергетика", + "companySize": "50-100", + "website": "https://energoservice.ru", + "logo": "https://via.placeholder.com/100x100/F6AD55/FFFFFF?text=ES", + "slogan": "Энергетические решения", + "rating": 4.6, + "verified": true, + "phone": "+7 (495) 555-66-77", + "email": "info@energoservice.ru", + "legalAddress": "г. Москва, ул. Энергетическая, д. 25", + "foundedYear": 2013, + "employeeCount": "70 сотрудников" + }, + { + "id": "company-17", + "inn": "7730048049", + "ogrn": "1047730048049", + "fullName": "Общество с ограниченной ответственностью \"МедТех\"", + "shortName": "ООО \"МедТех\"", + "legalForm": "ООО", + "industry": "Медицина", + "companySize": "100-250", + "website": "https://medtech.ru", + "logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MT", + "slogan": "Медицинские технологии будущего", + "rating": 4.8, + "verified": true, + "phone": "+7 (495) 777-00-11", + "email": "info@medtech.ru", + "legalAddress": "г. Москва, ул. Медицинская, д. 35", + "foundedYear": 2015, + "employeeCount": "200 сотрудников" + }, + { + "id": "company-18", + "inn": "7730048050", + "ogrn": "1047730048050", + "fullName": "Общество с ограниченной ответственностью \"ОбразЦентр\"", + "shortName": "ООО \"ОбразЦентр\"", + "legalForm": "ООО", + "industry": "Образование", + "companySize": "50-100", + "website": "https://obrazcentr.ru", + "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=OC", + "slogan": "Образование и развитие персонала", + "rating": 4.3, + "verified": true, + "phone": "+7 (495) 333-00-22", + "email": "info@obrazcentr.ru", + "legalAddress": "г. Москва, ул. Образовательная, д. 18", + "foundedYear": 2018, + "employeeCount": "60 сотрудников" + }, + { + "id": "company-19", + "inn": "7730048051", + "ogrn": "1047730048051", + "fullName": "Общество с ограниченной ответственностью \"ФинКонсалт\"", + "shortName": "ООО \"ФинКонсалт\"", + "legalForm": "ООО", + "industry": "Финансы", + "companySize": "10-50", + "website": "https://finkonsalt.ru", + "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=FK", + "slogan": "Финансовое консультирование", + "rating": 4.7, + "verified": true, + "phone": "+7 (495) 444-00-33", + "email": "info@finkonsalt.ru", + "legalAddress": "г. Москва, ул. Финансовая, д. 12", + "foundedYear": 2016, + "employeeCount": "35 сотрудников" + }, + { + "id": "company-20", + "inn": "7730048052", + "ogrn": "1047730048052", + "fullName": "Общество с ограниченной ответственностью \"АгроТех\"", + "shortName": "ООО \"АгроТех\"", + "legalForm": "ООО", + "industry": "Сельское хозяйство", + "companySize": "100-250", + "website": "https://agrotech.ru", + "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=AT", + "slogan": "Современные технологии в сельском хозяйстве", + "rating": 4.5, + "verified": true, + "phone": "+7 (495) 666-00-44", + "email": "info@agrotech.ru", + "legalAddress": "г. Москва, ул. Аграрная, д. 28", + "foundedYear": 2012, + "employeeCount": "160 сотрудников" + } + ] +} diff --git a/server/routers/procurement/mocks/products.json b/server/routers/procurement/mocks/products.json new file mode 100644 index 0000000..9b9b9a6 --- /dev/null +++ b/server/routers/procurement/mocks/products.json @@ -0,0 +1,158 @@ +{ + "mockProducts": [ + { + "id": "prod-1", + "name": "Металлические конструкции", + "description": "Производство и поставка металлических конструкций любой сложности для строительства", + "category": "Строительные материалы", + "type": "sell", + "companyId": "company-4", + "price": "от 50 000 руб/тонна", + "createdAt": "{{date-10-days}}", + "updatedAt": "{{date-2-days}}" + }, + { + "id": "prod-2", + "name": "Стальные балки и профили", + "description": "Высококачественные стальные балки и профили для промышленного строительства", + "category": "Металлопрокат", + "type": "sell", + "companyId": "company-8", + "price": "от 45 000 руб/тонна", + "createdAt": "{{date-8-days}}", + "updatedAt": "{{date-1-day}}" + }, + { + "id": "prod-3", + "name": "Пластиковые изделия", + "description": "Производство пластиковых изделий для различных отраслей промышленности", + "category": "Пластик", + "type": "sell", + "companyId": "company-9", + "price": "от 200 руб/кг", + "createdAt": "{{date-15-days}}", + "updatedAt": "{{date-3-days}}" + }, + { + "id": "prod-4", + "name": "Строительные материалы", + "description": "Полный спектр строительных материалов для жилого и коммерческого строительства", + "category": "Строительные материалы", + "type": "sell", + "companyId": "company-1", + "price": "по запросу", + "createdAt": "{{date-20-days}}", + "updatedAt": "{{date-5-days}}" + }, + { + "id": "prod-5", + "name": "IT-решения для бизнеса", + "description": "Разработка программного обеспечения и IT-консалтинг для предприятий", + "category": "IT-услуги", + "type": "sell", + "companyId": "company-12", + "price": "от 100 000 руб/проект", + "createdAt": "{{date-12-days}}", + "updatedAt": "{{date-2-days}}" + }, + { + "id": "prod-6", + "name": "Логистические услуги", + "description": "Комплексные логистические услуги по всей России и СНГ", + "category": "Логистика", + "type": "sell", + "companyId": "company-5", + "price": "от 15 руб/км", + "createdAt": "{{date-18-days}}", + "updatedAt": "{{date-4-days}}" + }, + { + "id": "prod-7", + "name": "Пищевая продукция", + "description": "Производство качественных продуктов питания для HoReCa и розничной торговли", + "category": "Пищевая продукция", + "type": "sell", + "companyId": "company-15", + "price": "по прайс-листу", + "createdAt": "{{date-25-days}}", + "updatedAt": "{{date-7-days}}" + }, + { + "id": "prod-8", + "name": "Медицинское оборудование", + "description": "Поставка современного медицинского оборудования и расходных материалов", + "category": "Медицинское оборудование", + "type": "sell", + "companyId": "company-17", + "price": "по запросу", + "createdAt": "{{date-30-days}}", + "updatedAt": "{{date-10-days}}" + }, + { + "id": "prod-9", + "name": "Запчасти для спецтехники", + "description": "Ищем надежного поставщика запчастей для строительной техники Caterpillar, Komatsu", + "category": "Запчасти", + "type": "buy", + "companyId": "company-2", + "budget": "до 500 000 руб", + "createdAt": "{{date-5-days}}", + "updatedAt": "{{date-1-day}}" + }, + { + "id": "prod-10", + "name": "Сырье для производства", + "description": "Требуется качественное сырье для производства пластиковых изделий", + "category": "Сырье", + "type": "buy", + "companyId": "company-9", + "budget": "до 1 000 000 руб", + "createdAt": "{{date-7-days}}", + "updatedAt": "{{date-2-days}}" + }, + { + "id": "prod-11", + "name": "IT-оборудование", + "description": "Закупка серверного оборудования и сетевого оборудования для офиса", + "category": "IT-оборудование", + "type": "buy", + "companyId": "company-13", + "budget": "до 2 000 000 руб", + "createdAt": "{{date-3-days}}", + "updatedAt": "{{date-1-day}}" + }, + { + "id": "prod-12", + "name": "Консалтинговые услуги", + "description": "Требуется консультация по оптимизации бизнес-процессов", + "category": "Консалтинг", + "type": "buy", + "companyId": "company-14", + "budget": "до 300 000 руб", + "createdAt": "{{date-4-days}}", + "updatedAt": "{{date-1-day}}" + }, + { + "id": "prod-13", + "name": "Образовательные программы", + "description": "Поиск поставщика корпоративного обучения для сотрудников", + "category": "Образование", + "type": "buy", + "companyId": "company-18", + "budget": "до 200 000 руб", + "createdAt": "{{date-6-days}}", + "updatedAt": "{{date-2-days}}" + }, + { + "id": "prod-14", + "name": "Финансовые услуги", + "description": "Требуется консультация по инвестиционному планированию", + "category": "Финансовые услуги", + "type": "buy", + "companyId": "company-19", + "budget": "до 150 000 руб", + "createdAt": "{{date-2-days}}", + "updatedAt": "{{date-1-day}}" + } + ] +} diff --git a/server/routers/procurement/mocks/search.json b/server/routers/procurement/mocks/search.json new file mode 100644 index 0000000..e9081c8 --- /dev/null +++ b/server/routers/procurement/mocks/search.json @@ -0,0 +1,122 @@ +{ + "suggestions": [ + "Строительные материалы", + "Металлоконструкции", + "Логистические услуги", + "Промышленное оборудование", + "Запчасти для спецтехники", + "IT-решения", + "Консалтинговые услуги", + "Пищевая продукция", + "Энергетическое оборудование", + "Медицинские технологии", + "Образовательные услуги", + "Финансовые услуги", + "Сельскохозяйственная техника", + "Торговое оборудование", + "Производственные услуги" + ], + "searchHistory": [ + { + "query": "строительные материалы", + "timestamp": "{{date-1-day}}" + }, + { + "query": "металлоконструкции", + "timestamp": "{{date-2-days}}" + }, + { + "query": "логистические услуги", + "timestamp": "{{date-3-days}}" + }, + { + "query": "IT-решения", + "timestamp": "{{date-5-days}}" + }, + { + "query": "консалтинг", + "timestamp": "{{date-7-days}}" + }, + { + "query": "пищевая продукция", + "timestamp": "{{date-10-days}}" + }, + { + "query": "медицинское оборудование", + "timestamp": "{{date-12-days}}" + }, + { + "query": "образовательные услуги", + "timestamp": "{{date-15-days}}" + }, + { + "query": "финансовые услуги", + "timestamp": "{{date-18-days}}" + }, + { + "query": "сельскохозяйственная техника", + "timestamp": "{{date-20-days}}" + } + ], + "savedSearches": [ + { + "id": "saved-1", + "name": "Строительные компании", + "params": { + "industries": ["Строительство"], + "minRating": 4.5 + }, + "createdAt": "{{date-7-days}}" + }, + { + "id": "saved-2", + "name": "Поставщики металла", + "params": { + "query": "металл", + "industries": ["Производство"] + }, + "createdAt": "{{date-14-days}}" + }, + { + "id": "saved-3", + "name": "IT-компании", + "params": { + "industries": ["IT"], + "minRating": 4.0 + }, + "createdAt": "{{date-21-days}}" + }, + { + "id": "saved-4", + "name": "Логистические услуги", + "params": { + "industries": ["Логистика"], + "companySize": ["100-250", "250-500"] + }, + "createdAt": "{{date-28-days}}" + }, + { + "id": "saved-5", + "name": "Консалтинговые услуги", + "params": { + "industries": ["Услуги"], + "minRating": 4.3 + }, + "createdAt": "{{date-35-days}}" + } + ], + "recommendationReasons": { + "Строительство": "Отличная репутация в строительной сфере", + "Производство": "Высокое качество производимой продукции", + "Логистика": "Надежные логистические решения", + "Торговля": "Широкий ассортимент и быстрые поставки", + "IT": "Инновационные IT-решения", + "Услуги": "Профессиональные консалтинговые услуги", + "Пищевая промышленность": "Качественная пищевая продукция", + "Энергетика": "Энергоэффективные решения", + "Медицина": "Современные медицинские технологии", + "Образование": "Эффективные образовательные программы", + "Финансы": "Надежные финансовые услуги", + "Сельское хозяйство": "Современные агротехнологии" + } +} diff --git a/server/routers/procurement/mocks/user.json b/server/routers/procurement/mocks/user.json new file mode 100644 index 0000000..1d12fb0 --- /dev/null +++ b/server/routers/procurement/mocks/user.json @@ -0,0 +1,13 @@ +{ + "mockUser": { + "id": "user-123", + "email": "test@company.com", + "firstName": "Иван", + "lastName": "Петров", + "position": "Генеральный директор" + }, + "mockTokens": { + "accessToken": "mock-access-token-{{timestamp}}", + "refreshToken": "mock-refresh-token-{{timestamp}}" + } +} From 9f72d5885e940b5649e9e73d4afe97b7b6c60231 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 14 Oct 2025 11:58:10 +0300 Subject: [PATCH 119/147] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/index.js | 56 +++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 8425a31..7acbcfa 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -19,10 +19,26 @@ router.use(timer()); const loadMockData = (filename) => { try { const filePath = path.join(__dirname, '..', 'mocks', filename); + + // Проверяем существование файла + if (!fs.existsSync(filePath)) { + console.error(`Файл ${filename} не найден по пути: ${filePath}`); + return {}; + } + const data = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(data); + + // Проверяем, что файл не пустой + if (!data || data.trim() === '') { + console.error(`Файл ${filename} пустой`); + return {}; + } + + const parsedData = JSON.parse(data); + console.log(`Успешно загружен файл ${filename}`); + return parsedData; } catch (error) { - console.error(`Ошибка загрузки ${filename}:`, error); + console.error(`Ошибка загрузки ${filename}:`, error.message); return {}; } }; @@ -44,8 +60,22 @@ const generateDate = (daysAgo) => new Date(Date.now() - 86400000 * daysAgo).toIS // Функция для замены плейсхолдеров в данных const processMockData = (data) => { + // Проверяем, что данные существуют + if (data === undefined || data === null) { + console.warn('processMockData: получены undefined или null данные'); + return data; + } + const timestamp = generateTimestamp(); - const processedData = JSON.stringify(data) + const jsonString = JSON.stringify(data); + + // Проверяем, что JSON.stringify вернул валидную строку + if (jsonString === undefined || jsonString === null) { + console.warn('processMockData: JSON.stringify вернул undefined или null'); + return data; + } + + const processedData = jsonString .replace(/{{timestamp}}/g, timestamp) .replace(/{{date-(\d+)-days?}}/g, (match, days) => generateDate(parseInt(days))) .replace(/{{date-1-day}}/g, generateDate(1)) @@ -67,7 +97,12 @@ const processMockData = (data) => { .replace(/{{date-30-days}}/g, generateDate(30)) .replace(/{{date-35-days}}/g, generateDate(35)); - return JSON.parse(processedData); + try { + return JSON.parse(processedData); + } catch (error) { + console.error('processMockData: ошибка при парсинге JSON:', error); + return data; // Возвращаем исходные данные в случае ошибки + } }; // Auth endpoints @@ -77,7 +112,7 @@ router.post('/auth/login', (req, res) => { if (!email || !password) { return res.status(400).json({ error: 'Validation failed', - message: authMocks.errorMessages?.validationFailed || 'Email и пароль обязательны' + message: authMocks?.errorMessages?.validationFailed || 'Email и пароль обязательны' }); } @@ -85,7 +120,16 @@ router.post('/auth/login', (req, res) => { if (password === 'wrong') { return res.status(401).json({ error: 'Unauthorized', - message: authMocks.errorMessages?.invalidCredentials || 'Неверный email или пароль' + message: authMocks?.errorMessages?.invalidCredentials || 'Неверный email или пароль' + }); + } + + // Проверяем, что данные загружены + if (!authMocks?.mockAuthResponse) { + console.error('authMocks.mockAuthResponse не загружен'); + return res.status(500).json({ + error: 'Internal Server Error', + message: 'Ошибка загрузки данных аутентификации' }); } From 7937be469b98d3502d426948bdc755292691436e Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 14 Oct 2025 12:24:31 +0300 Subject: [PATCH 120/147] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 7acbcfa..2af0799 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -18,7 +18,7 @@ router.use(timer()); // Загружаем моки из JSON файлов const loadMockData = (filename) => { try { - const filePath = path.join(__dirname, '..', 'mocks', filename); + const filePath = path.join(__dirname, 'mocks', filename); // Проверяем существование файла if (!fs.existsSync(filePath)) { From 2b5e5564c8b509c0893e00a84ca0e5ddd3ad67af Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 14 Oct 2025 13:44:18 +0300 Subject: [PATCH 121/147] =?UTF-8?q?=D0=BF=D1=80=D1=8F=D0=BC=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/index.js | 92 ++++++----------------------- 1 file changed, 19 insertions(+), 73 deletions(-) diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 2af0799..f1ba3bc 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,6 +1,4 @@ const router = require('express').Router(); -const fs = require('fs'); -const path = require('path'); const timer = (time = 300) => (req, res, next) => setTimeout(next, time); @@ -15,44 +13,13 @@ router.use((req, res, next) => { router.use(timer()); -// Загружаем моки из JSON файлов -const loadMockData = (filename) => { - try { - const filePath = path.join(__dirname, 'mocks', filename); - - // Проверяем существование файла - if (!fs.existsSync(filePath)) { - console.error(`Файл ${filename} не найден по пути: ${filePath}`); - return {}; - } - - const data = fs.readFileSync(filePath, 'utf8'); - - // Проверяем, что файл не пустой - if (!data || data.trim() === '') { - console.error(`Файл ${filename} пустой`); - return {}; - } - - const parsedData = JSON.parse(data); - console.log(`Успешно загружен файл ${filename}`); - return parsedData; - } catch (error) { - console.error(`Ошибка загрузки ${filename}:`, error.message); - return {}; - } -}; +// Загружаем моки через прямые импорты +const userMocks = require('./mocks/user.json'); +const companyMocks = require('./mocks/companies.json'); +const productMocks = require('./mocks/products.json'); +const searchMocks = require('./mocks/search.json'); +const authMocks = require('./mocks/auth.json'); -// Загружаем все моки -const userMocks = loadMockData('user.json'); -const companyMocks = loadMockData('companies.json'); -const productMocks = loadMockData('products.json'); -const searchMocks = loadMockData('search.json'); -const authMocks = loadMockData('auth.json'); - -// Логируем загруженные данные для отладки -console.log('SearchMocks loaded:', searchMocks); -console.log('Suggestions:', searchMocks.suggestions); // Вспомогательные функции для генерации динамических данных const generateTimestamp = () => Date.now(); @@ -60,18 +27,14 @@ const generateDate = (daysAgo) => new Date(Date.now() - 86400000 * daysAgo).toIS // Функция для замены плейсхолдеров в данных const processMockData = (data) => { - // Проверяем, что данные существуют if (data === undefined || data === null) { - console.warn('processMockData: получены undefined или null данные'); return data; } const timestamp = generateTimestamp(); const jsonString = JSON.stringify(data); - // Проверяем, что JSON.stringify вернул валидную строку if (jsonString === undefined || jsonString === null) { - console.warn('processMockData: JSON.stringify вернул undefined или null'); return data; } @@ -100,8 +63,7 @@ const processMockData = (data) => { try { return JSON.parse(processedData); } catch (error) { - console.error('processMockData: ошибка при парсинге JSON:', error); - return data; // Возвращаем исходные данные в случае ошибки + return data; } }; @@ -124,12 +86,15 @@ router.post('/auth/login', (req, res) => { }); } - // Проверяем, что данные загружены if (!authMocks?.mockAuthResponse) { - console.error('authMocks.mockAuthResponse не загружен'); return res.status(500).json({ error: 'Internal Server Error', - message: 'Ошибка загрузки данных аутентификации' + message: 'Ошибка загрузки данных аутентификации', + details: { + authMocksExists: !!authMocks, + authMocksType: typeof authMocks, + authMocksKeys: authMocks ? Object.keys(authMocks) : null + } }); } @@ -405,19 +370,11 @@ router.get('/search', (req, res) => { limit = 20 } = req.query; - console.log('Search query:', query); - console.log('Search params:', req.query); - const companies = processMockData(companyMocks.mockCompanies); - console.log('Companies loaded:', companies.length); - console.log('First company:', companies[0]); - let filtered = [...companies]; - // Поиск по тексту if (query) { const q = query.toLowerCase().trim(); - console.log('Searching for:', q); filtered = filtered.filter(c => { const fullName = (c.fullName || '').toLowerCase(); @@ -426,20 +383,12 @@ router.get('/search', (req, res) => { const slogan = (c.slogan || '').toLowerCase(); const legalAddress = (c.legalAddress || '').toLowerCase(); - const matches = fullName.includes(q) || - shortName.includes(q) || - industry.includes(q) || - slogan.includes(q) || - legalAddress.includes(q); - - if (matches) { - console.log('Found match:', c.shortName, 'in', { fullName, shortName, industry, slogan, legalAddress }); - } - - return matches; + return fullName.includes(q) || + shortName.includes(q) || + industry.includes(q) || + slogan.includes(q) || + legalAddress.includes(q); }); - - console.log('Filtered results:', filtered.length); } // Фильтр по отраслям @@ -545,14 +494,11 @@ router.get('/search/suggestions', (req, res) => { const { q } = req.query; const suggestions = searchMocks.suggestions || []; - console.log('Suggestions loaded:', suggestions); - console.log('Query:', q); const filtered = q ? suggestions.filter(s => s.toLowerCase().includes(q.toLowerCase())) - : suggestions.slice(0, 10); // Показываем только первые 10 если нет запроса + : suggestions.slice(0, 10); - console.log('Filtered suggestions:', filtered); res.status(200).json(filtered); }); From 599ccd15825515d8b01774c2d3d3f5c884ee96df Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Sat, 18 Oct 2025 11:30:18 +0300 Subject: [PATCH 122/147] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B1=D1=8D=D0=BA=20=D0=B7=D0=B0=D0=BA=D1=83=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 10 + package.json | 1 + server/routers/procurement/index.js | 607 ++---------------- server/routers/procurement/middleware/auth.js | 27 + server/routers/procurement/models/Company.js | 64 ++ server/routers/procurement/models/Message.js | 37 ++ server/routers/procurement/models/Product.js | 46 ++ server/routers/procurement/models/User.js | 67 ++ server/routers/procurement/routes/auth.js | 103 +++ server/routers/procurement/routes/buy.js | 186 ++++++ .../routers/procurement/routes/companies.js | 103 +++ .../routers/procurement/routes/experience.js | 114 ++++ server/routers/procurement/routes/messages.js | 130 ++++ server/routers/procurement/routes/products.js | 130 ++++ server/routers/procurement/routes/search.js | 99 +++ .../procurement/scripts/recreate-test-user.js | 90 +++ 16 files changed, 1260 insertions(+), 554 deletions(-) create mode 100644 server/routers/procurement/middleware/auth.js create mode 100644 server/routers/procurement/models/Company.js create mode 100644 server/routers/procurement/models/Message.js create mode 100644 server/routers/procurement/models/Product.js create mode 100644 server/routers/procurement/models/User.js create mode 100644 server/routers/procurement/routes/auth.js create mode 100644 server/routers/procurement/routes/buy.js create mode 100644 server/routers/procurement/routes/companies.js create mode 100644 server/routers/procurement/routes/experience.js create mode 100644 server/routers/procurement/routes/messages.js create mode 100644 server/routers/procurement/routes/products.js create mode 100644 server/routers/procurement/routes/search.js create mode 100644 server/routers/procurement/scripts/recreate-test-user.js diff --git a/package-lock.json b/package-lock.json index 540a4cd..a43c05c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", + "bcryptjs": "^3.0.2", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -3721,6 +3722,15 @@ "node": ">= 10.0.0" } }, + "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.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", diff --git a/package.json b/package.json index 948f3ea..a691838 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", + "bcryptjs": "^3.0.2", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index f1ba3bc..5765845 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,565 +1,64 @@ -const router = require('express').Router(); +const express = require('express') +const dotenv = require('dotenv') -const timer = (time = 300) => (req, res, next) => setTimeout(next, time); +// Загрузить переменные окружения +dotenv.config() -// Настройка кодировки UTF-8 -router.use((req, res, next) => { - res.setHeader('Content-Type', 'application/json; charset=utf-8'); - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - next(); -}); +// Подключение к MongoDB через mongoose +require('../../utils/mongoose') -router.use(timer()); +// Импортировать маршруты +const authRoutes = require('./routes/auth') +const companiesRoutes = require('./routes/companies') +const messagesRoutes = require('./routes/messages') +const searchRoutes = require('./routes/search') +const buyRoutes = require('./routes/buy') +const experienceRoutes = require('./routes/experience') +const productsRoutes = require('./routes/products') -// Загружаем моки через прямые импорты -const userMocks = require('./mocks/user.json'); -const companyMocks = require('./mocks/companies.json'); -const productMocks = require('./mocks/products.json'); -const searchMocks = require('./mocks/search.json'); -const authMocks = require('./mocks/auth.json'); +const mongoose = require('mongoose') +const app = express() -// Вспомогательные функции для генерации динамических данных -const generateTimestamp = () => Date.now(); -const generateDate = (daysAgo) => new Date(Date.now() - 86400000 * daysAgo).toISOString(); +// Задержка для имитации сети (опционально) +const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms) +app.use(delay()) -// Функция для замены плейсхолдеров в данных -const processMockData = (data) => { - if (data === undefined || data === null) { - return data; - } +// Health check endpoint +app.get('/health', (req, res) => { + const mongodbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected' + res.json({ + status: 'ok', + api: 'running', + database: mongodbStatus, + mongoUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db', + timestamp: new Date().toISOString() + }) +}) - const timestamp = generateTimestamp(); - const jsonString = JSON.stringify(data); - - if (jsonString === undefined || jsonString === null) { - return data; - } +// Маршруты +app.use('/auth', authRoutes) +app.use('/companies', companiesRoutes) +app.use('/messages', messagesRoutes) +app.use('/search', searchRoutes) +app.use('/buy', buyRoutes) +app.use('/experience', experienceRoutes) +app.use('/products', productsRoutes) - const processedData = jsonString - .replace(/{{timestamp}}/g, timestamp) - .replace(/{{date-(\d+)-days?}}/g, (match, days) => generateDate(parseInt(days))) - .replace(/{{date-1-day}}/g, generateDate(1)) - .replace(/{{date-2-days}}/g, generateDate(2)) - .replace(/{{date-3-days}}/g, generateDate(3)) - .replace(/{{date-4-days}}/g, generateDate(4)) - .replace(/{{date-5-days}}/g, generateDate(5)) - .replace(/{{date-6-days}}/g, generateDate(6)) - .replace(/{{date-7-days}}/g, generateDate(7)) - .replace(/{{date-8-days}}/g, generateDate(8)) - .replace(/{{date-10-days}}/g, generateDate(10)) - .replace(/{{date-12-days}}/g, generateDate(12)) - .replace(/{{date-15-days}}/g, generateDate(15)) - .replace(/{{date-18-days}}/g, generateDate(18)) - .replace(/{{date-20-days}}/g, generateDate(20)) - .replace(/{{date-21-days}}/g, generateDate(21)) - .replace(/{{date-25-days}}/g, generateDate(25)) - .replace(/{{date-28-days}}/g, generateDate(28)) - .replace(/{{date-30-days}}/g, generateDate(30)) - .replace(/{{date-35-days}}/g, generateDate(35)); - - try { - return JSON.parse(processedData); - } catch (error) { - return data; - } -}; +// Обработка ошибок +app.use((err, req, res, next) => { + console.error('API Error:', err) + res.status(err.status || 500).json({ + error: err.message || 'Internal server error' + }) +}) -// Auth endpoints -router.post('/auth/login', (req, res) => { - const { email, password } = req.body; - - if (!email || !password) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks?.errorMessages?.validationFailed || 'Email и пароль обязательны' - }); - } - - // Имитация неверных учетных данных - if (password === 'wrong') { - return res.status(401).json({ - error: 'Unauthorized', - message: authMocks?.errorMessages?.invalidCredentials || 'Неверный email или пароль' - }); - } - - if (!authMocks?.mockAuthResponse) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Ошибка загрузки данных аутентификации', - details: { - authMocksExists: !!authMocks, - authMocksType: typeof authMocks, - authMocksKeys: authMocks ? Object.keys(authMocks) : null - } - }); - } - - const authResponse = processMockData(authMocks.mockAuthResponse); - res.status(200).json(authResponse); -}); +// 404 handler +app.use((req, res) => { + res.status(404).json({ + error: 'Not found' + }) +}) -router.post('/auth/register', (req, res) => { - const { email, password, inn, agreeToTerms } = req.body; - - if (!email || !password || !inn) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.validationFailed || 'Заполните все обязательные поля' - }); - } - - if (!agreeToTerms) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.termsRequired || 'Необходимо принять условия использования' - }); - } - - // Создаем нового пользователя с данными из регистрации - const newUser = { - id: 'user-' + generateTimestamp(), - email: email, - firstName: req.body.firstName || 'Иван', - lastName: req.body.lastName || 'Петров', - position: req.body.position || 'Директор' - }; - - const newCompany = { - id: 'company-' + generateTimestamp(), - name: req.body.fullName || companyMocks.mockCompany?.name, - inn: req.body.inn, - ogrn: req.body.ogrn || companyMocks.mockCompany?.ogrn, - fullName: req.body.fullName || companyMocks.mockCompany?.fullName, - shortName: req.body.shortName, - legalForm: req.body.legalForm || 'ООО', - industry: req.body.industry || 'Другое', - companySize: req.body.companySize || '1-10', - website: req.body.website || '', - verified: false, - rating: 0 - }; - - res.status(201).json({ - user: newUser, - company: newCompany, - tokens: { - accessToken: 'mock-access-token-' + generateTimestamp(), - refreshToken: 'mock-refresh-token-' + generateTimestamp() - } - }); -}); - -router.post('/auth/logout', (req, res) => { - res.status(200).json({ - message: authMocks.successMessages?.logoutSuccess || 'Успешный выход' - }); -}); - -router.post('/auth/refresh', (req, res) => { - const { refreshToken } = req.body; - - if (!refreshToken) { - return res.status(401).json({ - error: 'Unauthorized', - message: authMocks.errorMessages?.refreshTokenRequired || 'Refresh token обязателен' - }); - } - - res.status(200).json({ - accessToken: 'mock-access-token-refreshed-' + generateTimestamp(), - refreshToken: 'mock-refresh-token-refreshed-' + generateTimestamp() - }); -}); - -router.get('/auth/verify-email/:token', (req, res) => { - res.status(200).json({ - message: authMocks.successMessages?.emailVerified || 'Email успешно подтвержден' - }); -}); - -router.post('/auth/request-password-reset', (req, res) => { - const { email } = req.body; - - if (!email) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.emailRequired || 'Email обязателен' - }); - } - - res.status(200).json({ - message: authMocks.successMessages?.passwordResetSent || 'Письмо для восстановления пароля отправлено' - }); -}); - -router.post('/auth/reset-password', (req, res) => { - const { token, newPassword } = req.body; - - if (!token || !newPassword) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.validationFailed || 'Token и новый пароль обязательны' - }); - } - - res.status(200).json({ - message: authMocks.successMessages?.passwordResetSuccess || 'Пароль успешно изменен' - }); -}); - -// Companies endpoints -router.get('/companies/my/stats', (req, res) => { - res.status(200).json({ - profileViews: 142, - profileViewsChange: 12, - sentRequests: 8, - sentRequestsChange: 2, - receivedRequests: 15, - receivedRequestsChange: 5, - newMessages: 3, - rating: 4.5 - }); -}); - -router.get('/companies/:id', (req, res) => { - const company = processMockData(companyMocks.mockCompany); - res.status(200).json(company); -}); - -router.patch('/companies/:id', (req, res) => { - const updatedCompany = { - ...processMockData(companyMocks.mockCompany), - ...req.body, - id: req.params.id - }; - - res.status(200).json(updatedCompany); -}); - -router.get('/companies/:id/stats', (req, res) => { - res.status(200).json({ - profileViews: 142, - profileViewsChange: 12, - sentRequests: 8, - sentRequestsChange: 2, - receivedRequests: 15, - receivedRequestsChange: 5, - newMessages: 3, - rating: 4.5 - }); -}); - -router.post('/companies/:id/logo', (req, res) => { - res.status(200).json({ - logoUrl: 'https://via.placeholder.com/200x200/4299E1/FFFFFF?text=Logo' - }); -}); - -router.get('/companies/check-inn/:inn', (req, res) => { - const inn = req.params.inn; - - // Имитация проверки ИНН - if (inn.length !== 10 && inn.length !== 12) { - return res.status(400).json({ - error: 'Validation failed', - message: authMocks.errorMessages?.innValidation || 'ИНН должен содержать 10 или 12 цифр' - }); - } - - const mockINNData = companyMocks.mockINNData || {}; - const companyData = mockINNData[inn] || { - name: 'ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ "ТЕСТОВАЯ КОМПАНИЯ ' + inn + '"', - ogrn: '10277' + inn, - legal_form: 'ООО' - }; - - res.status(200).json({ data: companyData }); -}); - -// Products endpoints -router.get('/products/my', (req, res) => { - const products = processMockData(productMocks.mockProducts); - res.status(200).json(products); -}); - -router.get('/products', (req, res) => { - const products = processMockData(productMocks.mockProducts); - res.status(200).json({ - items: products, - total: products.length, - page: 1, - pageSize: 20 - }); -}); - -router.post('/products', (req, res) => { - const newProduct = { - id: 'prod-' + generateTimestamp(), - ...req.body, - companyId: 'company-123', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; - - res.status(201).json(newProduct); -}); - -router.get('/products/:id', (req, res) => { - const products = processMockData(productMocks.mockProducts); - const product = products.find(p => p.id === req.params.id); - - if (product) { - res.status(200).json(product); - } else { - res.status(200).json({ - id: req.params.id, - name: 'Продукт ' + req.params.id, - description: 'Описание продукта', - category: 'Категория', - type: 'sell', - companyId: 'company-123', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }); - } -}); - -router.patch('/products/:id', (req, res) => { - const products = processMockData(productMocks.mockProducts); - const product = products.find(p => p.id === req.params.id); - - const updatedProduct = { - ...(product || {}), - ...req.body, - id: req.params.id, - updatedAt: new Date().toISOString() - }; - - res.status(200).json(updatedProduct); -}); - -router.delete('/products/:id', (req, res) => { - res.status(204).send(); -}); - -// Тестовый endpoint для проверки данных -router.get('/test-data', (req, res) => { - res.status(200).json({ - companiesCount: companyMocks.mockCompanies?.length || 0, - suggestionsCount: searchMocks.suggestions?.length || 0, - firstCompany: companyMocks.mockCompanies?.[0] || null, - firstSuggestion: searchMocks.suggestions?.[0] || null, - allSuggestions: searchMocks.suggestions || [] - }); -}); -router.get('/search', (req, res) => { - const { - query, - industries, - companySize, - geography, - minRating, - type, - sortBy = 'relevance', - sortOrder = 'desc', - page = 1, - limit = 20 - } = req.query; - - const companies = processMockData(companyMocks.mockCompanies); - let filtered = [...companies]; - - if (query) { - const q = query.toLowerCase().trim(); - - filtered = filtered.filter(c => { - const fullName = (c.fullName || '').toLowerCase(); - const shortName = (c.shortName || '').toLowerCase(); - const industry = (c.industry || '').toLowerCase(); - const slogan = (c.slogan || '').toLowerCase(); - const legalAddress = (c.legalAddress || '').toLowerCase(); - - return fullName.includes(q) || - shortName.includes(q) || - industry.includes(q) || - slogan.includes(q) || - legalAddress.includes(q); - }); - } - - // Фильтр по отраслям - if (industries && industries.length > 0) { - const industriesArray = Array.isArray(industries) ? industries : [industries]; - filtered = filtered.filter(c => industriesArray.includes(c.industry)); - } - - // Фильтр по размеру компании - if (companySize && companySize.length > 0) { - const sizeArray = Array.isArray(companySize) ? companySize : [companySize]; - filtered = filtered.filter(c => sizeArray.includes(c.companySize)); - } - - // Фильтр по рейтингу - if (minRating) { - filtered = filtered.filter(c => c.rating >= parseFloat(minRating)); - } - - // Сортировка - filtered.sort((a, b) => { - let comparison = 0; - - switch (sortBy) { - case 'rating': - comparison = a.rating - b.rating; - break; - case 'name': - comparison = (a.shortName || a.fullName).localeCompare(b.shortName || b.fullName); - break; - case 'relevance': - default: - // Для релевантности используем рейтинг как основной критерий - comparison = a.rating - b.rating; - break; - } - - return sortOrder === 'asc' ? comparison : -comparison; - }); - - const total = filtered.length; - const totalPages = Math.ceil(total / limit); - const startIndex = (page - 1) * limit; - const endIndex = startIndex + parseInt(limit); - const paginatedResults = filtered.slice(startIndex, endIndex); - - res.status(200).json({ - companies: paginatedResults, - total, - page: parseInt(page), - totalPages - }); -}); - -router.post('/search/ai', (req, res) => { - const { query } = req.body; - - // Простая логика AI поиска на основе ключевых слов - const companies = processMockData(companyMocks.mockCompanies); - let aiResults = [...companies]; - const q = query.toLowerCase(); - - // Определяем приоритетные отрасли на основе запроса - if (q.includes('строитель') || q.includes('строй') || q.includes('дом') || q.includes('здание')) { - aiResults = aiResults.filter(c => c.industry === 'Строительство'); - } else if (q.includes('металл') || q.includes('сталь') || q.includes('производств') || q.includes('завод')) { - aiResults = aiResults.filter(c => c.industry === 'Производство'); - } else if (q.includes('логистик') || q.includes('доставк') || q.includes('транспорт')) { - aiResults = aiResults.filter(c => c.industry === 'Логистика'); - } else if (q.includes('торговл') || q.includes('продаж') || q.includes('снабжени')) { - aiResults = aiResults.filter(c => c.industry === 'Торговля'); - } else if (q.includes('it') || q.includes('программ') || q.includes('технолог') || q.includes('софт')) { - aiResults = aiResults.filter(c => c.industry === 'IT'); - } else if (q.includes('услуг') || q.includes('консалт') || q.includes('помощь')) { - aiResults = aiResults.filter(c => c.industry === 'Услуги'); - } - - // Сортируем по рейтингу и берем топ-5 - aiResults.sort((a, b) => b.rating - a.rating); - const topResults = aiResults.slice(0, 5); - - // Генерируем AI предложение - let aiSuggestion = `На основе вашего запроса "${query}" мы нашли ${topResults.length} подходящих партнеров. `; - - if (topResults.length > 0) { - const industries = [...new Set(topResults.map(c => c.industry))]; - aiSuggestion += `Рекомендуем обратить внимание на компании в сфере ${industries.join(', ')}. `; - aiSuggestion += `Все предложенные партнеры имеют высокий рейтинг (от ${Math.min(...topResults.map(c => c.rating)).toFixed(1)} до ${Math.max(...topResults.map(c => c.rating)).toFixed(1)}) и подтвержденный статус.`; - } else { - aiSuggestion += 'Попробуйте изменить формулировку запроса или использовать фильтры для более точного поиска.'; - } - - res.status(200).json({ - companies: topResults, - total: topResults.length, - page: 1, - totalPages: 1, - aiSuggestion - }); -}); - -router.get('/search/suggestions', (req, res) => { - const { q } = req.query; - - const suggestions = searchMocks.suggestions || []; - - const filtered = q - ? suggestions.filter(s => s.toLowerCase().includes(q.toLowerCase())) - : suggestions.slice(0, 10); - - res.status(200).json(filtered); -}); - -router.get('/search/recommendations', (req, res) => { - // Динамически генерируем рекомендации на основе топовых компаний - const companies = processMockData(companyMocks.mockCompanies); - const topCompanies = companies - .filter(c => c.verified && c.rating >= 4.5) - .sort((a, b) => b.rating - a.rating) - .slice(0, 6); - - const recommendations = topCompanies.map(company => ({ - id: company.id, - name: company.shortName || company.fullName, - industry: company.industry, - logo: company.logo, - matchScore: Math.floor(company.rating * 20), // Конвертируем рейтинг в проценты - reason: getRecommendationReason(company, searchMocks.recommendationReasons) - })); - - res.status(200).json(recommendations); -}); - -// Вспомогательная функция для генерации причин рекомендаций -function getRecommendationReason(company, reasons) { - return reasons?.[company.industry] || 'Проверенный партнер с высоким рейтингом'; -} - -router.get('/search/history', (req, res) => { - const history = processMockData(searchMocks.searchHistory); - res.status(200).json(history); -}); - -router.get('/search/saved', (req, res) => { - const savedSearches = processMockData(searchMocks.savedSearches); - res.status(200).json(savedSearches); -}); - -router.post('/search/saved', (req, res) => { - const { name, params } = req.body; - - res.status(201).json({ - id: 'saved-' + generateTimestamp(), - name, - params, - createdAt: new Date().toISOString() - }); -}); - -router.delete('/search/saved/:id', (req, res) => { - res.status(204).send(); -}); - -router.post('/search/favorites/:companyId', (req, res) => { - res.status(200).json({ - message: authMocks.successMessages?.addedToFavorites || 'Добавлено в избранное' - }); -}); - -router.delete('/search/favorites/:companyId', (req, res) => { - res.status(204).send(); -}); - -module.exports = router; \ No newline at end of file +// Экспортировать для использования в brojs +module.exports = app \ No newline at end of file diff --git a/server/routers/procurement/middleware/auth.js b/server/routers/procurement/middleware/auth.js new file mode 100644 index 0000000..b126134 --- /dev/null +++ b/server/routers/procurement/middleware/auth.js @@ -0,0 +1,27 @@ +const jwt = require('jsonwebtoken') + +const verifyToken = (req, res, next) => { + const token = req.headers.authorization?.replace('Bearer ', '') + + if (!token) { + return res.status(401).json({ error: 'No token provided' }) + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') + req.user = decoded + next() + } catch (error) { + return res.status(401).json({ error: 'Invalid token' }) + } +} + +const generateToken = (userId, email) => { + return jwt.sign( + { userId, email }, + process.env.JWT_SECRET || 'your-secret-key', + { expiresIn: '7d' } + ) +} + +module.exports = { verifyToken, generateToken } diff --git a/server/routers/procurement/models/Company.js b/server/routers/procurement/models/Company.js new file mode 100644 index 0000000..ca1a87d --- /dev/null +++ b/server/routers/procurement/models/Company.js @@ -0,0 +1,64 @@ +const mongoose = require('mongoose') + +const companySchema = new mongoose.Schema({ + fullName: { + type: String, + required: true + }, + shortName: String, + inn: { + type: String, + unique: true, + sparse: true + }, + ogrn: String, + legalForm: String, + industry: String, + companySize: String, + website: String, + phone: String, + email: String, + slogan: String, + description: String, + foundedYear: Number, + employeeCount: String, + revenue: String, + legalAddress: String, + actualAddress: String, + bankDetails: String, + logo: String, + rating: { + type: Number, + default: 0, + min: 0, + max: 5 + }, + reviews: { + type: Number, + default: 0 + }, + ownerId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + platformGoals: [String], + productsOffered: String, + productsNeeded: String, + partnerIndustries: [String], + partnerGeography: [String], + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// Индексы для поиска +companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' }) +companySchema.index({ industry: 1 }) +companySchema.index({ rating: -1 }) + +module.exports = mongoose.model('Company', companySchema) diff --git a/server/routers/procurement/models/Message.js b/server/routers/procurement/models/Message.js new file mode 100644 index 0000000..3e29204 --- /dev/null +++ b/server/routers/procurement/models/Message.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose') + +const messageSchema = new mongoose.Schema({ + threadId: { + type: String, + required: true, + index: true + }, + senderCompanyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + recipientCompanyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + text: { + type: String, + required: true + }, + read: { + type: Boolean, + default: false + }, + timestamp: { + type: Date, + default: Date.now, + index: true + } +}) + +// Индекс для быстрого поиска сообщений потока +messageSchema.index({ threadId: 1, timestamp: -1 }) + +module.exports = mongoose.model('Message', messageSchema) diff --git a/server/routers/procurement/models/Product.js b/server/routers/procurement/models/Product.js new file mode 100644 index 0000000..4926923 --- /dev/null +++ b/server/routers/procurement/models/Product.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose') + +const productSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + category: { + type: String, + required: true + }, + description: { + type: String, + required: true, + minlength: 20, + maxlength: 500 + }, + type: { + type: String, + enum: ['sell', 'buy'], + required: true + }, + productUrl: String, + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + price: String, + unit: String, + minOrder: String, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// Индекс для поиска +productSchema.index({ companyId: 1, type: 1 }) +productSchema.index({ name: 'text', description: 'text' }) + +module.exports = mongoose.model('Product', productSchema) diff --git a/server/routers/procurement/models/User.js b/server/routers/procurement/models/User.js new file mode 100644 index 0000000..7a604d9 --- /dev/null +++ b/server/routers/procurement/models/User.js @@ -0,0 +1,67 @@ +const mongoose = require('mongoose') +const bcrypt = require('bcryptjs') + +const userSchema = new mongoose.Schema({ + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + }, + password: { + type: String, + required: true, + minlength: 8 + }, + firstName: { + type: String, + required: true + }, + lastName: { + type: String, + required: true + }, + position: String, + phone: String, + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company' + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// Хешировать пароль перед сохранением +userSchema.pre('save', async function(next) { + if (!this.isModified('password')) return next() + + try { + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) + next() + } catch (error) { + next(error) + } +}) + +// Метод для сравнения паролей +userSchema.methods.comparePassword = async function(candidatePassword) { + return await bcrypt.compare(candidatePassword, this.password) +} + +// Скрыть пароль при преобразовании в JSON +userSchema.methods.toJSON = function() { + const obj = this.toObject() + delete obj.password + return obj +} + +module.exports = mongoose.model('User', userSchema) diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js new file mode 100644 index 0000000..cd7bdc7 --- /dev/null +++ b/server/routers/procurement/routes/auth.js @@ -0,0 +1,103 @@ +const express = require('express') +const router = express.Router() +const User = require('../models/User') +const Company = require('../models/Company') +const { generateToken } = require('../middleware/auth') + +// Регистрация +router.post('/register', async (req, res) => { + try { + const { email, password, firstName, lastName, position, phone, fullName, inn, ogrn, legalForm, industry, companySize, website } = req.body; + + // Проверка обязательных полей + if (!email || !password || !firstName || !lastName || !fullName) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + // Проверка существования пользователя + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(409).json({ error: 'User already exists' }); + } + + // Создать компанию + const company = await Company.create({ + fullName, + inn, + ogrn, + legalForm, + industry, + companySize, + website + }); + + // Создать пользователя + const user = await User.create({ + email, + password, + firstName, + lastName, + position, + phone, + companyId: company._id + }); + + // Генерировать токен + const token = generateToken(user._id, user.email); + + res.status(201).json({ + tokens: { + accessToken: token, + refreshToken: token + }, + user: user.toJSON(), + company: company.toObject() + }); + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Логин +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password required' }); + } + + // Найти пользователя + const user = await User.findOne({ email }); + if (!user) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Проверить пароль + const isValid = await user.comparePassword(password); + if (!isValid) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Загрузить компанию + const company = await Company.findById(user.companyId); + + // Генерировать токен + const token = generateToken(user._id, user.email); + + res.json({ + tokens: { + accessToken: token, + refreshToken: token + }, + user: user.toJSON(), + company: company?.toObject() || null + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js new file mode 100644 index 0000000..8155341 --- /dev/null +++ b/server/routers/procurement/routes/buy.js @@ -0,0 +1,186 @@ +const express = require('express') +const fs = require('fs') +const path = require('path') +const router = express.Router() + +// Create remote-assets/docs directory if it doesn't exist +const docsDir = path.resolve('server/remote-assets/docs') +if (!fs.existsSync(docsDir)) { + fs.mkdirSync(docsDir, { recursive: true }) +} + +// In-memory store for documents metadata +const buyDocs = [] + +function generateId() { + return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` +} + +// GET /buy/docs?ownerCompanyId=... +router.get('/docs', (req, res) => { + const { ownerCompanyId } = req.query + console.log('[BUY API] GET /docs', { ownerCompanyId, totalDocs: buyDocs.length }) + let result = buyDocs + if (ownerCompanyId) { + result = result.filter((d) => d.ownerCompanyId === ownerCompanyId) + } + result = result.map(doc => ({ + ...doc, + url: `/api/buy/docs/${doc.id}/file` + })) + res.json(result) +}) + +// POST /buy/docs +router.post('/docs', (req, res) => { + const { ownerCompanyId, name, type, fileData } = req.body || {} + console.log('[BUY API] POST /docs', { ownerCompanyId, name, type }) + if (!ownerCompanyId || !name || !type) { + return res.status(400).json({ error: 'ownerCompanyId, name and type are required' }) + } + + if (!fileData) { + return res.status(400).json({ error: 'fileData is required' }) + } + + const id = generateId() + + // Save file to disk + try { + const binaryData = Buffer.from(fileData, 'base64') + const filePath = path.join(docsDir, `${id}.${type}`) + fs.writeFileSync(filePath, binaryData) + console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) + + const size = binaryData.length + const url = `/api/buy/docs/${id}/file` + const doc = { + id, + ownerCompanyId, + name, + type, + size, + url, + filePath, + acceptedBy: [], + createdAt: new Date().toISOString(), + } + buyDocs.unshift(doc) + console.log('[BUY API] Document created:', id) + res.status(201).json(doc) + } catch (e) { + console.error(`[BUY API] Error saving file: ${e.message}`) + res.status(500).json({ error: 'Failed to save file' }) + } +}) + +router.post('/docs/:id/accept', (req, res) => { + const { id } = req.params + const { companyId } = req.body || {} + console.log('[BUY API] POST /docs/:id/accept', { id, companyId }) + const doc = buyDocs.find((d) => d.id === id) + if (!doc) { + console.log('[BUY API] Document not found:', id) + return res.status(404).json({ error: 'Document not found' }) + } + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }) + } + if (!doc.acceptedBy.includes(companyId)) { + doc.acceptedBy.push(companyId) + } + res.json({ id: doc.id, acceptedBy: doc.acceptedBy }) +}) + +router.get('/docs/:id/delete', (req, res) => { + const { id } = req.params + console.log('[BUY API] GET /docs/:id/delete', { id, totalDocs: buyDocs.length }) + const index = buyDocs.findIndex((d) => d.id === id) + if (index === -1) { + console.log('[BUY API] Document not found for deletion:', id) + return res.status(404).json({ error: 'Document not found' }) + } + const deletedDoc = buyDocs.splice(index, 1)[0] + + // Delete file from disk + if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) { + try { + fs.unlinkSync(deletedDoc.filePath) + console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`) + } catch (e) { + console.error(`[BUY API] Error deleting file: ${e.message}`) + } + } + + console.log('[BUY API] Document deleted via GET:', id, { remainingDocs: buyDocs.length }) + res.json({ id: deletedDoc.id, success: true }) +}) + +router.delete('/docs/:id', (req, res) => { + const { id } = req.params + console.log('[BUY API] DELETE /docs/:id', { id, totalDocs: buyDocs.length }) + const index = buyDocs.findIndex((d) => d.id === id) + if (index === -1) { + console.log('[BUY API] Document not found for deletion:', id) + return res.status(404).json({ error: 'Document not found' }) + } + const deletedDoc = buyDocs.splice(index, 1)[0] + + // Delete file from disk + if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) { + try { + fs.unlinkSync(deletedDoc.filePath) + console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`) + } catch (e) { + console.error(`[BUY API] Error deleting file: ${e.message}`) + } + } + + console.log('[BUY API] Document deleted:', id, { remainingDocs: buyDocs.length }) + res.json({ id: deletedDoc.id, success: true }) +}) + +// GET /buy/docs/:id/file - Serve the file +router.get('/docs/:id/file', (req, res) => { + const { id } = req.params + console.log('[BUY API] GET /docs/:id/file', { id }) + + const doc = buyDocs.find(d => d.id === id) + if (!doc) { + console.log('[BUY API] Document not found:', id) + return res.status(404).json({ error: 'Document not found' }) + } + + const filePath = path.join(docsDir, `${id}.${doc.type}`) + if (!fs.existsSync(filePath)) { + console.log('[BUY API] File not found on disk:', filePath) + return res.status(404).json({ error: 'File not found on disk' }) + } + + try { + const fileBuffer = fs.readFileSync(filePath) + + const mimeTypes = { + 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'pdf': 'application/pdf' + } + + const mimeType = mimeTypes[doc.type] || 'application/octet-stream' + const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_') + + res.setHeader('Content-Type', mimeType) + // RFC 5987 encoding: filename for ASCII fallback, filename* for UTF-8 with percent-encoding + const encodedFilename = encodeURIComponent(`${doc.name}.${doc.type}`) + res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}.${doc.type}"; filename*=UTF-8''${encodedFilename}`) + res.setHeader('Content-Length', fileBuffer.length) + + console.log(`[BUY API] Serving file ${id} from ${filePath} (${fileBuffer.length} bytes)`) + res.send(fileBuffer) + } catch (e) { + console.error(`[BUY API] Error serving file: ${e.message}`) + res.status(500).json({ error: 'Error serving file' }) + } +}) + +module.exports = router \ No newline at end of file diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js new file mode 100644 index 0000000..380fecf --- /dev/null +++ b/server/routers/procurement/routes/companies.js @@ -0,0 +1,103 @@ +const express = require('express') +const router = express.Router() +const Company = require('../models/Company') +const { verifyToken } = require('../middleware/auth') + +// Получить все компании +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 10, search = '', industry = '' } = req.query; + + let query = {}; + + if (search) { + query.$text = { $search: search }; + } + + if (industry) { + query.industry = industry; + } + + const skip = (page - 1) * limit; + + const companies = await Company.find(query) + .limit(Number(limit)) + .skip(Number(skip)) + .sort({ rating: -1 }); + + const total = await Company.countDocuments(query); + + res.json({ + companies, + total, + page: Number(page), + limit: Number(limit), + pages: Math.ceil(total / limit) + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить компанию по ID +router.get('/:id', async (req, res) => { + try { + const company = await Company.findById(req.params.id).populate('ownerId', 'firstName lastName email'); + + if (!company) { + return res.status(404).json({ error: 'Company not found' }); + } + + res.json(company); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Обновить компанию (требует авторизации) +const updateCompanyHandler = async (req, res) => { + try { + const company = await Company.findByIdAndUpdate( + req.params.id, + { ...req.body, updatedAt: new Date() }, + { new: true } + ); + + if (!company) { + return res.status(404).json({ error: 'Company not found' }); + } + + res.json(company); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}; + +router.put('/:id', verifyToken, updateCompanyHandler); +router.patch('/:id', verifyToken, updateCompanyHandler); + +// Поиск с AI анализом +router.post('/ai-search', async (req, res) => { + try { + const { query } = req.body; + + if (!query) { + return res.status(400).json({ error: 'Query required' }); + } + + // Простой поиск по текстовым полям + const companies = await Company.find({ + $text: { $search: query } + }).limit(10); + + res.json({ + companies, + total: companies.length, + aiSuggestion: `Found ${companies.length} companies matching "${query}"` + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js new file mode 100644 index 0000000..5ca6d47 --- /dev/null +++ b/server/routers/procurement/routes/experience.js @@ -0,0 +1,114 @@ +const express = require('express') +const router = express.Router() +const { verifyToken } = require('../middleware/auth') + +// In-memory хранилище для опыта работы (mock) +let experiences = []; + +// GET /experience - Получить список опыта работы компании +router.get('/', verifyToken, (req, res) => { + try { + const { companyId } = req.query; + + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }); + } + + const companyExperiences = experiences.filter(exp => exp.companyId === companyId); + res.json(companyExperiences); + } catch (error) { + console.error('Get experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// POST /experience - Создать запись опыта работы +router.post('/', verifyToken, (req, res) => { + try { + const { companyId, data } = req.body; + + if (!companyId || !data) { + return res.status(400).json({ error: 'companyId and data are required' }); + } + + const { confirmed, customer, subject, volume, contact, comment } = data; + + if (!customer || !subject) { + return res.status(400).json({ error: 'customer and subject are required' }); + } + + const newExperience = { + id: `exp-${Date.now()}`, + companyId, + confirmed: confirmed || false, + customer, + subject, + volume: volume || '', + contact: contact || '', + comment: comment || '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + experiences.push(newExperience); + + res.status(201).json(newExperience); + } catch (error) { + console.error('Create experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PUT /experience/:id - Обновить запись опыта работы +router.put('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + const { data } = req.body; + + if (!data) { + return res.status(400).json({ error: 'data is required' }); + } + + const index = experiences.findIndex(exp => exp.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Experience not found' }); + } + + const updatedExperience = { + ...experiences[index], + ...data, + updatedAt: new Date().toISOString() + }; + + experiences[index] = updatedExperience; + + res.json(updatedExperience); + } catch (error) { + console.error('Update experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// DELETE /experience/:id - Удалить запись опыта работы +router.delete('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + + const index = experiences.findIndex(exp => exp.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Experience not found' }); + } + + experiences.splice(index, 1); + + res.json({ message: 'Experience deleted successfully' }); + } catch (error) { + console.error('Delete experience error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router + diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js new file mode 100644 index 0000000..6b52eca --- /dev/null +++ b/server/routers/procurement/routes/messages.js @@ -0,0 +1,130 @@ +const express = require('express') +const router = express.Router() +const Message = require('../models/Message') +const { verifyToken } = require('../middleware/auth') + +// Mock данные для тредов +const mockThreads = [ + { + id: 'thread-1', + lastMessage: 'Добрый день! Интересует поставка металлопроката.', + lastMessageAt: new Date(Date.now() - 3600000).toISOString(), + participants: ['company-123', 'company-1'] + }, + { + id: 'thread-2', + lastMessage: 'Можем предложить скидку 15% на оптовую партию.', + lastMessageAt: new Date(Date.now() - 7200000).toISOString(), + participants: ['company-123', 'company-2'] + }, + { + id: 'thread-3', + lastMessage: 'Спасибо за предложение, рассмотрим.', + lastMessageAt: new Date(Date.now() - 86400000).toISOString(), + participants: ['company-123', 'company-4'] + } +]; + +// Mock данные для сообщений +const mockMessages = { + 'thread-1': [ + { id: 'msg-1', senderCompanyId: 'company-1', text: 'Добрый день! Интересует поставка металлопроката.', timestamp: new Date(Date.now() - 3600000).toISOString() }, + { id: 'msg-2', senderCompanyId: 'company-123', text: 'Здравствуйте! Какой объем вас интересует?', timestamp: new Date(Date.now() - 3500000).toISOString() } + ], + 'thread-2': [ + { id: 'msg-3', senderCompanyId: 'company-2', text: 'Можем предложить скидку 15% на оптовую партию.', timestamp: new Date(Date.now() - 7200000).toISOString() } + ], + 'thread-3': [ + { id: 'msg-4', senderCompanyId: 'company-4', text: 'Спасибо за предложение, рассмотрим.', timestamp: new Date(Date.now() - 86400000).toISOString() } + ] +}; + +// Получить все потоки для компании +router.get('/threads', verifyToken, async (req, res) => { + try { + // Попытка получить из MongoDB + try { + const threads = await Message.aggregate([ + { + $match: { + $or: [ + { senderCompanyId: req.user.companyId }, + { recipientCompanyId: req.user.companyId } + ] + } + }, + { + $sort: { timestamp: -1 } + }, + { + $group: { + _id: '$threadId', + lastMessage: { $first: '$text' }, + lastMessageAt: { $first: '$timestamp' } + } + } + ]); + + if (threads && threads.length > 0) { + return res.json(threads); + } + } catch (dbError) { + console.log('MongoDB unavailable, using mock data'); + } + + // Fallback на mock данные + res.json(mockThreads); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить сообщения потока +router.get('/threads/:threadId', verifyToken, async (req, res) => { + try { + // Попытка получить из MongoDB + try { + const messages = await Message.find({ threadId: req.params.threadId }) + .sort({ timestamp: 1 }) + .populate('senderCompanyId', 'shortName fullName') + .populate('recipientCompanyId', 'shortName fullName'); + + if (messages && messages.length > 0) { + return res.json(messages); + } + } catch (dbError) { + console.log('MongoDB unavailable, using mock data'); + } + + // Fallback на mock данные + const threadMessages = mockMessages[req.params.threadId] || []; + res.json(threadMessages); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Отправить сообщение +router.post('/', verifyToken, async (req, res) => { + try { + const { threadId, text, recipientCompanyId } = req.body; + + if (!text || !threadId) { + return res.status(400).json({ error: 'Text and threadId required' }); + } + + const message = await Message.create({ + threadId, + senderCompanyId: req.user.companyId, + recipientCompanyId, + text, + timestamp: new Date() + }); + + res.status(201).json(message); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js new file mode 100644 index 0000000..03f7a29 --- /dev/null +++ b/server/routers/procurement/routes/products.js @@ -0,0 +1,130 @@ +const express = require('express') +const router = express.Router() +const { verifyToken } = require('../middleware/auth') + +// In-memory хранилище для продуктов/услуг (mock) +let products = []; + +// GET /products - Получить список продуктов/услуг компании +router.get('/', verifyToken, (req, res) => { + try { + const { companyId } = req.query; + + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }); + } + + const companyProducts = products.filter(p => p.companyId === companyId); + res.json(companyProducts); + } catch (error) { + console.error('Get products error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// POST /products - Создать продукт/услугу +router.post('/', verifyToken, (req, res) => { + try { + const { companyId, name, category, description, price, unit } = req.body; + + if (!companyId || !name) { + return res.status(400).json({ error: 'companyId and name are required' }); + } + + const newProduct = { + id: `prod-${Date.now()}`, + companyId, + name, + category: category || 'other', + description: description || '', + price: price || '', + unit: unit || '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + products.push(newProduct); + + res.status(201).json(newProduct); + } catch (error) { + console.error('Create product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PUT /products/:id - Обновить продукт/услугу +router.put('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + const index = products.findIndex(p => p.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Product not found' }); + } + + const updatedProduct = { + ...products[index], + ...updates, + updatedAt: new Date().toISOString() + }; + + products[index] = updatedProduct; + + res.json(updatedProduct); + } catch (error) { + console.error('Update product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// PATCH /products/:id - Частичное обновление продукта/услуги +router.patch('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + const updates = req.body; + + const index = products.findIndex(p => p.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Product not found' }); + } + + const updatedProduct = { + ...products[index], + ...updates, + updatedAt: new Date().toISOString() + }; + + products[index] = updatedProduct; + + res.json(updatedProduct); + } catch (error) { + console.error('Patch product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// DELETE /products/:id - Удалить продукт/услугу +router.delete('/:id', verifyToken, (req, res) => { + try { + const { id } = req.params; + + const index = products.findIndex(p => p.id === id); + + if (index === -1) { + return res.status(404).json({ error: 'Product not found' }); + } + + products.splice(index, 1); + + res.json({ message: 'Product deleted successfully' }); + } catch (error) { + console.error('Delete product error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router + diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js new file mode 100644 index 0000000..c852fdd --- /dev/null +++ b/server/routers/procurement/routes/search.js @@ -0,0 +1,99 @@ +const express = require('express') +const router = express.Router() +const { verifyToken } = require('../middleware/auth') +const mockCompaniesData = require('../mocks/companies.json') +const mockCompanies = mockCompaniesData.mockCompanies + +// Маппинг отраслей для фильтрации +const industryMapping = { + 'it': 'IT', + 'finance': 'Финансы', + 'manufacturing': 'Производство', + 'construction': 'Строительство', + 'retail': 'Торговля', + 'wholesale': 'Торговля', + 'logistics': 'Логистика', + 'healthcare': 'Медицина' +}; + +// GET /search/companies - Поиск компаний +router.get('/companies', verifyToken, (req, res) => { + try { + const { + query = '', + industries = '', + companySizes = '', + geographies = '', + minRating = 0, + hasReviews, + hasAccepts + } = req.query; + + let result = [...mockCompanies]; + + // Фильтр по текстовому запросу + if (query.trim()) { + const q = query.toLowerCase(); + result = result.filter(c => + c.fullName.toLowerCase().includes(q) || + c.shortName.toLowerCase().includes(q) || + c.slogan.toLowerCase().includes(q) || + c.industry.toLowerCase().includes(q) + ); + } + + // Фильтр по отраслям + if (industries) { + const selectedIndustries = industries.split(','); + result = result.filter(c => { + const mappedIndustries = selectedIndustries.map(i => industryMapping[i] || i); + return mappedIndustries.some(ind => + c.industry.toLowerCase().includes(ind.toLowerCase()) + ); + }); + } + + // Фильтр по размеру компании + if (companySizes) { + const sizes = companySizes.split(','); + result = result.filter(c => { + if (!c.companySize) return false; + return sizes.some(size => { + if (size === '1-10') return c.companySize === '1-10'; + if (size === '11-50') return c.companySize === '10-50'; + if (size === '51-250') return c.companySize.includes('50') || c.companySize.includes('100') || c.companySize.includes('250'); + if (size === '251-500') return c.companySize.includes('250') || c.companySize.includes('500'); + if (size === '500+') return c.companySize === '500+'; + return false; + }); + }); + } + + // Фильтр по рейтингу + const rating = parseFloat(minRating); + if (rating > 0) { + result = result.filter(c => c.rating >= rating); + } + + // Фильтр по наличию отзывов + if (hasReviews === 'true') { + result = result.filter(c => c.verified === true); + } + + // Фильтр по акцептам документов + if (hasAccepts === 'true') { + result = result.filter(c => c.verified === true); + } + + res.json({ + companies: result, + total: result.length + }); + } catch (error) { + console.error('Search error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router + diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js new file mode 100644 index 0000000..9e4ea96 --- /dev/null +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -0,0 +1,90 @@ +const mongoose = require('mongoose') +require('dotenv').config() + +// Импорт моделей +const User = require('../models/User') +const Company = require('../models/Company') + +const recreateTestUser = async () => { + try { + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db' + + console.log('\n🔄 Подключение к MongoDB...') + await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + console.log('✅ Подключено к MongoDB\n') + + // Удалить старого тестового пользователя + console.log('🗑️ Удаление старого тестового пользователя...') + const oldUser = await User.findOne({ email: 'admin@test-company.ru' }) + if (oldUser) { + // Удалить связанную компанию + if (oldUser.companyId) { + await Company.findByIdAndDelete(oldUser.companyId) + console.log(' ✓ Старая компания удалена') + } + await User.findByIdAndDelete(oldUser._id) + console.log(' ✓ Старый пользователь удален') + } else { + console.log(' ℹ️ Старый пользователь не найден') + } + + // Создать новую компанию с правильной кодировкой UTF-8 + console.log('\n🏢 Создание тестовой компании...') + const company = await Company.create({ + fullName: 'ООО "Тестовая Компания"', + inn: '1234567890', + ogrn: '1234567890123', + legalForm: 'ООО', + industry: 'IT', + companySize: '50-100', + website: 'https://test-company.ru', + description: 'Тестовая компания для разработки', + address: 'г. Москва, ул. Тестовая, д. 1', + rating: 4.5, + reviewsCount: 10, + dealsCount: 25, + }) + console.log(' ✓ Компания создана:', company.fullName) + + // Создать нового пользователя с правильной кодировкой UTF-8 + console.log('\n👤 Создание тестового пользователя...') + const user = await User.create({ + email: 'admin@test-company.ru', + password: 'SecurePass123!', + firstName: 'Иван', + lastName: 'Иванов', + position: 'Директор', + phone: '+7 (999) 123-45-67', + companyId: company._id, + }) + console.log(' ✓ Пользователь создан:', user.firstName, user.lastName) + + // Проверка что данные сохранены правильно + console.log('\n✅ Проверка данных:') + console.log(' Email:', user.email) + console.log(' Имя:', user.firstName) + console.log(' Фамилия:', user.lastName) + console.log(' Компания:', company.fullName) + console.log(' Должность:', user.position) + + console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8') + console.log('\n📋 Данные для входа:') + console.log(' Email: admin@test-company.ru') + console.log(' Пароль: SecurePass123!') + console.log('') + + await mongoose.connection.close() + process.exit(0) + } catch (error) { + console.error('\n❌ Ошибка:', error.message) + console.error(error) + process.exit(1) + } +} + +// Запуск +recreateTestUser() + From 99127c42e236fc8fb8c651d204df81463bba63eb Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Sat, 18 Oct 2025 12:08:25 +0300 Subject: [PATCH 123/147] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/routes/search.js | 145 ++++++++++---------- 1 file changed, 70 insertions(+), 75 deletions(-) diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index c852fdd..d64002b 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -1,99 +1,94 @@ -const express = require('express') -const router = express.Router() -const { verifyToken } = require('../middleware/auth') -const mockCompaniesData = require('../mocks/companies.json') -const mockCompanies = mockCompaniesData.mockCompanies +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const Company = require('../models/Company'); -// Маппинг отраслей для фильтрации -const industryMapping = { - 'it': 'IT', - 'finance': 'Финансы', - 'manufacturing': 'Производство', - 'construction': 'Строительство', - 'retail': 'Торговля', - 'wholesale': 'Торговля', - 'logistics': 'Логистика', - 'healthcare': 'Медицина' -}; - -// GET /search/companies - Поиск компаний -router.get('/companies', verifyToken, (req, res) => { +// GET /search - Поиск компаний (с использованием MongoDB) +router.get('/', verifyToken, async (req, res) => { try { - const { - query = '', - industries = '', - companySizes = '', - geographies = '', + const { + query = '', + page = 1, + limit = 10, minRating = 0, hasReviews, - hasAccepts + hasAcceptedDocs, + sortBy = 'relevance', + sortOrder = 'desc' } = req.query; - let result = [...mockCompanies]; + // Построение query для MongoDB + let mongoQuery = {}; - // Фильтр по текстовому запросу - if (query.trim()) { - const q = query.toLowerCase(); - result = result.filter(c => - c.fullName.toLowerCase().includes(q) || - c.shortName.toLowerCase().includes(q) || - c.slogan.toLowerCase().includes(q) || - c.industry.toLowerCase().includes(q) - ); - } - - // Фильтр по отраслям - if (industries) { - const selectedIndustries = industries.split(','); - result = result.filter(c => { - const mappedIndustries = selectedIndustries.map(i => industryMapping[i] || i); - return mappedIndustries.some(ind => - c.industry.toLowerCase().includes(ind.toLowerCase()) - ); - }); - } - - // Фильтр по размеру компании - if (companySizes) { - const sizes = companySizes.split(','); - result = result.filter(c => { - if (!c.companySize) return false; - return sizes.some(size => { - if (size === '1-10') return c.companySize === '1-10'; - if (size === '11-50') return c.companySize === '10-50'; - if (size === '51-250') return c.companySize.includes('50') || c.companySize.includes('100') || c.companySize.includes('250'); - if (size === '251-500') return c.companySize.includes('250') || c.companySize.includes('500'); - if (size === '500+') return c.companySize === '500+'; - return false; - }); - }); + // Текстовый поиск + if (query && query.trim()) { + mongoQuery.$or = [ + { fullName: { $regex: query, $options: 'i' } }, + { shortName: { $regex: query, $options: 'i' } }, + { slogan: { $regex: query, $options: 'i' } }, + { industry: { $regex: query, $options: 'i' } } + ]; } // Фильтр по рейтингу - const rating = parseFloat(minRating); - if (rating > 0) { - result = result.filter(c => c.rating >= rating); + if (minRating) { + const rating = parseFloat(minRating); + if (rating > 0) { + mongoQuery.rating = { $gte: rating }; + } } - // Фильтр по наличию отзывов + // Фильтр по отзывам if (hasReviews === 'true') { - result = result.filter(c => c.verified === true); + mongoQuery.verified = true; } - // Фильтр по акцептам документов - if (hasAccepts === 'true') { - result = result.filter(c => c.verified === true); + // Фильтр по акцептам + if (hasAcceptedDocs === 'true') { + mongoQuery.verified = true; } + // Пагинация + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const skip = (pageNum - 1) * limitNum; + + // Сортировка + let sortObj = { rating: -1 }; + if (sortBy === 'name') { + sortObj = { fullName: 1 }; + } + if (sortOrder === 'asc') { + Object.keys(sortObj).forEach(key => { + sortObj[key] = sortObj[key] === -1 ? 1 : -1; + }); + } + + // Запрос к MongoDB + const companies = await Company.find(mongoQuery) + .limit(limitNum) + .skip(skip) + .sort(sortObj) + .lean(); + + const total = await Company.countDocuments(mongoQuery); + + console.log('[Search] Returned', companies.length, 'companies'); + res.json({ - companies: result, - total: result.length + companies, + total, + page: pageNum, + totalPages: Math.ceil(total / limitNum) }); } catch (error) { - console.error('Search error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('[Search] Error:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); } }); -module.exports = router +module.exports = router; From a6065dd95c9f9d3e427ffa9148eed04ed3f144c9 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Thu, 23 Oct 2025 09:49:04 +0300 Subject: [PATCH 124/147] new procurement --- server/routers/procurement/config/db.js | 29 ++ server/routers/procurement/index.js | 100 +++-- server/routers/procurement/middleware/auth.js | 31 +- .../routers/procurement/models/BuyProduct.js | 58 +++ server/routers/procurement/models/Company.js | 20 +- server/routers/procurement/models/Message.js | 8 +- server/routers/procurement/models/Product.js | 29 +- server/routers/procurement/models/Review.js | 58 +++ server/routers/procurement/models/User.js | 38 +- .../routes/__tests__/buyProducts.test.js | 239 ++++++++++++ server/routers/procurement/routes/auth.js | 345 +++++++++++++++--- server/routers/procurement/routes/buy.js | 7 +- .../routers/procurement/routes/buyProducts.js | 144 ++++++++ .../routers/procurement/routes/companies.js | 222 ++++++++--- .../routers/procurement/routes/experience.js | 8 +- server/routers/procurement/routes/home.js | 48 +++ server/routers/procurement/routes/messages.js | 175 +++++---- server/routers/procurement/routes/products.js | 162 ++++---- server/routers/procurement/routes/reviews.js | 88 +++++ server/routers/procurement/routes/search.js | 104 ++++-- .../procurement/scripts/recreate-test-user.js | 82 ++--- server/utils/const.ts | 2 +- 22 files changed, 1588 insertions(+), 409 deletions(-) create mode 100644 server/routers/procurement/config/db.js create mode 100644 server/routers/procurement/models/BuyProduct.js create mode 100644 server/routers/procurement/models/Review.js create mode 100644 server/routers/procurement/routes/__tests__/buyProducts.test.js create mode 100644 server/routers/procurement/routes/buyProducts.js create mode 100644 server/routers/procurement/routes/home.js create mode 100644 server/routers/procurement/routes/reviews.js diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js new file mode 100644 index 0000000..3d03054 --- /dev/null +++ b/server/routers/procurement/config/db.js @@ -0,0 +1,29 @@ +const mongoose = require('mongoose'); + +const connectDB = async () => { + try { + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + + console.log('\n📡 Попытка подключения к MongoDB...'); + console.log(` URI: ${mongoUri}`); + + await mongoose.connect(mongoUri, { + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + + console.log('✅ MongoDB подключена успешно!'); + console.log(` Хост: ${mongoose.connection.host}`); + console.log(` БД: ${mongoose.connection.name}\n`); + + return true; + } catch (error) { + console.error('\n❌ Ошибка подключения к MongoDB:'); + console.error(` ${error.message}\n`); + console.warn('⚠️ Приложение продолжит работу с mock данными\n'); + + return false; + } +}; + +module.exports = connectDB; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 5765845..289b809 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,64 +1,94 @@ -const express = require('express') -const dotenv = require('dotenv') +const express = require('express'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const connectDB = require('./config/db'); // Загрузить переменные окружения -dotenv.config() - -// Подключение к MongoDB через mongoose -require('../../utils/mongoose') +dotenv.config(); // Импортировать маршруты -const authRoutes = require('./routes/auth') -const companiesRoutes = require('./routes/companies') -const messagesRoutes = require('./routes/messages') -const searchRoutes = require('./routes/search') -const buyRoutes = require('./routes/buy') -const experienceRoutes = require('./routes/experience') -const productsRoutes = require('./routes/products') +const authRoutes = require('./routes/auth'); +const companiesRoutes = require('./routes/companies'); +const messagesRoutes = require('./routes/messages'); +const searchRoutes = require('./routes/search'); +const buyRoutes = require('./routes/buy'); +const experienceRoutes = require('./routes/experience'); +const productsRoutes = require('./routes/products'); +const reviewsRoutes = require('./routes/reviews'); +const buyProductsRoutes = require('./routes/buyProducts'); +const homeRoutes = require('./routes/home'); -const mongoose = require('mongoose') +const app = express(); -const app = express() +// Подключить MongoDB при инициализации +let dbConnected = false; +connectDB().then(() => { + dbConnected = true; +}); + +// Middleware +app.use(cors()); +app.use(express.json({ charset: 'utf-8' })); +app.use(express.urlencoded({ extended: true, charset: 'utf-8' })); + +// Set UTF-8 encoding for all responses +app.use((req, res, next) => { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + next(); +}); + +// CORS headers +app.use((req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + if (req.method === 'OPTIONS') { + res.sendStatus(200); + } else { + next(); + } +}); // Задержка для имитации сети (опционально) -const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms) -app.use(delay()) +const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms); +app.use(delay()); // Health check endpoint app.get('/health', (req, res) => { - const mongodbStatus = mongoose.connection.readyState === 1 ? 'connected' : 'disconnected' res.json({ status: 'ok', api: 'running', - database: mongodbStatus, - mongoUri: process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db', + database: dbConnected ? 'mongodb' : 'mock', timestamp: new Date().toISOString() - }) -}) + }); +}); // Маршруты -app.use('/auth', authRoutes) -app.use('/companies', companiesRoutes) -app.use('/messages', messagesRoutes) -app.use('/search', searchRoutes) -app.use('/buy', buyRoutes) -app.use('/experience', experienceRoutes) -app.use('/products', productsRoutes) +app.use('/auth', authRoutes); +app.use('/companies', companiesRoutes); +app.use('/messages', messagesRoutes); +app.use('/search', searchRoutes); +app.use('/buy', buyRoutes); +app.use('/buy-products', buyProductsRoutes); +app.use('/experience', experienceRoutes); +app.use('/products', productsRoutes); +app.use('/reviews', reviewsRoutes); +app.use('/home', homeRoutes); // Обработка ошибок app.use((err, req, res, next) => { - console.error('API Error:', err) + console.error('API Error:', err); res.status(err.status || 500).json({ error: err.message || 'Internal server error' - }) -}) + }); +}); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Not found' - }) -}) + }); +}); // Экспортировать для использования в brojs -module.exports = app \ No newline at end of file +module.exports = app; \ No newline at end of file diff --git a/server/routers/procurement/middleware/auth.js b/server/routers/procurement/middleware/auth.js index b126134..ba36cd6 100644 --- a/server/routers/procurement/middleware/auth.js +++ b/server/routers/procurement/middleware/auth.js @@ -1,27 +1,32 @@ -const jwt = require('jsonwebtoken') +const jwt = require('jsonwebtoken'); const verifyToken = (req, res, next) => { - const token = req.headers.authorization?.replace('Bearer ', '') + const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) { - return res.status(401).json({ error: 'No token provided' }) + return res.status(401).json({ error: 'No token provided' }); } try { - const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') - req.user = decoded - next() + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key'); + req.userId = decoded.userId; + req.companyId = decoded.companyId; + req.user = decoded; + console.log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId); + next(); } catch (error) { - return res.status(401).json({ error: 'Invalid token' }) + console.error('[Auth] Token verification failed:', error.message); + return res.status(401).json({ error: 'Invalid token' }); } -} +}; -const generateToken = (userId, email) => { +const generateToken = (userId, companyId) => { + console.log('[Auth] Generating token for userId:', userId, 'companyId:', companyId); return jwt.sign( - { userId, email }, + { userId, companyId }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '7d' } - ) -} + ); +}; -module.exports = { verifyToken, generateToken } +module.exports = { verifyToken, generateToken }; diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js new file mode 100644 index 0000000..346623d --- /dev/null +++ b/server/routers/procurement/models/BuyProduct.js @@ -0,0 +1,58 @@ +const mongoose = require('mongoose'); + +const buyProductSchema = new mongoose.Schema({ + companyId: { + type: String, + required: true, + index: true + }, + name: { + type: String, + required: true + }, + description: { + type: String, + required: true, + minlength: 10, + maxlength: 1000 + }, + quantity: { + type: String, + required: true + }, + unit: { + type: String, + default: 'шт' + }, + files: [{ + id: String, + name: String, + url: String, + type: String, + size: Number, + uploadedAt: { + type: Date, + default: Date.now + } + }], + status: { + type: String, + enum: ['draft', 'published'], + default: 'published' + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +buyProductSchema.index({ companyId: 1, createdAt: -1 }); +buyProductSchema.index({ name: 'text', description: 'text' }); + +module.exports = mongoose.model('BuyProduct', buyProductSchema); diff --git a/server/routers/procurement/models/Company.js b/server/routers/procurement/models/Company.js index ca1a87d..dc34466 100644 --- a/server/routers/procurement/models/Company.js +++ b/server/routers/procurement/models/Company.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); const companySchema = new mongoose.Schema({ fullName: { @@ -8,7 +8,6 @@ const companySchema = new mongoose.Schema({ shortName: String, inn: { type: String, - unique: true, sparse: true }, ogrn: String, @@ -46,6 +45,10 @@ const companySchema = new mongoose.Schema({ productsNeeded: String, partnerIndustries: [String], partnerGeography: [String], + verified: { + type: Boolean, + default: false + }, createdAt: { type: Date, default: Date.now @@ -54,11 +57,14 @@ const companySchema = new mongoose.Schema({ type: Date, default: Date.now } -}) +}, { + collection: 'companies', + minimize: false +}); // Индексы для поиска -companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' }) -companySchema.index({ industry: 1 }) -companySchema.index({ rating: -1 }) +companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' }); +companySchema.index({ industry: 1 }); +companySchema.index({ rating: -1 }); -module.exports = mongoose.model('Company', companySchema) +module.exports = mongoose.model('Company', companySchema); diff --git a/server/routers/procurement/models/Message.js b/server/routers/procurement/models/Message.js index 3e29204..e8afd5a 100644 --- a/server/routers/procurement/models/Message.js +++ b/server/routers/procurement/models/Message.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); const messageSchema = new mongoose.Schema({ threadId: { @@ -29,9 +29,9 @@ const messageSchema = new mongoose.Schema({ default: Date.now, index: true } -}) +}); // Индекс для быстрого поиска сообщений потока -messageSchema.index({ threadId: 1, timestamp: -1 }) +messageSchema.index({ threadId: 1, timestamp: -1 }); -module.exports = mongoose.model('Message', messageSchema) +module.exports = mongoose.model('Message', messageSchema); diff --git a/server/routers/procurement/models/Product.js b/server/routers/procurement/models/Product.js index 4926923..2f194f7 100644 --- a/server/routers/procurement/models/Product.js +++ b/server/routers/procurement/models/Product.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose') +const mongoose = require('mongoose'); const productSchema = new mongoose.Schema({ name: { @@ -22,25 +22,36 @@ const productSchema = new mongoose.Schema({ }, productUrl: String, companyId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Company', - required: true + type: String, + required: true, + index: true }, price: String, unit: String, minOrder: String, createdAt: { type: Date, - default: Date.now + default: Date.now, + index: true }, updatedAt: { type: Date, default: Date.now } -}) +}); // Индекс для поиска -productSchema.index({ companyId: 1, type: 1 }) -productSchema.index({ name: 'text', description: 'text' }) +productSchema.index({ companyId: 1, type: 1 }); +productSchema.index({ name: 'text', description: 'text' }); -module.exports = mongoose.model('Product', productSchema) +// Transform _id to id in JSON output +productSchema.set('toJSON', { + transform: (doc, ret) => { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + } +}); + +module.exports = mongoose.model('Product', productSchema); diff --git a/server/routers/procurement/models/Review.js b/server/routers/procurement/models/Review.js new file mode 100644 index 0000000..327c027 --- /dev/null +++ b/server/routers/procurement/models/Review.js @@ -0,0 +1,58 @@ +const mongoose = require('mongoose'); + +const reviewSchema = new mongoose.Schema({ + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true, + index: true + }, + authorCompanyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true + }, + authorName: { + type: String, + required: true + }, + authorCompany: { + type: String, + required: true + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5 + }, + comment: { + type: String, + required: true, + minlength: 10, + maxlength: 1000 + }, + date: { + type: Date, + default: Date.now + }, + verified: { + type: Boolean, + default: true + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +reviewSchema.index({ companyId: 1, createdAt: -1 }); +reviewSchema.index({ authorCompanyId: 1 }); + +module.exports = mongoose.model('Review', reviewSchema); diff --git a/server/routers/procurement/models/User.js b/server/routers/procurement/models/User.js index 7a604d9..0a2ea56 100644 --- a/server/routers/procurement/models/User.js +++ b/server/routers/procurement/models/User.js @@ -1,5 +1,5 @@ -const mongoose = require('mongoose') -const bcrypt = require('bcryptjs') +const mongoose = require('mongoose'); +const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema({ email: { @@ -37,31 +37,37 @@ const userSchema = new mongoose.Schema({ type: Date, default: Date.now } -}) +}, { + collection: 'users', + minimize: false, + toObject: { versionKey: false } +}); + +userSchema.set('toObject', { virtuals: false, versionKey: false }); // Хешировать пароль перед сохранением userSchema.pre('save', async function(next) { - if (!this.isModified('password')) return next() + if (!this.isModified('password')) return next(); try { - const salt = await bcrypt.genSalt(10) - this.password = await bcrypt.hash(this.password, salt) - next() + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + next(); } catch (error) { - next(error) + next(error); } -}) +}); // Метод для сравнения паролей userSchema.methods.comparePassword = async function(candidatePassword) { - return await bcrypt.compare(candidatePassword, this.password) -} + return await bcrypt.compare(candidatePassword, this.password); +}; // Скрыть пароль при преобразовании в JSON userSchema.methods.toJSON = function() { - const obj = this.toObject() - delete obj.password - return obj -} + const obj = this.toObject(); + delete obj.password; + return obj; +}; -module.exports = mongoose.model('User', userSchema) +module.exports = mongoose.model('User', userSchema); diff --git a/server/routers/procurement/routes/__tests__/buyProducts.test.js b/server/routers/procurement/routes/__tests__/buyProducts.test.js new file mode 100644 index 0000000..b254a38 --- /dev/null +++ b/server/routers/procurement/routes/__tests__/buyProducts.test.js @@ -0,0 +1,239 @@ +const express = require('express') +const mongoose = require('mongoose') +const request = require('supertest') + +// Mock auth middleware +const mockAuthMiddleware = (req, res, next) => { + req.user = { + companyId: 'test-company-id', + id: 'test-user-id', + } + next() +} + +describe('Buy Products Routes', () => { + let app + let router + + beforeAll(() => { + app = express() + app.use(express.json()) + + // Create a test router with mock middleware + router = express.Router() + + // Mock endpoints for testing structure + router.get('/company/:companyId', mockAuthMiddleware, (req, res) => { + res.json([]) + }) + + router.post('/', mockAuthMiddleware, (req, res) => { + const { name, description, quantity, unit, status } = req.body + + if (!name || !description || !quantity) { + return res.status(400).json({ + error: 'name, description, and quantity are required', + }) + } + + if (description.trim().length < 10) { + return res.status(400).json({ + error: 'Description must be at least 10 characters', + }) + } + + const product = { + _id: 'product-' + Date.now(), + companyId: req.user.companyId, + name: name.trim(), + description: description.trim(), + quantity: quantity.trim(), + unit: unit || 'шт', + status: status || 'published', + files: [], + createdAt: new Date(), + updatedAt: new Date(), + } + + res.status(201).json(product) + }) + + app.use('/buy-products', router) + }) + + describe('GET /buy-products/company/:companyId', () => { + it('should return products list for a company', async () => { + const res = await request(app) + .get('/buy-products/company/test-company-id') + .expect(200) + + expect(Array.isArray(res.body)).toBe(true) + }) + + it('should require authentication', async () => { + // This test would fail without proper auth middleware + const res = await request(app) + .get('/buy-products/company/test-company-id') + + expect(res.status).toBeLessThan(500) + }) + }) + + describe('POST /buy-products', () => { + it('should create a new product with valid data', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product description', + quantity: '10', + unit: 'шт', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(201) + + expect(res.body).toHaveProperty('_id') + expect(res.body.name).toBe('Test Product') + expect(res.body.description).toBe(productData.description) + expect(res.body.status).toBe('published') + }) + + it('should reject product without name', async () => { + const productData = { + description: 'This is a test product description', + quantity: '10', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(400) + + expect(res.body.error).toContain('required') + }) + + it('should reject product without description', async () => { + const productData = { + name: 'Test Product', + quantity: '10', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(400) + + expect(res.body.error).toContain('required') + }) + + it('should reject product without quantity', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product description', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(400) + + expect(res.body.error).toContain('required') + }) + + it('should reject product with description less than 10 characters', async () => { + const productData = { + name: 'Test Product', + description: 'short', + quantity: '10', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(400) + + expect(res.body.error).toContain('10 characters') + }) + + it('should set default unit to "шт" if not provided', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product description', + quantity: '10', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(201) + + expect(res.body.unit).toBe('шт') + }) + + it('should use provided unit', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product description', + quantity: '10', + unit: 'кг', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(201) + + expect(res.body.unit).toBe('кг') + }) + + it('should set status to "published" by default', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product description', + quantity: '10', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(201) + + expect(res.body.status).toBe('published') + }) + }) + + describe('Data validation', () => { + it('should trim whitespace from product data', async () => { + const productData = { + name: ' Test Product ', + description: ' This is a test product description ', + quantity: ' 10 ', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(201) + + expect(res.body.name).toBe('Test Product') + expect(res.body.description).toBe('This is a test product description') + expect(res.body.quantity).toBe('10') + }) + + it('should include companyId from auth token', async () => { + const productData = { + name: 'Test Product', + description: 'This is a test product description', + quantity: '10', + } + + const res = await request(app) + .post('/buy-products') + .send(productData) + .expect(201) + + expect(res.body.companyId).toBe('test-company-id') + }) + }) +}) diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index cd7bdc7..1ac6b62 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -1,8 +1,133 @@ -const express = require('express') -const router = express.Router() -const User = require('../models/User') -const Company = require('../models/Company') -const { generateToken } = require('../middleware/auth') +const express = require('express'); +const router = express.Router(); +const { generateToken } = require('../middleware/auth'); +const User = require('../models/User'); +const Company = require('../models/Company'); + +// In-memory storage для логирования +let users = []; + +// Инициализация тестового пользователя +const initializeTestUser = async () => { + try { + const existingUser = await User.findOne({ email: 'admin@test-company.ru' }); + if (!existingUser) { + // Создать компанию + const company = await Company.create({ + fullName: 'ООО "Тестовая Компания"', + shortName: 'ООО "Тест"', + inn: '7707083893', + ogrn: '1027700132195', + legalForm: 'ООО', + industry: 'Производство', + companySize: '50-100', + website: 'https://test-company.ru', + verified: true, + rating: 4.5, + description: 'Ведущая компания в области производства', + slogan: 'Качество и инновация' + }); + + // Создать пользователя + const user = await User.create({ + email: 'admin@test-company.ru', + password: 'SecurePass123!', + firstName: 'Иван', + lastName: 'Петров', + position: 'Генеральный директор', + companyId: company._id + }); + + console.log('✅ Test user initialized'); + } + + // Инициализация других тестовых компаний + const mockCompanies = [ + { + fullName: 'ООО "СтройКомплект"', + shortName: 'ООО "СтройКомплект"', + inn: '7707083894', + ogrn: '1027700132196', + legalForm: 'ООО', + industry: 'Строительство', + companySize: '100-250', + website: 'https://stroykomplekt.ru', + verified: true, + rating: 4.8, + description: 'Компания строит будущее вместе', + slogan: 'Строим будущее вместе' + }, + { + fullName: 'АО "Московский Строй"', + shortName: 'АО "Московский Строй"', + inn: '7707083895', + ogrn: '1027700132197', + legalForm: 'АО', + industry: 'Строительство', + companySize: '500+', + website: 'https://moscow-stroy.ru', + verified: true, + rating: 4.9, + description: 'Качество и надежность с 1995 года', + slogan: 'Качество и надежность' + }, + { + fullName: 'ООО "ТеxПроект"', + shortName: 'ООО "ТеxПроект"', + inn: '7707083896', + ogrn: '1027700132198', + legalForm: 'ООО', + industry: 'IT', + companySize: '50-100', + website: 'https://techproject.ru', + verified: true, + rating: 4.6, + description: 'Решения в области информационных технологий', + slogan: 'Технологии для бизнеса' + }, + { + fullName: 'ООО "ТоргПартнер"', + shortName: 'ООО "ТоргПартнер"', + inn: '7707083897', + ogrn: '1027700132199', + legalForm: 'ООО', + industry: 'Торговля', + companySize: '100-250', + website: 'https://torgpartner.ru', + verified: true, + rating: 4.3, + description: 'Оптовые поставки и логистика', + slogan: 'Надежный партнер в торговле' + }, + { + fullName: 'ООО "ЭнергоПлюс"', + shortName: 'ООО "ЭнергоПлюс"', + inn: '7707083898', + ogrn: '1027700132200', + legalForm: 'ООО', + industry: 'Энергетика', + companySize: '250-500', + website: 'https://energoplus.ru', + verified: true, + rating: 4.7, + description: 'Энергетические решения и консалтинг', + slogan: 'Энергия для развития' + } + ]; + + for (const mockCompanyData of mockCompanies) { + const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); + if (!existingCompany) { + await Company.create(mockCompanyData); + console.log(`✅ Mock company created: ${mockCompanyData.fullName}`); + } + } + } catch (error) { + console.error('Error initializing test data:', error.message); + } +}; + +initializeTestUser(); // Регистрация router.post('/register', async (req, res) => { @@ -21,45 +146,75 @@ router.post('/register', async (req, res) => { } // Создать компанию - const company = await Company.create({ - fullName, - inn, - ogrn, - legalForm, - industry, - companySize, - website - }); + let company; + try { + company = new Company({ + fullName, + shortName: fullName.substring(0, 20), + inn, + ogrn, + legalForm, + industry, + companySize, + website, + verified: false, + rating: 0, + description: '', + slogan: '' + }); + const savedCompany = await company.save(); + company = savedCompany; + console.log('✅ Company saved:', company._id, 'Result:', savedCompany ? 'Success' : 'Failed'); + } catch (err) { + console.error('Company save error:', err); + return res.status(400).json({ error: 'Failed to create company: ' + err.message }); + } // Создать пользователя - const user = await User.create({ - email, - password, - firstName, - lastName, - position, - phone, - companyId: company._id - }); + try { + const newUser = await User.create({ + email, + password, + firstName, + lastName, + position: position || '', + phone: phone || '', + companyId: company._id + }); - // Генерировать токен - const token = generateToken(user._id, user.email); + console.log('✅ User created:', newUser._id); - res.status(201).json({ - tokens: { - accessToken: token, - refreshToken: token - }, - user: user.toJSON(), - company: company.toObject() - }); + const token = generateToken(newUser._id.toString(), newUser.companyId.toString()); + return res.status(201).json({ + tokens: { + accessToken: token, + refreshToken: token + }, + user: { + id: newUser._id.toString(), + email: newUser.email, + firstName: newUser.firstName, + lastName: newUser.lastName, + position: newUser.position, + companyId: newUser.companyId.toString() + }, + company: { + id: company._id.toString(), + name: company.fullName, + inn: company.inn + } + }); + } catch (err) { + console.error('User creation error:', err); + return res.status(400).json({ error: 'Failed to create user: ' + err.message }); + } } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: error.message }); } }); -// Логин +// Вход router.post('/login', async (req, res) => { try { const { email, password } = req.body; @@ -68,31 +223,125 @@ router.post('/login', async (req, res) => { return res.status(400).json({ error: 'Email and password required' }); } - // Найти пользователя const user = await User.findOne({ email }); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } - // Проверить пароль - const isValid = await user.comparePassword(password); - if (!isValid) { + const isMatch = await user.comparePassword(password); + if (!isMatch) { return res.status(401).json({ error: 'Invalid credentials' }); } - // Загрузить компанию - const company = await Company.findById(user.companyId); + // Инициализация других тестовых компаний + const mockCompanies = [ + { + fullName: 'ООО "СтройКомплект"', + shortName: 'ООО "СтройКомплект"', + inn: '7707083894', + ogrn: '1027700132196', + legalForm: 'ООО', + industry: 'Строительство', + companySize: '100-250', + website: 'https://stroykomplekt.ru', + verified: true, + rating: 4.8, + description: 'Компания строит будущее вместе', + slogan: 'Строим будущее вместе' + }, + { + fullName: 'АО "Московский Строй"', + shortName: 'АО "Московский Строй"', + inn: '7707083895', + ogrn: '1027700132197', + legalForm: 'АО', + industry: 'Строительство', + companySize: '500+', + website: 'https://moscow-stroy.ru', + verified: true, + rating: 4.9, + description: 'Качество и надежность с 1995 года', + slogan: 'Качество и надежность' + }, + { + fullName: 'ООО "ТеxПроект"', + shortName: 'ООО "ТеxПроект"', + inn: '7707083896', + ogrn: '1027700132198', + legalForm: 'ООО', + industry: 'IT', + companySize: '50-100', + website: 'https://techproject.ru', + verified: true, + rating: 4.6, + description: 'Решения в области информационных технологий', + slogan: 'Технологии для бизнеса' + }, + { + fullName: 'ООО "ТоргПартнер"', + shortName: 'ООО "ТоргПартнер"', + inn: '7707083897', + ogrn: '1027700132199', + legalForm: 'ООО', + industry: 'Торговля', + companySize: '100-250', + website: 'https://torgpartner.ru', + verified: true, + rating: 4.3, + description: 'Оптовые поставки и логистика', + slogan: 'Надежный партнер в торговле' + }, + { + fullName: 'ООО "ЭнергоПлюс"', + shortName: 'ООО "ЭнергоПлюс"', + inn: '7707083898', + ogrn: '1027700132200', + legalForm: 'ООО', + industry: 'Энергетика', + companySize: '250-500', + website: 'https://energoplus.ru', + verified: true, + rating: 4.7, + description: 'Энергетические решения и консалтинг', + slogan: 'Энергия для развития' + } + ]; - // Генерировать токен - const token = generateToken(user._id, user.email); + for (const mockCompanyData of mockCompanies) { + try { + const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); + if (!existingCompany) { + await Company.create(mockCompanyData); + } + } catch (err) { + // Ignore errors for mock company creation + } + } + + const token = generateToken(user._id.toString(), user.companyId.toString()); + console.log('✅ Token generated for user:', user._id); + + // Получить компанию + const company = await Company.findById(user.companyId); res.json({ tokens: { accessToken: token, refreshToken: token }, - user: user.toJSON(), - company: company?.toObject() || null + user: { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + position: user.position, + companyId: user.companyId.toString() + }, + company: company ? { + id: company._id.toString(), + name: company.fullName, + inn: company.inn + } : null }); } catch (error) { console.error('Login error:', error); @@ -100,4 +349,10 @@ router.post('/login', async (req, res) => { } }); -module.exports = router +// Обновить профиль +router.patch('/profile', (req, res) => { + // требует авторизации, добавить middleware + res.json({ message: 'Update profile endpoint' }); +}); + +module.exports = router; diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 8155341..8218ace 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -1,10 +1,9 @@ const express = require('express') const fs = require('fs') -const path = require('path') const router = express.Router() // Create remote-assets/docs directory if it doesn't exist -const docsDir = path.resolve('server/remote-assets/docs') +const docsDir = '../../remote-assets/docs' if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }) } @@ -48,7 +47,7 @@ router.post('/docs', (req, res) => { // Save file to disk try { const binaryData = Buffer.from(fileData, 'base64') - const filePath = path.join(docsDir, `${id}.${type}`) + const filePath = `${docsDir}/${id}.${type}` fs.writeFileSync(filePath, binaryData) console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) @@ -151,7 +150,7 @@ router.get('/docs/:id/file', (req, res) => { return res.status(404).json({ error: 'Document not found' }) } - const filePath = path.join(docsDir, `${id}.${doc.type}`) + const filePath = `${docsDir}/${id}.${doc.type}` if (!fs.existsSync(filePath)) { console.log('[BUY API] File not found on disk:', filePath) return res.status(404).json({ error: 'File not found on disk' }) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js new file mode 100644 index 0000000..175b104 --- /dev/null +++ b/server/routers/procurement/routes/buyProducts.js @@ -0,0 +1,144 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const BuyProduct = require('../models/BuyProduct'); + +// GET /buy-products/company/:companyId - получить товары компании +router.get('/company/:companyId', verifyToken, async (req, res) => { + try { + const { companyId } = req.params; + + console.log('[BuyProducts] Fetching products for company:', companyId); + const products = await BuyProduct.find({ companyId }) + .sort({ createdAt: -1 }) + .exec(); + + console.log('[BuyProducts] Found', products.length, 'products for company', companyId); + console.log('[BuyProducts] Products:', products); + + res.json(products); + } catch (error) { + console.error('[BuyProducts] Error fetching products:', error.message); + console.error('[BuyProducts] Error stack:', error.stack); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// POST /buy-products - создать новый товар +router.post('/', verifyToken, async (req, res) => { + try { + const { name, description, quantity, unit, status } = req.body; + + console.log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId }); + + if (!name || !description || !quantity) { + return res.status(400).json({ + error: 'name, description, and quantity are required', + }); + } + + if (description.trim().length < 10) { + return res.status(400).json({ + error: 'Description must be at least 10 characters', + }); + } + + const newProduct = new BuyProduct({ + companyId: req.user.companyId, + name: name.trim(), + description: description.trim(), + quantity: quantity.trim(), + unit: unit || 'шт', + status: status || 'published', + files: [], + }); + + console.log('[BuyProducts] Attempting to save product to DB...'); + const savedProduct = await newProduct.save(); + + console.log('[BuyProducts] New product created successfully:', savedProduct._id); + console.log('[BuyProducts] Product data:', savedProduct); + + res.status(201).json(savedProduct); + } catch (error) { + console.error('[BuyProducts] Error creating product:', error.message); + console.error('[BuyProducts] Error stack:', error.stack); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// PUT /buy-products/:id - обновить товар +router.put('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { name, description, quantity, unit, status } = req.body; + + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + // Проверить, что товар принадлежит текущей компании + if (product.companyId !== req.user.companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + + // Обновить поля + if (name) product.name = name.trim(); + if (description) product.description = description.trim(); + if (quantity) product.quantity = quantity.trim(); + if (unit) product.unit = unit; + if (status) product.status = status; + product.updatedAt = new Date(); + + const updatedProduct = await product.save(); + + console.log('[BuyProducts] Product updated:', id); + + res.json(updatedProduct); + } catch (error) { + console.error('[BuyProducts] Error:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// DELETE /buy-products/:id - удалить товар +router.delete('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + if (product.companyId.toString() !== req.user.companyId.toString()) { + return res.status(403).json({ error: 'Not authorized' }); + } + + await BuyProduct.findByIdAndDelete(id); + + console.log('[BuyProducts] Product deleted:', id); + + res.json({ message: 'Product deleted successfully' }); + } catch (error) { + console.error('[BuyProducts] Error:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +module.exports = router; diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 380fecf..0c70e21 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -1,55 +1,169 @@ -const express = require('express') -const router = express.Router() -const Company = require('../models/Company') -const { verifyToken } = require('../middleware/auth') +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const Company = require('../models/Company'); -// Получить все компании -router.get('/', async (req, res) => { +// Инициализация данных при запуске +const initializeCompanies = async () => { try { - const { page = 1, limit = 10, search = '', industry = '' } = req.query; - - let query = {}; - - if (search) { - query.$text = { $search: search }; - } - - if (industry) { - query.industry = industry; - } - - const skip = (page - 1) * limit; - - const companies = await Company.find(query) - .limit(Number(limit)) - .skip(Number(skip)) - .sort({ rating: -1 }); - - const total = await Company.countDocuments(query); - - res.json({ - companies, - total, - page: Number(page), - limit: Number(limit), - pages: Math.ceil(total / limit) - }); + // Уже не нужна инициализация, она производится через authAPI } catch (error) { - res.status(500).json({ error: error.message }); + console.error('Error initializing companies:', error); } -}); +}; -// Получить компанию по ID -router.get('/:id', async (req, res) => { +initializeCompanies(); + +// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id +router.get('/my/info', verifyToken, async (req, res) => { try { - const company = await Company.findById(req.params.id).populate('ownerId', 'firstName lastName email'); + const userId = req.userId; + const user = await require('../models/User').findById(userId); + + if (!user || !user.companyId) { + return res.status(404).json({ error: 'Company not found' }); + } + + const company = await Company.findById(user.companyId); if (!company) { return res.status(404).json({ error: 'Company not found' }); } - res.json(company); + res.json({ + ...company.toObject(), + id: company._id + }); } catch (error) { + console.error('Get my company error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// GET /my/stats - получить статистику компании - ДОЛЖНО быть ПЕРЕД /:id +router.get('/my/stats', verifyToken, async (req, res) => { + try { + const userId = req.userId; + const user = await require('../models/User').findById(userId); + + if (!user || !user.companyId) { + return res.status(404).json({ error: 'Company not found' }); + } + + const stats = { + profileViews: Math.floor(Math.random() * 1000), + profileViewsChange: Math.floor(Math.random() * 20 - 10), + sentRequests: Math.floor(Math.random() * 50), + sentRequestsChange: Math.floor(Math.random() * 10 - 5), + receivedRequests: Math.floor(Math.random() * 30), + receivedRequestsChange: Math.floor(Math.random() * 5 - 2), + newMessages: Math.floor(Math.random() * 10), + rating: Math.random() * 5 + }; + + res.json(stats); + } catch (error) { + console.error('Get company stats error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Experience endpoints ДОЛЖНЫ быть ДО получения компании по ID +let companyExperience = []; + +// GET /:id/experience - получить опыт компании +router.get('/:id/experience', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const experience = companyExperience.filter(e => e.companyId === id); + res.json(experience); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /:id/experience - добавить опыт компании +router.post('/:id/experience', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { confirmed, customer, subject, volume, contact, comment } = req.body; + + const expId = Math.random().toString(36).substr(2, 9); + const newExp = { + id: expId, + _id: expId, + companyId: id, + confirmed: confirmed || false, + customer: customer || '', + subject: subject || '', + volume: volume || '', + contact: contact || '', + comment: comment || '', + createdAt: new Date(), + updatedAt: new Date() + }; + + companyExperience.push(newExp); + res.status(201).json(newExp); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PUT /:id/experience/:expId - обновить опыт +router.put('/:id/experience/:expId', verifyToken, async (req, res) => { + try { + const { id, expId } = req.params; + const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id); + + if (index === -1) { + return res.status(404).json({ error: 'Experience not found' }); + } + + companyExperience[index] = { + ...companyExperience[index], + ...req.body, + updatedAt: new Date() + }; + + res.json(companyExperience[index]); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /:id/experience/:expId - удалить опыт +router.delete('/:id/experience/:expId', verifyToken, async (req, res) => { + try { + const { id, expId } = req.params; + const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id); + + if (index === -1) { + return res.status(404).json({ error: 'Experience not found' }); + } + + companyExperience.splice(index, 1); + res.json({ message: 'Experience deleted' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить компанию по ID (ДОЛЖНО быть ПОСЛЕ специфичных маршрутов) +router.get('/:id', async (req, res) => { + try { + const company = await Company.findById(req.params.id); + + if (!company) { + return res.status(404).json({ error: 'Company not found' }); + } + + res.json({ + ...company.toObject(), + id: company._id + }); + } catch (error) { + console.error('Get company error:', error); res.status(500).json({ error: error.message }); } }); @@ -67,7 +181,10 @@ const updateCompanyHandler = async (req, res) => { return res.status(404).json({ error: 'Company not found' }); } - res.json(company); + res.json({ + ...company.toObject(), + id: company._id + }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -85,19 +202,26 @@ router.post('/ai-search', async (req, res) => { return res.status(400).json({ error: 'Query required' }); } - // Простой поиск по текстовым полям - const companies = await Company.find({ - $text: { $search: query } - }).limit(10); + const q = query.toLowerCase(); + const result = await Company.find({ + $or: [ + { fullName: { $regex: q, $options: 'i' } }, + { shortName: { $regex: q, $options: 'i' } }, + { industry: { $regex: q, $options: 'i' } } + ] + }); res.json({ - companies, - total: companies.length, - aiSuggestion: `Found ${companies.length} companies matching "${query}"` + companies: result.map(c => ({ + ...c.toObject(), + id: c._id + })), + total: result.length, + aiSuggestion: `Found ${result.length} companies matching "${query}"` }); } catch (error) { res.status(500).json({ error: error.message }); } }); -module.exports = router +module.exports = router; diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js index 5ca6d47..fa224d4 100644 --- a/server/routers/procurement/routes/experience.js +++ b/server/routers/procurement/routes/experience.js @@ -1,6 +1,6 @@ -const express = require('express') -const router = express.Router() -const { verifyToken } = require('../middleware/auth') +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); // In-memory хранилище для опыта работы (mock) let experiences = []; @@ -110,5 +110,5 @@ router.delete('/:id', verifyToken, (req, res) => { } }); -module.exports = router +module.exports = router; diff --git a/server/routers/procurement/routes/home.js b/server/routers/procurement/routes/home.js new file mode 100644 index 0000000..eabc4f4 --- /dev/null +++ b/server/routers/procurement/routes/home.js @@ -0,0 +1,48 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); + +// Получить агрегированные данные для главной страницы +router.get('/aggregates', verifyToken, async (req, res) => { + try { + res.json({ + docsCount: 0, + acceptsCount: 0, + requestsCount: 0 + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить статистику компании +router.get('/stats', verifyToken, async (req, res) => { + try { + res.json({ + profileViews: 12, + profileViewsChange: 5, + sentRequests: 3, + sentRequestsChange: 1, + receivedRequests: 7, + receivedRequestsChange: 2, + newMessages: 4, + rating: 4.5 + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Получить рекомендации партнеров (AI) +router.get('/recommendations', verifyToken, async (req, res) => { + try { + res.json({ + recommendations: [], + message: 'No recommendations available yet' + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index 6b52eca..9de6796 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -1,110 +1,99 @@ -const express = require('express') -const router = express.Router() -const Message = require('../models/Message') -const { verifyToken } = require('../middleware/auth') +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); -// Mock данные для тредов -const mockThreads = [ - { - id: 'thread-1', - lastMessage: 'Добрый день! Интересует поставка металлопроката.', - lastMessageAt: new Date(Date.now() - 3600000).toISOString(), - participants: ['company-123', 'company-1'] - }, - { - id: 'thread-2', - lastMessage: 'Можем предложить скидку 15% на оптовую партию.', - lastMessageAt: new Date(Date.now() - 7200000).toISOString(), - participants: ['company-123', 'company-2'] - }, - { - id: 'thread-3', - lastMessage: 'Спасибо за предложение, рассмотрим.', - lastMessageAt: new Date(Date.now() - 86400000).toISOString(), - participants: ['company-123', 'company-4'] - } -]; +// In-memory storage +let messages = []; -// Mock данные для сообщений -const mockMessages = { - 'thread-1': [ - { id: 'msg-1', senderCompanyId: 'company-1', text: 'Добрый день! Интересует поставка металлопроката.', timestamp: new Date(Date.now() - 3600000).toISOString() }, - { id: 'msg-2', senderCompanyId: 'company-123', text: 'Здравствуйте! Какой объем вас интересует?', timestamp: new Date(Date.now() - 3500000).toISOString() } - ], - 'thread-2': [ - { id: 'msg-3', senderCompanyId: 'company-2', text: 'Можем предложить скидку 15% на оптовую партию.', timestamp: new Date(Date.now() - 7200000).toISOString() } - ], - 'thread-3': [ - { id: 'msg-4', senderCompanyId: 'company-4', text: 'Спасибо за предложение, рассмотрим.', timestamp: new Date(Date.now() - 86400000).toISOString() } - ] -}; - -// Получить все потоки для компании +// GET /messages/threads - получить все потоки для компании router.get('/threads', verifyToken, async (req, res) => { try { - // Попытка получить из MongoDB - try { - const threads = await Message.aggregate([ - { - $match: { - $or: [ - { senderCompanyId: req.user.companyId }, - { recipientCompanyId: req.user.companyId } - ] - } - }, - { - $sort: { timestamp: -1 } - }, - { - $group: { - _id: '$threadId', - lastMessage: { $first: '$text' }, - lastMessageAt: { $first: '$timestamp' } - } + const companyId = req.user.companyId; + + // Группировка сообщений по threadId + const threads = {}; + + messages.forEach(msg => { + if (msg.senderCompanyId === companyId || msg.recipientCompanyId === companyId) { + if (!threads[msg.threadId]) { + threads[msg.threadId] = msg; } - ]); - - if (threads && threads.length > 0) { - return res.json(threads); } - } catch (dbError) { - console.log('MongoDB unavailable, using mock data'); - } + }); - // Fallback на mock данные - res.json(mockThreads); + // Преобразование в массив и сортировка по времени + const threadsArray = Object.values(threads) + .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + + console.log('[Messages] Returned', threadsArray.length, 'threads for company', companyId); + + res.json(threadsArray); } catch (error) { + console.error('[Messages] Error:', error.message); res.status(500).json({ error: error.message }); } }); -// Получить сообщения потока -router.get('/threads/:threadId', verifyToken, async (req, res) => { +// GET /messages/:threadId - получить сообщения потока +router.get('/:threadId', verifyToken, async (req, res) => { try { - // Попытка получить из MongoDB - try { - const messages = await Message.find({ threadId: req.params.threadId }) - .sort({ timestamp: 1 }) - .populate('senderCompanyId', 'shortName fullName') - .populate('recipientCompanyId', 'shortName fullName'); + const { threadId } = req.params; - if (messages && messages.length > 0) { - return res.json(messages); - } - } catch (dbError) { - console.log('MongoDB unavailable, using mock data'); - } + const threadMessages = messages + .filter(msg => msg.threadId === threadId) + .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + + console.log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId); - // Fallback на mock данные - const threadMessages = mockMessages[req.params.threadId] || []; res.json(threadMessages); } catch (error) { + console.error('[Messages] Error:', error.message); res.status(500).json({ error: error.message }); } }); -// Отправить сообщение +// POST /messages/:threadId - добавить сообщение в поток +router.post('/:threadId', verifyToken, async (req, res) => { + try { + const { threadId } = req.params; + const { text, senderCompanyId } = req.body; + + if (!text || !threadId) { + return res.status(400).json({ error: 'Text and threadId required' }); + } + + // Определить получателя на основе threadId + const threadParts = threadId.split('-'); + let recipientCompanyId = null; + + if (threadParts.length >= 3) { + const companyId1 = threadParts[1]; + const companyId2 = threadParts[2]; + const currentSender = senderCompanyId || req.user.companyId; + recipientCompanyId = currentSender === companyId1 ? companyId2 : companyId1; + } + + const message = { + _id: 'msg-' + Date.now(), + threadId, + senderCompanyId: senderCompanyId || req.user.companyId, + recipientCompanyId, + text: text.trim(), + timestamp: new Date() + }; + + messages.push(message); + + console.log('[Messages] New message created:', message._id); + + res.status(201).json(message); + } catch (error) { + console.error('[Messages] Error:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// POST /messages - создать сообщение (старый endpoint для совместимости) router.post('/', verifyToken, async (req, res) => { try { const { threadId, text, recipientCompanyId } = req.body; @@ -113,18 +102,24 @@ router.post('/', verifyToken, async (req, res) => { return res.status(400).json({ error: 'Text and threadId required' }); } - const message = await Message.create({ + const message = { + _id: 'msg-' + Date.now(), threadId, senderCompanyId: req.user.companyId, recipientCompanyId, - text, + text: text.trim(), timestamp: new Date() - }); + }; + + messages.push(message); + + console.log('[Messages] New message created:', message._id); res.status(201).json(message); } catch (error) { + console.error('[Messages] Error:', error.message); res.status(500).json({ error: error.message }); } }); -module.exports = router +module.exports = router; diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js index 03f7a29..858d575 100644 --- a/server/routers/procurement/routes/products.js +++ b/server/routers/procurement/routes/products.js @@ -1,130 +1,164 @@ -const express = require('express') -const router = express.Router() -const { verifyToken } = require('../middleware/auth') +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const Product = require('../models/Product'); -// In-memory хранилище для продуктов/услуг (mock) -let products = []; +// Helper to transform _id to id +const transformProduct = (doc) => { + if (!doc) return null; + const obj = doc.toObject ? doc.toObject() : doc; + return { + ...obj, + id: obj._id, + _id: undefined + }; +}; -// GET /products - Получить список продуктов/услуг компании -router.get('/', verifyToken, (req, res) => { +// GET /products - Получить список продуктов/услуг компании (текущего пользователя) +router.get('/', verifyToken, async (req, res) => { try { - const { companyId } = req.query; - - if (!companyId) { - return res.status(400).json({ error: 'companyId is required' }); - } + const companyId = req.user.companyId; - const companyProducts = products.filter(p => p.companyId === companyId); - res.json(companyProducts); + console.log('[Products] GET Fetching products for companyId:', companyId); + + const products = await Product.find({ companyId }) + .sort({ createdAt: -1 }) + .exec(); + + console.log('[Products] Found', products.length, 'products'); + res.json(products.map(transformProduct)); } catch (error) { - console.error('Get products error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('[Products] Get error:', error.message); + res.status(500).json({ error: 'Internal server error', message: error.message }); } }); // POST /products - Создать продукт/услугу -router.post('/', verifyToken, (req, res) => { +router.post('/', verifyToken, async (req, res) => { try { - const { companyId, name, category, description, price, unit } = req.body; + const { name, category, description, type, productUrl, price, unit, minOrder } = req.body; + const companyId = req.user.companyId; - if (!companyId || !name) { - return res.status(400).json({ error: 'companyId and name are required' }); + console.log('[Products] POST Creating product:', { name, category, type }); + + // Валидация + if (!name || !category || !description || !type) { + return res.status(400).json({ error: 'name, category, description, and type are required' }); } - const newProduct = { - id: `prod-${Date.now()}`, + if (description.length < 20) { + return res.status(400).json({ error: 'Description must be at least 20 characters' }); + } + + const newProduct = new Product({ + name: name.trim(), + category: category.trim(), + description: description.trim(), + type, + productUrl: productUrl || '', companyId, - name, - category: category || 'other', - description: description || '', price: price || '', unit: unit || '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; + minOrder: minOrder || '' + }); - products.push(newProduct); + const savedProduct = await newProduct.save(); + console.log('[Products] Product created with ID:', savedProduct._id); - res.status(201).json(newProduct); + res.status(201).json(transformProduct(savedProduct)); } catch (error) { - console.error('Create product error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('[Products] Create error:', error.message); + res.status(500).json({ error: 'Internal server error', message: error.message }); } }); // PUT /products/:id - Обновить продукт/услугу -router.put('/:id', verifyToken, (req, res) => { +router.put('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; const updates = req.body; + const companyId = req.user.companyId; - const index = products.findIndex(p => p.id === id); + const product = await Product.findById(id); - if (index === -1) { + if (!product) { return res.status(404).json({ error: 'Product not found' }); } - const updatedProduct = { - ...products[index], - ...updates, - updatedAt: new Date().toISOString() - }; + // Проверить, что продукт принадлежит текущей компании + if (product.companyId !== companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } - products[index] = updatedProduct; + const updatedProduct = await Product.findByIdAndUpdate( + id, + { ...updates, updatedAt: new Date() }, + { new: true, runValidators: true } + ); - res.json(updatedProduct); + console.log('[Products] Product updated:', id); + res.json(transformProduct(updatedProduct)); } catch (error) { - console.error('Update product error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('[Products] Update error:', error.message); + res.status(500).json({ error: 'Internal server error', message: error.message }); } }); // PATCH /products/:id - Частичное обновление продукта/услуги -router.patch('/:id', verifyToken, (req, res) => { +router.patch('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; const updates = req.body; + const companyId = req.user.companyId; - const index = products.findIndex(p => p.id === id); + const product = await Product.findById(id); - if (index === -1) { + if (!product) { return res.status(404).json({ error: 'Product not found' }); } - const updatedProduct = { - ...products[index], - ...updates, - updatedAt: new Date().toISOString() - }; + if (product.companyId !== companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } - products[index] = updatedProduct; + const updatedProduct = await Product.findByIdAndUpdate( + id, + { ...updates, updatedAt: new Date() }, + { new: true, runValidators: true } + ); - res.json(updatedProduct); + console.log('[Products] Product patched:', id); + res.json(transformProduct(updatedProduct)); } catch (error) { - console.error('Patch product error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('[Products] Patch error:', error.message); + res.status(500).json({ error: 'Internal server error', message: error.message }); } }); // DELETE /products/:id - Удалить продукт/услугу -router.delete('/:id', verifyToken, (req, res) => { +router.delete('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; + const companyId = req.user.companyId; - const index = products.findIndex(p => p.id === id); + const product = await Product.findById(id); - if (index === -1) { + if (!product) { return res.status(404).json({ error: 'Product not found' }); } - products.splice(index, 1); + if (product.companyId !== companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + await Product.findByIdAndDelete(id); + + console.log('[Products] Product deleted:', id); res.json({ message: 'Product deleted successfully' }); } catch (error) { - console.error('Delete product error:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error('[Products] Delete error:', error.message); + res.status(500).json({ error: 'Internal server error', message: error.message }); } }); -module.exports = router - +module.exports = router; diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js new file mode 100644 index 0000000..05462db --- /dev/null +++ b/server/routers/procurement/routes/reviews.js @@ -0,0 +1,88 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); + +// In-memory storage for reviews +let reviews = []; + +// Reference to companies from search routes +let companies = []; + +// Синхронизация с companies из других routes +const syncCompanies = () => { + // После создания review обновляем рейтинг компании +}; + +// GET /reviews/company/:companyId - получить отзывы компании +router.get('/company/:companyId', verifyToken, async (req, res) => { + try { + const { companyId } = req.params; + + const companyReviews = reviews + .filter(r => r.companyId === companyId) + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + + console.log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId); + + res.json(companyReviews); + } catch (error) { + console.error('[Reviews] Error:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// POST /reviews - создать новый отзыв +router.post('/', verifyToken, async (req, res) => { + try { + const { companyId, rating, comment } = req.body; + + if (!companyId || !rating || !comment) { + return res.status(400).json({ + error: 'companyId, rating, and comment are required', + }); + } + + if (rating < 1 || rating > 5) { + return res.status(400).json({ + error: 'Rating must be between 1 and 5', + }); + } + + if (comment.length < 10 || comment.length > 1000) { + return res.status(400).json({ + error: 'Comment must be between 10 and 1000 characters', + }); + } + + // Создать новый отзыв + const newReview = { + _id: 'review-' + Date.now(), + companyId, + authorCompanyId: req.user.companyId, + authorName: req.user.firstName + ' ' + req.user.lastName, + authorCompany: req.user.companyName || 'Company', + rating: parseInt(rating), + comment: comment.trim(), + verified: true, + createdAt: new Date(), + updatedAt: new Date() + }; + + reviews.push(newReview); + + console.log('[Reviews] New review created:', newReview._id); + + res.status(201).json(newReview); + } catch (error) { + console.error('[Reviews] Error:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +module.exports = router; diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index d64002b..d00eed3 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -3,7 +3,44 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Company = require('../models/Company'); -// GET /search - Поиск компаний (с использованием MongoDB) +// GET /search/recommendations - получить рекомендации компаний (ДОЛЖЕН быть ПЕРЕД /*) +router.get('/recommendations', verifyToken, async (req, res) => { + try { + // Получить компанию пользователя, чтобы исключить её из результатов + const User = require('../models/User'); + const user = await User.findById(req.userId); + + let filter = {}; + if (user && user.companyId) { + filter._id = { $ne: user.companyId }; + } + + const companies = await Company.find(filter) + .sort({ rating: -1 }) + .limit(5); + + const recommendations = companies.map(company => ({ + id: company._id.toString(), + name: company.fullName || company.shortName, + industry: company.industry, + logo: company.logo, + matchScore: Math.floor(Math.random() * 30 + 70), // 70-100 + reason: 'Matches your search criteria' + })); + + console.log('[Search] Returned recommendations:', recommendations.length); + + res.json(recommendations); + } catch (error) { + console.error('[Search] Recommendations error:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message + }); + } +}); + +// GET /search - Поиск компаний router.get('/', verifyToken, async (req, res) => { try { const { @@ -17,66 +54,79 @@ router.get('/', verifyToken, async (req, res) => { sortOrder = 'desc' } = req.query; - // Построение query для MongoDB - let mongoQuery = {}; + // Получить компанию пользователя, чтобы исключить её из результатов + const User = require('../models/User'); + const user = await User.findById(req.userId); + + // Начальный фильтр: исключить собственную компанию + let filters = []; + + if (user && user.companyId) { + filters.push({ _id: { $ne: user.companyId } }); + } // Текстовый поиск if (query && query.trim()) { - mongoQuery.$or = [ - { fullName: { $regex: query, $options: 'i' } }, - { shortName: { $regex: query, $options: 'i' } }, - { slogan: { $regex: query, $options: 'i' } }, - { industry: { $regex: query, $options: 'i' } } - ]; + const q = query.toLowerCase(); + filters.push({ + $or: [ + { fullName: { $regex: q, $options: 'i' } }, + { shortName: { $regex: q, $options: 'i' } }, + { slogan: { $regex: q, $options: 'i' } }, + { industry: { $regex: q, $options: 'i' } } + ] + }); } // Фильтр по рейтингу if (minRating) { const rating = parseFloat(minRating); if (rating > 0) { - mongoQuery.rating = { $gte: rating }; + filters.push({ rating: { $gte: rating } }); } } // Фильтр по отзывам if (hasReviews === 'true') { - mongoQuery.verified = true; + filters.push({ verified: true }); } // Фильтр по акцептам if (hasAcceptedDocs === 'true') { - mongoQuery.verified = true; + filters.push({ verified: true }); } + // Комбинировать все фильтры + let filter = filters.length > 0 ? { $and: filters } : {}; + // Пагинация const pageNum = parseInt(page) || 1; const limitNum = parseInt(limit) || 10; const skip = (pageNum - 1) * limitNum; // Сортировка - let sortObj = { rating: -1 }; + let sortOptions = {}; if (sortBy === 'name') { - sortObj = { fullName: 1 }; - } - if (sortOrder === 'asc') { - Object.keys(sortObj).forEach(key => { - sortObj[key] = sortObj[key] === -1 ? 1 : -1; - }); + sortOptions.fullName = sortOrder === 'asc' ? 1 : -1; + } else { + sortOptions.rating = sortOrder === 'asc' ? 1 : -1; } - // Запрос к MongoDB - const companies = await Company.find(mongoQuery) - .limit(limitNum) + const total = await Company.countDocuments(filter); + const companies = await Company.find(filter) + .sort(sortOptions) .skip(skip) - .sort(sortObj) - .lean(); + .limit(limitNum); - const total = await Company.countDocuments(mongoQuery); + const paginatedResults = companies.map(c => ({ + ...c.toObject(), + id: c._id + })); - console.log('[Search] Returned', companies.length, 'companies'); + console.log('[Search] Returned', paginatedResults.length, 'companies'); res.json({ - companies, + companies: paginatedResults, total, page: pageNum, totalPages: Math.ceil(total / limitNum) diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 9e4ea96..09cd954 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,38 +1,38 @@ -const mongoose = require('mongoose') -require('dotenv').config() +const mongoose = require('mongoose'); +require('dotenv').config(); // Импорт моделей -const User = require('../models/User') -const Company = require('../models/Company') +const User = require('../models/User'); +const Company = require('../models/Company'); const recreateTestUser = async () => { try { - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db' + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - console.log('\n🔄 Подключение к MongoDB...') + console.log('\n🔄 Подключение к MongoDB...'); await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }) - console.log('✅ Подключено к MongoDB\n') + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('✅ Подключено к MongoDB\n'); // Удалить старого тестового пользователя - console.log('🗑️ Удаление старого тестового пользователя...') - const oldUser = await User.findOne({ email: 'admin@test-company.ru' }) + console.log('🗑️ Удаление старого тестового пользователя...'); + const oldUser = await User.findOne({ email: 'admin@test-company.ru' }); if (oldUser) { // Удалить связанную компанию if (oldUser.companyId) { - await Company.findByIdAndDelete(oldUser.companyId) - console.log(' ✓ Старая компания удалена') + await Company.findByIdAndDelete(oldUser.companyId); + console.log(' ✓ Старая компания удалена'); } - await User.findByIdAndDelete(oldUser._id) - console.log(' ✓ Старый пользователь удален') + await User.findByIdAndDelete(oldUser._id); + console.log(' ✓ Старый пользователь удален'); } else { - console.log(' ℹ️ Старый пользователь не найден') + console.log(' ℹ️ Старый пользователь не найден'); } // Создать новую компанию с правильной кодировкой UTF-8 - console.log('\n🏢 Создание тестовой компании...') + console.log('\n🏢 Создание тестовой компании...'); const company = await Company.create({ fullName: 'ООО "Тестовая Компания"', inn: '1234567890', @@ -46,11 +46,11 @@ const recreateTestUser = async () => { rating: 4.5, reviewsCount: 10, dealsCount: 25, - }) - console.log(' ✓ Компания создана:', company.fullName) + }); + console.log(' ✓ Компания создана:', company.fullName); // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('\n👤 Создание тестового пользователя...') + console.log('\n👤 Создание тестового пользователя...'); const user = await User.create({ email: 'admin@test-company.ru', password: 'SecurePass123!', @@ -59,32 +59,32 @@ const recreateTestUser = async () => { position: 'Директор', phone: '+7 (999) 123-45-67', companyId: company._id, - }) - console.log(' ✓ Пользователь создан:', user.firstName, user.lastName) + }); + console.log(' ✓ Пользователь создан:', user.firstName, user.lastName); // Проверка что данные сохранены правильно - console.log('\n✅ Проверка данных:') - console.log(' Email:', user.email) - console.log(' Имя:', user.firstName) - console.log(' Фамилия:', user.lastName) - console.log(' Компания:', company.fullName) - console.log(' Должность:', user.position) + console.log('\n✅ Проверка данных:'); + console.log(' Email:', user.email); + console.log(' Имя:', user.firstName); + console.log(' Фамилия:', user.lastName); + console.log(' Компания:', company.fullName); + console.log(' Должность:', user.position); - console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8') - console.log('\n📋 Данные для входа:') - console.log(' Email: admin@test-company.ru') - console.log(' Пароль: SecurePass123!') - console.log('') + console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8'); + console.log('\n📋 Данные для входа:'); + console.log(' Email: admin@test-company.ru'); + console.log(' Пароль: SecurePass123!'); + console.log(''); - await mongoose.connection.close() - process.exit(0) + await mongoose.connection.close(); + process.exit(0); } catch (error) { - console.error('\n❌ Ошибка:', error.message) - console.error(error) - process.exit(1) + console.error('\n❌ Ошибка:', error.message); + console.error(error); + process.exit(1); } -} +}; // Запуск -recreateTestUser() +recreateTestUser(); diff --git a/server/utils/const.ts b/server/utils/const.ts index 5d19080..3ab73e4 100644 --- a/server/utils/const.ts +++ b/server/utils/const.ts @@ -1,4 +1,4 @@ import 'dotenv/config'; // Connection URL -export const mongoUrl = process.env.MONGO_ADDR +export const mongoUrl = process.env.MONGO_ADDR || 'mongodb://localhost:27017' From 6c190b80fbd8ffc576e04300c6d8c85efaf11782 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Mon, 27 Oct 2025 18:58:38 +0300 Subject: [PATCH 125/147] add new back --- server/routers/procurement/config/db.js | 12 +- server/routers/procurement/index.js | 10 + server/routers/procurement/middleware/auth.js | 18 +- .../routers/procurement/models/BuyProduct.js | 10 + server/routers/procurement/models/Request.js | 62 +++++ server/routers/procurement/routes/auth.js | 78 ++++-- server/routers/procurement/routes/buy.js | 7 +- .../routers/procurement/routes/buyProducts.js | 168 +++++++++++- server/routers/procurement/routes/messages.js | 249 ++++++++++++++---- server/routers/procurement/routes/products.js | 49 ++-- server/routers/procurement/routes/requests.js | 171 ++++++++++++ server/routers/procurement/routes/reviews.js | 44 ++-- server/routers/procurement/routes/search.js | 92 ++++++- .../procurement/scripts/migrate-messages.js | 93 +++++++ .../procurement/scripts/recreate-test-user.js | 19 +- .../procurement/scripts/test-logging.js | 61 +++++ 16 files changed, 996 insertions(+), 147 deletions(-) create mode 100644 server/routers/procurement/models/Request.js create mode 100644 server/routers/procurement/routes/requests.js create mode 100644 server/routers/procurement/scripts/migrate-messages.js create mode 100644 server/routers/procurement/scripts/test-logging.js diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js index 3d03054..31e7ae4 100644 --- a/server/routers/procurement/config/db.js +++ b/server/routers/procurement/config/db.js @@ -7,22 +7,24 @@ const connectDB = async () => { console.log('\n📡 Попытка подключения к MongoDB...'); console.log(` URI: ${mongoUri}`); - await mongoose.connect(mongoUri, { + const connection = await mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, serverSelectionTimeoutMS: 5000, connectTimeoutMS: 5000, }); console.log('✅ MongoDB подключена успешно!'); - console.log(` Хост: ${mongoose.connection.host}`); - console.log(` БД: ${mongoose.connection.name}\n`); + console.log(` Хост: ${connection.connection.host}`); + console.log(` БД: ${connection.connection.name}\n`); - return true; + return connection; } catch (error) { console.error('\n❌ Ошибка подключения к MongoDB:'); console.error(` ${error.message}\n`); console.warn('⚠️ Приложение продолжит работу с mock данными\n'); - return false; + return null; } }; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 289b809..e08a238 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -6,6 +6,14 @@ const connectDB = require('./config/db'); // Загрузить переменные окружения dotenv.config(); +// Включить логирование при разработке: установите DEV=true в .env или при запуске +// export DEV=true && npm start (для Linux/Mac) +// set DEV=true && npm start (для Windows) +// По умолчанию логи отключены. Все console.log функции отключаются если DEV !== 'true' +if (process.env.DEV === 'true') { + console.log('ℹ️ DEBUG MODE ENABLED - All logs are visible'); +} + // Импортировать маршруты const authRoutes = require('./routes/auth'); const companiesRoutes = require('./routes/companies'); @@ -16,6 +24,7 @@ const experienceRoutes = require('./routes/experience'); const productsRoutes = require('./routes/products'); const reviewsRoutes = require('./routes/reviews'); const buyProductsRoutes = require('./routes/buyProducts'); +const requestsRoutes = require('./routes/requests'); const homeRoutes = require('./routes/home'); const app = express(); @@ -73,6 +82,7 @@ app.use('/buy-products', buyProductsRoutes); app.use('/experience', experienceRoutes); app.use('/products', productsRoutes); app.use('/reviews', reviewsRoutes); +app.use('/requests', requestsRoutes); app.use('/home', homeRoutes); // Обработка ошибок diff --git a/server/routers/procurement/middleware/auth.js b/server/routers/procurement/middleware/auth.js index ba36cd6..da6b4c1 100644 --- a/server/routers/procurement/middleware/auth.js +++ b/server/routers/procurement/middleware/auth.js @@ -1,5 +1,15 @@ const jwt = require('jsonwebtoken'); +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + const verifyToken = (req, res, next) => { const token = req.headers.authorization?.replace('Bearer ', ''); @@ -12,7 +22,7 @@ const verifyToken = (req, res, next) => { req.userId = decoded.userId; req.companyId = decoded.companyId; req.user = decoded; - console.log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId); + log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId); next(); } catch (error) { console.error('[Auth] Token verification failed:', error.message); @@ -20,10 +30,10 @@ const verifyToken = (req, res, next) => { } }; -const generateToken = (userId, companyId) => { - console.log('[Auth] Generating token for userId:', userId, 'companyId:', companyId); +const generateToken = (userId, companyId, firstName = '', lastName = '', companyName = '') => { + log('[Auth] Generating token for userId:', userId, 'companyId:', companyId); return jwt.sign( - { userId, companyId }, + { userId, companyId, firstName, lastName, companyName }, process.env.JWT_SECRET || 'your-secret-key', { expiresIn: '7d' } ); diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js index 346623d..5396ebd 100644 --- a/server/routers/procurement/models/BuyProduct.js +++ b/server/routers/procurement/models/BuyProduct.js @@ -35,6 +35,16 @@ const buyProductSchema = new mongoose.Schema({ default: Date.now } }], + acceptedBy: [{ + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company' + }, + acceptedAt: { + type: Date, + default: Date.now + } + }], status: { type: String, enum: ['draft', 'published'], diff --git a/server/routers/procurement/models/Request.js b/server/routers/procurement/models/Request.js new file mode 100644 index 0000000..6a14fab --- /dev/null +++ b/server/routers/procurement/models/Request.js @@ -0,0 +1,62 @@ +const mongoose = require('mongoose'); + +const requestSchema = new mongoose.Schema({ + senderCompanyId: { + type: String, + required: true, + index: true + }, + recipientCompanyId: { + type: String, + required: true, + index: true + }, + text: { + type: String, + required: true + }, + files: [{ + id: String, + name: String, + url: String, + type: String, + size: Number, + uploadedAt: { + type: Date, + default: Date.now + } + }], + productId: { + type: String, + ref: 'BuyProduct' + }, + status: { + type: String, + enum: ['pending', 'accepted', 'rejected'], + default: 'pending' + }, + response: { + type: String, + default: null + }, + respondedAt: { + type: Date, + default: null + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +requestSchema.index({ senderCompanyId: 1, createdAt: -1 }); +requestSchema.index({ recipientCompanyId: 1, createdAt: -1 }); +requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 }); + +module.exports = mongoose.model('Request', requestSchema); diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 1ac6b62..9f55a1f 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -4,6 +4,17 @@ const { generateToken } = require('../middleware/auth'); const User = require('../models/User'); const Company = require('../models/Company'); +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // In-memory storage для логирования let users = []; @@ -21,6 +32,7 @@ const initializeTestUser = async () => { legalForm: 'ООО', industry: 'Производство', companySize: '50-100', + partnerGeography: ['moscow', 'russia_all'], website: 'https://test-company.ru', verified: true, rating: 4.5, @@ -38,7 +50,7 @@ const initializeTestUser = async () => { companyId: company._id }); - console.log('✅ Test user initialized'); + log('✅ Test user initialized'); } // Инициализация других тестовых компаний @@ -50,7 +62,8 @@ const initializeTestUser = async () => { ogrn: '1027700132196', legalForm: 'ООО', industry: 'Строительство', - companySize: '100-250', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://stroykomplekt.ru', verified: true, rating: 4.8, @@ -65,6 +78,7 @@ const initializeTestUser = async () => { legalForm: 'АО', industry: 'Строительство', companySize: '500+', + partnerGeography: ['moscow', 'russia_all'], website: 'https://moscow-stroy.ru', verified: true, rating: 4.9, @@ -78,7 +92,8 @@ const initializeTestUser = async () => { ogrn: '1027700132198', legalForm: 'ООО', industry: 'IT', - companySize: '50-100', + companySize: '11-50', + partnerGeography: ['moscow', 'russia_all'], website: 'https://techproject.ru', verified: true, rating: 4.6, @@ -91,8 +106,9 @@ const initializeTestUser = async () => { inn: '7707083897', ogrn: '1027700132199', legalForm: 'ООО', - industry: 'Торговля', - companySize: '100-250', + industry: 'Оптовая торговля', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://torgpartner.ru', verified: true, rating: 4.3, @@ -106,7 +122,8 @@ const initializeTestUser = async () => { ogrn: '1027700132200', legalForm: 'ООО', industry: 'Энергетика', - companySize: '250-500', + companySize: '251-500', + partnerGeography: ['moscow', 'russia_all'], website: 'https://energoplus.ru', verified: true, rating: 4.7, @@ -119,7 +136,7 @@ const initializeTestUser = async () => { const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); if (!existingCompany) { await Company.create(mockCompanyData); - console.log(`✅ Mock company created: ${mockCompanyData.fullName}`); + log(`✅ Mock company created: ${mockCompanyData.fullName}`); } } } catch (error) { @@ -160,11 +177,12 @@ router.post('/register', async (req, res) => { verified: false, rating: 0, description: '', - slogan: '' + slogan: '', + partnerGeography: ['moscow', 'russia_all'] }); const savedCompany = await company.save(); company = savedCompany; - console.log('✅ Company saved:', company._id, 'Result:', savedCompany ? 'Success' : 'Failed'); + log('✅ Company saved:', company._id, 'Result:', savedCompany ? 'Success' : 'Failed'); } catch (err) { console.error('Company save error:', err); return res.status(400).json({ error: 'Failed to create company: ' + err.message }); @@ -182,9 +200,9 @@ router.post('/register', async (req, res) => { companyId: company._id }); - console.log('✅ User created:', newUser._id); + log('✅ User created:', newUser._id); - const token = generateToken(newUser._id.toString(), newUser.companyId.toString()); + const token = generateToken(newUser._id.toString(), newUser.companyId.toString(), newUser.firstName, newUser.lastName, company.fullName); return res.status(201).json({ tokens: { accessToken: token, @@ -242,7 +260,8 @@ router.post('/login', async (req, res) => { ogrn: '1027700132196', legalForm: 'ООО', industry: 'Строительство', - companySize: '100-250', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://stroykomplekt.ru', verified: true, rating: 4.8, @@ -257,6 +276,7 @@ router.post('/login', async (req, res) => { legalForm: 'АО', industry: 'Строительство', companySize: '500+', + partnerGeography: ['moscow', 'russia_all'], website: 'https://moscow-stroy.ru', verified: true, rating: 4.9, @@ -270,7 +290,8 @@ router.post('/login', async (req, res) => { ogrn: '1027700132198', legalForm: 'ООО', industry: 'IT', - companySize: '50-100', + companySize: '11-50', + partnerGeography: ['moscow', 'russia_all'], website: 'https://techproject.ru', verified: true, rating: 4.6, @@ -283,8 +304,9 @@ router.post('/login', async (req, res) => { inn: '7707083897', ogrn: '1027700132199', legalForm: 'ООО', - industry: 'Торговля', - companySize: '100-250', + industry: 'Оптовая торговля', + companySize: '51-250', + partnerGeography: ['moscow', 'russia_all'], website: 'https://torgpartner.ru', verified: true, rating: 4.3, @@ -298,7 +320,8 @@ router.post('/login', async (req, res) => { ogrn: '1027700132200', legalForm: 'ООО', industry: 'Энергетика', - companySize: '250-500', + companySize: '251-500', + partnerGeography: ['moscow', 'russia_all'], website: 'https://energoplus.ru', verified: true, rating: 4.7, @@ -318,12 +341,17 @@ router.post('/login', async (req, res) => { } } - const token = generateToken(user._id.toString(), user.companyId.toString()); - console.log('✅ Token generated for user:', user._id); - - // Получить компанию - const company = await Company.findById(user.companyId); + // Получить компанию до использования в generateToken + let companyData = null; + try { + companyData = await Company.findById(user.companyId); + } catch (err) { + console.error('Failed to fetch company:', err.message); + } + const token = generateToken(user._id.toString(), user.companyId.toString(), user.firstName, user.lastName, companyData?.fullName || 'Company'); + log('✅ Token generated for user:', user._id); + res.json({ tokens: { accessToken: token, @@ -337,10 +365,10 @@ router.post('/login', async (req, res) => { position: user.position, companyId: user.companyId.toString() }, - company: company ? { - id: company._id.toString(), - name: company.fullName, - inn: company.inn + company: companyData ? { + id: companyData._id.toString(), + name: companyData.fullName, + inn: companyData.inn } : null }); } catch (error) { diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 8218ace..53dadb5 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -1,9 +1,10 @@ const express = require('express') const fs = require('fs') +const path = require('path') const router = express.Router() // Create remote-assets/docs directory if it doesn't exist -const docsDir = '../../remote-assets/docs' +const docsDir = path.join(__dirname, '../../remote-assets/docs') if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }) } @@ -47,7 +48,7 @@ router.post('/docs', (req, res) => { // Save file to disk try { const binaryData = Buffer.from(fileData, 'base64') - const filePath = `${docsDir}/${id}.${type}` + const filePath = path.join(docsDir, `${id}.${type}`) fs.writeFileSync(filePath, binaryData) console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) @@ -150,7 +151,7 @@ router.get('/docs/:id/file', (req, res) => { return res.status(404).json({ error: 'Document not found' }) } - const filePath = `${docsDir}/${id}.${doc.type}` + const filePath = path.join(docsDir, `${id}.${doc.type}`) if (!fs.existsSync(filePath)) { console.log('[BUY API] File not found on disk:', filePath) return res.status(404).json({ error: 'File not found on disk' }) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index 175b104..2aabc93 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -3,18 +3,29 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const BuyProduct = require('../models/BuyProduct'); +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // GET /buy-products/company/:companyId - получить товары компании router.get('/company/:companyId', verifyToken, async (req, res) => { try { const { companyId } = req.params; - console.log('[BuyProducts] Fetching products for company:', companyId); + log('[BuyProducts] Fetching products for company:', companyId); const products = await BuyProduct.find({ companyId }) .sort({ createdAt: -1 }) .exec(); - console.log('[BuyProducts] Found', products.length, 'products for company', companyId); - console.log('[BuyProducts] Products:', products); + log('[BuyProducts] Found', products.length, 'products for company', companyId); + log('[BuyProducts] Products:', products); res.json(products); } catch (error) { @@ -32,7 +43,7 @@ router.post('/', verifyToken, async (req, res) => { try { const { name, description, quantity, unit, status } = req.body; - console.log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId }); + log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId }); if (!name || !description || !quantity) { return res.status(400).json({ @@ -56,11 +67,11 @@ router.post('/', verifyToken, async (req, res) => { files: [], }); - console.log('[BuyProducts] Attempting to save product to DB...'); + log('[BuyProducts] Attempting to save product to DB...'); const savedProduct = await newProduct.save(); - console.log('[BuyProducts] New product created successfully:', savedProduct._id); - console.log('[BuyProducts] Product data:', savedProduct); + log('[BuyProducts] New product created successfully:', savedProduct._id); + log('[BuyProducts] Product data:', savedProduct); res.status(201).json(savedProduct); } catch (error) { @@ -100,7 +111,7 @@ router.put('/:id', verifyToken, async (req, res) => { const updatedProduct = await product.save(); - console.log('[BuyProducts] Product updated:', id); + log('[BuyProducts] Product updated:', id); res.json(updatedProduct); } catch (error) { @@ -129,7 +140,7 @@ router.delete('/:id', verifyToken, async (req, res) => { await BuyProduct.findByIdAndDelete(id); - console.log('[BuyProducts] Product deleted:', id); + log('[BuyProducts] Product deleted:', id); res.json({ message: 'Product deleted successfully' }); } catch (error) { @@ -141,4 +152,143 @@ router.delete('/:id', verifyToken, async (req, res) => { } }); +// POST /buy-products/:id/files - добавить файл к товару +router.post('/:id/files', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { fileName, fileUrl, fileType, fileSize } = req.body; + + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + // Только владелец товара может добавить файл + if (product.companyId.toString() !== req.user.companyId.toString()) { + return res.status(403).json({ error: 'Not authorized' }); + } + + const file = { + id: 'file-' + Date.now(), + name: fileName, + url: fileUrl, + type: fileType, + size: fileSize, + uploadedAt: new Date() + }; + + product.files.push(file); + await product.save(); + + log('[BuyProducts] File added to product:', id); + + res.json(product); + } catch (error) { + console.error('[BuyProducts] Error adding file:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// DELETE /buy-products/:id/files/:fileId - удалить файл +router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { + try { + const { id, fileId } = req.params; + + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + if (product.companyId.toString() !== req.user.companyId.toString()) { + return res.status(403).json({ error: 'Not authorized' }); + } + + product.files = product.files.filter(f => f.id !== fileId); + await product.save(); + + log('[BuyProducts] File deleted from product:', id); + + res.json(product); + } catch (error) { + console.error('[BuyProducts] Error deleting file:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// POST /buy-products/:id/accept - акцептировать товар +router.post('/:id/accept', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const companyId = req.user.companyId; + + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + // Не можем акцептировать собственный товар + if (product.companyId.toString() === companyId.toString()) { + return res.status(403).json({ error: 'Cannot accept own product' }); + } + + // Проверить, не акцептировал ли уже + const alreadyAccepted = product.acceptedBy.some( + a => a.companyId.toString() === companyId.toString() + ); + + if (alreadyAccepted) { + return res.status(400).json({ error: 'Already accepted' }); + } + + product.acceptedBy.push({ + companyId, + acceptedAt: new Date() + }); + + await product.save(); + + log('[BuyProducts] Product accepted by company:', companyId); + + res.json(product); + } catch (error) { + console.error('[BuyProducts] Error accepting product:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + +// GET /buy-products/:id/acceptances - получить компании которые акцептовали +router.get('/:id/acceptances', verifyToken, async (req, res) => { + try { + const { id } = req.params; + + const product = await BuyProduct.findById(id).populate('acceptedBy.companyId', 'shortName fullName'); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + log('[BuyProducts] Returned acceptances for product:', id); + + res.json(product.acceptedBy); + } catch (error) { + console.error('[BuyProducts] Error fetching acceptances:', error.message); + res.status(500).json({ + error: 'Internal server error', + message: error.message, + }); + } +}); + module.exports = router; diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index 9de6796..b60a055 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -1,35 +1,88 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); +const Message = require('../models/Message'); -// In-memory storage -let messages = []; +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; // GET /messages/threads - получить все потоки для компании router.get('/threads', verifyToken, async (req, res) => { try { const companyId = req.user.companyId; + const { ObjectId } = require('mongoose').Types; - // Группировка сообщений по threadId - const threads = {}; + log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId); + + // Преобразовать в ObjectId если это строка + let companyObjectId = companyId; + let companyIdString = companyId.toString ? companyId.toString() : companyId; - messages.forEach(msg => { - if (msg.senderCompanyId === companyId || msg.recipientCompanyId === companyId) { - if (!threads[msg.threadId]) { - threads[msg.threadId] = msg; - } + try { + if (typeof companyId === 'string' && ObjectId.isValid(companyId)) { + companyObjectId = new ObjectId(companyId); + } + } catch (e) { + log('[Messages] Could not convert to ObjectId:', e.message); + } + + log('[Messages] Using companyObjectId:', companyObjectId, 'companyIdString:', companyIdString); + + // Получить все сообщения где текущая компания отправитель или получатель + // Поддерживаем оба формата - ObjectId и строки + const allMessages = await Message.find({ + $or: [ + { senderCompanyId: companyObjectId }, + { senderCompanyId: companyIdString }, + { recipientCompanyId: companyObjectId }, + { recipientCompanyId: companyIdString }, + // Также ищем по threadId который может содержать ID компании + { threadId: { $regex: companyIdString } } + ] + }) + .sort({ timestamp: -1 }) + .limit(500); + + log('[Messages] Found', allMessages.length, 'messages for company'); + + if (allMessages.length === 0) { + log('[Messages] No messages found'); + res.json([]); + return; + } + + // Группируем по потокам и берем последнее сообщение каждого потока + const threadsMap = new Map(); + allMessages.forEach(msg => { + const threadId = msg.threadId; + if (!threadsMap.has(threadId)) { + threadsMap.set(threadId, { + threadId, + lastMessage: msg.text, + lastMessageAt: msg.timestamp, + senderCompanyId: msg.senderCompanyId, + recipientCompanyId: msg.recipientCompanyId + }); } }); - // Преобразование в массив и сортировка по времени - const threadsArray = Object.values(threads) - .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); + const threads = Array.from(threadsMap.values()).sort((a, b) => + new Date(b.lastMessageAt) - new Date(a.lastMessageAt) + ); - console.log('[Messages] Returned', threadsArray.length, 'threads for company', companyId); + log('[Messages] Returned', threads.length, 'unique threads'); - res.json(threadsArray); + res.json(threads); } catch (error) { - console.error('[Messages] Error:', error.message); + console.error('[Messages] Error fetching threads:', error.message, error.stack); res.status(500).json({ error: error.message }); } }); @@ -38,16 +91,24 @@ router.get('/threads', verifyToken, async (req, res) => { router.get('/:threadId', verifyToken, async (req, res) => { try { const { threadId } = req.params; + const companyId = req.user.companyId; - const threadMessages = messages - .filter(msg => msg.threadId === threadId) - .sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + // Получить все сообщения потока + const threadMessages = await Message.find({ threadId }) + .sort({ timestamp: 1 }) + .exec(); - console.log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId); + // Отметить сообщения как прочитанные для текущей компании + await Message.updateMany( + { threadId, recipientCompanyId: companyId, read: false }, + { read: true } + ); + + log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId); res.json(threadMessages); } catch (error) { - console.error('[Messages] Error:', error.message); + console.error('[Messages] Error fetching messages:', error.message); res.status(500).json({ error: error.message }); } }); @@ -63,61 +124,139 @@ router.post('/:threadId', verifyToken, async (req, res) => { } // Определить получателя на основе threadId - const threadParts = threadId.split('-'); + // threadId формат: "thread-id1-id2" + const threadParts = threadId.replace('thread-', '').split('-'); let recipientCompanyId = null; - if (threadParts.length >= 3) { - const companyId1 = threadParts[1]; - const companyId2 = threadParts[2]; - const currentSender = senderCompanyId || req.user.companyId; - recipientCompanyId = currentSender === companyId1 ? companyId2 : companyId1; + const currentSender = senderCompanyId || req.user.companyId; + const currentSenderString = currentSender.toString ? currentSender.toString() : currentSender; + + if (threadParts.length >= 2) { + const companyId1 = threadParts[0]; + const companyId2 = threadParts[1]; + // Получатель - это другая сторона + recipientCompanyId = currentSenderString === companyId1 ? companyId2 : companyId1; } - const message = { - _id: 'msg-' + Date.now(), + log('[Messages] POST /messages/:threadId'); + log('[Messages] threadId:', threadId); + log('[Messages] Sender:', currentSender); + log('[Messages] SenderString:', currentSenderString); + log('[Messages] Recipient:', recipientCompanyId); + + // Найти recipientCompanyId по ObjectId если нужно + let recipientObjectId = recipientCompanyId; + const { ObjectId } = require('mongoose').Types; + try { + if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) { + recipientObjectId = new ObjectId(recipientCompanyId); + } + } catch (e) { + log('[Messages] Could not convert recipientId to ObjectId'); + } + + const message = new Message({ threadId, - senderCompanyId: senderCompanyId || req.user.companyId, - recipientCompanyId, + senderCompanyId: currentSender, + recipientCompanyId: recipientObjectId, text: text.trim(), + read: false, timestamp: new Date() - }; + }); - messages.push(message); + const savedMessage = await message.save(); - console.log('[Messages] New message created:', message._id); + log('[Messages] New message created:', savedMessage._id); + log('[Messages] Message data:', { + threadId: savedMessage.threadId, + senderCompanyId: savedMessage.senderCompanyId, + recipientCompanyId: savedMessage.recipientCompanyId + }); - res.status(201).json(message); + res.status(201).json(savedMessage); } catch (error) { - console.error('[Messages] Error:', error.message); + console.error('[Messages] Error creating message:', error.message, error.stack); res.status(500).json({ error: error.message }); } }); -// POST /messages - создать сообщение (старый endpoint для совместимости) -router.post('/', verifyToken, async (req, res) => { +// MIGRATION ENDPOINT - Fix recipientCompanyId for all messages +router.post('/admin/migrate-fix-recipients', async (req, res) => { try { - const { threadId, text, recipientCompanyId } = req.body; + const allMessages = await Message.find().exec(); + log('[Messages] Migrating', allMessages.length, 'messages...'); - if (!text || !threadId) { - return res.status(400).json({ error: 'Text and threadId required' }); + let fixedCount = 0; + let errorCount = 0; + + for (const message of allMessages) { + try { + const threadId = message.threadId; + if (!threadId) continue; + + // Parse threadId формат "thread-id1-id2" или "id1-id2" + const ids = threadId.replace('thread-', '').split('-'); + if (ids.length < 2) { + errorCount++; + continue; + } + + const companyId1 = ids[0]; + const companyId2 = ids[1]; + + // Compare with senderCompanyId + const senderIdString = message.senderCompanyId.toString ? message.senderCompanyId.toString() : message.senderCompanyId; + const expectedRecipient = senderIdString === companyId1 ? companyId2 : companyId1; + + // If recipientCompanyId is not set or wrong - fix it + if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) { + const { ObjectId } = require('mongoose').Types; + let recipientObjectId = expectedRecipient; + try { + if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { + recipientObjectId = new ObjectId(expectedRecipient); + } + } catch (e) { + // continue + } + + await Message.updateOne( + { _id: message._id }, + { recipientCompanyId: recipientObjectId } + ); + + fixedCount++; + } + } catch (err) { + console.error('[Messages] Migration error:', err.message); + errorCount++; + } } - const message = { - _id: 'msg-' + Date.now(), - threadId, - senderCompanyId: req.user.companyId, - recipientCompanyId, - text: text.trim(), - timestamp: new Date() - }; - - messages.push(message); - - console.log('[Messages] New message created:', message._id); - - res.status(201).json(message); + log('[Messages] Migration completed! Fixed:', fixedCount, 'Errors:', errorCount); + res.json({ success: true, fixed: fixedCount, errors: errorCount, total: allMessages.length }); + } catch (error) { + console.error('[Messages] Migration error:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// DEBUG ENDPOINT +router.get('/debug/all-messages', async (req, res) => { + try { + const allMessages = await Message.find().limit(10).exec(); + log('[Debug] Total messages in DB:', allMessages.length); + + const info = allMessages.map(m => ({ + _id: m._id, + threadId: m.threadId, + senderCompanyId: m.senderCompanyId?.toString ? m.senderCompanyId.toString() : m.senderCompanyId, + recipientCompanyId: m.recipientCompanyId?.toString ? m.recipientCompanyId.toString() : m.recipientCompanyId, + text: m.text.substring(0, 30) + })); + + res.json({ totalCount: allMessages.length, messages: info }); } catch (error) { - console.error('[Messages] Error:', error.message); res.status(500).json({ error: error.message }); } }); diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js index 858d575..44490d4 100644 --- a/server/routers/procurement/routes/products.js +++ b/server/routers/procurement/routes/products.js @@ -3,6 +3,17 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Product = require('../models/Product'); +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // Helper to transform _id to id const transformProduct = (doc) => { if (!doc) return null; @@ -19,13 +30,13 @@ router.get('/', verifyToken, async (req, res) => { try { const companyId = req.user.companyId; - console.log('[Products] GET Fetching products for companyId:', companyId); + log('[Products] GET Fetching products for companyId:', companyId); const products = await Product.find({ companyId }) .sort({ createdAt: -1 }) .exec(); - console.log('[Products] Found', products.length, 'products'); + log('[Products] Found', products.length, 'products'); res.json(products.map(transformProduct)); } catch (error) { console.error('[Products] Get error:', error.message); @@ -35,20 +46,20 @@ router.get('/', verifyToken, async (req, res) => { // POST /products - Создать продукт/услугу router.post('/', verifyToken, async (req, res) => { - try { + // try { const { name, category, description, type, productUrl, price, unit, minOrder } = req.body; const companyId = req.user.companyId; - console.log('[Products] POST Creating product:', { name, category, type }); + log('[Products] POST Creating product:', { name, category, type }); - // Валидация - if (!name || !category || !description || !type) { - return res.status(400).json({ error: 'name, category, description, and type are required' }); - } + // // Валидация + // if (!name || !category || !description || !type) { + // return res.status(400).json({ error: 'name, category, description, and type are required' }); + // } - if (description.length < 20) { - return res.status(400).json({ error: 'Description must be at least 20 characters' }); - } + // if (description.length < 20) { + // return res.status(400).json({ error: 'Description must be at least 20 characters' }); + // } const newProduct = new Product({ name: name.trim(), @@ -63,13 +74,13 @@ router.post('/', verifyToken, async (req, res) => { }); const savedProduct = await newProduct.save(); - console.log('[Products] Product created with ID:', savedProduct._id); + log('[Products] Product created with ID:', savedProduct._id); res.status(201).json(transformProduct(savedProduct)); - } catch (error) { - console.error('[Products] Create error:', error.message); - res.status(500).json({ error: 'Internal server error', message: error.message }); - } + // } catch (error) { + // console.error('[Products] Create error:', error.message); + // res.status(500).json({ error: 'Internal server error', message: error.message }); + // } }); // PUT /products/:id - Обновить продукт/услугу @@ -96,7 +107,7 @@ router.put('/:id', verifyToken, async (req, res) => { { new: true, runValidators: true } ); - console.log('[Products] Product updated:', id); + log('[Products] Product updated:', id); res.json(transformProduct(updatedProduct)); } catch (error) { console.error('[Products] Update error:', error.message); @@ -127,7 +138,7 @@ router.patch('/:id', verifyToken, async (req, res) => { { new: true, runValidators: true } ); - console.log('[Products] Product patched:', id); + log('[Products] Product patched:', id); res.json(transformProduct(updatedProduct)); } catch (error) { console.error('[Products] Patch error:', error.message); @@ -153,7 +164,7 @@ router.delete('/:id', verifyToken, async (req, res) => { await Product.findByIdAndDelete(id); - console.log('[Products] Product deleted:', id); + log('[Products] Product deleted:', id); res.json({ message: 'Product deleted successfully' }); } catch (error) { console.error('[Products] Delete error:', error.message); diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js new file mode 100644 index 0000000..a7ab999 --- /dev/null +++ b/server/routers/procurement/routes/requests.js @@ -0,0 +1,171 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const Request = require('../models/Request'); + +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + +// GET /requests/sent - получить отправленные запросы +router.get('/sent', verifyToken, async (req, res) => { + try { + const companyId = req.user.companyId; + + const requests = await Request.find({ senderCompanyId: companyId }) + .sort({ createdAt: -1 }) + .exec(); + + log('[Requests] Returned', requests.length, 'sent requests for company', companyId); + + res.json(requests); + } catch (error) { + console.error('[Requests] Error fetching sent requests:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// GET /requests/received - получить полученные запросы +router.get('/received', verifyToken, async (req, res) => { + try { + const companyId = req.user.companyId; + + const requests = await Request.find({ recipientCompanyId: companyId }) + .sort({ createdAt: -1 }) + .exec(); + + log('[Requests] Returned', requests.length, 'received requests for company', companyId); + + res.json(requests); + } catch (error) { + console.error('[Requests] Error fetching received requests:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// POST /requests - создать запрос +router.post('/', verifyToken, async (req, res) => { + try { + const { text, recipientCompanyIds, productId, files } = req.body; + const senderCompanyId = req.user.companyId; + + if (!text || !recipientCompanyIds || !Array.isArray(recipientCompanyIds) || recipientCompanyIds.length === 0) { + return res.status(400).json({ error: 'text and recipientCompanyIds array required' }); + } + + // Отправить запрос каждой компании + const results = []; + for (const recipientCompanyId of recipientCompanyIds) { + try { + const request = new Request({ + senderCompanyId, + recipientCompanyId, + text, + productId, + files: files || [], + status: 'pending' + }); + + await request.save(); + results.push({ + companyId: recipientCompanyId, + success: true, + message: 'Request sent successfully' + }); + + log('[Requests] Request sent to company:', recipientCompanyId); + } catch (err) { + results.push({ + companyId: recipientCompanyId, + success: false, + message: err.message + }); + } + } + + // Сохранить отчет + const report = { + text, + result: results, + createdAt: new Date() + }; + + res.status(201).json({ + id: 'bulk-' + Date.now(), + ...report, + files: files || [] + }); + } catch (error) { + console.error('[Requests] Error creating request:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// PUT /requests/:id - ответить на запрос +router.put('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + const { response, status } = req.body; + + const request = await Request.findById(id); + + if (!request) { + return res.status(404).json({ error: 'Request not found' }); + } + + // Только получатель может ответить на запрос + if (request.recipientCompanyId !== req.user.companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + + request.response = response; + request.status = status || 'accepted'; + request.respondedAt = new Date(); + request.updatedAt = new Date(); + + await request.save(); + + log('[Requests] Request responded:', id); + + res.json(request); + } catch (error) { + console.error('[Requests] Error responding to request:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /requests/:id - удалить запрос +router.delete('/:id', verifyToken, async (req, res) => { + try { + const { id } = req.params; + + const request = await Request.findById(id); + + if (!request) { + return res.status(404).json({ error: 'Request not found' }); + } + + // Может удалить отправитель или получатель + if (request.senderCompanyId !== req.user.companyId && request.recipientCompanyId !== req.user.companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + + await Request.findByIdAndDelete(id); + + log('[Requests] Request deleted:', id); + + res.json({ message: 'Request deleted successfully' }); + } catch (error) { + console.error('[Requests] Error deleting request:', error.message); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js index 05462db..40bcd7d 100644 --- a/server/routers/procurement/routes/reviews.js +++ b/server/routers/procurement/routes/reviews.js @@ -1,16 +1,17 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); +const Review = require('../models/Review'); -// In-memory storage for reviews -let reviews = []; - -// Reference to companies from search routes -let companies = []; - -// Синхронизация с companies из других routes -const syncCompanies = () => { - // После создания review обновляем рейтинг компании +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } }; // GET /reviews/company/:companyId - получить отзывы компании @@ -18,15 +19,15 @@ router.get('/company/:companyId', verifyToken, async (req, res) => { try { const { companyId } = req.params; - const companyReviews = reviews - .filter(r => r.companyId === companyId) - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); + const companyReviews = await Review.find({ companyId }) + .sort({ createdAt: -1 }) + .exec(); - console.log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId); + log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId); res.json(companyReviews); } catch (error) { - console.error('[Reviews] Error:', error.message); + console.error('[Reviews] Error fetching reviews:', error.message); res.status(500).json({ error: 'Internal server error', message: error.message, @@ -51,15 +52,14 @@ router.post('/', verifyToken, async (req, res) => { }); } - if (comment.length < 10 || comment.length > 1000) { + if (comment.trim().length < 10 || comment.trim().length > 1000) { return res.status(400).json({ error: 'Comment must be between 10 and 1000 characters', }); } // Создать новый отзыв - const newReview = { - _id: 'review-' + Date.now(), + const newReview = new Review({ companyId, authorCompanyId: req.user.companyId, authorName: req.user.firstName + ' ' + req.user.lastName, @@ -69,15 +69,15 @@ router.post('/', verifyToken, async (req, res) => { verified: true, createdAt: new Date(), updatedAt: new Date() - }; + }); - reviews.push(newReview); + const savedReview = await newReview.save(); - console.log('[Reviews] New review created:', newReview._id); + log('[Reviews] New review created:', savedReview._id); - res.status(201).json(newReview); + res.status(201).json(savedReview); } catch (error) { - console.error('[Reviews] Error:', error.message); + console.error('[Reviews] Error creating review:', error.message); res.status(500).json({ error: 'Internal server error', message: error.message, diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index d00eed3..84e38e6 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -3,6 +3,17 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Company = require('../models/Company'); +// Функция для логирования с проверкой DEV переменной +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + // GET /search/recommendations - получить рекомендации компаний (ДОЛЖЕН быть ПЕРЕД /*) router.get('/recommendations', verifyToken, async (req, res) => { try { @@ -28,7 +39,7 @@ router.get('/recommendations', verifyToken, async (req, res) => { reason: 'Matches your search criteria' })); - console.log('[Search] Returned recommendations:', recommendations.length); + log('[Search] Returned recommendations:', recommendations.length); res.json(recommendations); } catch (error) { @@ -47,6 +58,9 @@ router.get('/', verifyToken, async (req, res) => { query = '', page = 1, limit = 10, + industries, + companySize, + geography, minRating = 0, hasReviews, hasAcceptedDocs, @@ -58,6 +72,29 @@ router.get('/', verifyToken, async (req, res) => { const User = require('../models/User'); const user = await User.findById(req.userId); + log('[Search] Request params:', { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder }); + + // Маппинг кодов фильтров на значения в БД + const industryMap = { + 'it': 'IT', + 'finance': 'Финансы', + 'manufacturing': 'Производство', + 'construction': 'Строительство', + 'retail': 'Розничная торговля', + 'wholesale': 'Оптовая торговля', + 'logistics': 'Логистика', + 'healthcare': 'Здравоохранение', + 'education': 'Образование', + 'consulting': 'Консалтинг', + 'marketing': 'Маркетинг', + 'realestate': 'Недвижимость', + 'food': 'Пищевая промышленность', + 'agriculture': 'Сельское хозяйство', + 'energy': 'Энергетика', + 'telecom': 'Телекоммуникации', + 'media': 'Медиа' + }; + // Начальный фильтр: исключить собственную компанию let filters = []; @@ -78,6 +115,43 @@ router.get('/', verifyToken, async (req, res) => { }); } + // Фильтр по отраслям - преобразуем коды в значения БД + if (industries) { + const industryList = Array.isArray(industries) ? industries : [industries]; + if (industryList.length > 0) { + const dbIndustries = industryList + .map(code => industryMap[code]) + .filter(val => val !== undefined); + + log('[Search] Raw industries param:', industries); + log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries); + + if (dbIndustries.length > 0) { + filters.push({ industry: { $in: dbIndustries } }); + log('[Search] Added industry filter:', { industry: { $in: dbIndustries } }); + } else { + log('[Search] No industries mapped! Codes were:', industryList); + } + } + } + + // Фильтр по размеру компании + if (companySize) { + const sizeList = Array.isArray(companySize) ? companySize : [companySize]; + if (sizeList.length > 0) { + filters.push({ companySize: { $in: sizeList } }); + } + } + + // Фильтр по географии + if (geography) { + const geoList = Array.isArray(geography) ? geography : [geography]; + if (geoList.length > 0) { + filters.push({ partnerGeography: { $in: geoList } }); + log('[Search] Geography filter:', { partnerGeography: { $in: geoList } }); + } + } + // Фильтр по рейтингу if (minRating) { const rating = parseFloat(minRating); @@ -112,6 +186,12 @@ router.get('/', verifyToken, async (req, res) => { sortOptions.rating = sortOrder === 'asc' ? 1 : -1; } + log('[Search] Final MongoDB filter:', JSON.stringify(filter, null, 2)); + + let filterDebug = filters.length > 0 ? { $and: filters } : {}; + const allCompanies = await Company.find({}); + log('[Search] All companies in DB:', allCompanies.map(c => ({ name: c.fullName, geography: c.partnerGeography, industry: c.industry }))); + const total = await Company.countDocuments(filter); const companies = await Company.find(filter) .sort(sortOptions) @@ -123,13 +203,19 @@ router.get('/', verifyToken, async (req, res) => { id: c._id })); - console.log('[Search] Returned', paginatedResults.length, 'companies'); + log('[Search] Query:', query, 'Industries:', industries, 'Size:', companySize, 'Geo:', geography); + log('[Search] Total found:', total, 'Returning:', paginatedResults.length, 'companies'); + log('[Search] Company details:', paginatedResults.map(c => ({ name: c.fullName, industry: c.industry }))); res.json({ companies: paginatedResults, total, page: pageNum, - totalPages: Math.ceil(total / limitNum) + totalPages: Math.ceil(total / limitNum), + _debug: { + filter: JSON.stringify(filter), + industriesReceived: industries + } }); } catch (error) { console.error('[Search] Error:', error.message); diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js new file mode 100644 index 0000000..d342f44 --- /dev/null +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -0,0 +1,93 @@ +const mongoose = require('mongoose'); +const Message = require('../models/Message'); +require('dotenv').config({ path: '../../.env' }); + +const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + +async function migrateMessages() { + try { + console.log('[Migration] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Migration] Connected to MongoDB'); + + // Найти все сообщения + const allMessages = await Message.find().exec(); + console.log('[Migration] Found', allMessages.length, 'total messages'); + + let fixedCount = 0; + let errorCount = 0; + + // Проходим по каждому сообщению + for (const message of allMessages) { + try { + const threadId = message.threadId; + if (!threadId) { + console.log('[Migration] Skipping message', message._id, '- no threadId'); + continue; + } + + // Парсим threadId формата "thread-id1-id2" или "id1-id2" + let ids = threadId.replace('thread-', '').split('-'); + + if (ids.length < 2) { + console.log('[Migration] Invalid threadId format:', threadId); + errorCount++; + continue; + } + + const companyId1 = ids[0]; + const companyId2 = ids[1]; + + // Сравниваем с senderCompanyId + const senderIdString = message.senderCompanyId.toString ? message.senderCompanyId.toString() : message.senderCompanyId; + const expectedRecipient = senderIdString === companyId1 ? companyId2 : companyId1; + + // Если recipientCompanyId не установлена или неправильная - исправляем + if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) { + console.log('[Migration] Fixing message', message._id); + console.log(' Old recipientCompanyId:', message.recipientCompanyId); + console.log(' Expected:', expectedRecipient); + + // Конвертируем в ObjectId если нужно + const { ObjectId } = require('mongoose').Types; + let recipientObjectId = expectedRecipient; + try { + if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { + recipientObjectId = new ObjectId(expectedRecipient); + } + } catch (e) { + console.log(' Could not convert to ObjectId'); + } + + await Message.updateOne( + { _id: message._id }, + { recipientCompanyId: recipientObjectId } + ); + + fixedCount++; + console.log(' ✅ Fixed'); + } + } catch (err) { + console.error('[Migration] Error processing message', message._id, ':', err.message); + errorCount++; + } + } + + console.log('[Migration] ✅ Migration completed!'); + console.log('[Migration] Fixed:', fixedCount, 'messages'); + console.log('[Migration] Errors:', errorCount); + + await mongoose.connection.close(); + console.log('[Migration] Disconnected from MongoDB'); + } catch (err) { + console.error('[Migration] ❌ Error:', err.message); + process.exit(1); + } +} + +migrateMessages(); diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 09cd954..a641bb5 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -11,8 +11,8 @@ const recreateTestUser = async () => { console.log('\n🔄 Подключение к MongoDB...'); await mongoose.connect(mongoUri, { - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, + useNewUrlParser: true, + useUnifiedTopology: true, }); console.log('✅ Подключено к MongoDB\n'); @@ -76,6 +76,21 @@ const recreateTestUser = async () => { console.log(' Пароль: SecurePass123!'); console.log(''); + // Обновить существующие mock компании + console.log('\n🔄 Обновление существующих mock компаний...'); + const updates = [ + { inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083896', updates: { companySize: '11-50', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083897', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, + { inn: '7707083898', updates: { companySize: '251-500', partnerGeography: ['moscow', 'russia_all'] } }, + ]; + + for (const item of updates) { + await Company.updateOne({ inn: item.inn }, { $set: item.updates }); + console.log(` ✓ Компания обновлена: INN ${item.inn}`); + } + await mongoose.connection.close(); process.exit(0); } catch (error) { diff --git a/server/routers/procurement/scripts/test-logging.js b/server/routers/procurement/scripts/test-logging.js new file mode 100644 index 0000000..e914f23 --- /dev/null +++ b/server/routers/procurement/scripts/test-logging.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/** + * Скрипт для тестирования логирования + * + * Использование: + * node stubs/scripts/test-logging.js # Логи скрыты (DEV не установлена) + * DEV=true node stubs/scripts/test-logging.js # Логи видны + */ + +// Функция логирования из маршрутов +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + +console.log(''); +console.log('='.repeat(60)); +console.log('TEST: Логирование с переменной окружения DEV'); +console.log('='.repeat(60)); +console.log(''); + +console.log('Значение DEV:', process.env.DEV || '(не установлена)'); +console.log(''); + +// Тестируем различные логи +log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796'); +log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799'); +log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796'); +log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799'); +log('[Products] Found', 1, 'products'); +log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796'); +log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796'); +log('[Messages] Found', 4, 'messages for company'); +log('[Messages] Returned', 3, 'unique threads'); +log('[Search] Request params:', { query: '', page: 1 }); + +console.log(''); +console.log('='.repeat(60)); +console.log('РЕЗУЛЬТАТ:'); +console.log('='.repeat(60)); + +if (process.env.DEV === 'true') { + console.log('✅ DEV=true - логи ВИДНЫ выше'); +} else { + console.log('❌ DEV не установлена или != "true" - логи СКРЫТЫ'); + console.log(''); + console.log('Для включения логов запустите:'); + console.log(' export DEV=true && npm start (Linux/Mac)'); + console.log(' $env:DEV = "true"; npm start (PowerShell)'); + console.log(' set DEV=true && npm start (CMD)'); +} + +console.log(''); +console.log('='.repeat(60)); +console.log(''); From eca5cba85806b4cf855d16d8a7a40bf160a25850 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Mon, 27 Oct 2025 19:37:21 +0300 Subject: [PATCH 126/147] =?UTF-8?q?=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/index.js | 31 ++++- server/routers/procurement/routes/search.js | 14 +- .../procurement/scripts/init-database.js | 74 +++++++++++ .../procurement/scripts/migrate-companies.js | 124 ++++++++++++++++++ .../procurement/scripts/migrate-messages.js | 12 +- .../procurement/scripts/recreate-test-user.js | 72 +++++----- .../procurement/scripts/run-migrations.js | 105 +++++++++++++++ .../procurement/scripts/test-logging.js | 61 --------- .../procurement/scripts/validate-companies.js | 93 +++++++++++++ 9 files changed, 477 insertions(+), 109 deletions(-) create mode 100644 server/routers/procurement/scripts/init-database.js create mode 100644 server/routers/procurement/scripts/migrate-companies.js create mode 100644 server/routers/procurement/scripts/run-migrations.js delete mode 100644 server/routers/procurement/scripts/test-logging.js create mode 100644 server/routers/procurement/scripts/validate-companies.js diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index e08a238..1f29b3d 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -2,6 +2,7 @@ const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); const connectDB = require('./config/db'); +const { runMigrations } = require('./scripts/run-migrations'); // Загрузить переменные окружения dotenv.config(); @@ -29,11 +30,32 @@ const homeRoutes = require('./routes/home'); const app = express(); -// Подключить MongoDB при инициализации +// Подключить MongoDB и запустить миграции при инициализации let dbConnected = false; -connectDB().then(() => { - dbConnected = true; -}); +let migrationsCompleted = false; + +const initializeApp = async () => { + try { + await connectDB().then(() => { + dbConnected = true; + }); + + // Запустить миграции после успешного подключения + if (dbConnected) { + try { + await runMigrations(); + migrationsCompleted = true; + } catch (migrationError) { + console.error('⚠️ Migrations failed but app will continue:', migrationError.message); + } + } + } catch (err) { + console.error('Error during app initialization:', err); + } +}; + +// Запустить инициализацию +initializeApp(); // Middleware app.use(cors()); @@ -68,6 +90,7 @@ app.get('/health', (req, res) => { status: 'ok', api: 'running', database: dbConnected ? 'mongodb' : 'mock', + migrations: migrationsCompleted ? 'completed' : 'pending', timestamp: new Date().toISOString() }); }); diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index 84e38e6..b983e7f 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -127,8 +127,14 @@ router.get('/', verifyToken, async (req, res) => { log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries); if (dbIndustries.length > 0) { - filters.push({ industry: { $in: dbIndustries } }); - log('[Search] Added industry filter:', { industry: { $in: dbIndustries } }); + // Handle both string and array industry values + filters.push({ + $or: [ + { industry: { $in: dbIndustries } }, + { industry: { $elemMatch: { $in: dbIndustries } } } + ] + }); + log('[Search] Added industry filter:', { $or: [{ industry: { $in: dbIndustries } }, { industry: { $elemMatch: { $in: dbIndustries } } }] }); } else { log('[Search] No industries mapped! Codes were:', industryList); } @@ -213,8 +219,10 @@ router.get('/', verifyToken, async (req, res) => { page: pageNum, totalPages: Math.ceil(total / limitNum), _debug: { + requestParams: { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder }, filter: JSON.stringify(filter), - industriesReceived: industries + filtersCount: filters.length, + appliedFilters: filters.map(f => JSON.stringify(f)) } }); } catch (error) { diff --git a/server/routers/procurement/scripts/init-database.js b/server/routers/procurement/scripts/init-database.js new file mode 100644 index 0000000..819bbff --- /dev/null +++ b/server/routers/procurement/scripts/init-database.js @@ -0,0 +1,74 @@ +const mongoose = require('mongoose'); +const { migrateCompanies } = require('./migrate-companies'); +require('dotenv').config({ path: '../../.env' }); + +const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + +// Migration history model +const migrationSchema = new mongoose.Schema({ + name: { type: String, unique: true, required: true }, + executedAt: { type: Date, default: Date.now }, + status: { type: String, enum: ['completed', 'failed'], default: 'completed' }, + message: String +}, { collection: 'migrations' }); + +const Migration = mongoose.model('Migration', migrationSchema); + +async function initializeDatabase() { + try { + console.log('[Init] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Init] Connected to MongoDB\n'); + + // Check if migrations already ran + const migrateCompaniesRan = await Migration.findOne({ name: 'migrate-companies' }); + + if (!migrateCompaniesRan) { + console.log('[Init] Running migrate-companies migration...'); + try { + await migrateCompanies(); + + // Record successful migration + await Migration.create({ + name: 'migrate-companies', + status: 'completed', + message: 'Company data migration completed successfully' + }); + + console.log('[Init] ✅ migrate-companies recorded in database\n'); + } catch (err) { + // Record failed migration + await Migration.create({ + name: 'migrate-companies', + status: 'failed', + message: err.message + }); + console.error('[Init] ❌ migrate-companies failed:', err.message); + } + } else { + console.log('[Init] ℹ️ migrate-companies already executed:', migrateCompaniesRan.executedAt); + console.log('[Init] Skipping migration...\n'); + } + + await mongoose.connection.close(); + console.log('[Init] Database initialization complete\n'); + } catch (err) { + console.error('[Init] ❌ Error during database initialization:', err.message); + process.exit(1); + } +} + +module.exports = initializeDatabase; + +// Run directly if called as script +if (require.main === module) { + initializeDatabase().catch(err => { + console.error('Initialization failed:', err); + process.exit(1); + }); +} diff --git a/server/routers/procurement/scripts/migrate-companies.js b/server/routers/procurement/scripts/migrate-companies.js new file mode 100644 index 0000000..dd4cf64 --- /dev/null +++ b/server/routers/procurement/scripts/migrate-companies.js @@ -0,0 +1,124 @@ +const mongoose = require('mongoose'); +const Company = require('../models/Company'); +require('dotenv').config({ path: '../../.env' }); + +const industryMap = { + 'it': 'IT', + 'finance': 'Финансы', + 'manufacturing': 'Производство', + 'construction': 'Строительство', + 'retail': 'Розничная торговля', + 'wholesale': 'Оптовая торговля', + 'logistics': 'Логистика', + 'healthcare': 'Здравоохранение', + 'education': 'Образование', + 'consulting': 'Консалтинг', + 'marketing': 'Маркетинг', + 'realestate': 'Недвижимость', + 'food': 'Пищевая промышленность', + 'agriculture': 'Сельское хозяйство', + 'energy': 'Энергетика', + 'telecom': 'Телекоммуникации', + 'media': 'Медиа', + 'tourism': 'Туризм', + 'legal': 'Юридические услуги', + 'other': 'Другое' +}; + +const validIndustries = Object.values(industryMap); + +const industryAliases = { + 'Торговля': 'Розничная торговля', + 'торговля': 'Розничная торговля', + 'Trade': 'Розничная торговля' +}; + +async function migrateCompanies() { + try { + const allCompanies = await Company.find().exec(); + console.log(`[Migration] Found ${allCompanies.length} companies to process`); + + let fixedCount = 0; + let errorCount = 0; + + for (const company of allCompanies) { + let needsUpdate = false; + let updates = {}; + + // Check and fix industry field + if (company.industry) { + if (Array.isArray(company.industry)) { + console.log(`[FIX] ${company.fullName}: industry is array, converting to string`); + updates.industry = company.industry[0] || 'Другое'; + needsUpdate = true; + } else if (!validIndustries.includes(company.industry)) { + const mapped = industryAliases[company.industry]; + if (mapped) { + console.log(`[FIX] ${company.fullName}: "${company.industry}" → "${mapped}"`); + updates.industry = mapped; + needsUpdate = true; + } else { + console.log(`[WARN] ${company.fullName}: unknown industry "${company.industry}"`); + } + } + } + + // Check and fix companySize field + if (company.companySize && Array.isArray(company.companySize)) { + console.log(`[FIX] ${company.fullName}: companySize is array, converting to string`); + updates.companySize = company.companySize[0] || ''; + needsUpdate = true; + } + + if (needsUpdate) { + try { + await Company.updateOne({ _id: company._id }, { $set: updates }); + fixedCount++; + console.log(` ✅ Updated`); + } catch (err) { + console.error(` ❌ Error: ${err.message}`); + errorCount++; + } + } + } + + console.log('\n[Migration] === MIGRATION SUMMARY ==='); + console.log(`[Migration] Total companies: ${allCompanies.length}`); + console.log(`[Migration] Fixed: ${fixedCount}`); + console.log(`[Migration] Errors: ${errorCount}`); + + if (fixedCount === 0 && errorCount === 0) { + console.log('[Migration] ✅ No migration needed - all data is valid!'); + } else if (errorCount === 0) { + console.log('[Migration] ✅ Migration completed successfully!'); + } else { + console.log('[Migration] ⚠️ Migration completed with errors.'); + } + } catch (err) { + console.error('[Migration] ❌ Error:', err.message); + throw err; + } +} + +module.exports = { + migrateCompanies: migrateCompanies +}; + +// Run directly if called as script +if (require.main === module) { + const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; + + mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }).then(async () => { + console.log('[Migration] Connected to MongoDB\n'); + await migrateCompanies(); + await mongoose.connection.close(); + }).catch(err => { + console.error('[Migration] ❌ Error:', err.message); + process.exit(1); + }); +} diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js index d342f44..56a877e 100644 --- a/server/routers/procurement/scripts/migrate-messages.js +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -86,8 +86,16 @@ async function migrateMessages() { console.log('[Migration] Disconnected from MongoDB'); } catch (err) { console.error('[Migration] ❌ Error:', err.message); - process.exit(1); + throw err; } } -migrateMessages(); +module.exports = { migrateMessages }; + +// Run directly if called as script +if (require.main === module) { + migrateMessages().catch(err => { + console.error('Migration failed:', err); + process.exit(1); + }); +} diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index a641bb5..211b8a6 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -7,32 +7,25 @@ const Company = require('../models/Company'); const recreateTestUser = async () => { try { - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - - console.log('\n🔄 Подключение к MongoDB...'); - await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - console.log('✅ Подключено к MongoDB\n'); + console.log('[Migration] Processing test user creation...'); // Удалить старого тестового пользователя - console.log('🗑️ Удаление старого тестового пользователя...'); + console.log('[Migration] Removing old test user...'); const oldUser = await User.findOne({ email: 'admin@test-company.ru' }); if (oldUser) { // Удалить связанную компанию if (oldUser.companyId) { await Company.findByIdAndDelete(oldUser.companyId); - console.log(' ✓ Старая компания удалена'); + console.log('[Migration] ✓ Old company removed'); } await User.findByIdAndDelete(oldUser._id); - console.log(' ✓ Старый пользователь удален'); + console.log('[Migration] ✓ Old user removed'); } else { - console.log(' ℹ️ Старый пользователь не найден'); + console.log('[Migration] ℹ️ Old user not found'); } // Создать новую компанию с правильной кодировкой UTF-8 - console.log('\n🏢 Создание тестовой компании...'); + console.log('[Migration] Creating test company...'); const company = await Company.create({ fullName: 'ООО "Тестовая Компания"', inn: '1234567890', @@ -47,10 +40,10 @@ const recreateTestUser = async () => { reviewsCount: 10, dealsCount: 25, }); - console.log(' ✓ Компания создана:', company.fullName); + console.log('[Migration] ✓ Company created:', company.fullName); // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('\n👤 Создание тестового пользователя...'); + console.log('[Migration] Creating test user...'); const user = await User.create({ email: 'admin@test-company.ru', password: 'SecurePass123!', @@ -60,24 +53,10 @@ const recreateTestUser = async () => { phone: '+7 (999) 123-45-67', companyId: company._id, }); - console.log(' ✓ Пользователь создан:', user.firstName, user.lastName); - - // Проверка что данные сохранены правильно - console.log('\n✅ Проверка данных:'); - console.log(' Email:', user.email); - console.log(' Имя:', user.firstName); - console.log(' Фамилия:', user.lastName); - console.log(' Компания:', company.fullName); - console.log(' Должность:', user.position); - - console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8'); - console.log('\n📋 Данные для входа:'); - console.log(' Email: admin@test-company.ru'); - console.log(' Пароль: SecurePass123!'); - console.log(''); + console.log('[Migration] ✓ User created:', user.firstName, user.lastName); // Обновить существующие mock компании - console.log('\n🔄 Обновление существующих mock компаний...'); + console.log('[Migration] Updating existing companies...'); const updates = [ { inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, { inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } }, @@ -88,18 +67,33 @@ const recreateTestUser = async () => { for (const item of updates) { await Company.updateOne({ inn: item.inn }, { $set: item.updates }); - console.log(` ✓ Компания обновлена: INN ${item.inn}`); + console.log(`[Migration] ✓ Company updated: INN ${item.inn}`); } - await mongoose.connection.close(); - process.exit(0); + console.log('[Migration] ✅ Test user migration completed!'); } catch (error) { - console.error('\n❌ Ошибка:', error.message); - console.error(error); - process.exit(1); + console.error('[Migration] ❌ Error:', error.message); + throw error; } }; -// Запуск -recreateTestUser(); +module.exports = { recreateTestUser }; + +// Run directly if called as script +if (require.main === module) { + const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + + mongoose.connect(mongoUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }).then(async () => { + console.log('[Migration] Connected to MongoDB\n'); + await recreateTestUser(); + await mongoose.connection.close(); + process.exit(0); + }).catch(err => { + console.error('[Migration] ❌ Error:', err.message); + process.exit(1); + }); +} diff --git a/server/routers/procurement/scripts/run-migrations.js b/server/routers/procurement/scripts/run-migrations.js new file mode 100644 index 0000000..39a99c9 --- /dev/null +++ b/server/routers/procurement/scripts/run-migrations.js @@ -0,0 +1,105 @@ +const mongoose = require('mongoose'); +const { migrateCompanies } = require('./migrate-companies'); +const { migrateMessages } = require('./migrate-messages'); +const { recreateTestUser } = require('./recreate-test-user'); +require('dotenv').config(); + +const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + +// Migration history model +const migrationSchema = new mongoose.Schema({ + name: { type: String, unique: true, required: true }, + executedAt: { type: Date, default: Date.now }, + status: { type: String, enum: ['completed', 'failed'], default: 'completed' }, + message: String +}, { collection: 'migrations' }); + +const Migration = mongoose.model('Migration', migrationSchema); + +const migrations = [ + { name: 'migrate-companies', fn: migrateCompanies }, + { name: 'migrate-messages', fn: migrateMessages }, + { name: 'recreate-test-user', fn: recreateTestUser } +]; + +async function runMigrations() { + let mongooseConnected = false; + + try { + console.log('\n' + '='.repeat(60)); + console.log('🚀 Starting Database Migrations'); + console.log('='.repeat(60) + '\n'); + + console.log('[Migrations] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + mongooseConnected = true; + console.log('[Migrations] ✅ Connected to MongoDB\n'); + + for (const migration of migrations) { + console.log(`[${migration.name}] Starting...`); + + try { + // Check if already executed + const existing = await Migration.findOne({ name: migration.name }); + + if (existing) { + console.log(`[${migration.name}] ℹ️ Already executed at: ${existing.executedAt.toISOString()}`); + console.log(`[${migration.name}] Status: ${existing.status}`); + if (existing.message) console.log(`[${migration.name}] Message: ${existing.message}`); + console.log(''); + continue; + } + + // Run migration + await migration.fn(); + + // Record successful migration + await Migration.create({ + name: migration.name, + status: 'completed', + message: `${migration.name} executed successfully` + }); + + console.log(`[${migration.name}] ✅ Completed and recorded\n`); + } catch (error) { + console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); + + // Record failed migration + await Migration.create({ + name: migration.name, + status: 'failed', + message: error.message + }); + } + } + + console.log('='.repeat(60)); + console.log('✅ All migrations processed'); + console.log('='.repeat(60) + '\n'); + + } catch (error) { + console.error('\n❌ Fatal migration error:', error.message); + console.error(error); + process.exit(1); + } finally { + if (mongooseConnected) { + await mongoose.connection.close(); + console.log('[Migrations] Disconnected from MongoDB\n'); + } + } +} + +module.exports = { runMigrations, Migration }; + +// Run directly if called as script +if (require.main === module) { + runMigrations().catch(err => { + console.error('Migration failed:', err); + process.exit(1); + }); +} diff --git a/server/routers/procurement/scripts/test-logging.js b/server/routers/procurement/scripts/test-logging.js deleted file mode 100644 index e914f23..0000000 --- a/server/routers/procurement/scripts/test-logging.js +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node - -/** - * Скрипт для тестирования логирования - * - * Использование: - * node stubs/scripts/test-logging.js # Логи скрыты (DEV не установлена) - * DEV=true node stubs/scripts/test-logging.js # Логи видны - */ - -// Функция логирования из маршрутов -const log = (message, data = '') => { - if (process.env.DEV === 'true') { - if (data) { - console.log(message, data); - } else { - console.log(message); - } - } -}; - -console.log(''); -console.log('='.repeat(60)); -console.log('TEST: Логирование с переменной окружения DEV'); -console.log('='.repeat(60)); -console.log(''); - -console.log('Значение DEV:', process.env.DEV || '(не установлена)'); -console.log(''); - -// Тестируем различные логи -log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796'); -log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799'); -log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796'); -log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799'); -log('[Products] Found', 1, 'products'); -log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796'); -log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796'); -log('[Messages] Found', 4, 'messages for company'); -log('[Messages] Returned', 3, 'unique threads'); -log('[Search] Request params:', { query: '', page: 1 }); - -console.log(''); -console.log('='.repeat(60)); -console.log('РЕЗУЛЬТАТ:'); -console.log('='.repeat(60)); - -if (process.env.DEV === 'true') { - console.log('✅ DEV=true - логи ВИДНЫ выше'); -} else { - console.log('❌ DEV не установлена или != "true" - логи СКРЫТЫ'); - console.log(''); - console.log('Для включения логов запустите:'); - console.log(' export DEV=true && npm start (Linux/Mac)'); - console.log(' $env:DEV = "true"; npm start (PowerShell)'); - console.log(' set DEV=true && npm start (CMD)'); -} - -console.log(''); -console.log('='.repeat(60)); -console.log(''); diff --git a/server/routers/procurement/scripts/validate-companies.js b/server/routers/procurement/scripts/validate-companies.js new file mode 100644 index 0000000..cbf1147 --- /dev/null +++ b/server/routers/procurement/scripts/validate-companies.js @@ -0,0 +1,93 @@ +const mongoose = require('mongoose'); +const Company = require('../models/Company'); +require('dotenv').config({ path: '../../.env' }); + +const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; + +const industryMap = { + 'it': 'IT', + 'finance': 'Финансы', + 'manufacturing': 'Производство', + 'construction': 'Строительство', + 'retail': 'Розничная торговля', + 'wholesale': 'Оптовая торговля', + 'logistics': 'Логистика', + 'healthcare': 'Здравоохранение', + 'education': 'Образование', + 'consulting': 'Консалтинг', + 'marketing': 'Маркетинг', + 'realestate': 'Недвижимость', + 'food': 'Пищевая промышленность', + 'agriculture': 'Сельское хозяйство', + 'energy': 'Энергетика', + 'telecom': 'Телекоммуникации', + 'media': 'Медиа', + 'tourism': 'Туризм', + 'legal': 'Юридические услуги', + 'other': 'Другое' +}; + +async function validateCompanies() { + try { + console.log('[Validation] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Validation] Connected to MongoDB\n'); + + const allCompanies = await Company.find().exec(); + console.log(`Found ${allCompanies.length} total companies\n`); + + console.log('=== COMPANY DATA VALIDATION REPORT ===\n'); + + let issuesFound = 0; + let validCompanies = 0; + + for (const company of allCompanies) { + console.log(`📋 Company: ${company.fullName}`); + console.log(` ID: ${company._id}`); + console.log(` Industry: ${company.industry} (type: ${typeof company.industry})`); + console.log(` Company Size: ${company.companySize}`); + + let hasIssues = false; + + if (company.industry) { + if (Array.isArray(company.industry)) { + console.log(` ⚠️ WARNING: industry is array!`); + issuesFound++; + hasIssues = true; + } else if (!Object.values(industryMap).includes(company.industry)) { + console.log(` ⚠️ industry value unknown: "${company.industry}"`); + issuesFound++; + hasIssues = true; + } else { + console.log(` ✅ industry OK`); + } + } + + if (!hasIssues) validCompanies++; + console.log(''); + } + + console.log('\n=== SUMMARY ==='); + console.log(`Total: ${allCompanies.length}`); + console.log(`Valid: ${validCompanies}`); + console.log(`Issues: ${issuesFound}`); + + if (issuesFound === 0) { + console.log('\n✅ All data OK. No migration needed.'); + } else { + console.log('\n⚠️ Migration recommended.'); + } + + await mongoose.connection.close(); + } catch (err) { + console.error('❌ Error:', err.message); + process.exit(1); + } +} + +validateCompanies(); From 390d97e6d517b9fd0d2db8e6729885e62e6b88b0 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Mon, 27 Oct 2025 19:52:35 +0300 Subject: [PATCH 127/147] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=82=D0=B0=D0=B9=D0=BC=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/routes/auth.js | 27 ++++++++++---- .../procurement/scripts/migrate-companies.js | 2 +- .../procurement/scripts/run-migrations.js | 35 +++++++++++++------ 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 9f55a1f..035d0b0 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -133,18 +133,33 @@ const initializeTestUser = async () => { ]; for (const mockCompanyData of mockCompanies) { - const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); - if (!existingCompany) { - await Company.create(mockCompanyData); - log(`✅ Mock company created: ${mockCompanyData.fullName}`); + try { + const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); + if (!existingCompany) { + await Company.create(mockCompanyData); + log(`✅ Mock company created: ${mockCompanyData.fullName}`); + } + } catch (err) { + // Ignore errors for mock company creation - это может быть ошибка аутентификации + log(`ℹ️ Mock company init failed: ${mockCompanyData.fullName}`); } } } catch (error) { - console.error('Error initializing test data:', error.message); + // Ошибка аутентификации или другие ошибки БД - продолжаем работу + if (error.message && error.message.includes('authentication')) { + log('ℹ️ Database authentication required - test data initialization deferred'); + } else { + console.error('Error initializing test data:', error.message); + } } }; -initializeTestUser(); +// Пытаемся инициализировать с задержкой (даёт время на подключение) +setTimeout(() => { + initializeTestUser().catch(err => { + log(`⚠️ Deferred test data initialization failed: ${err.message}`); + }); +}, 2000); // Регистрация router.post('/register', async (req, res) => { diff --git a/server/routers/procurement/scripts/migrate-companies.js b/server/routers/procurement/scripts/migrate-companies.js index dd4cf64..44fb465 100644 --- a/server/routers/procurement/scripts/migrate-companies.js +++ b/server/routers/procurement/scripts/migrate-companies.js @@ -106,7 +106,7 @@ module.exports = { // Run directly if called as script if (require.main === module) { - const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; + const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; mongoose.connect(mongoUrl, { useNewUrlParser: true, diff --git a/server/routers/procurement/scripts/run-migrations.js b/server/routers/procurement/scripts/run-migrations.js index 39a99c9..d2dde49 100644 --- a/server/routers/procurement/scripts/run-migrations.js +++ b/server/routers/procurement/scripts/run-migrations.js @@ -67,14 +67,23 @@ async function runMigrations() { console.log(`[${migration.name}] ✅ Completed and recorded\n`); } catch (error) { - console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); + // Обработка ошибок аутентификации и других ошибок + if (error.message && error.message.includes('authentication')) { + console.warn(`[${migration.name}] ⚠️ Skipped (authentication required): ${error.message}\n`); + } else { + console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); - // Record failed migration - await Migration.create({ - name: migration.name, - status: 'failed', - message: error.message - }); + // Record failed migration + try { + await Migration.create({ + name: migration.name, + status: 'failed', + message: error.message + }); + } catch (recordErr) { + // Ignore if we can't record the failure + } + } } } @@ -83,9 +92,15 @@ async function runMigrations() { console.log('='.repeat(60) + '\n'); } catch (error) { - console.error('\n❌ Fatal migration error:', error.message); - console.error(error); - process.exit(1); + // Обработка ошибок подключения + if (error.message && error.message.includes('authentication')) { + console.warn('\n⚠️ Database authentication required - migrations skipped'); + console.warn('This is normal if the database is shared with other projects.\n'); + } else { + console.error('\n❌ Fatal migration error:', error.message); + console.error(error); + process.exit(1); + } } finally { if (mongooseConnected) { await mongoose.connection.close(); From 35493a09b5f14b6097affc2d980ebf4689b32923 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Mon, 27 Oct 2025 20:04:02 +0300 Subject: [PATCH 128/147] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/config/db.js | 4 +- server/routers/procurement/index.js | 2 +- .../procurement/scripts/init-database.js | 2 +- .../procurement/scripts/migrate-messages.js | 22 +++--- .../procurement/scripts/run-migrations.js | 67 +++++++++---------- server/utils/const.ts | 2 +- 6 files changed, 48 insertions(+), 51 deletions(-) diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js index 31e7ae4..0e3adf3 100644 --- a/server/routers/procurement/config/db.js +++ b/server/routers/procurement/config/db.js @@ -2,10 +2,10 @@ const mongoose = require('mongoose'); const connectDB = async () => { try { - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; + const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; console.log('\n📡 Попытка подключения к MongoDB...'); - console.log(` URI: ${mongoUri}`); + console.log(` URI: ${mongoUri.replace(/\/\/:.*@/, '//***:***@')}`); const connection = await mongoose.connect(mongoUri, { useNewUrlParser: true, diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 1f29b3d..fdbc8fd 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -43,7 +43,7 @@ const initializeApp = async () => { // Запустить миграции после успешного подключения if (dbConnected) { try { - await runMigrations(); + await runMigrations(false); migrationsCompleted = true; } catch (migrationError) { console.error('⚠️ Migrations failed but app will continue:', migrationError.message); diff --git a/server/routers/procurement/scripts/init-database.js b/server/routers/procurement/scripts/init-database.js index 819bbff..afae63b 100644 --- a/server/routers/procurement/scripts/init-database.js +++ b/server/routers/procurement/scripts/init-database.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const { migrateCompanies } = require('./migrate-companies'); require('dotenv').config({ path: '../../.env' }); -const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; +const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; // Migration history model const migrationSchema = new mongoose.Schema({ diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js index 56a877e..dba2b66 100644 --- a/server/routers/procurement/scripts/migrate-messages.js +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -6,14 +6,17 @@ const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procureme async function migrateMessages() { try { - console.log('[Migration] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Migration] Connected to MongoDB'); + // Check if connection exists, if not connect + if (mongoose.connection.readyState === 0) { + console.log('[Migration] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Migration] Connected to MongoDB'); + } // Найти все сообщения const allMessages = await Message.find().exec(); @@ -81,9 +84,6 @@ async function migrateMessages() { console.log('[Migration] ✅ Migration completed!'); console.log('[Migration] Fixed:', fixedCount, 'messages'); console.log('[Migration] Errors:', errorCount); - - await mongoose.connection.close(); - console.log('[Migration] Disconnected from MongoDB'); } catch (err) { console.error('[Migration] ❌ Error:', err.message); throw err; diff --git a/server/routers/procurement/scripts/run-migrations.js b/server/routers/procurement/scripts/run-migrations.js index d2dde49..1c59fbc 100644 --- a/server/routers/procurement/scripts/run-migrations.js +++ b/server/routers/procurement/scripts/run-migrations.js @@ -4,7 +4,7 @@ const { migrateMessages } = require('./migrate-messages'); const { recreateTestUser } = require('./recreate-test-user'); require('dotenv').config(); -const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; +const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; // Migration history model const migrationSchema = new mongoose.Schema({ @@ -22,7 +22,7 @@ const migrations = [ { name: 'recreate-test-user', fn: recreateTestUser } ]; -async function runMigrations() { +async function runMigrations(shouldCloseConnection = false) { let mongooseConnected = false; try { @@ -30,15 +30,20 @@ async function runMigrations() { console.log('🚀 Starting Database Migrations'); console.log('='.repeat(60) + '\n'); - console.log('[Migrations] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - mongooseConnected = true; - console.log('[Migrations] ✅ Connected to MongoDB\n'); + // Only connect if not already connected + if (mongoose.connection.readyState === 0) { + console.log('[Migrations] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + mongooseConnected = true; + console.log('[Migrations] ✅ Connected to MongoDB\n'); + } else { + console.log('[Migrations] ✅ Using existing MongoDB connection\n'); + } for (const migration of migrations) { console.log(`[${migration.name}] Starting...`); @@ -67,22 +72,17 @@ async function runMigrations() { console.log(`[${migration.name}] ✅ Completed and recorded\n`); } catch (error) { - // Обработка ошибок аутентификации и других ошибок - if (error.message && error.message.includes('authentication')) { - console.warn(`[${migration.name}] ⚠️ Skipped (authentication required): ${error.message}\n`); - } else { - console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); + console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); - // Record failed migration - try { - await Migration.create({ - name: migration.name, - status: 'failed', - message: error.message - }); - } catch (recordErr) { - // Ignore if we can't record the failure - } + // Record failed migration + try { + await Migration.create({ + name: migration.name, + status: 'failed', + message: error.message + }); + } catch (recordErr) { + // Ignore if we can't record the failure } } } @@ -92,17 +92,14 @@ async function runMigrations() { console.log('='.repeat(60) + '\n'); } catch (error) { - // Обработка ошибок подключения - if (error.message && error.message.includes('authentication')) { - console.warn('\n⚠️ Database authentication required - migrations skipped'); - console.warn('This is normal if the database is shared with other projects.\n'); - } else { - console.error('\n❌ Fatal migration error:', error.message); - console.error(error); + console.error('\n❌ Fatal migration error:', error.message); + console.error(error); + if (shouldCloseConnection) { process.exit(1); } } finally { - if (mongooseConnected) { + // Only close connection if we created it and requested to close + if (mongooseConnected && shouldCloseConnection) { await mongoose.connection.close(); console.log('[Migrations] Disconnected from MongoDB\n'); } @@ -113,7 +110,7 @@ module.exports = { runMigrations, Migration }; // Run directly if called as script if (require.main === module) { - runMigrations().catch(err => { + runMigrations(true).catch(err => { console.error('Migration failed:', err); process.exit(1); }); diff --git a/server/utils/const.ts b/server/utils/const.ts index 3ab73e4..70afa42 100644 --- a/server/utils/const.ts +++ b/server/utils/const.ts @@ -1,4 +1,4 @@ import 'dotenv/config'; // Connection URL -export const mongoUrl = process.env.MONGO_ADDR || 'mongodb://localhost:27017' +export const mongoUrl = process.env.MONGO_ADDR || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; From 0d1dcf21c157798fa7d7c01aba740e7b24584d0c Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Sun, 2 Nov 2025 12:40:42 +0300 Subject: [PATCH 129/147] =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/procurement/config/db.js | 116 ++++- server/routers/procurement/index.js | 45 +- server/routers/procurement/mocks/auth.json | 46 -- .../routers/procurement/mocks/companies.json | 430 --------------- .../routers/procurement/mocks/products.json | 158 ------ server/routers/procurement/mocks/search.json | 122 ----- server/routers/procurement/mocks/user.json | 13 - .../routers/procurement/models/BuyDocument.js | 43 ++ .../routers/procurement/models/BuyProduct.js | 1 + .../routers/procurement/models/Experience.js | 46 ++ server/routers/procurement/models/Request.js | 20 + server/routers/procurement/routes/auth.js | 488 ++++++++++-------- server/routers/procurement/routes/buy.js | 250 +++++---- .../routers/procurement/routes/buyProducts.js | 126 ++++- .../routers/procurement/routes/companies.js | 183 +++++-- .../routers/procurement/routes/experience.js | 89 ++-- server/routers/procurement/routes/home.js | 113 +++- server/routers/procurement/routes/messages.js | 6 +- server/routers/procurement/routes/products.js | 10 +- server/routers/procurement/routes/requests.js | 398 +++++++++++--- server/routers/procurement/routes/reviews.js | 2 +- server/routers/procurement/routes/search.js | 14 +- .../procurement/scripts/init-database.js | 74 --- .../procurement/scripts/migrate-companies.js | 124 ----- .../procurement/scripts/migrate-messages.js | 34 +- .../procurement/scripts/recreate-test-user.js | 103 ++-- .../procurement/scripts/run-migrations.js | 117 ----- .../procurement/scripts/test-logging.js | 61 +++ .../procurement/scripts/validate-companies.js | 93 ---- 29 files changed, 1498 insertions(+), 1827 deletions(-) delete mode 100644 server/routers/procurement/mocks/auth.json delete mode 100644 server/routers/procurement/mocks/companies.json delete mode 100644 server/routers/procurement/mocks/products.json delete mode 100644 server/routers/procurement/mocks/search.json delete mode 100644 server/routers/procurement/mocks/user.json create mode 100644 server/routers/procurement/models/BuyDocument.js create mode 100644 server/routers/procurement/models/Experience.js delete mode 100644 server/routers/procurement/scripts/init-database.js delete mode 100644 server/routers/procurement/scripts/migrate-companies.js delete mode 100644 server/routers/procurement/scripts/run-migrations.js create mode 100644 server/routers/procurement/scripts/test-logging.js delete mode 100644 server/routers/procurement/scripts/validate-companies.js diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js index 0e3adf3..601687e 100644 --- a/server/routers/procurement/config/db.js +++ b/server/routers/procurement/config/db.js @@ -1,31 +1,97 @@ const mongoose = require('mongoose'); -const connectDB = async () => { - try { - const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - - console.log('\n📡 Попытка подключения к MongoDB...'); - console.log(` URI: ${mongoUri.replace(/\/\/:.*@/, '//***:***@')}`); - - const connection = await mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - - console.log('✅ MongoDB подключена успешно!'); - console.log(` Хост: ${connection.connection.host}`); - console.log(` БД: ${connection.connection.name}\n`); - - return connection; - } catch (error) { - console.error('\n❌ Ошибка подключения к MongoDB:'); - console.error(` ${error.message}\n`); - console.warn('⚠️ Приложение продолжит работу с mock данными\n'); - - return null; +// Get MongoDB URL from environment variables +// MONGO_ADDR is a centralized env variable from server/utils/const.ts +const primaryUri = process.env.MONGO_ADDR || process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; +const fallbackUri = process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; + +/** + * Check if error is related to authentication + */ +const isAuthError = (error) => { + if (!error) { + return false; } + + const authCodes = new Set([18, 13]); + if (error.code && authCodes.has(error.code)) { + return true; + } + + const message = String(error.message || '').toLowerCase(); + return message.includes('auth') || message.includes('authentication'); +}; + +/** + * Try to connect to MongoDB with specific URI + */ +const connectWithUri = async (uri, label) => { + console.log(`\n📡 Попытка подключения к MongoDB (${label})...`); + if (process.env.DEV === 'true') { + console.log(` URI: ${uri}`); + } + + const connection = await mongoose.connect(uri, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + + try { + await connection.connection.db.admin().command({ ping: 1 }); + } catch (pingError) { + if (isAuthError(pingError)) { + await mongoose.connection.close().catch(() => {}); + throw pingError; + } + console.error('⚠️ MongoDB ping error:', pingError.message); + } + + console.log('✅ MongoDB подключена успешно!'); + console.log(` Хост: ${connection.connection.host}`); + console.log(` БД: ${connection.connection.name}\n`); + if (process.env.DEV === 'true') { + console.log(` Пользователь: ${connection.connection.user || 'anonymous'}`); + } + + return connection; +}; + +/** + * Connect to MongoDB with fallback strategy + */ +const connectDB = async () => { + const attempts = []; + + if (fallbackUri) { + attempts.push({ uri: fallbackUri, label: 'AUTH' }); + } + + attempts.push({ uri: primaryUri, label: 'PRIMARY' }); + + let lastError = null; + + for (const attempt of attempts) { + try { + console.log(`[MongoDB] Trying ${attempt.label} connection...`); + return await connectWithUri(attempt.uri, attempt.label); + } catch (error) { + lastError = error; + console.error(`\n❌ Ошибка подключения к MongoDB (${attempt.label}):`); + console.error(` ${error.message}\n`); + + if (!isAuthError(error)) { + break; + } + } + } + + if (lastError) { + console.warn('⚠️ Приложение продолжит работу с mock данными\n'); + } + + return null; }; module.exports = connectDB; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index fdbc8fd..b20542e 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -1,8 +1,8 @@ const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); -const connectDB = require('./config/db'); -const { runMigrations } = require('./scripts/run-migrations'); +const fs = require('fs'); +const path = require('path'); // Загрузить переменные окружения dotenv.config(); @@ -15,7 +15,7 @@ if (process.env.DEV === 'true') { console.log('ℹ️ DEBUG MODE ENABLED - All logs are visible'); } -// Импортировать маршруты +// Импортировать маршруты - прямые пути без path.join и __dirname const authRoutes = require('./routes/auth'); const companiesRoutes = require('./routes/companies'); const messagesRoutes = require('./routes/messages'); @@ -28,34 +28,15 @@ const buyProductsRoutes = require('./routes/buyProducts'); const requestsRoutes = require('./routes/requests'); const homeRoutes = require('./routes/home'); +const connectDB = require('./config/db'); + const app = express(); -// Подключить MongoDB и запустить миграции при инициализации +// Подключить MongoDB при инициализации let dbConnected = false; -let migrationsCompleted = false; - -const initializeApp = async () => { - try { - await connectDB().then(() => { - dbConnected = true; - }); - - // Запустить миграции после успешного подключения - if (dbConnected) { - try { - await runMigrations(false); - migrationsCompleted = true; - } catch (migrationError) { - console.error('⚠️ Migrations failed but app will continue:', migrationError.message); - } - } - } catch (err) { - console.error('Error during app initialization:', err); - } -}; - -// Запустить инициализацию -initializeApp(); +connectDB().then(() => { + dbConnected = true; +}); // Middleware app.use(cors()); @@ -84,13 +65,19 @@ app.use((req, res, next) => { const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms); app.use(delay()); +// Статика для загруженных файлов +const uploadsRoot = path.join(__dirname, '..', '..', 'remote-assets', 'uploads'); +if (!fs.existsSync(uploadsRoot)) { + fs.mkdirSync(uploadsRoot, { recursive: true }); +} +app.use('/uploads', express.static(uploadsRoot)); + // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', api: 'running', database: dbConnected ? 'mongodb' : 'mock', - migrations: migrationsCompleted ? 'completed' : 'pending', timestamp: new Date().toISOString() }); }); diff --git a/server/routers/procurement/mocks/auth.json b/server/routers/procurement/mocks/auth.json deleted file mode 100644 index eabb81d..0000000 --- a/server/routers/procurement/mocks/auth.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "mockAuthResponse": { - "user": { - "id": "user-123", - "email": "test@company.com", - "firstName": "Иван", - "lastName": "Петров", - "position": "Генеральный директор" - }, - "company": { - "id": "company-123", - "name": "ООО \"Тестовая Компания\"", - "inn": "7707083893", - "ogrn": "1027700132195", - "fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"", - "shortName": "ООО \"Тест\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "50-100", - "website": "https://test-company.ru", - "verified": true, - "rating": 4.5 - }, - "tokens": { - "accessToken": "mock-access-token-{{timestamp}}", - "refreshToken": "mock-refresh-token-{{timestamp}}" - } - }, - "errorMessages": { - "validationFailed": "Заполните все обязательные поля", - "emailRequired": "Email обязателен", - "passwordRequired": "Пароль обязателен", - "termsRequired": "Необходимо принять условия использования", - "invalidCredentials": "Неверный email или пароль", - "refreshTokenRequired": "Refresh token обязателен", - "innValidation": "ИНН должен содержать 10 или 12 цифр" - }, - "successMessages": { - "logoutSuccess": "Успешный выход", - "emailVerified": "Email успешно подтвержден", - "passwordResetSent": "Письмо для восстановления пароля отправлено", - "passwordResetSuccess": "Пароль успешно изменен", - "logoUploaded": "Логотип успешно загружен", - "addedToFavorites": "Добавлено в избранное" - } -} diff --git a/server/routers/procurement/mocks/companies.json b/server/routers/procurement/mocks/companies.json deleted file mode 100644 index 8e3bffe..0000000 --- a/server/routers/procurement/mocks/companies.json +++ /dev/null @@ -1,430 +0,0 @@ -{ - "mockCompany": { - "id": "company-123", - "name": "ООО \"Тестовая Компания\"", - "inn": "7707083893", - "ogrn": "1027700132195", - "fullName": "Общество с ограниченной ответственностью \"Тестовая Компания\"", - "shortName": "ООО \"Тест\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "50-100", - "website": "https://test-company.ru", - "verified": true, - "rating": 4.5 - }, - "mockINNData": { - "7707083893": { - "name": "ПУБЛИЧНОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО \"СБЕРБАНК РОССИИ\"", - "ogrn": "1027700132195", - "legal_form": "ПАО" - }, - "7730048036": { - "name": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ \"КОМПАНИЯ\"", - "ogrn": "1047730048036", - "legal_form": "ООО" - } - }, - "mockCompanies": [ - { - "id": "company-1", - "inn": "7707083893", - "ogrn": "1027700132195", - "fullName": "Общество с ограниченной ответственностью \"СтройКомплект\"", - "shortName": "ООО \"СтройКомплект\"", - "legalForm": "ООО", - "industry": "Строительство", - "companySize": "100-250", - "website": "https://stroykomplekt.ru", - "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=SK", - "slogan": "Строим будущее вместе", - "rating": 4.8, - "verified": true, - "phone": "+7 (495) 123-45-67", - "email": "info@stroykomplekt.ru", - "legalAddress": "г. Москва, ул. Строительная, д. 15", - "foundedYear": 2010, - "employeeCount": "150 сотрудников" - }, - { - "id": "company-6", - "inn": "7707083894", - "ogrn": "1027700132196", - "fullName": "Акционерное общество \"Московский Строй\"", - "shortName": "АО \"Московский Строй\"", - "legalForm": "АО", - "industry": "Строительство", - "companySize": "500+", - "website": "https://moscow-stroy.ru", - "logo": "https://via.placeholder.com/100x100/1A365D/FFFFFF?text=MS", - "slogan": "Качество и надежность с 1995 года", - "rating": 4.9, - "verified": true, - "phone": "+7 (495) 987-65-43", - "email": "info@moscow-stroy.ru", - "legalAddress": "г. Москва, пр. Мира, д. 100", - "foundedYear": 1995, - "employeeCount": "800+ сотрудников" - }, - { - "id": "company-7", - "inn": "7707083895", - "ogrn": "1027700132197", - "fullName": "Общество с ограниченной ответственностью \"ДомСтрой\"", - "shortName": "ООО \"ДомСтрой\"", - "legalForm": "ООО", - "industry": "Строительство", - "companySize": "50-100", - "website": "https://domstroy.ru", - "logo": "https://via.placeholder.com/100x100/2D3748/FFFFFF?text=DS", - "slogan": "Строим дома мечты", - "rating": 4.3, - "verified": true, - "phone": "+7 (495) 555-12-34", - "email": "info@domstroy.ru", - "legalAddress": "г. Москва, ул. Жилстроительная, д. 25", - "foundedYear": 2015, - "employeeCount": "75 сотрудников" - }, - { - "id": "company-4", - "inn": "7730048038", - "ogrn": "1047730048038", - "fullName": "Общество с ограниченной ответственностью \"МеталлПром\"", - "shortName": "ООО \"МеталлПром\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "250-500", - "website": "https://metallprom.ru", - "logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MP", - "slogan": "Металл высшего качества", - "rating": 4.7, - "verified": true, - "phone": "+7 (495) 456-78-90", - "email": "info@metallprom.ru", - "legalAddress": "г. Москва, ул. Промышленная, д. 50", - "foundedYear": 2008, - "employeeCount": "300 сотрудников" - }, - { - "id": "company-8", - "inn": "7730048040", - "ogrn": "1047730048040", - "fullName": "Общество с ограниченной ответственностью \"СтальМет\"", - "shortName": "ООО \"СтальМет\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "100-250", - "website": "https://stalmet.ru", - "logo": "https://via.placeholder.com/100x100/9C4221/FFFFFF?text=SM", - "slogan": "Сталь для промышленности", - "rating": 4.6, - "verified": true, - "phone": "+7 (495) 777-88-99", - "email": "sales@stalmet.ru", - "legalAddress": "г. Москва, ул. Металлургическая, д. 30", - "foundedYear": 2012, - "employeeCount": "180 сотрудников" - }, - { - "id": "company-9", - "inn": "7730048041", - "ogrn": "1047730048041", - "fullName": "Общество с ограниченной ответственностью \"ПластМаш\"", - "shortName": "ООО \"ПластМаш\"", - "legalForm": "ООО", - "industry": "Производство", - "companySize": "50-100", - "website": "https://plastmash.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PM", - "slogan": "Пластиковые изделия для всех отраслей", - "rating": 4.4, - "verified": true, - "phone": "+7 (495) 333-44-55", - "email": "info@plastmash.ru", - "legalAddress": "г. Москва, ул. Пластиковая, д. 12", - "foundedYear": 2018, - "employeeCount": "80 сотрудников" - }, - { - "id": "company-2", - "inn": "7730048036", - "ogrn": "1047730048036", - "fullName": "Общество с ограниченной ответственностью \"ТехСнаб\"", - "shortName": "ООО \"ТехСнаб\"", - "legalForm": "ООО", - "industry": "Торговля", - "companySize": "50-100", - "website": "https://techsnab.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=TS", - "slogan": "Снабжение для профессионалов", - "rating": 4.5, - "verified": true, - "phone": "+7 (495) 234-56-78", - "email": "sales@techsnab.ru", - "legalAddress": "г. Москва, ул. Торговая, д. 8", - "foundedYear": 2010, - "employeeCount": "90 сотрудников" - }, - { - "id": "company-10", - "inn": "7730048042", - "ogrn": "1047730048042", - "fullName": "Общество с ограниченной ответственностью \"ОптТорг\"", - "shortName": "ООО \"ОптТорг\"", - "legalForm": "ООО", - "industry": "Торговля", - "companySize": "100-250", - "website": "https://opttorg.ru", - "logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=OT", - "slogan": "Оптовые поставки по всей России", - "rating": 4.2, - "verified": true, - "phone": "+7 (495) 111-22-33", - "email": "info@opttorg.ru", - "legalAddress": "г. Москва, ул. Оптовая, д. 45", - "foundedYear": 2005, - "employeeCount": "200 сотрудников" - }, - { - "id": "company-5", - "inn": "7730048039", - "ogrn": "1047730048039", - "fullName": "Общество с ограниченной ответственностью \"ЛогистикПлюс\"", - "shortName": "ООО \"ЛогистикПлюс\"", - "legalForm": "ООО", - "industry": "Логистика", - "companySize": "100-250", - "website": "https://logistikplus.ru", - "logo": "https://via.placeholder.com/100x100/805AD5/FFFFFF?text=LP", - "slogan": "Доставляем быстро и надежно", - "rating": 4.6, - "verified": true, - "phone": "+7 (495) 567-89-01", - "email": "info@logistikplus.ru", - "legalAddress": "г. Москва, ул. Логистическая, д. 20", - "foundedYear": 2013, - "employeeCount": "150 сотрудников" - }, - { - "id": "company-11", - "inn": "7730048043", - "ogrn": "1047730048043", - "fullName": "Общество с ограниченной ответственностью \"ТрансЛогист\"", - "shortName": "ООО \"ТрансЛогист\"", - "legalForm": "ООО", - "industry": "Логистика", - "companySize": "250-500", - "website": "https://translogist.ru", - "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=TL", - "slogan": "Транспортные решения для бизнеса", - "rating": 4.8, - "verified": true, - "phone": "+7 (495) 999-88-77", - "email": "info@translogist.ru", - "legalAddress": "г. Москва, ул. Транспортная, д. 60", - "foundedYear": 2007, - "employeeCount": "350 сотрудников" - }, - { - "id": "company-12", - "inn": "7730048044", - "ogrn": "1047730048044", - "fullName": "Общество с ограниченной ответственностью \"ТехСофт\"", - "shortName": "ООО \"ТехСофт\"", - "legalForm": "ООО", - "industry": "IT", - "companySize": "50-100", - "website": "https://techsoft.ru", - "logo": "https://via.placeholder.com/100x100/3182CE/FFFFFF?text=TS", - "slogan": "IT-решения для бизнеса", - "rating": 4.7, - "verified": true, - "phone": "+7 (495) 444-55-66", - "email": "info@techsoft.ru", - "legalAddress": "г. Москва, ул. Программистов, д. 10", - "foundedYear": 2016, - "employeeCount": "85 сотрудников" - }, - { - "id": "company-13", - "inn": "7730048045", - "ogrn": "1047730048045", - "fullName": "Общество с ограниченной ответственностью \"КиберТех\"", - "shortName": "ООО \"КиберТех\"", - "legalForm": "ООО", - "industry": "IT", - "companySize": "100-250", - "website": "https://cybertech.ru", - "logo": "https://via.placeholder.com/100x100/553C9A/FFFFFF?text=CT", - "slogan": "Кибербезопасность и автоматизация", - "rating": 4.9, - "verified": true, - "phone": "+7 (495) 666-77-88", - "email": "info@cybertech.ru", - "legalAddress": "г. Москва, ул. Кибернетическая, д. 5", - "foundedYear": 2014, - "employeeCount": "120 сотрудников" - }, - { - "id": "company-3", - "inn": "7730048037", - "ogrn": "1047730048037", - "fullName": "Индивидуальный предприниматель Сидоров Петр Иванович", - "shortName": "ИП Сидоров П.И.", - "legalForm": "ИП", - "industry": "Услуги", - "companySize": "1-10", - "website": "https://sidorov-service.ru", - "logo": "https://via.placeholder.com/100x100/D69E2E/FFFFFF?text=SI", - "slogan": "Качественные услуги для малого бизнеса", - "rating": 4.2, - "verified": false, - "phone": "+7 (495) 345-67-89", - "email": "info@sidorov-service.ru", - "legalAddress": "г. Москва, ул. Сервисная, д. 3", - "foundedYear": 2020, - "employeeCount": "5 сотрудников" - }, - { - "id": "company-14", - "inn": "7730048046", - "ogrn": "1047730048046", - "fullName": "Общество с ограниченной ответственностью \"КонсалтПро\"", - "shortName": "ООО \"КонсалтПро\"", - "legalForm": "ООО", - "industry": "Услуги", - "companySize": "10-50", - "website": "https://konsultpro.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=KP", - "slogan": "Консалтинг для роста бизнеса", - "rating": 4.5, - "verified": true, - "phone": "+7 (495) 222-33-44", - "email": "info@konsultpro.ru", - "legalAddress": "г. Москва, ул. Консультационная, д. 15", - "foundedYear": 2017, - "employeeCount": "25 сотрудников" - }, - { - "id": "company-15", - "inn": "7730048047", - "ogrn": "1047730048047", - "fullName": "Общество с ограниченной ответственностью \"ПищеПром\"", - "shortName": "ООО \"ПищеПром\"", - "legalForm": "ООО", - "industry": "Пищевая промышленность", - "companySize": "100-250", - "website": "https://pishcheprom.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=PP", - "slogan": "Качественные продукты питания", - "rating": 4.4, - "verified": true, - "phone": "+7 (495) 888-99-00", - "email": "info@pishcheprom.ru", - "legalAddress": "г. Москва, ул. Пищевая, д. 40", - "foundedYear": 2011, - "employeeCount": "180 сотрудников" - }, - { - "id": "company-16", - "inn": "7730048048", - "ogrn": "1047730048048", - "fullName": "Общество с ограниченной ответственностью \"ЭнергоСервис\"", - "shortName": "ООО \"ЭнергоСервис\"", - "legalForm": "ООО", - "industry": "Энергетика", - "companySize": "50-100", - "website": "https://energoservice.ru", - "logo": "https://via.placeholder.com/100x100/F6AD55/FFFFFF?text=ES", - "slogan": "Энергетические решения", - "rating": 4.6, - "verified": true, - "phone": "+7 (495) 555-66-77", - "email": "info@energoservice.ru", - "legalAddress": "г. Москва, ул. Энергетическая, д. 25", - "foundedYear": 2013, - "employeeCount": "70 сотрудников" - }, - { - "id": "company-17", - "inn": "7730048049", - "ogrn": "1047730048049", - "fullName": "Общество с ограниченной ответственностью \"МедТех\"", - "shortName": "ООО \"МедТех\"", - "legalForm": "ООО", - "industry": "Медицина", - "companySize": "100-250", - "website": "https://medtech.ru", - "logo": "https://via.placeholder.com/100x100/E53E3E/FFFFFF?text=MT", - "slogan": "Медицинские технологии будущего", - "rating": 4.8, - "verified": true, - "phone": "+7 (495) 777-00-11", - "email": "info@medtech.ru", - "legalAddress": "г. Москва, ул. Медицинская, д. 35", - "foundedYear": 2015, - "employeeCount": "200 сотрудников" - }, - { - "id": "company-18", - "inn": "7730048050", - "ogrn": "1047730048050", - "fullName": "Общество с ограниченной ответственностью \"ОбразЦентр\"", - "shortName": "ООО \"ОбразЦентр\"", - "legalForm": "ООО", - "industry": "Образование", - "companySize": "50-100", - "website": "https://obrazcentr.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=OC", - "slogan": "Образование и развитие персонала", - "rating": 4.3, - "verified": true, - "phone": "+7 (495) 333-00-22", - "email": "info@obrazcentr.ru", - "legalAddress": "г. Москва, ул. Образовательная, д. 18", - "foundedYear": 2018, - "employeeCount": "60 сотрудников" - }, - { - "id": "company-19", - "inn": "7730048051", - "ogrn": "1047730048051", - "fullName": "Общество с ограниченной ответственностью \"ФинКонсалт\"", - "shortName": "ООО \"ФинКонсалт\"", - "legalForm": "ООО", - "industry": "Финансы", - "companySize": "10-50", - "website": "https://finkonsalt.ru", - "logo": "https://via.placeholder.com/100x100/2B6CB0/FFFFFF?text=FK", - "slogan": "Финансовое консультирование", - "rating": 4.7, - "verified": true, - "phone": "+7 (495) 444-00-33", - "email": "info@finkonsalt.ru", - "legalAddress": "г. Москва, ул. Финансовая, д. 12", - "foundedYear": 2016, - "employeeCount": "35 сотрудников" - }, - { - "id": "company-20", - "inn": "7730048052", - "ogrn": "1047730048052", - "fullName": "Общество с ограниченной ответственностью \"АгроТех\"", - "shortName": "ООО \"АгроТех\"", - "legalForm": "ООО", - "industry": "Сельское хозяйство", - "companySize": "100-250", - "website": "https://agrotech.ru", - "logo": "https://via.placeholder.com/100x100/38A169/FFFFFF?text=AT", - "slogan": "Современные технологии в сельском хозяйстве", - "rating": 4.5, - "verified": true, - "phone": "+7 (495) 666-00-44", - "email": "info@agrotech.ru", - "legalAddress": "г. Москва, ул. Аграрная, д. 28", - "foundedYear": 2012, - "employeeCount": "160 сотрудников" - } - ] -} diff --git a/server/routers/procurement/mocks/products.json b/server/routers/procurement/mocks/products.json deleted file mode 100644 index 9b9b9a6..0000000 --- a/server/routers/procurement/mocks/products.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "mockProducts": [ - { - "id": "prod-1", - "name": "Металлические конструкции", - "description": "Производство и поставка металлических конструкций любой сложности для строительства", - "category": "Строительные материалы", - "type": "sell", - "companyId": "company-4", - "price": "от 50 000 руб/тонна", - "createdAt": "{{date-10-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-2", - "name": "Стальные балки и профили", - "description": "Высококачественные стальные балки и профили для промышленного строительства", - "category": "Металлопрокат", - "type": "sell", - "companyId": "company-8", - "price": "от 45 000 руб/тонна", - "createdAt": "{{date-8-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-3", - "name": "Пластиковые изделия", - "description": "Производство пластиковых изделий для различных отраслей промышленности", - "category": "Пластик", - "type": "sell", - "companyId": "company-9", - "price": "от 200 руб/кг", - "createdAt": "{{date-15-days}}", - "updatedAt": "{{date-3-days}}" - }, - { - "id": "prod-4", - "name": "Строительные материалы", - "description": "Полный спектр строительных материалов для жилого и коммерческого строительства", - "category": "Строительные материалы", - "type": "sell", - "companyId": "company-1", - "price": "по запросу", - "createdAt": "{{date-20-days}}", - "updatedAt": "{{date-5-days}}" - }, - { - "id": "prod-5", - "name": "IT-решения для бизнеса", - "description": "Разработка программного обеспечения и IT-консалтинг для предприятий", - "category": "IT-услуги", - "type": "sell", - "companyId": "company-12", - "price": "от 100 000 руб/проект", - "createdAt": "{{date-12-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-6", - "name": "Логистические услуги", - "description": "Комплексные логистические услуги по всей России и СНГ", - "category": "Логистика", - "type": "sell", - "companyId": "company-5", - "price": "от 15 руб/км", - "createdAt": "{{date-18-days}}", - "updatedAt": "{{date-4-days}}" - }, - { - "id": "prod-7", - "name": "Пищевая продукция", - "description": "Производство качественных продуктов питания для HoReCa и розничной торговли", - "category": "Пищевая продукция", - "type": "sell", - "companyId": "company-15", - "price": "по прайс-листу", - "createdAt": "{{date-25-days}}", - "updatedAt": "{{date-7-days}}" - }, - { - "id": "prod-8", - "name": "Медицинское оборудование", - "description": "Поставка современного медицинского оборудования и расходных материалов", - "category": "Медицинское оборудование", - "type": "sell", - "companyId": "company-17", - "price": "по запросу", - "createdAt": "{{date-30-days}}", - "updatedAt": "{{date-10-days}}" - }, - { - "id": "prod-9", - "name": "Запчасти для спецтехники", - "description": "Ищем надежного поставщика запчастей для строительной техники Caterpillar, Komatsu", - "category": "Запчасти", - "type": "buy", - "companyId": "company-2", - "budget": "до 500 000 руб", - "createdAt": "{{date-5-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-10", - "name": "Сырье для производства", - "description": "Требуется качественное сырье для производства пластиковых изделий", - "category": "Сырье", - "type": "buy", - "companyId": "company-9", - "budget": "до 1 000 000 руб", - "createdAt": "{{date-7-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-11", - "name": "IT-оборудование", - "description": "Закупка серверного оборудования и сетевого оборудования для офиса", - "category": "IT-оборудование", - "type": "buy", - "companyId": "company-13", - "budget": "до 2 000 000 руб", - "createdAt": "{{date-3-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-12", - "name": "Консалтинговые услуги", - "description": "Требуется консультация по оптимизации бизнес-процессов", - "category": "Консалтинг", - "type": "buy", - "companyId": "company-14", - "budget": "до 300 000 руб", - "createdAt": "{{date-4-days}}", - "updatedAt": "{{date-1-day}}" - }, - { - "id": "prod-13", - "name": "Образовательные программы", - "description": "Поиск поставщика корпоративного обучения для сотрудников", - "category": "Образование", - "type": "buy", - "companyId": "company-18", - "budget": "до 200 000 руб", - "createdAt": "{{date-6-days}}", - "updatedAt": "{{date-2-days}}" - }, - { - "id": "prod-14", - "name": "Финансовые услуги", - "description": "Требуется консультация по инвестиционному планированию", - "category": "Финансовые услуги", - "type": "buy", - "companyId": "company-19", - "budget": "до 150 000 руб", - "createdAt": "{{date-2-days}}", - "updatedAt": "{{date-1-day}}" - } - ] -} diff --git a/server/routers/procurement/mocks/search.json b/server/routers/procurement/mocks/search.json deleted file mode 100644 index e9081c8..0000000 --- a/server/routers/procurement/mocks/search.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "suggestions": [ - "Строительные материалы", - "Металлоконструкции", - "Логистические услуги", - "Промышленное оборудование", - "Запчасти для спецтехники", - "IT-решения", - "Консалтинговые услуги", - "Пищевая продукция", - "Энергетическое оборудование", - "Медицинские технологии", - "Образовательные услуги", - "Финансовые услуги", - "Сельскохозяйственная техника", - "Торговое оборудование", - "Производственные услуги" - ], - "searchHistory": [ - { - "query": "строительные материалы", - "timestamp": "{{date-1-day}}" - }, - { - "query": "металлоконструкции", - "timestamp": "{{date-2-days}}" - }, - { - "query": "логистические услуги", - "timestamp": "{{date-3-days}}" - }, - { - "query": "IT-решения", - "timestamp": "{{date-5-days}}" - }, - { - "query": "консалтинг", - "timestamp": "{{date-7-days}}" - }, - { - "query": "пищевая продукция", - "timestamp": "{{date-10-days}}" - }, - { - "query": "медицинское оборудование", - "timestamp": "{{date-12-days}}" - }, - { - "query": "образовательные услуги", - "timestamp": "{{date-15-days}}" - }, - { - "query": "финансовые услуги", - "timestamp": "{{date-18-days}}" - }, - { - "query": "сельскохозяйственная техника", - "timestamp": "{{date-20-days}}" - } - ], - "savedSearches": [ - { - "id": "saved-1", - "name": "Строительные компании", - "params": { - "industries": ["Строительство"], - "minRating": 4.5 - }, - "createdAt": "{{date-7-days}}" - }, - { - "id": "saved-2", - "name": "Поставщики металла", - "params": { - "query": "металл", - "industries": ["Производство"] - }, - "createdAt": "{{date-14-days}}" - }, - { - "id": "saved-3", - "name": "IT-компании", - "params": { - "industries": ["IT"], - "minRating": 4.0 - }, - "createdAt": "{{date-21-days}}" - }, - { - "id": "saved-4", - "name": "Логистические услуги", - "params": { - "industries": ["Логистика"], - "companySize": ["100-250", "250-500"] - }, - "createdAt": "{{date-28-days}}" - }, - { - "id": "saved-5", - "name": "Консалтинговые услуги", - "params": { - "industries": ["Услуги"], - "minRating": 4.3 - }, - "createdAt": "{{date-35-days}}" - } - ], - "recommendationReasons": { - "Строительство": "Отличная репутация в строительной сфере", - "Производство": "Высокое качество производимой продукции", - "Логистика": "Надежные логистические решения", - "Торговля": "Широкий ассортимент и быстрые поставки", - "IT": "Инновационные IT-решения", - "Услуги": "Профессиональные консалтинговые услуги", - "Пищевая промышленность": "Качественная пищевая продукция", - "Энергетика": "Энергоэффективные решения", - "Медицина": "Современные медицинские технологии", - "Образование": "Эффективные образовательные программы", - "Финансы": "Надежные финансовые услуги", - "Сельское хозяйство": "Современные агротехнологии" - } -} diff --git a/server/routers/procurement/mocks/user.json b/server/routers/procurement/mocks/user.json deleted file mode 100644 index 1d12fb0..0000000 --- a/server/routers/procurement/mocks/user.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "mockUser": { - "id": "user-123", - "email": "test@company.com", - "firstName": "Иван", - "lastName": "Петров", - "position": "Генеральный директор" - }, - "mockTokens": { - "accessToken": "mock-access-token-{{timestamp}}", - "refreshToken": "mock-refresh-token-{{timestamp}}" - } -} diff --git a/server/routers/procurement/models/BuyDocument.js b/server/routers/procurement/models/BuyDocument.js new file mode 100644 index 0000000..34d2661 --- /dev/null +++ b/server/routers/procurement/models/BuyDocument.js @@ -0,0 +1,43 @@ +const mongoose = require('mongoose'); + +const buyDocumentSchema = new mongoose.Schema({ + id: { + type: String, + required: true, + unique: true, + index: true + }, + ownerCompanyId: { + type: String, + required: true, + index: true + }, + name: { + type: String, + required: true + }, + type: { + type: String, + required: true + }, + size: { + type: Number, + required: true + }, + filePath: { + type: String, + required: true + }, + acceptedBy: { + type: [String], + default: [] + }, + createdAt: { + type: Date, + default: Date.now, + index: true + } +}); + +module.exports = mongoose.model('BuyDocument', buyDocumentSchema); + diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js index 5396ebd..6828b12 100644 --- a/server/routers/procurement/models/BuyProduct.js +++ b/server/routers/procurement/models/BuyProduct.js @@ -30,6 +30,7 @@ const buyProductSchema = new mongoose.Schema({ url: String, type: String, size: Number, + storagePath: String, uploadedAt: { type: Date, default: Date.now diff --git a/server/routers/procurement/models/Experience.js b/server/routers/procurement/models/Experience.js new file mode 100644 index 0000000..09dd018 --- /dev/null +++ b/server/routers/procurement/models/Experience.js @@ -0,0 +1,46 @@ +const mongoose = require('mongoose'); + +const experienceSchema = new mongoose.Schema({ + companyId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Company', + required: true, + index: true + }, + confirmed: { + type: Boolean, + default: false + }, + customer: { + type: String, + required: true + }, + subject: { + type: String, + required: true + }, + volume: { + type: String + }, + contact: { + type: String + }, + comment: { + type: String + }, + createdAt: { + type: Date, + default: Date.now, + index: true + }, + updatedAt: { + type: Date, + default: Date.now + } +}); + +// Индексы для оптимизации поиска +experienceSchema.index({ companyId: 1, createdAt: -1 }); + +module.exports = mongoose.model('Experience', experienceSchema); + diff --git a/server/routers/procurement/models/Request.js b/server/routers/procurement/models/Request.js index 6a14fab..88f921d 100644 --- a/server/routers/procurement/models/Request.js +++ b/server/routers/procurement/models/Request.js @@ -11,6 +11,12 @@ const requestSchema = new mongoose.Schema({ required: true, index: true }, + subject: { + type: String, + required: false, + trim: true, + default: '' + }, text: { type: String, required: true @@ -21,6 +27,7 @@ const requestSchema = new mongoose.Schema({ url: String, type: String, size: Number, + storagePath: String, uploadedAt: { type: Date, default: Date.now @@ -39,6 +46,18 @@ const requestSchema = new mongoose.Schema({ type: String, default: null }, + responseFiles: [{ + id: String, + name: String, + url: String, + type: String, + size: Number, + storagePath: String, + uploadedAt: { + type: Date, + default: Date.now + } + }], respondedAt: { type: Date, default: null @@ -58,5 +77,6 @@ const requestSchema = new mongoose.Schema({ requestSchema.index({ senderCompanyId: 1, createdAt: -1 }); requestSchema.index({ recipientCompanyId: 1, createdAt: -1 }); requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 }); +requestSchema.index({ subject: 1, createdAt: -1 }); module.exports = mongoose.model('Request', requestSchema); diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 035d0b0..c9a2bba 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -1,8 +1,101 @@ const express = require('express'); const router = express.Router(); -const { generateToken } = require('../middleware/auth'); +const { generateToken, verifyToken } = require('../middleware/auth'); const User = require('../models/User'); const Company = require('../models/Company'); +const Request = require('../models/Request'); +const BuyProduct = require('../models/BuyProduct'); +const Message = require('../models/Message'); +const Review = require('../models/Review'); +const mongoose = require('mongoose'); +const { Types } = mongoose; +const connectDB = require('../config/db'); + +const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796'); +const PRESET_USER_EMAIL = 'admin@test-company.ru'; + +const changePasswordFlow = async (userId, currentPassword, newPassword) => { + if (!currentPassword || !newPassword) { + return { status: 400, body: { error: 'Current password and new password are required' } }; + } + + if (typeof newPassword !== 'string' || newPassword.trim().length < 8) { + return { status: 400, body: { error: 'New password must be at least 8 characters long' } }; + } + + const user = await User.findById(userId); + + if (!user) { + return { status: 404, body: { error: 'User not found' } }; + } + + const isMatch = await user.comparePassword(currentPassword); + + if (!isMatch) { + return { status: 400, body: { error: 'Current password is incorrect' } }; + } + + user.password = newPassword; + user.updatedAt = new Date(); + await user.save(); + + return { status: 200, body: { message: 'Password updated successfully' } }; +}; + +const deleteAccountFlow = async (userId, password) => { + if (!password) { + return { status: 400, body: { error: 'Password is required to delete account' } }; + } + + const user = await User.findById(userId); + + if (!user) { + return { status: 404, body: { error: 'User not found' } }; + } + + const validPassword = await user.comparePassword(password); + + if (!validPassword) { + return { status: 400, body: { error: 'Password is incorrect' } }; + } + + const companyId = user.companyId ? user.companyId.toString() : null; + const companyObjectId = companyId && Types.ObjectId.isValid(companyId) ? new Types.ObjectId(companyId) : null; + + const cleanupTasks = []; + + if (companyId) { + cleanupTasks.push(Request.deleteMany({ + $or: [{ senderCompanyId: companyId }, { recipientCompanyId: companyId }], + })); + + cleanupTasks.push(BuyProduct.deleteMany({ companyId })); + + if (companyObjectId) { + cleanupTasks.push(Message.deleteMany({ + $or: [ + { senderCompanyId: companyObjectId }, + { recipientCompanyId: companyObjectId }, + ], + })); + + cleanupTasks.push(Review.deleteMany({ + $or: [ + { companyId: companyObjectId }, + { authorCompanyId: companyObjectId }, + ], + })); + } + + cleanupTasks.push(Company.findByIdAndDelete(companyId)); + } + + cleanupTasks.push(User.findByIdAndDelete(user._id)); + + await Promise.all(cleanupTasks); + + return { status: 200, body: { message: 'Account deleted successfully' } }; +}; // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -15,16 +108,65 @@ const log = (message, data = '') => { } }; -// In-memory storage для логирования -let users = []; +const waitForDatabaseConnection = async () => { + const isAuthFailure = (error) => { + if (!error) return false; + if (error.code === 13 || error.code === 18) return true; + return /auth/i.test(String(error.message || '')); + }; + + const verifyAuth = async () => { + try { + await mongoose.connection.db.admin().command({ listDatabases: 1 }); + return true; + } catch (error) { + if (isAuthFailure(error)) { + return false; + } + throw error; + } + }; + + for (let attempt = 0; attempt < 3; attempt++) { + if (mongoose.connection.readyState === 1) { + const authed = await verifyAuth(); + if (authed) { + return; + } + await mongoose.connection.close().catch(() => {}); + } + + try { + const connection = await connectDB(); + if (!connection) { + break; + } + + const authed = await verifyAuth(); + if (authed) { + return; + } + + await mongoose.connection.close().catch(() => {}); + } catch (error) { + if (!isAuthFailure(error)) { + throw error; + } + } + } + + throw new Error('Unable to authenticate with MongoDB'); +}; // Инициализация тестового пользователя const initializeTestUser = async () => { try { - const existingUser = await User.findOne({ email: 'admin@test-company.ru' }); - if (!existingUser) { - // Создать компанию - const company = await Company.create({ + await waitForDatabaseConnection(); + + let company = await Company.findById(PRESET_COMPANY_ID); + if (!company) { + company = await Company.create({ + _id: PRESET_COMPANY_ID, fullName: 'ООО "Тестовая Компания"', shortName: 'ООО "Тест"', inn: '7707083893', @@ -39,131 +181,61 @@ const initializeTestUser = async () => { description: 'Ведущая компания в области производства', slogan: 'Качество и инновация' }); + log('✅ Test company initialized'); + } else { + await Company.updateOne( + { _id: PRESET_COMPANY_ID }, + { + $set: { + fullName: 'ООО "Тестовая Компания"', + shortName: 'ООО "Тест"', + industry: 'Производство', + companySize: '50-100', + partnerGeography: ['moscow', 'russia_all'], + website: 'https://test-company.ru', + }, + } + ); + } - // Создать пользователя - const user = await User.create({ - email: 'admin@test-company.ru', + let existingUser = await User.findOne({ email: PRESET_USER_EMAIL }); + if (!existingUser) { + existingUser = await User.create({ + email: PRESET_USER_EMAIL, password: 'SecurePass123!', firstName: 'Иван', lastName: 'Петров', position: 'Генеральный директор', - companyId: company._id + companyId: PRESET_COMPANY_ID }); - log('✅ Test user initialized'); - } - - // Инициализация других тестовых компаний - const mockCompanies = [ - { - fullName: 'ООО "СтройКомплект"', - shortName: 'ООО "СтройКомплект"', - inn: '7707083894', - ogrn: '1027700132196', - legalForm: 'ООО', - industry: 'Строительство', - companySize: '51-250', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://stroykomplekt.ru', - verified: true, - rating: 4.8, - description: 'Компания строит будущее вместе', - slogan: 'Строим будущее вместе' - }, - { - fullName: 'АО "Московский Строй"', - shortName: 'АО "Московский Строй"', - inn: '7707083895', - ogrn: '1027700132197', - legalForm: 'АО', - industry: 'Строительство', - companySize: '500+', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://moscow-stroy.ru', - verified: true, - rating: 4.9, - description: 'Качество и надежность с 1995 года', - slogan: 'Качество и надежность' - }, - { - fullName: 'ООО "ТеxПроект"', - shortName: 'ООО "ТеxПроект"', - inn: '7707083896', - ogrn: '1027700132198', - legalForm: 'ООО', - industry: 'IT', - companySize: '11-50', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://techproject.ru', - verified: true, - rating: 4.6, - description: 'Решения в области информационных технологий', - slogan: 'Технологии для бизнеса' - }, - { - fullName: 'ООО "ТоргПартнер"', - shortName: 'ООО "ТоргПартнер"', - inn: '7707083897', - ogrn: '1027700132199', - legalForm: 'ООО', - industry: 'Оптовая торговля', - companySize: '51-250', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://torgpartner.ru', - verified: true, - rating: 4.3, - description: 'Оптовые поставки и логистика', - slogan: 'Надежный партнер в торговле' - }, - { - fullName: 'ООО "ЭнергоПлюс"', - shortName: 'ООО "ЭнергоПлюс"', - inn: '7707083898', - ogrn: '1027700132200', - legalForm: 'ООО', - industry: 'Энергетика', - companySize: '251-500', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://energoplus.ru', - verified: true, - rating: 4.7, - description: 'Энергетические решения и консалтинг', - slogan: 'Энергия для развития' - } - ]; - - for (const mockCompanyData of mockCompanies) { - try { - const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); - if (!existingCompany) { - await Company.create(mockCompanyData); - log(`✅ Mock company created: ${mockCompanyData.fullName}`); - } - } catch (err) { - // Ignore errors for mock company creation - это может быть ошибка аутентификации - log(`ℹ️ Mock company init failed: ${mockCompanyData.fullName}`); - } + } else if (!existingUser.companyId || existingUser.companyId.toString() !== PRESET_COMPANY_ID.toString()) { + existingUser.companyId = PRESET_COMPANY_ID; + existingUser.updatedAt = new Date(); + await existingUser.save(); + log('ℹ️ Test user company reference was fixed'); } } catch (error) { - // Ошибка аутентификации или другие ошибки БД - продолжаем работу - if (error.message && error.message.includes('authentication')) { - log('ℹ️ Database authentication required - test data initialization deferred'); - } else { - console.error('Error initializing test data:', error.message); + console.error('Error initializing test data:', error.message); + if (error?.code === 13 || /auth/i.test(error?.message || '')) { + try { + await connectDB(); + } catch (connectError) { + if (process.env.DEV === 'true') { + console.error('Failed to re-connect after auth error:', connectError.message); + } + } } } }; -// Пытаемся инициализировать с задержкой (даёт время на подключение) -setTimeout(() => { - initializeTestUser().catch(err => { - log(`⚠️ Deferred test data initialization failed: ${err.message}`); - }); -}, 2000); +initializeTestUser(); // Регистрация router.post('/register', async (req, res) => { try { + await waitForDatabaseConnection(); + const { email, password, firstName, lastName, position, phone, fullName, inn, ogrn, legalForm, industry, companySize, website } = req.body; // Проверка обязательных полей @@ -250,6 +322,14 @@ router.post('/register', async (req, res) => { // Вход router.post('/login', async (req, res) => { try { + if (process.env.DEV === 'true') { + console.log('[Auth] /login called'); + } + await waitForDatabaseConnection(); + if (process.env.DEV === 'true') { + console.log('[Auth] DB ready, running login query'); + } + const { email, password } = req.body; if (!email || !password) { @@ -266,104 +346,54 @@ router.post('/login', async (req, res) => { return res.status(401).json({ error: 'Invalid credentials' }); } - // Инициализация других тестовых компаний - const mockCompanies = [ - { - fullName: 'ООО "СтройКомплект"', - shortName: 'ООО "СтройКомплект"', - inn: '7707083894', - ogrn: '1027700132196', - legalForm: 'ООО', - industry: 'Строительство', - companySize: '51-250', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://stroykomplekt.ru', - verified: true, - rating: 4.8, - description: 'Компания строит будущее вместе', - slogan: 'Строим будущее вместе' - }, - { - fullName: 'АО "Московский Строй"', - shortName: 'АО "Московский Строй"', - inn: '7707083895', - ogrn: '1027700132197', - legalForm: 'АО', - industry: 'Строительство', - companySize: '500+', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://moscow-stroy.ru', - verified: true, - rating: 4.9, - description: 'Качество и надежность с 1995 года', - slogan: 'Качество и надежность' - }, - { - fullName: 'ООО "ТеxПроект"', - shortName: 'ООО "ТеxПроект"', - inn: '7707083896', - ogrn: '1027700132198', - legalForm: 'ООО', - industry: 'IT', - companySize: '11-50', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://techproject.ru', - verified: true, - rating: 4.6, - description: 'Решения в области информационных технологий', - slogan: 'Технологии для бизнеса' - }, - { - fullName: 'ООО "ТоргПартнер"', - shortName: 'ООО "ТоргПартнер"', - inn: '7707083897', - ogrn: '1027700132199', - legalForm: 'ООО', - industry: 'Оптовая торговля', - companySize: '51-250', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://torgpartner.ru', - verified: true, - rating: 4.3, - description: 'Оптовые поставки и логистика', - slogan: 'Надежный партнер в торговле' - }, - { - fullName: 'ООО "ЭнергоПлюс"', - shortName: 'ООО "ЭнергоПлюс"', - inn: '7707083898', - ogrn: '1027700132200', - legalForm: 'ООО', - industry: 'Энергетика', - companySize: '251-500', - partnerGeography: ['moscow', 'russia_all'], - website: 'https://energoplus.ru', - verified: true, - rating: 4.7, - description: 'Энергетические решения и консалтинг', - slogan: 'Энергия для развития' - } - ]; - - for (const mockCompanyData of mockCompanies) { - try { - const existingCompany = await Company.findOne({ inn: mockCompanyData.inn }); - if (!existingCompany) { - await Company.create(mockCompanyData); - } - } catch (err) { - // Ignore errors for mock company creation - } + if ( + user.email === PRESET_USER_EMAIL && + (!user.companyId || user.companyId.toString() !== PRESET_COMPANY_ID.toString()) + ) { + await User.updateOne( + { _id: user._id }, + { $set: { companyId: PRESET_COMPANY_ID, updatedAt: new Date() } } + ); + user.companyId = PRESET_COMPANY_ID; } // Получить компанию до использования в generateToken let companyData = null; try { - companyData = await Company.findById(user.companyId); + companyData = user.companyId ? await Company.findById(user.companyId) : null; } catch (err) { console.error('Failed to fetch company:', err.message); } + if (user.email === PRESET_USER_EMAIL) { + try { + companyData = await Company.findByIdAndUpdate( + PRESET_COMPANY_ID, + { + $set: { + fullName: 'ООО "Тестовая Компания"', + shortName: 'ООО "Тест"', + inn: '7707083893', + ogrn: '1027700132195', + legalForm: 'ООО', + industry: 'Производство', + companySize: '50-100', + partnerGeography: ['moscow', 'russia_all'], + website: 'https://test-company.ru', + verified: true, + rating: 4.5, + description: 'Ведущая компания в области производства', + slogan: 'Качество и инновация', + updatedAt: new Date(), + }, + }, + { upsert: true, new: true, setDefaultsOnInsert: true } + ); + } catch (err) { + console.error('Failed to ensure preset company:', err.message); + } + } + const token = generateToken(user._id.toString(), user.companyId.toString(), user.firstName, user.lastName, companyData?.fullName || 'Company'); log('✅ Token generated for user:', user._id); @@ -388,14 +418,56 @@ router.post('/login', async (req, res) => { }); } catch (error) { console.error('Login error:', error); + res.status(500).json({ error: `LOGIN_ERROR: ${error.message}` }); + } +}); + +// Смена пароля +router.post('/change-password', verifyToken, async (req, res) => { + try { + const { currentPassword, newPassword } = req.body || {}; + const result = await changePasswordFlow(req.userId, currentPassword, newPassword); + res.status(result.status).json(result.body); + } catch (error) { + console.error('Change password error:', error); res.status(500).json({ error: error.message }); } }); -// Обновить профиль -router.patch('/profile', (req, res) => { - // требует авторизации, добавить middleware - res.json({ message: 'Update profile endpoint' }); +// Удаление аккаунта +router.delete('/account', verifyToken, async (req, res) => { + try { + const { password } = req.body || {}; + const result = await deleteAccountFlow(req.userId, password); + res.status(result.status).json(result.body); + } catch (error) { + console.error('Delete account error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Обновить профиль / универсальные действия +router.patch('/profile', verifyToken, async (req, res) => { + try { + const rawAction = req.body?.action || req.query?.action || req.body?.type; + const payload = req.body?.payload || req.body || {}; + const action = typeof rawAction === 'string' ? rawAction : ''; + + if (action === 'changePassword') { + const result = await changePasswordFlow(req.userId, payload.currentPassword, payload.newPassword); + return res.status(result.status).json(result.body); + } + + if (action === 'deleteAccount') { + const result = await deleteAccountFlow(req.userId, payload.password); + return res.status(result.status).json(result.body); + } + + res.json({ message: 'Profile endpoint' }); + } catch (error) { + console.error('Profile update error:', error); + res.status(500).json({ error: error.message }); + } }); module.exports = router; diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 53dadb5..d23ea01 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -2,6 +2,7 @@ const express = require('express') const fs = require('fs') const path = require('path') const router = express.Router() +const BuyDocument = require('../models/BuyDocument') // Create remote-assets/docs directory if it doesn't exist const docsDir = path.join(__dirname, '../../remote-assets/docs') @@ -9,155 +10,189 @@ if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }) } -// In-memory store for documents metadata -const buyDocs = [] - function generateId() { return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` } // GET /buy/docs?ownerCompanyId=... -router.get('/docs', (req, res) => { - const { ownerCompanyId } = req.query - console.log('[BUY API] GET /docs', { ownerCompanyId, totalDocs: buyDocs.length }) - let result = buyDocs - if (ownerCompanyId) { - result = result.filter((d) => d.ownerCompanyId === ownerCompanyId) +router.get('/docs', async (req, res) => { + try { + const { ownerCompanyId } = req.query + console.log('[BUY API] GET /docs', { ownerCompanyId }) + + let query = {} + if (ownerCompanyId) { + query.ownerCompanyId = ownerCompanyId + } + + const docs = await BuyDocument.find(query).sort({ createdAt: -1 }) + + const result = docs.map(doc => ({ + ...doc.toObject(), + url: `/api/buy/docs/${doc.id}/file` + })) + + res.json(result) + } catch (error) { + console.error('[BUY API] Error fetching docs:', error) + res.status(500).json({ error: 'Failed to fetch documents' }) } - result = result.map(doc => ({ - ...doc, - url: `/api/buy/docs/${doc.id}/file` - })) - res.json(result) }) // POST /buy/docs -router.post('/docs', (req, res) => { - const { ownerCompanyId, name, type, fileData } = req.body || {} - console.log('[BUY API] POST /docs', { ownerCompanyId, name, type }) - if (!ownerCompanyId || !name || !type) { - return res.status(400).json({ error: 'ownerCompanyId, name and type are required' }) - } - - if (!fileData) { - return res.status(400).json({ error: 'fileData is required' }) - } - - const id = generateId() - - // Save file to disk +router.post('/docs', async (req, res) => { try { + const { ownerCompanyId, name, type, fileData } = req.body || {} + console.log('[BUY API] POST /docs', { ownerCompanyId, name, type }) + + if (!ownerCompanyId || !name || !type) { + return res.status(400).json({ error: 'ownerCompanyId, name and type are required' }) + } + + if (!fileData) { + return res.status(400).json({ error: 'fileData is required' }) + } + + const id = generateId() + + // Save file to disk const binaryData = Buffer.from(fileData, 'base64') const filePath = path.join(docsDir, `${id}.${type}`) fs.writeFileSync(filePath, binaryData) console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) const size = binaryData.length - const url = `/api/buy/docs/${id}/file` - const doc = { + + const doc = await BuyDocument.create({ id, ownerCompanyId, name, type, size, - url, filePath, - acceptedBy: [], - createdAt: new Date().toISOString(), - } - buyDocs.unshift(doc) + acceptedBy: [] + }) + console.log('[BUY API] Document created:', id) - res.status(201).json(doc) + + res.status(201).json({ + ...doc.toObject(), + url: `/api/buy/docs/${doc.id}/file` + }) } catch (e) { console.error(`[BUY API] Error saving file: ${e.message}`) res.status(500).json({ error: 'Failed to save file' }) } }) -router.post('/docs/:id/accept', (req, res) => { - const { id } = req.params - const { companyId } = req.body || {} - console.log('[BUY API] POST /docs/:id/accept', { id, companyId }) - const doc = buyDocs.find((d) => d.id === id) - if (!doc) { - console.log('[BUY API] Document not found:', id) - return res.status(404).json({ error: 'Document not found' }) +router.post('/docs/:id/accept', async (req, res) => { + try { + const { id } = req.params + const { companyId } = req.body || {} + console.log('[BUY API] POST /docs/:id/accept', { id, companyId }) + + if (!companyId) { + return res.status(400).json({ error: 'companyId is required' }) + } + + const doc = await BuyDocument.findOne({ id }) + if (!doc) { + console.log('[BUY API] Document not found:', id) + return res.status(404).json({ error: 'Document not found' }) + } + + if (!doc.acceptedBy.includes(companyId)) { + doc.acceptedBy.push(companyId) + await doc.save() + } + + res.json({ id: doc.id, acceptedBy: doc.acceptedBy }) + } catch (error) { + console.error('[BUY API] Error accepting document:', error) + res.status(500).json({ error: 'Failed to accept document' }) } - if (!companyId) { - return res.status(400).json({ error: 'companyId is required' }) - } - if (!doc.acceptedBy.includes(companyId)) { - doc.acceptedBy.push(companyId) - } - res.json({ id: doc.id, acceptedBy: doc.acceptedBy }) }) -router.get('/docs/:id/delete', (req, res) => { - const { id } = req.params - console.log('[BUY API] GET /docs/:id/delete', { id, totalDocs: buyDocs.length }) - const index = buyDocs.findIndex((d) => d.id === id) - if (index === -1) { - console.log('[BUY API] Document not found for deletion:', id) - return res.status(404).json({ error: 'Document not found' }) - } - const deletedDoc = buyDocs.splice(index, 1)[0] - - // Delete file from disk - if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) { - try { - fs.unlinkSync(deletedDoc.filePath) - console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`) - } catch (e) { - console.error(`[BUY API] Error deleting file: ${e.message}`) +router.get('/docs/:id/delete', async (req, res) => { + try { + const { id } = req.params + console.log('[BUY API] GET /docs/:id/delete', { id }) + + const doc = await BuyDocument.findOne({ id }) + if (!doc) { + console.log('[BUY API] Document not found for deletion:', id) + return res.status(404).json({ error: 'Document not found' }) } + + // Delete file from disk + if (doc.filePath && fs.existsSync(doc.filePath)) { + try { + fs.unlinkSync(doc.filePath) + console.log(`[BUY API] File deleted: ${doc.filePath}`) + } catch (e) { + console.error(`[BUY API] Error deleting file: ${e.message}`) + } + } + + await BuyDocument.deleteOne({ id }) + + console.log('[BUY API] Document deleted via GET:', id) + res.json({ id: doc.id, success: true }) + } catch (error) { + console.error('[BUY API] Error deleting document:', error) + res.status(500).json({ error: 'Failed to delete document' }) } - - console.log('[BUY API] Document deleted via GET:', id, { remainingDocs: buyDocs.length }) - res.json({ id: deletedDoc.id, success: true }) }) -router.delete('/docs/:id', (req, res) => { - const { id } = req.params - console.log('[BUY API] DELETE /docs/:id', { id, totalDocs: buyDocs.length }) - const index = buyDocs.findIndex((d) => d.id === id) - if (index === -1) { - console.log('[BUY API] Document not found for deletion:', id) - return res.status(404).json({ error: 'Document not found' }) - } - const deletedDoc = buyDocs.splice(index, 1)[0] - - // Delete file from disk - if (deletedDoc.filePath && fs.existsSync(deletedDoc.filePath)) { - try { - fs.unlinkSync(deletedDoc.filePath) - console.log(`[BUY API] File deleted: ${deletedDoc.filePath}`) - } catch (e) { - console.error(`[BUY API] Error deleting file: ${e.message}`) +router.delete('/docs/:id', async (req, res) => { + try { + const { id } = req.params + console.log('[BUY API] DELETE /docs/:id', { id }) + + const doc = await BuyDocument.findOne({ id }) + if (!doc) { + console.log('[BUY API] Document not found for deletion:', id) + return res.status(404).json({ error: 'Document not found' }) } + + // Delete file from disk + if (doc.filePath && fs.existsSync(doc.filePath)) { + try { + fs.unlinkSync(doc.filePath) + console.log(`[BUY API] File deleted: ${doc.filePath}`) + } catch (e) { + console.error(`[BUY API] Error deleting file: ${e.message}`) + } + } + + await BuyDocument.deleteOne({ id }) + + console.log('[BUY API] Document deleted:', id) + res.json({ id: doc.id, success: true }) + } catch (error) { + console.error('[BUY API] Error deleting document:', error) + res.status(500).json({ error: 'Failed to delete document' }) } - - console.log('[BUY API] Document deleted:', id, { remainingDocs: buyDocs.length }) - res.json({ id: deletedDoc.id, success: true }) }) // GET /buy/docs/:id/file - Serve the file -router.get('/docs/:id/file', (req, res) => { - const { id } = req.params - console.log('[BUY API] GET /docs/:id/file', { id }) - - const doc = buyDocs.find(d => d.id === id) - if (!doc) { - console.log('[BUY API] Document not found:', id) - return res.status(404).json({ error: 'Document not found' }) - } - - const filePath = path.join(docsDir, `${id}.${doc.type}`) - if (!fs.existsSync(filePath)) { - console.log('[BUY API] File not found on disk:', filePath) - return res.status(404).json({ error: 'File not found on disk' }) - } - +router.get('/docs/:id/file', async (req, res) => { try { + const { id } = req.params + console.log('[BUY API] GET /docs/:id/file', { id }) + + const doc = await BuyDocument.findOne({ id }) + if (!doc) { + console.log('[BUY API] Document not found:', id) + return res.status(404).json({ error: 'Document not found' }) + } + + const filePath = path.join(docsDir, `${id}.${doc.type}`) + if (!fs.existsSync(filePath)) { + console.log('[BUY API] File not found on disk:', filePath) + return res.status(404).json({ error: 'File not found on disk' }) + } + const fileBuffer = fs.readFileSync(filePath) const mimeTypes = { @@ -170,7 +205,6 @@ router.get('/docs/:id/file', (req, res) => { const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_') res.setHeader('Content-Type', mimeType) - // RFC 5987 encoding: filename for ASCII fallback, filename* for UTF-8 with percent-encoding const encodedFilename = encodeURIComponent(`${doc.name}.${doc.type}`) res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}.${doc.type}"; filename*=UTF-8''${encodedFilename}`) res.setHeader('Content-Length', fileBuffer.length) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index 2aabc93..9ee74fe 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -2,6 +2,74 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const BuyProduct = require('../models/BuyProduct'); +const path = require('path'); +const fs = require('fs'); +const multer = require('multer'); +const UPLOADS_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'buy-products'); +const ensureDirectory = (dirPath) => { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +}; + +ensureDirectory(UPLOADS_ROOT); + +const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB +const ALLOWED_MIME_TYPES = new Set([ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', +]); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const productId = req.params.id || 'common'; + const productDir = path.join(UPLOADS_ROOT, productId); + ensureDirectory(productDir); + cb(null, productDir); + }, + filename: (req, file, cb) => { + const originalExtension = path.extname(file.originalname) || ''; + const baseName = path + .basename(file.originalname, originalExtension) + .replace(/[^a-zA-Z0-9-_]+/g, '_') + .toLowerCase(); + cb(null, `${Date.now()}_${baseName}${originalExtension}`); + }, +}); + +const upload = multer({ + storage, + limits: { + fileSize: MAX_FILE_SIZE, + }, + fileFilter: (req, file, cb) => { + if (ALLOWED_MIME_TYPES.has(file.mimetype)) { + cb(null, true); + return; + } + + req.fileValidationError = 'UNSUPPORTED_FILE_TYPE'; + cb(null, false); + }, +}); + +const handleSingleFileUpload = (req, res, next) => { + upload.single('file')(req, res, (err) => { + if (err) { + console.error('[BuyProducts] Multer error:', err.message); + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ error: 'File is too large. Maximum size is 15MB.' }); + } + return res.status(400).json({ error: err.message }); + } + next(); + }); +}; + // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -43,7 +111,7 @@ router.post('/', verifyToken, async (req, res) => { try { const { name, description, quantity, unit, status } = req.body; - log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.user.companyId }); + log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.companyId }); if (!name || !description || !quantity) { return res.status(400).json({ @@ -58,7 +126,7 @@ router.post('/', verifyToken, async (req, res) => { } const newProduct = new BuyProduct({ - companyId: req.user.companyId, + companyId: req.companyId, name: name.trim(), description: description.trim(), quantity: quantity.trim(), @@ -97,7 +165,7 @@ router.put('/:id', verifyToken, async (req, res) => { } // Проверить, что товар принадлежит текущей компании - if (product.companyId !== req.user.companyId) { + if (product.companyId !== req.companyId) { return res.status(403).json({ error: 'Not authorized' }); } @@ -134,7 +202,7 @@ router.delete('/:id', verifyToken, async (req, res) => { return res.status(404).json({ error: 'Product not found' }); } - if (product.companyId.toString() !== req.user.companyId.toString()) { + if (product.companyId.toString() !== req.companyId.toString()) { return res.status(403).json({ error: 'Not authorized' }); } @@ -153,11 +221,9 @@ router.delete('/:id', verifyToken, async (req, res) => { }); // POST /buy-products/:id/files - добавить файл к товару -router.post('/:id/files', verifyToken, async (req, res) => { +router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) => { try { const { id } = req.params; - const { fileName, fileUrl, fileType, fileSize } = req.body; - const product = await BuyProduct.findById(id); if (!product) { @@ -165,23 +231,33 @@ router.post('/:id/files', verifyToken, async (req, res) => { } // Только владелец товара может добавить файл - if (product.companyId.toString() !== req.user.companyId.toString()) { + if (product.companyId.toString() !== req.companyId.toString()) { return res.status(403).json({ error: 'Not authorized' }); } + if (req.fileValidationError) { + return res.status(400).json({ error: 'Unsupported file type. Use PDF, DOC, DOCX, XLS, XLSX or CSV.' }); + } + + if (!req.file) { + return res.status(400).json({ error: 'File is required' }); + } + + const relativePath = path.join('buy-products', id, req.file.filename).replace(/\\/g, '/'); const file = { - id: 'file-' + Date.now(), - name: fileName, - url: fileUrl, - type: fileType, - size: fileSize, - uploadedAt: new Date() + id: `file-${Date.now()}`, + name: req.file.originalname, + url: `/uploads/${relativePath}`, + type: req.file.mimetype, + size: req.file.size, + uploadedAt: new Date(), + storagePath: relativePath, }; product.files.push(file); await product.save(); - log('[BuyProducts] File added to product:', id); + log('[BuyProducts] File added to product:', id, file.name); res.json(product); } catch (error) { @@ -204,14 +280,28 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { return res.status(404).json({ error: 'Product not found' }); } - if (product.companyId.toString() !== req.user.companyId.toString()) { + if (product.companyId.toString() !== req.companyId.toString()) { return res.status(403).json({ error: 'Not authorized' }); } + const fileToRemove = product.files.find((f) => f.id === fileId); + if (!fileToRemove) { + return res.status(404).json({ error: 'File not found' }); + } + product.files = product.files.filter(f => f.id !== fileId); await product.save(); - log('[BuyProducts] File deleted from product:', id); + const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, ''); + const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', storedPath); + + fs.promises.unlink(absolutePath).catch((unlinkError) => { + if (unlinkError && unlinkError.code !== 'ENOENT') { + console.error('[BuyProducts] Failed to remove file from disk:', unlinkError.message); + } + }); + + log('[BuyProducts] File deleted from product:', id, fileId); res.json(product); } catch (error) { @@ -227,7 +317,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { router.post('/:id/accept', verifyToken, async (req, res) => { try { const { id } = req.params; - const companyId = req.user.companyId; + const companyId = req.companyId; const product = await BuyProduct.findById(id); diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 0c70e21..5fd84e9 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -2,17 +2,10 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Company = require('../models/Company'); - -// Инициализация данных при запуске -const initializeCompanies = async () => { - try { - // Уже не нужна инициализация, она производится через authAPI - } catch (error) { - console.error('Error initializing companies:', error); - } -}; - -initializeCompanies(); +const Experience = require('../models/Experience'); +const Request = require('../models/Request'); +const Message = require('../models/Message'); +const { Types } = require('mongoose'); // GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id router.get('/my/info', verifyToken, async (req, res) => { @@ -44,23 +37,64 @@ router.get('/my/info', verifyToken, async (req, res) => { router.get('/my/stats', verifyToken, async (req, res) => { try { const userId = req.userId; - const user = await require('../models/User').findById(userId); - - if (!user || !user.companyId) { - return res.status(404).json({ error: 'Company not found' }); + const User = require('../models/User'); + const user = await User.findById(userId); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); } - + + let companyId = user.companyId; + + if (!companyId) { + const fallbackCompany = await Company.create({ + fullName: 'Компания пользователя', + shortName: 'Компания пользователя', + verified: false, + partnerGeography: [], + }); + + user.companyId = fallbackCompany._id; + user.updatedAt = new Date(); + await user.save(); + companyId = fallbackCompany._id; + } + + let company = await Company.findById(companyId); + + if (!company) { + company = await Company.create({ + _id: companyId, + fullName: 'Компания пользователя', + verified: false, + partnerGeography: [], + }); + } + + const companyIdString = company._id.toString(); + const companyObjectId = Types.ObjectId.isValid(companyIdString) + ? new Types.ObjectId(companyIdString) + : null; + + const [sentRequests, receivedRequests, unreadMessages] = await Promise.all([ + Request.countDocuments({ senderCompanyId: companyIdString }), + Request.countDocuments({ recipientCompanyId: companyIdString }), + companyObjectId + ? Message.countDocuments({ recipientCompanyId: companyObjectId, read: false }) + : Promise.resolve(0), + ]); + const stats = { - profileViews: Math.floor(Math.random() * 1000), - profileViewsChange: Math.floor(Math.random() * 20 - 10), - sentRequests: Math.floor(Math.random() * 50), - sentRequestsChange: Math.floor(Math.random() * 10 - 5), - receivedRequests: Math.floor(Math.random() * 30), - receivedRequestsChange: Math.floor(Math.random() * 5 - 2), - newMessages: Math.floor(Math.random() * 10), - rating: Math.random() * 5 + profileViews: company?.metrics?.profileViews || 0, + profileViewsChange: 0, + sentRequests, + sentRequestsChange: 0, + receivedRequests, + receivedRequestsChange: 0, + newMessages: unreadMessages, + rating: Number.isFinite(company?.rating) ? Number(company.rating) : 0, }; - + res.json(stats); } catch (error) { console.error('Get company stats error:', error); @@ -68,15 +102,22 @@ router.get('/my/stats', verifyToken, async (req, res) => { } }); -// Experience endpoints ДОЛЖНЫ быть ДО получения компании по ID -let companyExperience = []; - // GET /:id/experience - получить опыт компании router.get('/:id/experience', verifyToken, async (req, res) => { try { const { id } = req.params; - const experience = companyExperience.filter(e => e.companyId === id); - res.json(experience); + + if (!Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: 'Invalid company ID' }); + } + + const experience = await Experience.find({ companyId: new Types.ObjectId(id) }) + .sort({ createdAt: -1 }); + + res.json(experience.map(exp => ({ + ...exp.toObject(), + id: exp._id + }))); } catch (error) { res.status(500).json({ error: error.message }); } @@ -88,23 +129,24 @@ router.post('/:id/experience', verifyToken, async (req, res) => { const { id } = req.params; const { confirmed, customer, subject, volume, contact, comment } = req.body; - const expId = Math.random().toString(36).substr(2, 9); - const newExp = { - id: expId, - _id: expId, - companyId: id, + if (!Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: 'Invalid company ID' }); + } + + const newExp = await Experience.create({ + companyId: new Types.ObjectId(id), confirmed: confirmed || false, customer: customer || '', subject: subject || '', volume: volume || '', contact: contact || '', - comment: comment || '', - createdAt: new Date(), - updatedAt: new Date() - }; + comment: comment || '' + }); - companyExperience.push(newExp); - res.status(201).json(newExp); + res.status(201).json({ + ...newExp.toObject(), + id: newExp._id + }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -114,19 +156,28 @@ router.post('/:id/experience', verifyToken, async (req, res) => { router.put('/:id/experience/:expId', verifyToken, async (req, res) => { try { const { id, expId } = req.params; - const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id); - if (index === -1) { + if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) { + return res.status(400).json({ error: 'Invalid IDs' }); + } + + const experience = await Experience.findByIdAndUpdate( + new Types.ObjectId(expId), + { + ...req.body, + updatedAt: new Date() + }, + { new: true } + ); + + if (!experience || experience.companyId.toString() !== id) { return res.status(404).json({ error: 'Experience not found' }); } - companyExperience[index] = { - ...companyExperience[index], - ...req.body, - updatedAt: new Date() - }; - - res.json(companyExperience[index]); + res.json({ + ...experience.toObject(), + id: experience._id + }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -136,13 +187,18 @@ router.put('/:id/experience/:expId', verifyToken, async (req, res) => { router.delete('/:id/experience/:expId', verifyToken, async (req, res) => { try { const { id, expId } = req.params; - const index = companyExperience.findIndex(e => (e.id === expId || e._id === expId) && e.companyId === id); - if (index === -1) { + if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) { + return res.status(400).json({ error: 'Invalid IDs' }); + } + + const experience = await Experience.findById(new Types.ObjectId(expId)); + + if (!experience || experience.companyId.toString() !== id) { return res.status(404).json({ error: 'Experience not found' }); } - - companyExperience.splice(index, 1); + + await Experience.findByIdAndDelete(new Types.ObjectId(expId)); res.json({ message: 'Experience deleted' }); } catch (error) { res.status(500).json({ error: error.message }); @@ -155,7 +211,24 @@ router.get('/:id', async (req, res) => { const company = await Company.findById(req.params.id); if (!company) { - return res.status(404).json({ error: 'Company not found' }); + if (!Types.ObjectId.isValid(req.params.id)) { + return res.status(404).json({ error: 'Company not found' }); + } + + const placeholder = await Company.create({ + _id: new Types.ObjectId(req.params.id), + fullName: 'Новая компания', + shortName: 'Новая компания', + verified: false, + partnerGeography: [], + industry: '', + companySize: '', + }); + + return res.json({ + ...placeholder.toObject(), + id: placeholder._id, + }); } res.json({ diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js index fa224d4..dcea942 100644 --- a/server/routers/procurement/routes/experience.js +++ b/server/routers/procurement/routes/experience.js @@ -1,12 +1,11 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); - -// In-memory хранилище для опыта работы (mock) -let experiences = []; +const Experience = require('../models/Experience'); +const { Types } = require('mongoose'); // GET /experience - Получить список опыта работы компании -router.get('/', verifyToken, (req, res) => { +router.get('/', verifyToken, async (req, res) => { try { const { companyId } = req.query; @@ -14,8 +13,18 @@ router.get('/', verifyToken, (req, res) => { return res.status(400).json({ error: 'companyId is required' }); } - const companyExperiences = experiences.filter(exp => exp.companyId === companyId); - res.json(companyExperiences); + if (!Types.ObjectId.isValid(companyId)) { + return res.status(400).json({ error: 'Invalid company ID' }); + } + + const companyExperiences = await Experience.find({ + companyId: new Types.ObjectId(companyId) + }).sort({ createdAt: -1 }); + + res.json(companyExperiences.map(exp => ({ + ...exp.toObject(), + id: exp._id + }))); } catch (error) { console.error('Get experience error:', error); res.status(500).json({ error: 'Internal server error' }); @@ -23,7 +32,7 @@ router.get('/', verifyToken, (req, res) => { }); // POST /experience - Создать запись опыта работы -router.post('/', verifyToken, (req, res) => { +router.post('/', verifyToken, async (req, res) => { try { const { companyId, data } = req.body; @@ -31,28 +40,30 @@ router.post('/', verifyToken, (req, res) => { return res.status(400).json({ error: 'companyId and data are required' }); } + if (!Types.ObjectId.isValid(companyId)) { + return res.status(400).json({ error: 'Invalid company ID' }); + } + const { confirmed, customer, subject, volume, contact, comment } = data; if (!customer || !subject) { return res.status(400).json({ error: 'customer and subject are required' }); } - const newExperience = { - id: `exp-${Date.now()}`, - companyId, + const newExperience = await Experience.create({ + companyId: new Types.ObjectId(companyId), confirmed: confirmed || false, customer, subject, volume: volume || '', contact: contact || '', - comment: comment || '', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - }; + comment: comment || '' + }); - experiences.push(newExperience); - - res.status(201).json(newExperience); + res.status(201).json({ + ...newExperience.toObject(), + id: newExperience._id + }); } catch (error) { console.error('Create experience error:', error); res.status(500).json({ error: 'Internal server error' }); @@ -60,7 +71,7 @@ router.post('/', verifyToken, (req, res) => { }); // PUT /experience/:id - Обновить запись опыта работы -router.put('/:id', verifyToken, (req, res) => { +router.put('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; const { data } = req.body; @@ -69,21 +80,27 @@ router.put('/:id', verifyToken, (req, res) => { return res.status(400).json({ error: 'data is required' }); } - const index = experiences.findIndex(exp => exp.id === id); + if (!Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: 'Invalid experience ID' }); + } - if (index === -1) { + const updatedExperience = await Experience.findByIdAndUpdate( + new Types.ObjectId(id), + { + ...data, + updatedAt: new Date() + }, + { new: true } + ); + + if (!updatedExperience) { return res.status(404).json({ error: 'Experience not found' }); } - const updatedExperience = { - ...experiences[index], - ...data, - updatedAt: new Date().toISOString() - }; - - experiences[index] = updatedExperience; - - res.json(updatedExperience); + res.json({ + ...updatedExperience.toObject(), + id: updatedExperience._id + }); } catch (error) { console.error('Update experience error:', error); res.status(500).json({ error: 'Internal server error' }); @@ -91,17 +108,19 @@ router.put('/:id', verifyToken, (req, res) => { }); // DELETE /experience/:id - Удалить запись опыта работы -router.delete('/:id', verifyToken, (req, res) => { +router.delete('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; - const index = experiences.findIndex(exp => exp.id === id); - - if (index === -1) { - return res.status(404).json({ error: 'Experience not found' }); + if (!Types.ObjectId.isValid(id)) { + return res.status(400).json({ error: 'Invalid experience ID' }); } - experiences.splice(index, 1); + const deletedExperience = await Experience.findByIdAndDelete(new Types.ObjectId(id)); + + if (!deletedExperience) { + return res.status(404).json({ error: 'Experience not found' }); + } res.json({ message: 'Experience deleted successfully' }); } catch (error) { diff --git a/server/routers/procurement/routes/home.js b/server/routers/procurement/routes/home.js index eabc4f4..82c87d2 100644 --- a/server/routers/procurement/routes/home.js +++ b/server/routers/procurement/routes/home.js @@ -1,16 +1,49 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); +const BuyProduct = require('../models/BuyProduct'); +const Request = require('../models/Request'); // Получить агрегированные данные для главной страницы router.get('/aggregates', verifyToken, async (req, res) => { try { + const userId = req.userId; + const User = require('../models/User'); + const user = await User.findById(userId); + + if (!user || !user.companyId) { + return res.json({ + docsCount: 0, + acceptsCount: 0, + requestsCount: 0 + }); + } + + const companyId = user.companyId.toString(); + + const [docsCount, acceptsCount, requestsCount] = await Promise.all([ + BuyProduct.countDocuments({ companyId }), + Request.countDocuments({ + $or: [ + { senderCompanyId: companyId, status: 'accepted' }, + { recipientCompanyId: companyId, status: 'accepted' } + ] + }), + Request.countDocuments({ + $or: [ + { senderCompanyId: companyId }, + { recipientCompanyId: companyId } + ] + }) + ]); + res.json({ - docsCount: 0, - acceptsCount: 0, - requestsCount: 0 + docsCount, + acceptsCount, + requestsCount }); } catch (error) { + console.error('Error getting aggregates:', error); res.status(500).json({ error: error.message }); } }); @@ -18,17 +51,42 @@ router.get('/aggregates', verifyToken, async (req, res) => { // Получить статистику компании router.get('/stats', verifyToken, async (req, res) => { try { + const userId = req.userId; + const User = require('../models/User'); + const Company = require('../models/Company'); + const user = await User.findById(userId); + + if (!user || !user.companyId) { + return res.json({ + profileViews: 0, + profileViewsChange: 0, + sentRequests: 0, + sentRequestsChange: 0, + receivedRequests: 0, + receivedRequestsChange: 0, + newMessages: 0, + rating: 0 + }); + } + + const companyId = user.companyId.toString(); + const company = await Company.findById(user.companyId); + + const sentRequests = await Request.countDocuments({ senderCompanyId: companyId }); + const receivedRequests = await Request.countDocuments({ recipientCompanyId: companyId }); + res.json({ - profileViews: 12, - profileViewsChange: 5, - sentRequests: 3, - sentRequestsChange: 1, - receivedRequests: 7, - receivedRequestsChange: 2, - newMessages: 4, - rating: 4.5 + profileViews: company?.metrics?.profileViews || 0, + profileViewsChange: 0, + sentRequests, + sentRequestsChange: 0, + receivedRequests, + receivedRequestsChange: 0, + newMessages: 0, + rating: company?.rating || 0 }); } catch (error) { + console.error('Error getting stats:', error); res.status(500).json({ error: error.message }); } }); @@ -36,11 +94,40 @@ router.get('/stats', verifyToken, async (req, res) => { // Получить рекомендации партнеров (AI) router.get('/recommendations', verifyToken, async (req, res) => { try { + const userId = req.userId; + const User = require('../models/User'); + const Company = require('../models/Company'); + const user = await User.findById(userId); + + if (!user || !user.companyId) { + return res.json({ + recommendations: [], + message: 'No recommendations available' + }); + } + + // Получить компании кроме текущей + const companies = await Company.find({ + _id: { $ne: user.companyId } + }) + .sort({ rating: -1 }) + .limit(5); + + const recommendations = companies.map(company => ({ + id: company._id.toString(), + name: company.fullName || company.shortName, + industry: company.industry, + logo: company.logo, + matchScore: company.rating ? Math.min(100, Math.round(company.rating * 20)) : 50, + reason: 'Matches your industry' + })); + res.json({ - recommendations: [], - message: 'No recommendations available yet' + recommendations, + message: recommendations.length > 0 ? 'Recommendations available' : 'No recommendations available' }); } catch (error) { + console.error('Error getting recommendations:', error); res.status(500).json({ error: error.message }); } }); diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index b60a055..7573d1f 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -17,7 +17,7 @@ const log = (message, data = '') => { // GET /messages/threads - получить все потоки для компании router.get('/threads', verifyToken, async (req, res) => { try { - const companyId = req.user.companyId; + const companyId = req.companyId; const { ObjectId } = require('mongoose').Types; log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId); @@ -91,7 +91,7 @@ router.get('/threads', verifyToken, async (req, res) => { router.get('/:threadId', verifyToken, async (req, res) => { try { const { threadId } = req.params; - const companyId = req.user.companyId; + const companyId = req.companyId; // Получить все сообщения потока const threadMessages = await Message.find({ threadId }) @@ -128,7 +128,7 @@ router.post('/:threadId', verifyToken, async (req, res) => { const threadParts = threadId.replace('thread-', '').split('-'); let recipientCompanyId = null; - const currentSender = senderCompanyId || req.user.companyId; + const currentSender = senderCompanyId || req.companyId; const currentSenderString = currentSender.toString ? currentSender.toString() : currentSender; if (threadParts.length >= 2) { diff --git a/server/routers/procurement/routes/products.js b/server/routers/procurement/routes/products.js index 44490d4..9a09aaf 100644 --- a/server/routers/procurement/routes/products.js +++ b/server/routers/procurement/routes/products.js @@ -28,7 +28,7 @@ const transformProduct = (doc) => { // GET /products - Получить список продуктов/услуг компании (текущего пользователя) router.get('/', verifyToken, async (req, res) => { try { - const companyId = req.user.companyId; + const companyId = req.companyId; log('[Products] GET Fetching products for companyId:', companyId); @@ -48,7 +48,7 @@ router.get('/', verifyToken, async (req, res) => { router.post('/', verifyToken, async (req, res) => { // try { const { name, category, description, type, productUrl, price, unit, minOrder } = req.body; - const companyId = req.user.companyId; + const companyId = req.companyId; log('[Products] POST Creating product:', { name, category, type }); @@ -88,7 +88,7 @@ router.put('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; const updates = req.body; - const companyId = req.user.companyId; + const companyId = req.companyId; const product = await Product.findById(id); @@ -120,7 +120,7 @@ router.patch('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; const updates = req.body; - const companyId = req.user.companyId; + const companyId = req.companyId; const product = await Product.findById(id); @@ -150,7 +150,7 @@ router.patch('/:id', verifyToken, async (req, res) => { router.delete('/:id', verifyToken, async (req, res) => { try { const { id } = req.params; - const companyId = req.user.companyId; + const companyId = req.companyId; const product = await Product.findById(id); diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js index a7ab999..7e62b15 100644 --- a/server/routers/procurement/routes/requests.js +++ b/server/routers/procurement/routes/requests.js @@ -2,6 +2,10 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Request = require('../models/Request'); +const BuyProduct = require('../models/BuyProduct'); +const path = require('path'); +const fs = require('fs'); +const multer = require('multer'); // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -14,10 +18,166 @@ const log = (message, data = '') => { } }; +const REQUESTS_UPLOAD_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'requests'); + +const ensureDirectory = (dirPath) => { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +}; + +ensureDirectory(REQUESTS_UPLOAD_ROOT); + +const MAX_REQUEST_FILE_SIZE = 20 * 1024 * 1024; // 20MB +const ALLOWED_REQUEST_MIME_TYPES = new Set([ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', +]); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const subfolder = req.requestUploadSubfolder || ''; + const destinationDir = path.join(REQUESTS_UPLOAD_ROOT, subfolder); + ensureDirectory(destinationDir); + cb(null, destinationDir); + }, + filename: (req, file, cb) => { + const extension = path.extname(file.originalname) || ''; + const baseName = path + .basename(file.originalname, extension) + .replace(/[^a-zA-Z0-9-_]+/g, '_') + .toLowerCase(); + cb(null, `${Date.now()}_${baseName}${extension}`); + }, +}); + +const upload = multer({ + storage, + limits: { + fileSize: MAX_REQUEST_FILE_SIZE, + }, + fileFilter: (req, file, cb) => { + if (ALLOWED_REQUEST_MIME_TYPES.has(file.mimetype)) { + cb(null, true); + return; + } + + if (!req.invalidFiles) { + req.invalidFiles = []; + } + req.invalidFiles.push(file.originalname); + cb(null, false); + }, +}); + +const handleFilesUpload = (fieldName, subfolderResolver, maxCount = 10) => (req, res, next) => { + req.invalidFiles = []; + req.requestUploadSubfolder = subfolderResolver(req); + + upload.array(fieldName, maxCount)(req, res, (err) => { + if (err) { + console.error('[Requests] Multer error:', err.message); + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ error: 'File is too large. Maximum size is 20MB.' }); + } + return res.status(400).json({ error: err.message }); + } + next(); + }); +}; + +const cleanupUploadedFiles = async (req) => { + if (!Array.isArray(req.files) || req.files.length === 0) { + return; + } + + const subfolder = req.requestUploadSubfolder || ''; + const removalTasks = req.files.map((file) => { + const filePath = path.join(REQUESTS_UPLOAD_ROOT, subfolder, file.filename); + return fs.promises.unlink(filePath).catch((error) => { + if (error.code !== 'ENOENT') { + console.error('[Requests] Failed to cleanup uploaded file:', error.message); + } + }); + }); + + await Promise.all(removalTasks); +}; + +const mapFilesToMetadata = (req) => { + if (!Array.isArray(req.files) || req.files.length === 0) { + return []; + } + + const subfolder = req.requestUploadSubfolder || ''; + return req.files.map((file) => { + const relativePath = path.join('requests', subfolder, file.filename).replace(/\\/g, '/'); + return { + id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + name: file.originalname, + url: `/uploads/${relativePath}`, + type: file.mimetype, + size: file.size, + uploadedAt: new Date(), + storagePath: relativePath, + }; + }); +}; + +const normalizeToArray = (value) => { + if (!value) { + return []; + } + if (Array.isArray(value)) { + return value; + } + + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return parsed; + } + } catch (error) { + // ignore JSON parse errors + } + + return String(value) + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +}; + +const removeStoredFiles = async (files = []) => { + if (!files || files.length === 0) { + return; + } + + const tasks = files + .filter((file) => file && file.storagePath) + .map((file) => { + const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', file.storagePath); + return fs.promises.unlink(absolutePath).catch((error) => { + if (error.code !== 'ENOENT') { + console.error('[Requests] Failed to remove stored file:', error.message); + } + }); + }); + + await Promise.all(tasks); +}; + // GET /requests/sent - получить отправленные запросы router.get('/sent', verifyToken, async (req, res) => { try { - const companyId = req.user.companyId; + const companyId = req.companyId; + + if (!companyId) { + return res.status(400).json({ error: 'Company ID is required' }); + } const requests = await Request.find({ senderCompanyId: companyId }) .sort({ createdAt: -1 }) @@ -35,7 +195,11 @@ router.get('/sent', verifyToken, async (req, res) => { // GET /requests/received - получить полученные запросы router.get('/received', verifyToken, async (req, res) => { try { - const companyId = req.user.companyId; + const companyId = req.companyId; + + if (!companyId) { + return res.status(400).json({ error: 'Company ID is required' }); + } const requests = await Request.find({ recipientCompanyId: companyId }) .sort({ createdAt: -1 }) @@ -51,95 +215,164 @@ router.get('/received', verifyToken, async (req, res) => { }); // POST /requests - создать запрос -router.post('/', verifyToken, async (req, res) => { - try { - const { text, recipientCompanyIds, productId, files } = req.body; - const senderCompanyId = req.user.companyId; +router.post( + '/', + verifyToken, + handleFilesUpload('files', (req) => path.join('sent', (req.companyId || 'unknown').toString()), 10), + async (req, res) => { + try { + const senderCompanyId = req.companyId; + const recipients = normalizeToArray(req.body.recipientCompanyIds); + const text = (req.body.text || '').trim(); + const productId = req.body.productId ? String(req.body.productId) : null; + let subject = (req.body.subject || '').trim(); - if (!text || !recipientCompanyIds || !Array.isArray(recipientCompanyIds) || recipientCompanyIds.length === 0) { - return res.status(400).json({ error: 'text and recipientCompanyIds array required' }); - } - - // Отправить запрос каждой компании - const results = []; - for (const recipientCompanyId of recipientCompanyIds) { - try { - const request = new Request({ - senderCompanyId, - recipientCompanyId, - text, - productId, - files: files || [], - status: 'pending' - }); - - await request.save(); - results.push({ - companyId: recipientCompanyId, - success: true, - message: 'Request sent successfully' - }); - - log('[Requests] Request sent to company:', recipientCompanyId); - } catch (err) { - results.push({ - companyId: recipientCompanyId, - success: false, - message: err.message + if (req.invalidFiles && req.invalidFiles.length > 0) { + await cleanupUploadedFiles(req); + return res.status(400).json({ + error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.', + details: req.invalidFiles, }); } + + if (!text) { + await cleanupUploadedFiles(req); + return res.status(400).json({ error: 'Request text is required' }); + } + + if (!recipients.length) { + await cleanupUploadedFiles(req); + return res.status(400).json({ error: 'At least one recipient is required' }); + } + + if (!subject && productId) { + try { + const product = await BuyProduct.findById(productId); + if (product) { + subject = product.name; + } + } catch (lookupError) { + console.error('[Requests] Failed to lookup product for subject:', lookupError.message); + } + } + + if (!subject) { + await cleanupUploadedFiles(req); + return res.status(400).json({ error: 'Subject is required' }); + } + + const uploadedFiles = mapFilesToMetadata(req); + + const results = []; + for (const recipientCompanyId of recipients) { + try { + const request = new Request({ + senderCompanyId, + recipientCompanyId, + text, + productId, + subject, + files: uploadedFiles, + responseFiles: [], + status: 'pending', + }); + + await request.save(); + results.push({ + companyId: recipientCompanyId, + success: true, + message: 'Request sent successfully', + }); + + log('[Requests] Request sent to company:', recipientCompanyId); + } catch (err) { + console.error('[Requests] Error storing request for company:', recipientCompanyId, err.message); + results.push({ + companyId: recipientCompanyId, + success: false, + message: err.message, + }); + } + } + + const createdAt = new Date(); + + res.status(201).json({ + id: 'bulk-' + Date.now(), + text, + subject, + productId, + files: uploadedFiles, + result: results, + createdAt, + }); + } catch (error) { + console.error('[Requests] Error creating request:', error.message); + res.status(500).json({ error: error.message }); } - - // Сохранить отчет - const report = { - text, - result: results, - createdAt: new Date() - }; - - res.status(201).json({ - id: 'bulk-' + Date.now(), - ...report, - files: files || [] - }); - } catch (error) { - console.error('[Requests] Error creating request:', error.message); - res.status(500).json({ error: error.message }); } -}); +); // PUT /requests/:id - ответить на запрос -router.put('/:id', verifyToken, async (req, res) => { - try { - const { id } = req.params; - const { response, status } = req.body; +router.put( + '/:id', + verifyToken, + handleFilesUpload('responseFiles', (req) => path.join('responses', req.params.id || 'unknown'), 5), + async (req, res) => { + try { + const { id } = req.params; + const responseText = (req.body.response || '').trim(); + const statusRaw = (req.body.status || 'accepted').toLowerCase(); + const status = statusRaw === 'rejected' ? 'rejected' : 'accepted'; - const request = await Request.findById(id); + if (req.invalidFiles && req.invalidFiles.length > 0) { + await cleanupUploadedFiles(req); + return res.status(400).json({ + error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.', + details: req.invalidFiles, + }); + } - if (!request) { - return res.status(404).json({ error: 'Request not found' }); + if (!responseText) { + await cleanupUploadedFiles(req); + return res.status(400).json({ error: 'Response text is required' }); + } + + const request = await Request.findById(id); + + if (!request) { + await cleanupUploadedFiles(req); + return res.status(404).json({ error: 'Request not found' }); + } + + if (request.recipientCompanyId !== req.companyId) { + await cleanupUploadedFiles(req); + return res.status(403).json({ error: 'Not authorized' }); + } + + const uploadedResponseFiles = mapFilesToMetadata(req); + + if (uploadedResponseFiles.length > 0) { + await removeStoredFiles(request.responseFiles || []); + request.responseFiles = uploadedResponseFiles; + } + + request.response = responseText; + request.status = status; + request.respondedAt = new Date(); + request.updatedAt = new Date(); + + await request.save(); + + log('[Requests] Request responded:', id); + + res.json(request); + } catch (error) { + console.error('[Requests] Error responding to request:', error.message); + res.status(500).json({ error: error.message }); } - - // Только получатель может ответить на запрос - if (request.recipientCompanyId !== req.user.companyId) { - return res.status(403).json({ error: 'Not authorized' }); - } - - request.response = response; - request.status = status || 'accepted'; - request.respondedAt = new Date(); - request.updatedAt = new Date(); - - await request.save(); - - log('[Requests] Request responded:', id); - - res.json(request); - } catch (error) { - console.error('[Requests] Error responding to request:', error.message); - res.status(500).json({ error: error.message }); } -}); +); // DELETE /requests/:id - удалить запрос router.delete('/:id', verifyToken, async (req, res) => { @@ -153,10 +386,13 @@ router.delete('/:id', verifyToken, async (req, res) => { } // Может удалить отправитель или получатель - if (request.senderCompanyId !== req.user.companyId && request.recipientCompanyId !== req.user.companyId) { + if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) { return res.status(403).json({ error: 'Not authorized' }); } + await removeStoredFiles(request.files || []); + await removeStoredFiles(request.responseFiles || []); + await Request.findByIdAndDelete(id); log('[Requests] Request deleted:', id); diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js index 40bcd7d..820c9f8 100644 --- a/server/routers/procurement/routes/reviews.js +++ b/server/routers/procurement/routes/reviews.js @@ -61,7 +61,7 @@ router.post('/', verifyToken, async (req, res) => { // Создать новый отзыв const newReview = new Review({ companyId, - authorCompanyId: req.user.companyId, + authorCompanyId: req.companyId, authorName: req.user.firstName + ' ' + req.user.lastName, authorCompany: req.user.companyName || 'Company', rating: parseInt(rating), diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index b983e7f..84e38e6 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -127,14 +127,8 @@ router.get('/', verifyToken, async (req, res) => { log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries); if (dbIndustries.length > 0) { - // Handle both string and array industry values - filters.push({ - $or: [ - { industry: { $in: dbIndustries } }, - { industry: { $elemMatch: { $in: dbIndustries } } } - ] - }); - log('[Search] Added industry filter:', { $or: [{ industry: { $in: dbIndustries } }, { industry: { $elemMatch: { $in: dbIndustries } } }] }); + filters.push({ industry: { $in: dbIndustries } }); + log('[Search] Added industry filter:', { industry: { $in: dbIndustries } }); } else { log('[Search] No industries mapped! Codes were:', industryList); } @@ -219,10 +213,8 @@ router.get('/', verifyToken, async (req, res) => { page: pageNum, totalPages: Math.ceil(total / limitNum), _debug: { - requestParams: { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder }, filter: JSON.stringify(filter), - filtersCount: filters.length, - appliedFilters: filters.map(f => JSON.stringify(f)) + industriesReceived: industries } }); } catch (error) { diff --git a/server/routers/procurement/scripts/init-database.js b/server/routers/procurement/scripts/init-database.js deleted file mode 100644 index afae63b..0000000 --- a/server/routers/procurement/scripts/init-database.js +++ /dev/null @@ -1,74 +0,0 @@ -const mongoose = require('mongoose'); -const { migrateCompanies } = require('./migrate-companies'); -require('dotenv').config({ path: '../../.env' }); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -// Migration history model -const migrationSchema = new mongoose.Schema({ - name: { type: String, unique: true, required: true }, - executedAt: { type: Date, default: Date.now }, - status: { type: String, enum: ['completed', 'failed'], default: 'completed' }, - message: String -}, { collection: 'migrations' }); - -const Migration = mongoose.model('Migration', migrationSchema); - -async function initializeDatabase() { - try { - console.log('[Init] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Init] Connected to MongoDB\n'); - - // Check if migrations already ran - const migrateCompaniesRan = await Migration.findOne({ name: 'migrate-companies' }); - - if (!migrateCompaniesRan) { - console.log('[Init] Running migrate-companies migration...'); - try { - await migrateCompanies(); - - // Record successful migration - await Migration.create({ - name: 'migrate-companies', - status: 'completed', - message: 'Company data migration completed successfully' - }); - - console.log('[Init] ✅ migrate-companies recorded in database\n'); - } catch (err) { - // Record failed migration - await Migration.create({ - name: 'migrate-companies', - status: 'failed', - message: err.message - }); - console.error('[Init] ❌ migrate-companies failed:', err.message); - } - } else { - console.log('[Init] ℹ️ migrate-companies already executed:', migrateCompaniesRan.executedAt); - console.log('[Init] Skipping migration...\n'); - } - - await mongoose.connection.close(); - console.log('[Init] Database initialization complete\n'); - } catch (err) { - console.error('[Init] ❌ Error during database initialization:', err.message); - process.exit(1); - } -} - -module.exports = initializeDatabase; - -// Run directly if called as script -if (require.main === module) { - initializeDatabase().catch(err => { - console.error('Initialization failed:', err); - process.exit(1); - }); -} diff --git a/server/routers/procurement/scripts/migrate-companies.js b/server/routers/procurement/scripts/migrate-companies.js deleted file mode 100644 index 44fb465..0000000 --- a/server/routers/procurement/scripts/migrate-companies.js +++ /dev/null @@ -1,124 +0,0 @@ -const mongoose = require('mongoose'); -const Company = require('../models/Company'); -require('dotenv').config({ path: '../../.env' }); - -const industryMap = { - 'it': 'IT', - 'finance': 'Финансы', - 'manufacturing': 'Производство', - 'construction': 'Строительство', - 'retail': 'Розничная торговля', - 'wholesale': 'Оптовая торговля', - 'logistics': 'Логистика', - 'healthcare': 'Здравоохранение', - 'education': 'Образование', - 'consulting': 'Консалтинг', - 'marketing': 'Маркетинг', - 'realestate': 'Недвижимость', - 'food': 'Пищевая промышленность', - 'agriculture': 'Сельское хозяйство', - 'energy': 'Энергетика', - 'telecom': 'Телекоммуникации', - 'media': 'Медиа', - 'tourism': 'Туризм', - 'legal': 'Юридические услуги', - 'other': 'Другое' -}; - -const validIndustries = Object.values(industryMap); - -const industryAliases = { - 'Торговля': 'Розничная торговля', - 'торговля': 'Розничная торговля', - 'Trade': 'Розничная торговля' -}; - -async function migrateCompanies() { - try { - const allCompanies = await Company.find().exec(); - console.log(`[Migration] Found ${allCompanies.length} companies to process`); - - let fixedCount = 0; - let errorCount = 0; - - for (const company of allCompanies) { - let needsUpdate = false; - let updates = {}; - - // Check and fix industry field - if (company.industry) { - if (Array.isArray(company.industry)) { - console.log(`[FIX] ${company.fullName}: industry is array, converting to string`); - updates.industry = company.industry[0] || 'Другое'; - needsUpdate = true; - } else if (!validIndustries.includes(company.industry)) { - const mapped = industryAliases[company.industry]; - if (mapped) { - console.log(`[FIX] ${company.fullName}: "${company.industry}" → "${mapped}"`); - updates.industry = mapped; - needsUpdate = true; - } else { - console.log(`[WARN] ${company.fullName}: unknown industry "${company.industry}"`); - } - } - } - - // Check and fix companySize field - if (company.companySize && Array.isArray(company.companySize)) { - console.log(`[FIX] ${company.fullName}: companySize is array, converting to string`); - updates.companySize = company.companySize[0] || ''; - needsUpdate = true; - } - - if (needsUpdate) { - try { - await Company.updateOne({ _id: company._id }, { $set: updates }); - fixedCount++; - console.log(` ✅ Updated`); - } catch (err) { - console.error(` ❌ Error: ${err.message}`); - errorCount++; - } - } - } - - console.log('\n[Migration] === MIGRATION SUMMARY ==='); - console.log(`[Migration] Total companies: ${allCompanies.length}`); - console.log(`[Migration] Fixed: ${fixedCount}`); - console.log(`[Migration] Errors: ${errorCount}`); - - if (fixedCount === 0 && errorCount === 0) { - console.log('[Migration] ✅ No migration needed - all data is valid!'); - } else if (errorCount === 0) { - console.log('[Migration] ✅ Migration completed successfully!'); - } else { - console.log('[Migration] ⚠️ Migration completed with errors.'); - } - } catch (err) { - console.error('[Migration] ❌ Error:', err.message); - throw err; - } -} - -module.exports = { - migrateCompanies: migrateCompanies -}; - -// Run directly if called as script -if (require.main === module) { - const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - - mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }).then(async () => { - console.log('[Migration] Connected to MongoDB\n'); - await migrateCompanies(); - await mongoose.connection.close(); - }).catch(err => { - console.error('[Migration] ❌ Error:', err.message); - process.exit(1); - }); -} diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js index dba2b66..d342f44 100644 --- a/server/routers/procurement/scripts/migrate-messages.js +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -6,17 +6,14 @@ const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procureme async function migrateMessages() { try { - // Check if connection exists, if not connect - if (mongoose.connection.readyState === 0) { - console.log('[Migration] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Migration] Connected to MongoDB'); - } + console.log('[Migration] Connecting to MongoDB...'); + await mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }); + console.log('[Migration] Connected to MongoDB'); // Найти все сообщения const allMessages = await Message.find().exec(); @@ -84,18 +81,13 @@ async function migrateMessages() { console.log('[Migration] ✅ Migration completed!'); console.log('[Migration] Fixed:', fixedCount, 'messages'); console.log('[Migration] Errors:', errorCount); + + await mongoose.connection.close(); + console.log('[Migration] Disconnected from MongoDB'); } catch (err) { console.error('[Migration] ❌ Error:', err.message); - throw err; + process.exit(1); } } -module.exports = { migrateMessages }; - -// Run directly if called as script -if (require.main === module) { - migrateMessages().catch(err => { - console.error('Migration failed:', err); - process.exit(1); - }); -} +migrateMessages(); diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 211b8a6..40694c9 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,32 +1,62 @@ const mongoose = require('mongoose'); +const path = require('path'); require('dotenv').config(); // Импорт моделей -const User = require('../models/User'); -const Company = require('../models/Company'); +const User = require(path.join(__dirname, '..', 'models', 'User')); +const Company = require(path.join(__dirname, '..', 'models', 'Company')); + +const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; +const fallbackUri = + process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; + +const connectWithFallback = async () => { + try { + console.log('\n📡 Подключение к MongoDB (PRIMARY)...'); + await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.log('✅ Подключено к PRIMARY MongoDB'); + } catch (primaryError) { + console.error('❌ Ошибка PRIMARY подключения:', primaryError.message); + + const requiresFallback = + primaryError.code === 18 || primaryError.code === 13 || String(primaryError.message || '').includes('auth'); + + if (!requiresFallback) { + throw primaryError; + } + + console.log('\n📡 Подключение к MongoDB (FALLBACK)...'); + await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.log('✅ Подключено к FALLBACK MongoDB'); + } +}; const recreateTestUser = async () => { try { - console.log('[Migration] Processing test user creation...'); + await connectWithFallback(); + + const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796'); + const presetUserEmail = 'admin@test-company.ru'; // Удалить старого тестового пользователя - console.log('[Migration] Removing old test user...'); - const oldUser = await User.findOne({ email: 'admin@test-company.ru' }); + console.log('🗑️ Удаление старого тестового пользователя...'); + const oldUser = await User.findOne({ email: presetUserEmail }); if (oldUser) { // Удалить связанную компанию if (oldUser.companyId) { await Company.findByIdAndDelete(oldUser.companyId); - console.log('[Migration] ✓ Old company removed'); + console.log(' ✓ Старая компания удалена'); } await User.findByIdAndDelete(oldUser._id); - console.log('[Migration] ✓ Old user removed'); + console.log(' ✓ Старый пользователь удален'); } else { - console.log('[Migration] ℹ️ Old user not found'); + console.log(' ℹ️ Старый пользователь не найден'); } // Создать новую компанию с правильной кодировкой UTF-8 - console.log('[Migration] Creating test company...'); + console.log('\n🏢 Создание тестовой компании...'); const company = await Company.create({ + _id: presetCompanyId, fullName: 'ООО "Тестовая Компания"', inn: '1234567890', ogrn: '1234567890123', @@ -40,12 +70,12 @@ const recreateTestUser = async () => { reviewsCount: 10, dealsCount: 25, }); - console.log('[Migration] ✓ Company created:', company.fullName); + console.log(' ✓ Компания создана:', company.fullName); // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('[Migration] Creating test user...'); + console.log('\n👤 Создание тестового пользователя...'); const user = await User.create({ - email: 'admin@test-company.ru', + email: presetUserEmail, password: 'SecurePass123!', firstName: 'Иван', lastName: 'Иванов', @@ -53,10 +83,24 @@ const recreateTestUser = async () => { phone: '+7 (999) 123-45-67', companyId: company._id, }); - console.log('[Migration] ✓ User created:', user.firstName, user.lastName); + console.log(' ✓ Пользователь создан:', user.firstName, user.lastName); + + // Проверка что данные сохранены правильно + console.log('\n✅ Проверка данных:'); + console.log(' Email:', user.email); + console.log(' Имя:', user.firstName); + console.log(' Фамилия:', user.lastName); + console.log(' Компания:', company.fullName); + console.log(' Должность:', user.position); + + console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8'); + console.log('\n📋 Данные для входа:'); + console.log(' Email: admin@test-company.ru'); + console.log(' Пароль: SecurePass123!'); + console.log(''); // Обновить существующие mock компании - console.log('[Migration] Updating existing companies...'); + console.log('\n🔄 Обновление существующих mock компаний...'); const updates = [ { inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, { inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } }, @@ -67,33 +111,18 @@ const recreateTestUser = async () => { for (const item of updates) { await Company.updateOne({ inn: item.inn }, { $set: item.updates }); - console.log(`[Migration] ✓ Company updated: INN ${item.inn}`); + console.log(` ✓ Компания обновлена: INN ${item.inn}`); } - console.log('[Migration] ✅ Test user migration completed!'); + await mongoose.connection.close(); + process.exit(0); } catch (error) { - console.error('[Migration] ❌ Error:', error.message); - throw error; + console.error('\n❌ Ошибка:', error.message); + console.error(error); + process.exit(1); } }; -module.exports = { recreateTestUser }; - -// Run directly if called as script -if (require.main === module) { - const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; - - mongoose.connect(mongoUri, { - useNewUrlParser: true, - useUnifiedTopology: true, - }).then(async () => { - console.log('[Migration] Connected to MongoDB\n'); - await recreateTestUser(); - await mongoose.connection.close(); - process.exit(0); - }).catch(err => { - console.error('[Migration] ❌ Error:', err.message); - process.exit(1); - }); -} +// Запуск +recreateTestUser(); diff --git a/server/routers/procurement/scripts/run-migrations.js b/server/routers/procurement/scripts/run-migrations.js deleted file mode 100644 index 1c59fbc..0000000 --- a/server/routers/procurement/scripts/run-migrations.js +++ /dev/null @@ -1,117 +0,0 @@ -const mongoose = require('mongoose'); -const { migrateCompanies } = require('./migrate-companies'); -const { migrateMessages } = require('./migrate-messages'); -const { recreateTestUser } = require('./recreate-test-user'); -require('dotenv').config(); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -// Migration history model -const migrationSchema = new mongoose.Schema({ - name: { type: String, unique: true, required: true }, - executedAt: { type: Date, default: Date.now }, - status: { type: String, enum: ['completed', 'failed'], default: 'completed' }, - message: String -}, { collection: 'migrations' }); - -const Migration = mongoose.model('Migration', migrationSchema); - -const migrations = [ - { name: 'migrate-companies', fn: migrateCompanies }, - { name: 'migrate-messages', fn: migrateMessages }, - { name: 'recreate-test-user', fn: recreateTestUser } -]; - -async function runMigrations(shouldCloseConnection = false) { - let mongooseConnected = false; - - try { - console.log('\n' + '='.repeat(60)); - console.log('🚀 Starting Database Migrations'); - console.log('='.repeat(60) + '\n'); - - // Only connect if not already connected - if (mongoose.connection.readyState === 0) { - console.log('[Migrations] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - mongooseConnected = true; - console.log('[Migrations] ✅ Connected to MongoDB\n'); - } else { - console.log('[Migrations] ✅ Using existing MongoDB connection\n'); - } - - for (const migration of migrations) { - console.log(`[${migration.name}] Starting...`); - - try { - // Check if already executed - const existing = await Migration.findOne({ name: migration.name }); - - if (existing) { - console.log(`[${migration.name}] ℹ️ Already executed at: ${existing.executedAt.toISOString()}`); - console.log(`[${migration.name}] Status: ${existing.status}`); - if (existing.message) console.log(`[${migration.name}] Message: ${existing.message}`); - console.log(''); - continue; - } - - // Run migration - await migration.fn(); - - // Record successful migration - await Migration.create({ - name: migration.name, - status: 'completed', - message: `${migration.name} executed successfully` - }); - - console.log(`[${migration.name}] ✅ Completed and recorded\n`); - } catch (error) { - console.error(`[${migration.name}] ❌ Error: ${error.message}\n`); - - // Record failed migration - try { - await Migration.create({ - name: migration.name, - status: 'failed', - message: error.message - }); - } catch (recordErr) { - // Ignore if we can't record the failure - } - } - } - - console.log('='.repeat(60)); - console.log('✅ All migrations processed'); - console.log('='.repeat(60) + '\n'); - - } catch (error) { - console.error('\n❌ Fatal migration error:', error.message); - console.error(error); - if (shouldCloseConnection) { - process.exit(1); - } - } finally { - // Only close connection if we created it and requested to close - if (mongooseConnected && shouldCloseConnection) { - await mongoose.connection.close(); - console.log('[Migrations] Disconnected from MongoDB\n'); - } - } -} - -module.exports = { runMigrations, Migration }; - -// Run directly if called as script -if (require.main === module) { - runMigrations(true).catch(err => { - console.error('Migration failed:', err); - process.exit(1); - }); -} diff --git a/server/routers/procurement/scripts/test-logging.js b/server/routers/procurement/scripts/test-logging.js new file mode 100644 index 0000000..e914f23 --- /dev/null +++ b/server/routers/procurement/scripts/test-logging.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/** + * Скрипт для тестирования логирования + * + * Использование: + * node stubs/scripts/test-logging.js # Логи скрыты (DEV не установлена) + * DEV=true node stubs/scripts/test-logging.js # Логи видны + */ + +// Функция логирования из маршрутов +const log = (message, data = '') => { + if (process.env.DEV === 'true') { + if (data) { + console.log(message, data); + } else { + console.log(message); + } + } +}; + +console.log(''); +console.log('='.repeat(60)); +console.log('TEST: Логирование с переменной окружения DEV'); +console.log('='.repeat(60)); +console.log(''); + +console.log('Значение DEV:', process.env.DEV || '(не установлена)'); +console.log(''); + +// Тестируем различные логи +log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796'); +log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799'); +log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796'); +log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799'); +log('[Products] Found', 1, 'products'); +log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796'); +log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796'); +log('[Messages] Found', 4, 'messages for company'); +log('[Messages] Returned', 3, 'unique threads'); +log('[Search] Request params:', { query: '', page: 1 }); + +console.log(''); +console.log('='.repeat(60)); +console.log('РЕЗУЛЬТАТ:'); +console.log('='.repeat(60)); + +if (process.env.DEV === 'true') { + console.log('✅ DEV=true - логи ВИДНЫ выше'); +} else { + console.log('❌ DEV не установлена или != "true" - логи СКРЫТЫ'); + console.log(''); + console.log('Для включения логов запустите:'); + console.log(' export DEV=true && npm start (Linux/Mac)'); + console.log(' $env:DEV = "true"; npm start (PowerShell)'); + console.log(' set DEV=true && npm start (CMD)'); +} + +console.log(''); +console.log('='.repeat(60)); +console.log(''); diff --git a/server/routers/procurement/scripts/validate-companies.js b/server/routers/procurement/scripts/validate-companies.js deleted file mode 100644 index cbf1147..0000000 --- a/server/routers/procurement/scripts/validate-companies.js +++ /dev/null @@ -1,93 +0,0 @@ -const mongoose = require('mongoose'); -const Company = require('../models/Company'); -require('dotenv').config({ path: '../../.env' }); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -const industryMap = { - 'it': 'IT', - 'finance': 'Финансы', - 'manufacturing': 'Производство', - 'construction': 'Строительство', - 'retail': 'Розничная торговля', - 'wholesale': 'Оптовая торговля', - 'logistics': 'Логистика', - 'healthcare': 'Здравоохранение', - 'education': 'Образование', - 'consulting': 'Консалтинг', - 'marketing': 'Маркетинг', - 'realestate': 'Недвижимость', - 'food': 'Пищевая промышленность', - 'agriculture': 'Сельское хозяйство', - 'energy': 'Энергетика', - 'telecom': 'Телекоммуникации', - 'media': 'Медиа', - 'tourism': 'Туризм', - 'legal': 'Юридические услуги', - 'other': 'Другое' -}; - -async function validateCompanies() { - try { - console.log('[Validation] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - console.log('[Validation] Connected to MongoDB\n'); - - const allCompanies = await Company.find().exec(); - console.log(`Found ${allCompanies.length} total companies\n`); - - console.log('=== COMPANY DATA VALIDATION REPORT ===\n'); - - let issuesFound = 0; - let validCompanies = 0; - - for (const company of allCompanies) { - console.log(`📋 Company: ${company.fullName}`); - console.log(` ID: ${company._id}`); - console.log(` Industry: ${company.industry} (type: ${typeof company.industry})`); - console.log(` Company Size: ${company.companySize}`); - - let hasIssues = false; - - if (company.industry) { - if (Array.isArray(company.industry)) { - console.log(` ⚠️ WARNING: industry is array!`); - issuesFound++; - hasIssues = true; - } else if (!Object.values(industryMap).includes(company.industry)) { - console.log(` ⚠️ industry value unknown: "${company.industry}"`); - issuesFound++; - hasIssues = true; - } else { - console.log(` ✅ industry OK`); - } - } - - if (!hasIssues) validCompanies++; - console.log(''); - } - - console.log('\n=== SUMMARY ==='); - console.log(`Total: ${allCompanies.length}`); - console.log(`Valid: ${validCompanies}`); - console.log(`Issues: ${issuesFound}`); - - if (issuesFound === 0) { - console.log('\n✅ All data OK. No migration needed.'); - } else { - console.log('\n⚠️ Migration recommended.'); - } - - await mongoose.connection.close(); - } catch (err) { - console.error('❌ Error:', err.message); - process.exit(1); - } -} - -validateCompanies(); From 71f3f353ab9d7faca6d44037533e374946907fa0 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 4 Nov 2025 18:20:19 +0300 Subject: [PATCH 130/147] update project --- server/routers/procurement/models/Company.js | 6 + server/routers/procurement/routes/auth.js | 43 +++ .../routers/procurement/routes/companies.js | 43 ++- server/routers/procurement/routes/reviews.js | 75 ++++- .../procurement/scripts/recreate-test-user.js | 262 ++++++++++++++++-- 5 files changed, 386 insertions(+), 43 deletions(-) diff --git a/server/routers/procurement/models/Company.js b/server/routers/procurement/models/Company.js index dc34466..f9010b3 100644 --- a/server/routers/procurement/models/Company.js +++ b/server/routers/procurement/models/Company.js @@ -49,6 +49,12 @@ const companySchema = new mongoose.Schema({ type: Boolean, default: false }, + metrics: { + type: { + profileViews: { type: Number, default: 0 } + }, + default: {} + }, createdAt: { type: Date, default: Date.now diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index c9a2bba..4f64182 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -463,6 +463,49 @@ router.patch('/profile', verifyToken, async (req, res) => { return res.status(result.status).json(result.body); } + if (action === 'updateProfile') { + await waitForDatabaseConnection(); + + const { firstName, lastName, position, phone } = payload; + + if (!firstName && !lastName && !position && !phone) { + return res.status(400).json({ error: 'At least one field must be provided' }); + } + + const user = await User.findById(req.userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + if (firstName) user.firstName = firstName; + if (lastName) user.lastName = lastName; + if (position !== undefined) user.position = position; + if (phone !== undefined) user.phone = phone; + user.updatedAt = new Date(); + + await user.save(); + + const company = user.companyId ? await Company.findById(user.companyId) : null; + + return res.json({ + message: 'Profile updated successfully', + user: { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + position: user.position, + phone: user.phone, + companyId: user.companyId?.toString() + }, + company: company ? { + id: company._id.toString(), + name: company.fullName, + inn: company.inn + } : null + }); + } + res.json({ message: 'Profile endpoint' }); } catch (error) { console.error('Profile update error:', error); diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 5fd84e9..914cd72 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -84,13 +84,30 @@ router.get('/my/stats', verifyToken, async (req, res) => { : Promise.resolve(0), ]); + // Подсчитываем просмотры профиля из запросов к профилю компании + const profileViews = company?.metrics?.profileViews || 0; + + // Получаем статистику за последнюю неделю для изменений + const weekAgo = new Date(); + weekAgo.setDate(weekAgo.getDate() - 7); + + const sentRequestsLastWeek = await Request.countDocuments({ + senderCompanyId: companyIdString, + createdAt: { $gte: weekAgo } + }); + + const receivedRequestsLastWeek = await Request.countDocuments({ + recipientCompanyId: companyIdString, + createdAt: { $gte: weekAgo } + }); + const stats = { - profileViews: company?.metrics?.profileViews || 0, - profileViewsChange: 0, + profileViews: profileViews, + profileViewsChange: 0, // Можно добавить отслеживание просмотров, если нужно sentRequests, - sentRequestsChange: 0, + sentRequestsChange: sentRequestsLastWeek, receivedRequests, - receivedRequestsChange: 0, + receivedRequestsChange: receivedRequestsLastWeek, newMessages: unreadMessages, rating: Number.isFinite(company?.rating) ? Number(company.rating) : 0, }; @@ -231,6 +248,24 @@ router.get('/:id', async (req, res) => { }); } + // Отслеживаем просмотр профиля (если это не владелец компании) + const userId = req.userId; + if (userId) { + const User = require('../models/User'); + const user = await User.findById(userId); + if (user && user.companyId && user.companyId.toString() !== company._id.toString()) { + // Инкрементируем просмотры профиля + if (!company.metrics) { + company.metrics = {}; + } + if (!company.metrics.profileViews) { + company.metrics.profileViews = 0; + } + company.metrics.profileViews = (company.metrics.profileViews || 0) + 1; + await company.save(); + } + } + res.json({ ...company.toObject(), id: company._id diff --git a/server/routers/procurement/routes/reviews.js b/server/routers/procurement/routes/reviews.js index 820c9f8..a3740ed 100644 --- a/server/routers/procurement/routes/reviews.js +++ b/server/routers/procurement/routes/reviews.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Review = require('../models/Review'); +const Company = require('../models/Company'); // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -14,6 +15,35 @@ const log = (message, data = '') => { } }; +// Функция для пересчета рейтинга компании +const updateCompanyRating = async (companyId) => { + try { + const reviews = await Review.find({ companyId }); + + if (reviews.length === 0) { + await Company.findByIdAndUpdate(companyId, { + rating: 0, + reviews: 0, + updatedAt: new Date() + }); + return; + } + + const totalRating = reviews.reduce((sum, review) => sum + review.rating, 0); + const averageRating = totalRating / reviews.length; + + await Company.findByIdAndUpdate(companyId, { + rating: averageRating, + reviews: reviews.length, + updatedAt: new Date() + }); + + log('[Reviews] Updated company rating:', companyId, 'New rating:', averageRating); + } catch (error) { + console.error('[Reviews] Error updating company rating:', error.message); + } +}; + // GET /reviews/company/:companyId - получить отзывы компании router.get('/company/:companyId', verifyToken, async (req, res) => { try { @@ -42,30 +72,54 @@ router.post('/', verifyToken, async (req, res) => { if (!companyId || !rating || !comment) { return res.status(400).json({ - error: 'companyId, rating, and comment are required', + error: 'Заполните все обязательные поля: компания, рейтинг и комментарий', }); } if (rating < 1 || rating > 5) { return res.status(400).json({ - error: 'Rating must be between 1 and 5', + error: 'Рейтинг должен быть от 1 до 5', }); } - if (comment.trim().length < 10 || comment.trim().length > 1000) { + const trimmedComment = comment.trim(); + if (trimmedComment.length < 10) { return res.status(400).json({ - error: 'Comment must be between 10 and 1000 characters', + error: 'Отзыв должен содержать минимум 10 символов', + }); + } + + if (trimmedComment.length > 1000) { + return res.status(400).json({ + error: 'Отзыв не должен превышать 1000 символов', + }); + } + + // Получить данные пользователя из БД для актуальной информации + const User = require('../models/User'); + const Company = require('../models/Company'); + + const user = await User.findById(req.userId); + const userCompany = user && user.companyId ? await Company.findById(user.companyId) : null; + + if (!user) { + return res.status(404).json({ + error: 'Пользователь не найден', }); } // Создать новый отзыв const newReview = new Review({ companyId, - authorCompanyId: req.companyId, - authorName: req.user.firstName + ' ' + req.user.lastName, - authorCompany: req.user.companyName || 'Company', + authorCompanyId: user.companyId || req.companyId, + authorName: user.firstName && user.lastName + ? `${user.firstName} ${user.lastName}` + : req.user?.firstName && req.user?.lastName + ? `${req.user.firstName} ${req.user.lastName}` + : 'Аноним', + authorCompany: userCompany?.fullName || userCompany?.shortName || req.user?.companyName || 'Компания', rating: parseInt(rating), - comment: comment.trim(), + comment: trimmedComment, verified: true, createdAt: new Date(), updatedAt: new Date() @@ -75,11 +129,14 @@ router.post('/', verifyToken, async (req, res) => { log('[Reviews] New review created:', savedReview._id); + // Пересчитываем рейтинг компании + await updateCompanyRating(companyId); + res.status(201).json(savedReview); } catch (error) { console.error('[Reviews] Error creating review:', error.message); res.status(500).json({ - error: 'Internal server error', + error: 'Ошибка при сохранении отзыва', message: error.message, }); } diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 40694c9..57a287f 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -5,29 +5,31 @@ require('dotenv').config(); // Импорт моделей const User = require(path.join(__dirname, '..', 'models', 'User')); const Company = require(path.join(__dirname, '..', 'models', 'Company')); +const Request = require(path.join(__dirname, '..', 'models', 'Request')); const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; const fallbackUri = process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; const connectWithFallback = async () => { + // Сначала пробуем FALLBACK (с аутентификацией) try { - console.log('\n📡 Подключение к MongoDB (PRIMARY)...'); - await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true }); - console.log('✅ Подключено к PRIMARY MongoDB'); - } catch (primaryError) { - console.error('❌ Ошибка PRIMARY подключения:', primaryError.message); - - const requiresFallback = - primaryError.code === 18 || primaryError.code === 13 || String(primaryError.message || '').includes('auth'); - - if (!requiresFallback) { - throw primaryError; - } - - console.log('\n📡 Подключение к MongoDB (FALLBACK)...'); + console.log('\n📡 Подключение к MongoDB (с аутентификацией)...'); await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true }); - console.log('✅ Подключено к FALLBACK MongoDB'); + console.log('✅ Подключено к MongoDB'); + return; + } catch (fallbackError) { + console.log('❌ Ошибка подключения с аутентификацией:', fallbackError.message); + } + + // Если не получилось, пробуем без аутентификации + try { + console.log('\n📡 Подключение к MongoDB (без аутентификации)...'); + await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true }); + console.log('✅ Подключено к MongoDB'); + } catch (primaryError) { + console.error('❌ Не удалось подключиться к MongoDB:', primaryError.message); + throw primaryError; } }; @@ -58,17 +60,26 @@ const recreateTestUser = async () => { const company = await Company.create({ _id: presetCompanyId, fullName: 'ООО "Тестовая Компания"', + shortName: 'Тестовая Компания', inn: '1234567890', ogrn: '1234567890123', legalForm: 'ООО', industry: 'IT', - companySize: '50-100', + companySize: '51-250', website: 'https://test-company.ru', + phone: '+7 (999) 123-45-67', + email: 'info@test-company.ru', description: 'Тестовая компания для разработки', - address: 'г. Москва, ул. Тестовая, д. 1', + legalAddress: 'г. Москва, ул. Тестовая, д. 1', + actualAddress: 'г. Москва, ул. Тестовая, д. 1', + foundedYear: 2015, + employeeCount: '51-250', + revenue: 'До 120 млн ₽', rating: 4.5, - reviewsCount: 10, - dealsCount: 25, + reviews: 10, + verified: true, + partnerGeography: ['moscow', 'russia_all'], + slogan: 'Ваш надежный партнер в IT', }); console.log(' ✓ Компания создана:', company.fullName); @@ -99,19 +110,210 @@ const recreateTestUser = async () => { console.log(' Пароль: SecurePass123!'); console.log(''); - // Обновить существующие mock компании - console.log('\n🔄 Обновление существующих mock компаний...'); - const updates = [ - { inn: '7707083894', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, - { inn: '7707083895', updates: { companySize: '500+', partnerGeography: ['moscow', 'russia_all'] } }, - { inn: '7707083896', updates: { companySize: '11-50', partnerGeography: ['moscow', 'russia_all'] } }, - { inn: '7707083897', updates: { companySize: '51-250', partnerGeography: ['moscow', 'russia_all'] } }, - { inn: '7707083898', updates: { companySize: '251-500', partnerGeography: ['moscow', 'russia_all'] } }, + // Создать дополнительные тестовые компании для поиска + console.log('\n🏢 Создание дополнительных тестовых компаний...'); + const testCompanies = [ + { + fullName: 'ООО "ТехноСтрой"', + shortName: 'ТехноСтрой', + inn: '7707083894', + ogrn: '1077707083894', + legalForm: 'ООО', + industry: 'Строительство', + companySize: '51-250', + website: 'https://technostroy.ru', + phone: '+7 (495) 111-22-33', + email: 'info@technostroy.ru', + description: 'Строительство промышленных объектов', + foundedYear: 2010, + employeeCount: '51-250', + revenue: 'До 2 млрд ₽', + rating: 4.2, + reviews: 15, + verified: true, + partnerGeography: ['moscow', 'russia_all'], + slogan: 'Строим будущее вместе', + }, + { + fullName: 'АО "ФинансГрупп"', + shortName: 'ФинансГрупп', + inn: '7707083895', + ogrn: '1077707083895', + legalForm: 'АО', + industry: 'Финансы', + companySize: '500+', + website: 'https://finansgrupp.ru', + phone: '+7 (495) 222-33-44', + email: 'contact@finansgrupp.ru', + description: 'Финансовые услуги для бизнеса', + foundedYear: 2005, + employeeCount: '500+', + revenue: 'Более 2 млрд ₽', + rating: 4.8, + reviews: 50, + verified: true, + partnerGeography: ['moscow', 'russia_all', 'international'], + slogan: 'Финансовая стабильность', + }, + { + fullName: 'ООО "ИТ Решения"', + shortName: 'ИТ Решения', + inn: '7707083896', + ogrn: '1077707083896', + legalForm: 'ООО', + industry: 'IT', + companySize: '11-50', + website: 'https://it-solutions.ru', + phone: '+7 (495) 333-44-55', + email: 'hello@it-solutions.ru', + description: 'Разработка программного обеспечения', + foundedYear: 2018, + employeeCount: '11-50', + revenue: 'До 60 млн ₽', + rating: 4.5, + reviews: 8, + verified: true, + partnerGeography: ['moscow', 'spb', 'russia_all'], + slogan: 'Инновации для вашего бизнеса', + }, + { + fullName: 'ООО "ЛогистикПро"', + shortName: 'ЛогистикПро', + inn: '7707083897', + ogrn: '1077707083897', + legalForm: 'ООО', + industry: 'Логистика', + companySize: '51-250', + website: 'https://logistikpro.ru', + phone: '+7 (495) 444-55-66', + email: 'info@logistikpro.ru', + description: 'Транспортные и логистические услуги', + foundedYear: 2012, + employeeCount: '51-250', + revenue: 'До 120 млн ₽', + rating: 4.3, + reviews: 20, + verified: true, + partnerGeography: ['russia_all', 'cis'], + slogan: 'Доставим в срок', + }, + { + fullName: 'ООО "ПродуктТрейд"', + shortName: 'ПродуктТрейд', + inn: '7707083898', + ogrn: '1077707083898', + legalForm: 'ООО', + industry: 'Оптовая торговля', + companySize: '251-500', + website: 'https://produkttrade.ru', + phone: '+7 (495) 555-66-77', + email: 'sales@produkttrade.ru', + description: 'Оптовая торговля продуктами питания', + foundedYear: 2008, + employeeCount: '251-500', + revenue: 'До 2 млрд ₽', + rating: 4.1, + reviews: 30, + verified: true, + partnerGeography: ['moscow', 'russia_all'], + slogan: 'Качество и надежность', + }, + { + fullName: 'ООО "МедСервис"', + shortName: 'МедСервис', + inn: '7707083899', + ogrn: '1077707083899', + legalForm: 'ООО', + industry: 'Здравоохранение', + companySize: '11-50', + website: 'https://medservice.ru', + phone: '+7 (495) 666-77-88', + email: 'info@medservice.ru', + description: 'Медицинские услуги и оборудование', + foundedYear: 2016, + employeeCount: '11-50', + revenue: 'До 60 млн ₽', + rating: 4.6, + reviews: 12, + verified: true, + partnerGeography: ['moscow', 'central'], + slogan: 'Забота о вашем здоровье', + }, ]; - for (const item of updates) { - await Company.updateOne({ inn: item.inn }, { $set: item.updates }); - console.log(` ✓ Компания обновлена: INN ${item.inn}`); + for (const companyData of testCompanies) { + await Company.updateOne( + { inn: companyData.inn }, + { $set: companyData }, + { upsert: true } + ); + console.log(` ✓ Компания создана/обновлена: ${companyData.shortName}`); + } + + // Создать тестовые запросы + console.log('\n📨 Создание тестовых запросов...'); + await Request.deleteMany({}); + + const companies = await Company.find().limit(10).exec(); + const testCompanyId = company._id.toString(); + const requests = []; + const now = new Date(); + + // Создаем отправленные запросы (от тестовой компании) + for (let i = 0; i < 5; i++) { + const recipientCompany = companies[i % companies.length]; + if (recipientCompany._id.toString() === testCompanyId) { + continue; + } + + const createdAt = new Date(now.getTime() - i * 24 * 60 * 60 * 1000); + + requests.push({ + senderCompanyId: testCompanyId, + recipientCompanyId: recipientCompany._id.toString(), + subject: `Запрос на поставку ${i + 1}`, + text: `Здравствуйте! Интересует поставка товаров/услуг. Запрос ${i + 1}. Прошу предоставить коммерческое предложение.`, + files: [], + responseFiles: [], + status: i % 3 === 0 ? 'accepted' : i % 3 === 1 ? 'rejected' : 'pending', + response: i % 3 === 0 + ? 'Благодарим за запрос! Готовы предоставить услуги. Отправили КП на почту.' + : i % 3 === 1 + ? 'К сожалению, в данный момент не можем предоставить эти услуги.' + : null, + respondedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : null, + createdAt, + updatedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : createdAt, + }); + } + + // Создаем полученные запросы (к тестовой компании) + for (let i = 0; i < 3; i++) { + const senderCompany = companies[(i + 2) % companies.length]; + if (senderCompany._id.toString() === testCompanyId) { + continue; + } + + const createdAt = new Date(now.getTime() - (i + 1) * 12 * 60 * 60 * 1000); + + requests.push({ + senderCompanyId: senderCompany._id.toString(), + recipientCompanyId: testCompanyId, + subject: `Предложение о сотрудничестве ${i + 1}`, + text: `Добрый день! Предлагаем сотрудничество. Запрос ${i + 1}. Заинтересованы в вашей продукции.`, + files: [], + responseFiles: [], + status: 'pending', + response: null, + respondedAt: null, + createdAt, + updatedAt: createdAt, + }); + } + + if (requests.length > 0) { + await Request.insertMany(requests); + console.log(` ✓ Создано ${requests.length} тестовых запросов`); } await mongoose.connection.close(); From 69eddf47db069098216fd557fe739533cbd100a6 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 4 Nov 2025 19:32:58 +0300 Subject: [PATCH 131/147] fix auth --- server/routers/procurement/config/db.js | 10 ++++++---- server/routers/procurement/routes/auth.js | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js index 601687e..7452f7e 100644 --- a/server/routers/procurement/config/db.js +++ b/server/routers/procurement/config/db.js @@ -39,7 +39,9 @@ const connectWithUri = async (uri, label) => { }); try { - await connection.connection.db.admin().command({ ping: 1 }); + if (connection?.connection?.db) { + await connection.connection.db.admin().command({ ping: 1 }); + } } catch (pingError) { if (isAuthError(pingError)) { await mongoose.connection.close().catch(() => {}); @@ -49,10 +51,10 @@ const connectWithUri = async (uri, label) => { } console.log('✅ MongoDB подключена успешно!'); - console.log(` Хост: ${connection.connection.host}`); - console.log(` БД: ${connection.connection.name}\n`); + console.log(` Хост: ${connection?.connection?.host || 'не указан'}`); + console.log(` БД: ${connection?.connection?.name || 'не указана'}\n`); if (process.env.DEV === 'true') { - console.log(` Пользователь: ${connection.connection.user || 'anonymous'}`); + console.log(` Пользователь: ${connection?.connection?.user || 'anonymous'}`); } return connection; diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 4f64182..aa2943c 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -117,6 +117,9 @@ const waitForDatabaseConnection = async () => { const verifyAuth = async () => { try { + if (!mongoose.connection.db) { + return false; + } await mongoose.connection.db.admin().command({ listDatabases: 1 }); return true; } catch (error) { From c4664edd7eb042fbe8791dc11856f93d039c0a81 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 4 Nov 2025 19:46:39 +0300 Subject: [PATCH 132/147] fix mongo --- server/routers/procurement/config/db.js | 99 ------------------- server/routers/procurement/index.js | 13 +-- server/routers/procurement/routes/auth.js | 27 ++--- server/routers/procurement/routes/buy.js | 7 +- .../routers/procurement/routes/buyProducts.js | 26 +++-- server/routers/procurement/routes/requests.js | 32 +++--- .../procurement/scripts/recreate-test-user.js | 9 +- 7 files changed, 60 insertions(+), 153 deletions(-) delete mode 100644 server/routers/procurement/config/db.js diff --git a/server/routers/procurement/config/db.js b/server/routers/procurement/config/db.js deleted file mode 100644 index 7452f7e..0000000 --- a/server/routers/procurement/config/db.js +++ /dev/null @@ -1,99 +0,0 @@ -const mongoose = require('mongoose'); - -// Get MongoDB URL from environment variables -// MONGO_ADDR is a centralized env variable from server/utils/const.ts -const primaryUri = process.env.MONGO_ADDR || process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; -const fallbackUri = process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -/** - * Check if error is related to authentication - */ -const isAuthError = (error) => { - if (!error) { - return false; - } - - const authCodes = new Set([18, 13]); - if (error.code && authCodes.has(error.code)) { - return true; - } - - const message = String(error.message || '').toLowerCase(); - return message.includes('auth') || message.includes('authentication'); -}; - -/** - * Try to connect to MongoDB with specific URI - */ -const connectWithUri = async (uri, label) => { - console.log(`\n📡 Попытка подключения к MongoDB (${label})...`); - if (process.env.DEV === 'true') { - console.log(` URI: ${uri}`); - } - - const connection = await mongoose.connect(uri, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); - - try { - if (connection?.connection?.db) { - await connection.connection.db.admin().command({ ping: 1 }); - } - } catch (pingError) { - if (isAuthError(pingError)) { - await mongoose.connection.close().catch(() => {}); - throw pingError; - } - console.error('⚠️ MongoDB ping error:', pingError.message); - } - - console.log('✅ MongoDB подключена успешно!'); - console.log(` Хост: ${connection?.connection?.host || 'не указан'}`); - console.log(` БД: ${connection?.connection?.name || 'не указана'}\n`); - if (process.env.DEV === 'true') { - console.log(` Пользователь: ${connection?.connection?.user || 'anonymous'}`); - } - - return connection; -}; - -/** - * Connect to MongoDB with fallback strategy - */ -const connectDB = async () => { - const attempts = []; - - if (fallbackUri) { - attempts.push({ uri: fallbackUri, label: 'AUTH' }); - } - - attempts.push({ uri: primaryUri, label: 'PRIMARY' }); - - let lastError = null; - - for (const attempt of attempts) { - try { - console.log(`[MongoDB] Trying ${attempt.label} connection...`); - return await connectWithUri(attempt.uri, attempt.label); - } catch (error) { - lastError = error; - console.error(`\n❌ Ошибка подключения к MongoDB (${attempt.label}):`); - console.error(` ${error.message}\n`); - - if (!isAuthError(error)) { - break; - } - } - } - - if (lastError) { - console.warn('⚠️ Приложение продолжит работу с mock данными\n'); - } - - return null; -}; - -module.exports = connectDB; diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index b20542e..518ff7a 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -2,7 +2,7 @@ const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); const fs = require('fs'); -const path = require('path'); +const mongoose = require('mongoose'); // Загрузить переменные окружения dotenv.config(); @@ -28,15 +28,10 @@ const buyProductsRoutes = require('./routes/buyProducts'); const requestsRoutes = require('./routes/requests'); const homeRoutes = require('./routes/home'); -const connectDB = require('./config/db'); - const app = express(); -// Подключить MongoDB при инициализации -let dbConnected = false; -connectDB().then(() => { - dbConnected = true; -}); +// Проверить подключение к MongoDB (подключение происходит в server/utils/mongoose.ts) +const dbConnected = mongoose.connection.readyState === 1; // Middleware app.use(cors()); @@ -66,7 +61,7 @@ const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms); app.use(delay()); // Статика для загруженных файлов -const uploadsRoot = path.join(__dirname, '..', '..', 'remote-assets', 'uploads'); +const uploadsRoot = 'server/remote-assets/uploads'; if (!fs.existsSync(uploadsRoot)) { fs.mkdirSync(uploadsRoot, { recursive: true }); } diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index aa2943c..9b5437c 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -9,7 +9,6 @@ const Message = require('../models/Message'); const Review = require('../models/Review'); const mongoose = require('mongoose'); const { Types } = mongoose; -const connectDB = require('../config/db'); const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796'); const PRESET_USER_EMAIL = 'admin@test-company.ru'; @@ -140,17 +139,15 @@ const waitForDatabaseConnection = async () => { } try { - const connection = await connectDB(); - if (!connection) { - break; + // Ожидаем подключения (подключение происходит автоматически через server/utils/mongoose.ts) + await new Promise(resolve => setTimeout(resolve, 500)); + + if (mongoose.connection.readyState === 1) { + const authed = await verifyAuth(); + if (authed) { + return; + } } - - const authed = await verifyAuth(); - if (authed) { - return; - } - - await mongoose.connection.close().catch(() => {}); } catch (error) { if (!isAuthFailure(error)) { throw error; @@ -221,12 +218,8 @@ const initializeTestUser = async () => { } catch (error) { console.error('Error initializing test data:', error.message); if (error?.code === 13 || /auth/i.test(error?.message || '')) { - try { - await connectDB(); - } catch (connectError) { - if (process.env.DEV === 'true') { - console.error('Failed to re-connect after auth error:', connectError.message); - } + if (process.env.DEV === 'true') { + console.error('Auth error detected. Connection managed by server/utils/mongoose.ts'); } } } diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index d23ea01..da95f94 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -1,11 +1,10 @@ const express = require('express') const fs = require('fs') -const path = require('path') const router = express.Router() const BuyDocument = require('../models/BuyDocument') // Create remote-assets/docs directory if it doesn't exist -const docsDir = path.join(__dirname, '../../remote-assets/docs') +const docsDir = 'server/remote-assets/docs' if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }) } @@ -57,7 +56,7 @@ router.post('/docs', async (req, res) => { // Save file to disk const binaryData = Buffer.from(fileData, 'base64') - const filePath = path.join(docsDir, `${id}.${type}`) + const filePath = `${docsDir}/${id}.${type}` fs.writeFileSync(filePath, binaryData) console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`) @@ -187,7 +186,7 @@ router.get('/docs/:id/file', async (req, res) => { return res.status(404).json({ error: 'Document not found' }) } - const filePath = path.join(docsDir, `${id}.${doc.type}`) + const filePath = `${docsDir}/${id}.${doc.type}` if (!fs.existsSync(filePath)) { console.log('[BUY API] File not found on disk:', filePath) return res.status(404).json({ error: 'File not found on disk' }) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index 9ee74fe..f480130 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -2,10 +2,9 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const BuyProduct = require('../models/BuyProduct'); -const path = require('path'); const fs = require('fs'); const multer = require('multer'); -const UPLOADS_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'buy-products'); +const UPLOADS_ROOT = 'server/remote-assets/uploads/buy-products'; const ensureDirectory = (dirPath) => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); @@ -24,17 +23,28 @@ const ALLOWED_MIME_TYPES = new Set([ 'text/csv', ]); +const getExtension = (filename) => { + const lastDot = filename.lastIndexOf('.'); + return lastDot > 0 ? filename.slice(lastDot) : ''; +}; + +const getBasename = (filename) => { + const lastDot = filename.lastIndexOf('.'); + const name = lastDot > 0 ? filename.slice(0, lastDot) : filename; + const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); + return lastSlash >= 0 ? name.slice(lastSlash + 1) : name; +}; + const storage = multer.diskStorage({ destination: (req, file, cb) => { const productId = req.params.id || 'common'; - const productDir = path.join(UPLOADS_ROOT, productId); + const productDir = `${UPLOADS_ROOT}/${productId}`; ensureDirectory(productDir); cb(null, productDir); }, filename: (req, file, cb) => { - const originalExtension = path.extname(file.originalname) || ''; - const baseName = path - .basename(file.originalname, originalExtension) + const originalExtension = getExtension(file.originalname); + const baseName = getBasename(file.originalname) .replace(/[^a-zA-Z0-9-_]+/g, '_') .toLowerCase(); cb(null, `${Date.now()}_${baseName}${originalExtension}`); @@ -243,7 +253,7 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) return res.status(400).json({ error: 'File is required' }); } - const relativePath = path.join('buy-products', id, req.file.filename).replace(/\\/g, '/'); + const relativePath = `buy-products/${id}/${req.file.filename}`; const file = { id: `file-${Date.now()}`, name: req.file.originalname, @@ -293,7 +303,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { await product.save(); const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, ''); - const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', storedPath); + const absolutePath = `server/remote-assets/uploads/${storedPath}`; fs.promises.unlink(absolutePath).catch((unlinkError) => { if (unlinkError && unlinkError.code !== 'ENOENT') { diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js index 7e62b15..31d0f48 100644 --- a/server/routers/procurement/routes/requests.js +++ b/server/routers/procurement/routes/requests.js @@ -3,7 +3,6 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Request = require('../models/Request'); const BuyProduct = require('../models/BuyProduct'); -const path = require('path'); const fs = require('fs'); const multer = require('multer'); @@ -18,7 +17,7 @@ const log = (message, data = '') => { } }; -const REQUESTS_UPLOAD_ROOT = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', 'requests'); +const REQUESTS_UPLOAD_ROOT = 'server/remote-assets/uploads/requests'; const ensureDirectory = (dirPath) => { if (!fs.existsSync(dirPath)) { @@ -38,17 +37,28 @@ const ALLOWED_REQUEST_MIME_TYPES = new Set([ 'text/csv', ]); +const getExtension = (filename) => { + const lastDot = filename.lastIndexOf('.'); + return lastDot > 0 ? filename.slice(lastDot) : ''; +}; + +const getBasename = (filename) => { + const lastDot = filename.lastIndexOf('.'); + const name = lastDot > 0 ? filename.slice(0, lastDot) : filename; + const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); + return lastSlash >= 0 ? name.slice(lastSlash + 1) : name; +}; + const storage = multer.diskStorage({ destination: (req, file, cb) => { const subfolder = req.requestUploadSubfolder || ''; - const destinationDir = path.join(REQUESTS_UPLOAD_ROOT, subfolder); + const destinationDir = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}` : REQUESTS_UPLOAD_ROOT; ensureDirectory(destinationDir); cb(null, destinationDir); }, filename: (req, file, cb) => { - const extension = path.extname(file.originalname) || ''; - const baseName = path - .basename(file.originalname, extension) + const extension = getExtension(file.originalname); + const baseName = getBasename(file.originalname) .replace(/[^a-zA-Z0-9-_]+/g, '_') .toLowerCase(); cb(null, `${Date.now()}_${baseName}${extension}`); @@ -97,7 +107,7 @@ const cleanupUploadedFiles = async (req) => { const subfolder = req.requestUploadSubfolder || ''; const removalTasks = req.files.map((file) => { - const filePath = path.join(REQUESTS_UPLOAD_ROOT, subfolder, file.filename); + const filePath = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}` : `${REQUESTS_UPLOAD_ROOT}/${file.filename}`; return fs.promises.unlink(filePath).catch((error) => { if (error.code !== 'ENOENT') { console.error('[Requests] Failed to cleanup uploaded file:', error.message); @@ -115,7 +125,7 @@ const mapFilesToMetadata = (req) => { const subfolder = req.requestUploadSubfolder || ''; return req.files.map((file) => { - const relativePath = path.join('requests', subfolder, file.filename).replace(/\\/g, '/'); + const relativePath = subfolder ? `requests/${subfolder}/${file.filename}` : `requests/${file.filename}`; return { id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name: file.originalname, @@ -159,7 +169,7 @@ const removeStoredFiles = async (files = []) => { const tasks = files .filter((file) => file && file.storagePath) .map((file) => { - const absolutePath = path.join(__dirname, '..', '..', 'remote-assets', 'uploads', file.storagePath); + const absolutePath = `server/remote-assets/uploads/${file.storagePath}`; return fs.promises.unlink(absolutePath).catch((error) => { if (error.code !== 'ENOENT') { console.error('[Requests] Failed to remove stored file:', error.message); @@ -218,7 +228,7 @@ router.get('/received', verifyToken, async (req, res) => { router.post( '/', verifyToken, - handleFilesUpload('files', (req) => path.join('sent', (req.companyId || 'unknown').toString()), 10), + handleFilesUpload('files', (req) => `sent/${(req.companyId || 'unknown').toString()}`, 10), async (req, res) => { try { const senderCompanyId = req.companyId; @@ -317,7 +327,7 @@ router.post( router.put( '/:id', verifyToken, - handleFilesUpload('responseFiles', (req) => path.join('responses', req.params.id || 'unknown'), 5), + handleFilesUpload('responseFiles', (req) => `responses/${req.params.id || 'unknown'}`, 5), async (req, res) => { try { const { id } = req.params; diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 57a287f..80db85d 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,11 +1,10 @@ const mongoose = require('mongoose'); -const path = require('path'); require('dotenv').config(); -// Импорт моделей -const User = require(path.join(__dirname, '..', 'models', 'User')); -const Company = require(path.join(__dirname, '..', 'models', 'Company')); -const Request = require(path.join(__dirname, '..', 'models', 'Request')); +// Импорт моделей - прямые пути без path.join и __dirname +const User = require('../models/User'); +const Company = require('../models/Company'); +const Request = require('../models/Request'); const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; const fallbackUri = From 41b5cb6faeb40233fe177b95b3a38bdee8da0919 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Tue, 4 Nov 2025 22:39:29 +0300 Subject: [PATCH 133/147] update --- server/routers/procurement/index.js | 6 +- server/routers/procurement/models/Activity.js | 61 +++++++++ server/routers/procurement/routes/activity.js | 101 +++++++++++++++ .../procurement/scripts/seed-activities.js | 122 ++++++++++++++++++ .../procurement/scripts/seed-requests.js | 114 ++++++++++++++++ server/utils/mongoose.ts | 5 + 6 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 server/routers/procurement/models/Activity.js create mode 100644 server/routers/procurement/routes/activity.js create mode 100644 server/routers/procurement/scripts/seed-activities.js create mode 100644 server/routers/procurement/scripts/seed-requests.js diff --git a/server/routers/procurement/index.js b/server/routers/procurement/index.js index 518ff7a..c912fa9 100644 --- a/server/routers/procurement/index.js +++ b/server/routers/procurement/index.js @@ -2,7 +2,9 @@ const express = require('express'); const cors = require('cors'); const dotenv = require('dotenv'); const fs = require('fs'); -const mongoose = require('mongoose'); + +// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts) +const mongoose = require('../../utils/mongoose'); // Загрузить переменные окружения dotenv.config(); @@ -27,6 +29,7 @@ const reviewsRoutes = require('./routes/reviews'); const buyProductsRoutes = require('./routes/buyProducts'); const requestsRoutes = require('./routes/requests'); const homeRoutes = require('./routes/home'); +const activityRoutes = require('./routes/activity'); const app = express(); @@ -89,6 +92,7 @@ app.use('/products', productsRoutes); app.use('/reviews', reviewsRoutes); app.use('/requests', requestsRoutes); app.use('/home', homeRoutes); +app.use('/activities', activityRoutes); // Обработка ошибок app.use((err, req, res, next) => { diff --git a/server/routers/procurement/models/Activity.js b/server/routers/procurement/models/Activity.js new file mode 100644 index 0000000..1b94578 --- /dev/null +++ b/server/routers/procurement/models/Activity.js @@ -0,0 +1,61 @@ +const mongoose = require('mongoose'); + +const activitySchema = new mongoose.Schema({ + companyId: { + type: String, + required: true, + index: true + }, + userId: { + type: String, + required: true + }, + type: { + type: String, + enum: [ + 'message_received', + 'message_sent', + 'request_received', + 'request_sent', + 'request_response', + 'product_accepted', + 'review_received', + 'profile_updated', + 'product_added', + 'buy_product_added' + ], + required: true + }, + title: { + type: String, + required: true + }, + description: { + type: String + }, + relatedCompanyId: { + type: String + }, + relatedCompanyName: { + type: String + }, + metadata: { + type: mongoose.Schema.Types.Mixed + }, + read: { + type: Boolean, + default: false + }, + createdAt: { + type: Date, + default: Date.now, + index: true + } +}); + +// Индексы для оптимизации +activitySchema.index({ companyId: 1, createdAt: -1 }); +activitySchema.index({ companyId: 1, read: 1, createdAt: -1 }); + +module.exports = mongoose.model('Activity', activitySchema); + diff --git a/server/routers/procurement/routes/activity.js b/server/routers/procurement/routes/activity.js new file mode 100644 index 0000000..207ecc1 --- /dev/null +++ b/server/routers/procurement/routes/activity.js @@ -0,0 +1,101 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middleware/auth'); +const Activity = require('../models/Activity'); +const User = require('../models/User'); + +// Получить последние активности компании +router.get('/', verifyToken, async (req, res) => { + try { + const userId = req.userId; + const user = await User.findById(userId); + + if (!user || !user.companyId) { + return res.json({ activities: [] }); + } + + const companyId = user.companyId.toString(); + const limit = parseInt(req.query.limit) || 10; + + const activities = await Activity.find({ companyId }) + .sort({ createdAt: -1 }) + .limit(limit) + .lean(); + + res.json({ activities }); + } catch (error) { + console.error('Error getting activities:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Отметить активность как прочитанную +router.patch('/:id/read', verifyToken, async (req, res) => { + try { + const userId = req.userId; + const user = await User.findById(userId); + + if (!user || !user.companyId) { + return res.status(403).json({ error: 'Access denied' }); + } + + const companyId = user.companyId.toString(); + const activityId = req.params.id; + + const activity = await Activity.findOne({ + _id: activityId, + companyId + }); + + if (!activity) { + return res.status(404).json({ error: 'Activity not found' }); + } + + activity.read = true; + await activity.save(); + + res.json({ success: true, activity }); + } catch (error) { + console.error('Error updating activity:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Отметить все активности как прочитанные +router.post('/mark-all-read', verifyToken, async (req, res) => { + try { + const userId = req.userId; + const user = await User.findById(userId); + + if (!user || !user.companyId) { + return res.status(403).json({ error: 'Access denied' }); + } + + const companyId = user.companyId.toString(); + + await Activity.updateMany( + { companyId, read: false }, + { $set: { read: true } } + ); + + res.json({ success: true }); + } catch (error) { + console.error('Error marking all as read:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Создать активность (вспомогательная функция) +router.createActivity = async (data) => { + try { + const activity = new Activity(data); + await activity.save(); + return activity; + } catch (error) { + console.error('Error creating activity:', error); + throw error; + } +}; + +module.exports = router; + diff --git a/server/routers/procurement/scripts/seed-activities.js b/server/routers/procurement/scripts/seed-activities.js new file mode 100644 index 0000000..9490f28 --- /dev/null +++ b/server/routers/procurement/scripts/seed-activities.js @@ -0,0 +1,122 @@ +const mongoose = require('mongoose'); +require('dotenv').config(); + +// Подключение моделей - прямые пути без path.join и __dirname +const Activity = require('../models/Activity'); +const User = require('../models/User'); +const Company = require('../models/Company'); + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement-platform'; + +const activityTemplates = [ + { + type: 'request_received', + title: 'Получен новый запрос', + description: 'Компания отправила вам запрос на поставку товаров', + }, + { + type: 'request_sent', + title: 'Запрос отправлен', + description: 'Ваш запрос был отправлен компании', + }, + { + type: 'request_response', + title: 'Получен ответ на запрос', + description: 'Компания ответила на ваш запрос', + }, + { + type: 'product_accepted', + title: 'Товар акцептован', + description: 'Ваш товар был акцептован компанией', + }, + { + type: 'message_received', + title: 'Новое сообщение', + description: 'Вы получили новое сообщение от компании', + }, + { + type: 'review_received', + title: 'Новый отзыв', + description: 'Компания оставила отзыв о сотрудничестве', + }, + { + type: 'profile_updated', + title: 'Профиль обновлен', + description: 'Информация о вашей компании была обновлена', + }, + { + type: 'buy_product_added', + title: 'Добавлен товар для закупки', + description: 'В раздел "Я покупаю" добавлен новый товар', + }, +]; + +async function seedActivities() { + try { + console.log('🌱 Connecting to MongoDB...'); + await mongoose.connect(MONGODB_URI); + console.log('✅ Connected to MongoDB'); + + // Найти тестового пользователя + const testUser = await User.findOne({ email: 'admin@test-company.ru' }); + if (!testUser) { + console.log('❌ Test user not found. Please run recreate-test-user.js first.'); + process.exit(1); + } + + const company = await Company.findById(testUser.companyId); + if (!company) { + console.log('❌ Company not found'); + process.exit(1); + } + + // Найти другие компании для связанных активностей + const otherCompanies = await Company.find({ + _id: { $ne: company._id } + }).limit(3); + + console.log('🗑️ Clearing existing activities...'); + await Activity.deleteMany({ companyId: company._id.toString() }); + + console.log('➕ Creating activities...'); + const activities = []; + + for (let i = 0; i < 8; i++) { + const template = activityTemplates[i % activityTemplates.length]; + const relatedCompany = otherCompanies[i % otherCompanies.length]; + + const activity = { + companyId: company._id.toString(), + userId: testUser._id.toString(), + type: template.type, + title: template.title, + description: template.description, + relatedCompanyId: relatedCompany?._id.toString(), + relatedCompanyName: relatedCompany?.shortName || relatedCompany?.fullName, + read: i >= 5, // Первые 5 непрочитанные + createdAt: new Date(Date.now() - i * 3600000), // Каждый час назад + }; + + activities.push(activity); + } + + await Activity.insertMany(activities); + + console.log(`✅ Created ${activities.length} activities`); + console.log('✨ Activities seeded successfully!'); + + await mongoose.connection.close(); + console.log('👋 Database connection closed'); + } catch (error) { + console.error('❌ Error seeding activities:', error); + process.exit(1); + } +} + +// Запуск +if (require.main === module) { + seedActivities(); +} + +module.exports = { seedActivities }; + diff --git a/server/routers/procurement/scripts/seed-requests.js b/server/routers/procurement/scripts/seed-requests.js new file mode 100644 index 0000000..f435260 --- /dev/null +++ b/server/routers/procurement/scripts/seed-requests.js @@ -0,0 +1,114 @@ +const mongoose = require('mongoose'); +const Request = require('../models/Request'); +const Company = require('../models/Company'); +const User = require('../models/User'); + +const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; + +async function seedRequests() { + try { + await mongoose.connect(mongoUri); + console.log('✅ Connected to MongoDB'); + + // Получаем все компании + const companies = await Company.find().limit(10).exec(); + if (companies.length < 2) { + console.error('❌ Need at least 2 companies in database'); + process.exit(1); + } + + // Получаем тестового пользователя + const testUser = await User.findOne({ email: 'admin@test-company.ru' }).exec(); + if (!testUser) { + console.error('❌ Test user not found'); + process.exit(1); + } + + const testCompanyId = testUser.companyId.toString(); + console.log('📋 Test company ID:', testCompanyId); + console.log('📋 Found', companies.length, 'companies'); + + // Удаляем старые запросы + await Request.deleteMany({}); + console.log('🗑️ Cleared old requests'); + + const requests = []; + const now = new Date(); + + // Создаем отправленные запросы (от тестовой компании) + for (let i = 0; i < 5; i++) { + const recipientCompany = companies[i % companies.length]; + if (recipientCompany._id.toString() === testCompanyId) { + continue; + } + + const createdAt = new Date(now.getTime() - i * 24 * 60 * 60 * 1000); // За последние 5 дней + + requests.push({ + senderCompanyId: testCompanyId, + recipientCompanyId: recipientCompany._id.toString(), + subject: `Запрос на поставку ${i + 1}`, + text: `Здравствуйте! Интересует поставка товаров/услуг. Запрос ${i + 1}. Прошу предоставить коммерческое предложение.`, + files: [], + responseFiles: [], + status: i % 3 === 0 ? 'accepted' : i % 3 === 1 ? 'rejected' : 'pending', + response: i % 3 === 0 + ? 'Благодарим за запрос! Готовы предоставить услуги. Отправили КП на почту.' + : i % 3 === 1 + ? 'К сожалению, в данный момент не можем предоставить эти услуги.' + : null, + respondedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : null, + createdAt, + updatedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : createdAt, + }); + } + + // Создаем полученные запросы (к тестовой компании) + for (let i = 0; i < 3; i++) { + const senderCompany = companies[(i + 2) % companies.length]; + if (senderCompany._id.toString() === testCompanyId) { + continue; + } + + const createdAt = new Date(now.getTime() - (i + 1) * 12 * 60 * 60 * 1000); // За последние 1.5 дня + + requests.push({ + senderCompanyId: senderCompany._id.toString(), + recipientCompanyId: testCompanyId, + subject: `Предложение о сотрудничестве ${i + 1}`, + text: `Добрый день! Предлагаем сотрудничество. Запрос ${i + 1}. Заинтересованы в вашей продукции.`, + files: [], + responseFiles: [], + status: 'pending', + response: null, + respondedAt: null, + createdAt, + updatedAt: createdAt, + }); + } + + // Сохраняем все запросы + const savedRequests = await Request.insertMany(requests); + console.log('✅ Created', savedRequests.length, 'test requests'); + + // Статистика + const sentCount = await Request.countDocuments({ senderCompanyId: testCompanyId }); + const receivedCount = await Request.countDocuments({ recipientCompanyId: testCompanyId }); + const withResponses = await Request.countDocuments({ senderCompanyId: testCompanyId, response: { $ne: null } }); + + console.log('📊 Statistics:'); + console.log(' - Sent requests:', sentCount); + console.log(' - Received requests:', receivedCount); + console.log(' - With responses:', withResponses); + + } catch (error) { + console.error('❌ Error:', error); + process.exit(1); + } finally { + await mongoose.connection.close(); + console.log('👋 Disconnected from MongoDB'); + } +} + +seedRequests(); + diff --git a/server/utils/mongoose.ts b/server/utils/mongoose.ts index f797605..34278ef 100644 --- a/server/utils/mongoose.ts +++ b/server/utils/mongoose.ts @@ -9,3 +9,8 @@ mongoose.connect(mongoUrl).then(() => { console.error(err) }) +export default mongoose + +// Для совместимости с CommonJS +module.exports = mongoose +module.exports.default = mongoose From 284be82e1e44a98f7ba25a35a2888edeac932554 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Wed, 5 Nov 2025 19:06:11 +0300 Subject: [PATCH 134/147] Refactor file handling in BuyProduct and Request models; implement file schema for better structure. Update routes to handle file uploads and downloads with improved error handling and logging. Adjust MongoDB connection management across scripts and routes for consistency. --- .../routers/procurement/models/BuyProduct.js | 42 ++-- server/routers/procurement/models/Request.js | 24 +-- server/routers/procurement/routes/auth.js | 31 +-- server/routers/procurement/routes/buy.js | 3 +- .../routers/procurement/routes/buyProducts.js | 160 +++++++++++--- .../routers/procurement/routes/companies.js | 3 +- .../routers/procurement/routes/experience.js | 3 +- server/routers/procurement/routes/home.js | 32 +-- server/routers/procurement/routes/messages.js | 5 +- server/routers/procurement/routes/requests.js | 202 +++++++++++++++--- server/routers/procurement/routes/search.js | 125 ++++++++++- .../procurement/scripts/migrate-messages.js | 23 +- .../procurement/scripts/recreate-test-user.js | 133 ++++++++---- .../procurement/scripts/seed-activities.js | 16 +- .../procurement/scripts/seed-requests.js | 12 +- 15 files changed, 630 insertions(+), 184 deletions(-) diff --git a/server/routers/procurement/models/BuyProduct.js b/server/routers/procurement/models/BuyProduct.js index 6828b12..24ee7e0 100644 --- a/server/routers/procurement/models/BuyProduct.js +++ b/server/routers/procurement/models/BuyProduct.js @@ -1,5 +1,34 @@ const mongoose = require('mongoose'); +// Явно определяем схему для файлов +const fileSchema = new mongoose.Schema({ + id: { + type: String, + required: true + }, + name: { + type: String, + required: true + }, + url: { + type: String, + required: true + }, + type: { + type: String, + required: true + }, + size: { + type: Number, + required: true + }, + storagePath: String, + uploadedAt: { + type: Date, + default: Date.now + } +}, { _id: false }); + const buyProductSchema = new mongoose.Schema({ companyId: { type: String, @@ -24,18 +53,7 @@ const buyProductSchema = new mongoose.Schema({ type: String, default: 'шт' }, - files: [{ - id: String, - name: String, - url: String, - type: String, - size: Number, - storagePath: String, - uploadedAt: { - type: Date, - default: Date.now - } - }], + files: [fileSchema], acceptedBy: [{ companyId: { type: mongoose.Schema.Types.ObjectId, diff --git a/server/routers/procurement/models/Request.js b/server/routers/procurement/models/Request.js index 88f921d..6cd2412 100644 --- a/server/routers/procurement/models/Request.js +++ b/server/routers/procurement/models/Request.js @@ -22,12 +22,12 @@ const requestSchema = new mongoose.Schema({ required: true }, files: [{ - id: String, - name: String, - url: String, - type: String, - size: Number, - storagePath: String, + id: { type: String }, + name: { type: String }, + url: { type: String }, + type: { type: String }, + size: { type: Number }, + storagePath: { type: String }, uploadedAt: { type: Date, default: Date.now @@ -47,12 +47,12 @@ const requestSchema = new mongoose.Schema({ default: null }, responseFiles: [{ - id: String, - name: String, - url: String, - type: String, - size: Number, - storagePath: String, + id: { type: String }, + name: { type: String }, + url: { type: String }, + type: { type: String }, + size: { type: Number }, + storagePath: { type: String }, uploadedAt: { type: Date, default: Date.now diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index 9b5437c..ec54893 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -7,7 +7,7 @@ const Request = require('../models/Request'); const BuyProduct = require('../models/BuyProduct'); const Message = require('../models/Message'); const Review = require('../models/Review'); -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); const { Types } = mongoose; const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796'); @@ -116,9 +116,6 @@ const waitForDatabaseConnection = async () => { const verifyAuth = async () => { try { - if (!mongoose.connection.db) { - return false; - } await mongoose.connection.db.admin().command({ listDatabases: 1 }); return true; } catch (error) { @@ -139,15 +136,17 @@ const waitForDatabaseConnection = async () => { } try { - // Ожидаем подключения (подключение происходит автоматически через server/utils/mongoose.ts) - await new Promise(resolve => setTimeout(resolve, 500)); - - if (mongoose.connection.readyState === 1) { - const authed = await verifyAuth(); - if (authed) { - return; - } + const connection = await connectDB(); + if (!connection) { + break; } + + const authed = await verifyAuth(); + if (authed) { + return; + } + + await mongoose.connection.close().catch(() => {}); } catch (error) { if (!isAuthFailure(error)) { throw error; @@ -218,8 +217,12 @@ const initializeTestUser = async () => { } catch (error) { console.error('Error initializing test data:', error.message); if (error?.code === 13 || /auth/i.test(error?.message || '')) { - if (process.env.DEV === 'true') { - console.error('Auth error detected. Connection managed by server/utils/mongoose.ts'); + try { + await connectDB(); + } catch (connectError) { + if (process.env.DEV === 'true') { + console.error('Failed to re-connect after auth error:', connectError.message); + } } } } diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index da95f94..69f22f2 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -1,10 +1,11 @@ const express = require('express') const fs = require('fs') +const path = require('path') const router = express.Router() const BuyDocument = require('../models/BuyDocument') // Create remote-assets/docs directory if it doesn't exist -const docsDir = 'server/remote-assets/docs' +const docsDir = 'server/routers/remote-assets/docs' if (!fs.existsSync(docsDir)) { fs.mkdirSync(docsDir, { recursive: true }) } diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index f480130..b844b18 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -2,9 +2,10 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const BuyProduct = require('../models/BuyProduct'); +const path = require('path'); const fs = require('fs'); const multer = require('multer'); -const UPLOADS_ROOT = 'server/remote-assets/uploads/buy-products'; +const UPLOADS_ROOT = 'server/routers/remote-assets/uploads/buy-products'; const ensureDirectory = (dirPath) => { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); @@ -23,18 +24,6 @@ const ALLOWED_MIME_TYPES = new Set([ 'text/csv', ]); -const getExtension = (filename) => { - const lastDot = filename.lastIndexOf('.'); - return lastDot > 0 ? filename.slice(lastDot) : ''; -}; - -const getBasename = (filename) => { - const lastDot = filename.lastIndexOf('.'); - const name = lastDot > 0 ? filename.slice(0, lastDot) : filename; - const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); - return lastSlash >= 0 ? name.slice(lastSlash + 1) : name; -}; - const storage = multer.diskStorage({ destination: (req, file, cb) => { const productId = req.params.id || 'common'; @@ -43,10 +32,12 @@ const storage = multer.diskStorage({ cb(null, productDir); }, filename: (req, file, cb) => { - const originalExtension = getExtension(file.originalname); - const baseName = getBasename(file.originalname) - .replace(/[^a-zA-Z0-9-_]+/g, '_') - .toLowerCase(); + // Исправляем кодировку имени файла из Latin1 в UTF-8 + const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf8'); + const originalExtension = path.extname(fixedName) || ''; + const baseName = path + .basename(fixedName, originalExtension) + .replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу cb(null, `${Date.now()}_${baseName}${originalExtension}`); }, }); @@ -241,7 +232,16 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) } // Только владелец товара может добавить файл - if (product.companyId.toString() !== req.companyId.toString()) { + const productCompanyId = product.companyId?.toString() || product.companyId; + const requestCompanyId = req.companyId?.toString() || req.companyId; + + console.log('[BuyProducts] Comparing company IDs:', { + productCompanyId, + requestCompanyId, + match: productCompanyId === requestCompanyId + }); + + if (productCompanyId !== requestCompanyId) { return res.status(403).json({ error: 'Not authorized' }); } @@ -253,28 +253,75 @@ router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) return res.status(400).json({ error: 'File is required' }); } - const relativePath = `buy-products/${id}/${req.file.filename}`; + // Исправляем кодировку имени файла из Latin1 в UTF-8 + const fixedFileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8'); + + // Извлекаем timestamp из имени файла, созданного multer (формат: {timestamp}_{name}.ext) + const fileTimestamp = req.file.filename.split('_')[0]; + + // storagePath относительно UPLOADS_ROOT (который уже включает 'buy-products') + const relativePath = `${id}/${req.file.filename}`; const file = { - id: `file-${Date.now()}`, - name: req.file.originalname, - url: `/uploads/${relativePath}`, + id: `file-${fileTimestamp}`, // Используем тот же timestamp, что и в имени файла + name: fixedFileName, + url: `/uploads/buy-products/${relativePath}`, type: req.file.mimetype, size: req.file.size, uploadedAt: new Date(), storagePath: relativePath, }; - product.files.push(file); - await product.save(); + console.log('[BuyProducts] Adding file to product:', { + productId: id, + fileName: file.name, + fileSize: file.size, + filePath: relativePath + }); + + console.log('[BuyProducts] File object:', JSON.stringify(file, null, 2)); + + // Используем findByIdAndUpdate вместо save() для избежания проблем с валидацией + let updatedProduct; + try { + console.log('[BuyProducts] Calling findByIdAndUpdate with id:', id); + updatedProduct = await BuyProduct.findByIdAndUpdate( + id, + { + $push: { files: file }, + $set: { updatedAt: new Date() } + }, + { new: true, runValidators: false } + ); + console.log('[BuyProducts] findByIdAndUpdate completed'); + } catch (updateError) { + console.error('[BuyProducts] findByIdAndUpdate error:', { + message: updateError.message, + name: updateError.name, + code: updateError.code + }); + throw updateError; + } + + if (!updatedProduct) { + throw new Error('Failed to update product with file'); + } + + console.log('[BuyProducts] File added successfully to product:', id); log('[BuyProducts] File added to product:', id, file.name); - res.json(product); + res.json(updatedProduct); } catch (error) { console.error('[BuyProducts] Error adding file:', error.message); + console.error('[BuyProducts] Error stack:', error.stack); + console.error('[BuyProducts] Error name:', error.name); + if (error.errors) { + console.error('[BuyProducts] Validation errors:', JSON.stringify(error.errors, null, 2)); + } res.status(500).json({ error: 'Internal server error', message: error.message, + details: error.errors || {}, }); } }); @@ -303,7 +350,7 @@ router.delete('/:id/files/:fileId', verifyToken, async (req, res) => { await product.save(); const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, ''); - const absolutePath = `server/remote-assets/uploads/${storedPath}`; + const absolutePath = `server/routers/remote-assets/uploads/${storedPath}`; fs.promises.unlink(absolutePath).catch((unlinkError) => { if (unlinkError && unlinkError.code !== 'ENOENT') { @@ -391,4 +438,65 @@ router.get('/:id/acceptances', verifyToken, async (req, res) => { } }); +// GET /buy-products/download/:id/:fileId - скачать файл +router.get('/download/:id/:fileId', verifyToken, async (req, res) => { + try { + console.log('[BuyProducts] Download request received:', { + productId: req.params.id, + fileId: req.params.fileId, + userId: req.userId, + companyId: req.companyId, + headers: req.headers.authorization + }); + + const { id, fileId } = req.params; + const product = await BuyProduct.findById(id); + + if (!product) { + return res.status(404).json({ error: 'Product not found' }); + } + + const file = product.files.find((f) => f.id === fileId); + if (!file) { + return res.status(404).json({ error: 'File not found' }); + } + + // Создаем абсолютный путь к файлу + const filePath = path.resolve(UPLOADS_ROOT, file.storagePath); + + console.log('[BuyProducts] Trying to download file:', { + fileId: file.id, + fileName: file.name, + storagePath: file.storagePath, + absolutePath: filePath, + exists: fs.existsSync(filePath) + }); + + // Проверяем существование файла + if (!fs.existsSync(filePath)) { + console.error('[BuyProducts] File not found on disk:', filePath); + return res.status(404).json({ error: 'File not found on disk' }); + } + + // Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы + const encodedFileName = encodeURIComponent(file.name); + res.setHeader('Content-Type', file.type || 'application/octet-stream'); + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`); + res.setHeader('Content-Length', file.size); + + // Отправляем файл + res.sendFile(filePath, (err) => { + if (err) { + console.error('[BuyProducts] Error sending file:', err.message); + if (!res.headersSent) { + res.status(500).json({ error: 'Error downloading file' }); + } + } + }); + } catch (error) { + console.error('[BuyProducts] Error downloading file:', error.message); + res.status(500).json({ error: 'Internal server error' }); + } +}); + module.exports = router; diff --git a/server/routers/procurement/routes/companies.js b/server/routers/procurement/routes/companies.js index 914cd72..8da9bd5 100644 --- a/server/routers/procurement/routes/companies.js +++ b/server/routers/procurement/routes/companies.js @@ -5,7 +5,8 @@ const Company = require('../models/Company'); const Experience = require('../models/Experience'); const Request = require('../models/Request'); const Message = require('../models/Message'); -const { Types } = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); +const { Types } = mongoose; // GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id router.get('/my/info', verifyToken, async (req, res) => { diff --git a/server/routers/procurement/routes/experience.js b/server/routers/procurement/routes/experience.js index dcea942..47a2d27 100644 --- a/server/routers/procurement/routes/experience.js +++ b/server/routers/procurement/routes/experience.js @@ -2,7 +2,8 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Experience = require('../models/Experience'); -const { Types } = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); +const { Types } = mongoose; // GET /experience - Получить список опыта работы компании router.get('/', verifyToken, async (req, res) => { diff --git a/server/routers/procurement/routes/home.js b/server/routers/procurement/routes/home.js index 82c87d2..3914a31 100644 --- a/server/routers/procurement/routes/home.js +++ b/server/routers/procurement/routes/home.js @@ -21,21 +21,23 @@ router.get('/aggregates', verifyToken, async (req, res) => { const companyId = user.companyId.toString(); - const [docsCount, acceptsCount, requestsCount] = await Promise.all([ - BuyProduct.countDocuments({ companyId }), - Request.countDocuments({ - $or: [ - { senderCompanyId: companyId, status: 'accepted' }, - { recipientCompanyId: companyId, status: 'accepted' } - ] - }), - Request.countDocuments({ - $or: [ - { senderCompanyId: companyId }, - { recipientCompanyId: companyId } - ] - }) - ]); + // Получить все BuyProduct для подсчета файлов и акцептов + const buyProducts = await BuyProduct.find({ companyId }); + + // Подсчет документов - сумма всех файлов во всех BuyProduct + const docsCount = buyProducts.reduce((total, product) => { + return total + (product.files ? product.files.length : 0); + }, 0); + + // Подсчет акцептов - сумма всех acceptedBy во всех BuyProduct + const acceptsCount = buyProducts.reduce((total, product) => { + return total + (product.acceptedBy ? product.acceptedBy.length : 0); + }, 0); + + // Подсчет исходящих запросов (только отправленные этой компанией) + const requestsCount = await Request.countDocuments({ + senderCompanyId: companyId + }); res.json({ docsCount, diff --git a/server/routers/procurement/routes/messages.js b/server/routers/procurement/routes/messages.js index 7573d1f..61766ac 100644 --- a/server/routers/procurement/routes/messages.js +++ b/server/routers/procurement/routes/messages.js @@ -2,6 +2,8 @@ const express = require('express'); const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Message = require('../models/Message'); +const mongoose = require('../../../utils/mongoose'); +const { ObjectId } = mongoose.Types; // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -18,7 +20,6 @@ const log = (message, data = '') => { router.get('/threads', verifyToken, async (req, res) => { try { const companyId = req.companyId; - const { ObjectId } = require('mongoose').Types; log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId); @@ -146,7 +147,6 @@ router.post('/:threadId', verifyToken, async (req, res) => { // Найти recipientCompanyId по ObjectId если нужно let recipientObjectId = recipientCompanyId; - const { ObjectId } = require('mongoose').Types; try { if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) { recipientObjectId = new ObjectId(recipientCompanyId); @@ -210,7 +210,6 @@ router.post('/admin/migrate-fix-recipients', async (req, res) => { // If recipientCompanyId is not set or wrong - fix it if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) { - const { ObjectId } = require('mongoose').Types; let recipientObjectId = expectedRecipient; try { if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { diff --git a/server/routers/procurement/routes/requests.js b/server/routers/procurement/routes/requests.js index 31d0f48..93c072b 100644 --- a/server/routers/procurement/routes/requests.js +++ b/server/routers/procurement/routes/requests.js @@ -3,8 +3,10 @@ const router = express.Router(); const { verifyToken } = require('../middleware/auth'); const Request = require('../models/Request'); const BuyProduct = require('../models/BuyProduct'); +const path = require('path'); const fs = require('fs'); const multer = require('multer'); +const mongoose = require('../../../utils/mongoose'); // Функция для логирования с проверкой DEV переменной const log = (message, data = '') => { @@ -17,7 +19,7 @@ const log = (message, data = '') => { } }; -const REQUESTS_UPLOAD_ROOT = 'server/remote-assets/uploads/requests'; +const REQUESTS_UPLOAD_ROOT = 'server/routers/remote-assets/uploads/requests'; const ensureDirectory = (dirPath) => { if (!fs.existsSync(dirPath)) { @@ -37,28 +39,17 @@ const ALLOWED_REQUEST_MIME_TYPES = new Set([ 'text/csv', ]); -const getExtension = (filename) => { - const lastDot = filename.lastIndexOf('.'); - return lastDot > 0 ? filename.slice(lastDot) : ''; -}; - -const getBasename = (filename) => { - const lastDot = filename.lastIndexOf('.'); - const name = lastDot > 0 ? filename.slice(0, lastDot) : filename; - const lastSlash = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')); - return lastSlash >= 0 ? name.slice(lastSlash + 1) : name; -}; - const storage = multer.diskStorage({ destination: (req, file, cb) => { const subfolder = req.requestUploadSubfolder || ''; - const destinationDir = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}` : REQUESTS_UPLOAD_ROOT; + const destinationDir = `${REQUESTS_UPLOAD_ROOT}/${subfolder}`; ensureDirectory(destinationDir); cb(null, destinationDir); }, filename: (req, file, cb) => { - const extension = getExtension(file.originalname); - const baseName = getBasename(file.originalname) + const extension = path.extname(file.originalname) || ''; + const baseName = path + .basename(file.originalname, extension) .replace(/[^a-zA-Z0-9-_]+/g, '_') .toLowerCase(); cb(null, `${Date.now()}_${baseName}${extension}`); @@ -107,7 +98,7 @@ const cleanupUploadedFiles = async (req) => { const subfolder = req.requestUploadSubfolder || ''; const removalTasks = req.files.map((file) => { - const filePath = subfolder ? `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}` : `${REQUESTS_UPLOAD_ROOT}/${file.filename}`; + const filePath = `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}`; return fs.promises.unlink(filePath).catch((error) => { if (error.code !== 'ENOENT') { console.error('[Requests] Failed to cleanup uploaded file:', error.message); @@ -125,7 +116,7 @@ const mapFilesToMetadata = (req) => { const subfolder = req.requestUploadSubfolder || ''; return req.files.map((file) => { - const relativePath = subfolder ? `requests/${subfolder}/${file.filename}` : `requests/${file.filename}`; + const relativePath = `requests/${subfolder}/${file.filename}`; return { id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name: file.originalname, @@ -169,7 +160,7 @@ const removeStoredFiles = async (files = []) => { const tasks = files .filter((file) => file && file.storagePath) .map((file) => { - const absolutePath = `server/remote-assets/uploads/${file.storagePath}`; + const absolutePath = `server/routers/remote-assets/uploads/${file.storagePath}`; return fs.promises.unlink(absolutePath).catch((error) => { if (error.code !== 'ENOENT') { console.error('[Requests] Failed to remove stored file:', error.message); @@ -255,24 +246,61 @@ router.post( return res.status(400).json({ error: 'At least one recipient is required' }); } - if (!subject && productId) { + let uploadedFiles = mapFilesToMetadata(req); + + console.log('========================'); + console.log('[Requests] Initial uploadedFiles:', uploadedFiles.length); + console.log('[Requests] ProductId:', productId); + + // Если есть productId, получаем данные товара + if (productId) { try { const product = await BuyProduct.findById(productId); + console.log('[Requests] Product found:', product ? product.name : 'null'); + console.log('[Requests] Product files count:', product?.files?.length || 0); + if (product && product.files) { + console.log('[Requests] Product files:', JSON.stringify(product.files, null, 2)); + } + if (product) { - subject = product.name; + // Берем subject из товара, если не указан + if (!subject) { + subject = product.name; + } + + // Если файлы не загружены вручную, используем файлы из товара + if (uploadedFiles.length === 0 && product.files && product.files.length > 0) { + console.log('[Requests] ✅ Copying files from product...'); + // Копируем файлы из товара, изменяя путь для запроса + uploadedFiles = product.files.map(file => ({ + id: file.id || `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + name: file.name, + url: file.url, + type: file.type, + size: file.size, + uploadedAt: file.uploadedAt || new Date(), + storagePath: file.storagePath || file.url.replace('/uploads/', ''), + })); + console.log('[Requests] ✅ Using', uploadedFiles.length, 'files from product:', productId); + console.log('[Requests] ✅ Copied files:', JSON.stringify(uploadedFiles, null, 2)); + } else { + console.log('[Requests] ❌ NOT copying files. uploadedFiles.length:', uploadedFiles.length, 'product.files.length:', product.files?.length || 0); + } } } catch (lookupError) { - console.error('[Requests] Failed to lookup product for subject:', lookupError.message); + console.error('[Requests] ❌ Failed to lookup product:', lookupError.message); + console.error(lookupError.stack); } } + + console.log('[Requests] Final uploadedFiles for saving:', JSON.stringify(uploadedFiles, null, 2)); + console.log('========================'); if (!subject) { await cleanupUploadedFiles(req); return res.status(400).json({ error: 'Subject is required' }); } - const uploadedFiles = mapFilesToMetadata(req); - const results = []; for (const recipientCompanyId of recipients) { try { @@ -331,9 +359,17 @@ router.put( async (req, res) => { try { const { id } = req.params; + console.log('[Requests] PUT /requests/:id called with id:', id); + console.log('[Requests] Request body:', req.body); + console.log('[Requests] Files:', req.files); + console.log('[Requests] CompanyId:', req.companyId); + const responseText = (req.body.response || '').trim(); const statusRaw = (req.body.status || 'accepted').toLowerCase(); const status = statusRaw === 'rejected' ? 'rejected' : 'accepted'; + + console.log('[Requests] Response text:', responseText); + console.log('[Requests] Status:', status); if (req.invalidFiles && req.invalidFiles.length > 0) { await cleanupUploadedFiles(req); @@ -361,6 +397,8 @@ router.put( } const uploadedResponseFiles = mapFilesToMetadata(req); + console.log('[Requests] Uploaded response files count:', uploadedResponseFiles.length); + console.log('[Requests] Uploaded response files:', JSON.stringify(uploadedResponseFiles, null, 2)); if (uploadedResponseFiles.length > 0) { await removeStoredFiles(request.responseFiles || []); @@ -372,18 +410,126 @@ router.put( request.respondedAt = new Date(); request.updatedAt = new Date(); - await request.save(); + let savedRequest; + try { + savedRequest = await request.save(); + log('[Requests] Request responded:', id); + } catch (saveError) { + console.error('[Requests] Mongoose save failed, trying direct MongoDB update:', saveError.message); + // Fallback: использовать MongoDB драйвер напрямую + const updateData = { + response: responseText, + status: status, + respondedAt: new Date(), + updatedAt: new Date() + }; + if (uploadedResponseFiles.length > 0) { + updateData.responseFiles = uploadedResponseFiles; + } + + const result = await mongoose.connection.collection('requests').findOneAndUpdate( + { _id: new mongoose.Types.ObjectId(id) }, + { $set: updateData }, + { returnDocument: 'after' } + ); + + if (!result) { + throw new Error('Failed to update request'); + } + savedRequest = result; + log('[Requests] Request responded via direct MongoDB update:', id); + } - log('[Requests] Request responded:', id); - - res.json(request); + res.json(savedRequest); } catch (error) { console.error('[Requests] Error responding to request:', error.message); + console.error('[Requests] Error stack:', error.stack); + if (error.name === 'ValidationError') { + console.error('[Requests] Validation errors:', JSON.stringify(error.errors, null, 2)); + } res.status(500).json({ error: error.message }); } } ); +// GET /requests/download/:id/:fileId - скачать файл ответа +router.get('/download/:id/:fileId', verifyToken, async (req, res) => { + try { + console.log('[Requests] Download request received:', { + requestId: req.params.id, + fileId: req.params.fileId, + userId: req.userId, + companyId: req.companyId, + }); + + const { id, fileId } = req.params; + const request = await Request.findById(id); + + if (!request) { + return res.status(404).json({ error: 'Request not found' }); + } + + // Проверяем, что пользователь имеет доступ к запросу (отправитель или получатель) + if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) { + return res.status(403).json({ error: 'Not authorized' }); + } + + // Ищем файл в responseFiles или в обычных files + let file = request.responseFiles?.find((f) => f.id === fileId); + if (!file) { + file = request.files?.find((f) => f.id === fileId); + } + if (!file) { + return res.status(404).json({ error: 'File not found' }); + } + + // Создаем абсолютный путь к файлу + // Если storagePath не начинается с 'requests/', значит это файл из buy-products + let fullPath = file.storagePath; + if (!fullPath.startsWith('requests/')) { + fullPath = `buy-products/${fullPath}`; + } + const filePath = path.resolve(`server/routers/remote-assets/uploads/${fullPath}`); + + console.log('[Requests] Trying to download file:', { + fileId: file.id, + fileName: file.name, + storagePath: file.storagePath, + absolutePath: filePath, + exists: fs.existsSync(filePath), + }); + + // Проверяем существование файла + if (!fs.existsSync(filePath)) { + console.error('[Requests] File not found on disk:', filePath); + return res.status(404).json({ error: 'File not found on disk' }); + } + + // Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы + const encodedFileName = encodeURIComponent(file.name); + res.setHeader('Content-Type', file.type || 'application/octet-stream'); + res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`); + res.setHeader('Content-Length', file.size); + + // Отправляем файл + res.sendFile(filePath, (err) => { + if (err) { + console.error('[Requests] Error sending file:', err.message); + if (!res.headersSent) { + res.status(500).json({ error: 'Error sending file' }); + } + } else { + log('[Requests] File downloaded:', file.name); + } + }); + } catch (error) { + console.error('[Requests] Error downloading file:', error.message); + if (!res.headersSent) { + res.status(500).json({ error: error.message }); + } + } +}); + // DELETE /requests/:id - удалить запрос router.delete('/:id', verifyToken, async (req, res) => { try { diff --git a/server/routers/procurement/routes/search.js b/server/routers/procurement/routes/search.js index 84e38e6..3f064b0 100644 --- a/server/routers/procurement/routes/search.js +++ b/server/routers/procurement/routes/search.js @@ -54,10 +54,13 @@ router.get('/recommendations', verifyToken, async (req, res) => { // GET /search - Поиск компаний router.get('/', verifyToken, async (req, res) => { try { + console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ==='); + const { query = '', page = 1, limit = 10, + offset, // Добавляем поддержку offset для точной пагинации industries, companySize, geography, @@ -65,8 +68,12 @@ router.get('/', verifyToken, async (req, res) => { hasReviews, hasAcceptedDocs, sortBy = 'relevance', - sortOrder = 'desc' + sortOrder = 'desc', + minEmployees, // Кастомный фильтр: минимум сотрудников + maxEmployees // Кастомный фильтр: максимум сотрудников } = req.query; + + console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize }); // Получить компанию пользователя, чтобы исключить её из результатов const User = require('../models/User'); @@ -135,12 +142,99 @@ router.get('/', verifyToken, async (req, res) => { } } - // Фильтр по размеру компании - if (companySize) { - const sizeList = Array.isArray(companySize) ? companySize : [companySize]; - if (sizeList.length > 0) { - filters.push({ companySize: { $in: sizeList } }); + // Функция для парсинга диапазона из строки вида "51-250" или "500+" + const parseEmployeeRange = (sizeStr) => { + if (sizeStr.includes('+')) { + const min = parseInt(sizeStr.replace('+', '')); + return { min, max: Infinity }; } + const parts = sizeStr.split('-'); + return { + min: parseInt(parts[0]), + max: parts[1] ? parseInt(parts[1]) : parseInt(parts[0]) + }; + }; + + // Функция для проверки пересечения двух диапазонов + const rangesOverlap = (range1, range2) => { + return range1.min <= range2.max && range1.max >= range2.min; + }; + + // Фильтр по размеру компании (чекбоксы) или кастомный диапазон + // Важно: этот фильтр должен получить все компании для корректной работы пересечения диапазонов + let sizeFilteredIds = null; + if ((companySize && companySize.length > 0) || minEmployees || maxEmployees) { + // Получаем все компании (без других фильтров, так как размер компании - это property-based фильтр) + const allCompanies = await Company.find({}); + + log('[Search] Employee size filter - checking companies:', allCompanies.length); + + let matchingIds = []; + + // Если есть кастомный диапазон - используем его + if (minEmployees || maxEmployees) { + const customRange = { + min: minEmployees ? parseInt(minEmployees, 10) : 0, + max: maxEmployees ? parseInt(maxEmployees, 10) : Infinity + }; + + log('[Search] Custom employee range filter:', customRange); + + matchingIds = allCompanies + .filter(company => { + if (!company.companySize) { + log('[Search] Company has no size:', company.fullName); + return false; + } + + const companyRange = parseEmployeeRange(company.companySize); + const overlaps = rangesOverlap(companyRange, customRange); + + log('[Search] Checking overlap:', { + company: company.fullName, + companyRange, + customRange, + overlaps + }); + + return overlaps; + }) + .map(c => c._id); + + log('[Search] Matching companies by custom range:', matchingIds.length); + } + // Иначе используем чекбоксы + else if (companySize && companySize.length > 0) { + const sizeList = Array.isArray(companySize) ? companySize : [companySize]; + + log('[Search] Company size checkboxes filter:', sizeList); + + matchingIds = allCompanies + .filter(company => { + if (!company.companySize) { + return false; + } + + const companyRange = parseEmployeeRange(company.companySize); + + // Проверяем пересечение с любым из выбранных диапазонов + const matches = sizeList.some(selectedSize => { + const filterRange = parseEmployeeRange(selectedSize); + const overlaps = rangesOverlap(companyRange, filterRange); + log('[Search] Check:', company.fullName, companyRange, 'vs', filterRange, '=', overlaps); + return overlaps; + }); + + return matches; + }) + .map(c => c._id); + + log('[Search] Matching companies by size checkboxes:', matchingIds.length); + } + + // Сохраняем ID для дальнейшей фильтрации + sizeFilteredIds = matchingIds; + log('[Search] Size filtered IDs count:', sizeFilteredIds.length); } // Фильтр по географии @@ -170,13 +264,25 @@ router.get('/', verifyToken, async (req, res) => { filters.push({ verified: true }); } + // Применяем фильтр по размеру компании (если был задан) + if (sizeFilteredIds !== null) { + if (sizeFilteredIds.length > 0) { + filters.push({ _id: { $in: sizeFilteredIds } }); + log('[Search] Applied size filter, IDs:', sizeFilteredIds.length); + } else { + // Если нет подходящих компаний по размеру, возвращаем пустой результат + filters.push({ _id: null }); + log('[Search] No companies match size criteria'); + } + } + // Комбинировать все фильтры let filter = filters.length > 0 ? { $and: filters } : {}; - // Пагинация - const pageNum = parseInt(page) || 1; + // Пагинация - используем offset если передан, иначе вычисляем из page const limitNum = parseInt(limit) || 10; - const skip = (pageNum - 1) * limitNum; + const skip = offset !== undefined ? parseInt(offset) : ((parseInt(page) || 1) - 1) * limitNum; + const pageNum = offset !== undefined ? Math.floor(skip / limitNum) + 1 : parseInt(page) || 1; // Сортировка let sortOptions = {}; @@ -228,3 +334,4 @@ router.get('/', verifyToken, async (req, res) => { module.exports = router; + diff --git a/server/routers/procurement/scripts/migrate-messages.js b/server/routers/procurement/scripts/migrate-messages.js index d342f44..f77ba9b 100644 --- a/server/routers/procurement/scripts/migrate-messages.js +++ b/server/routers/procurement/scripts/migrate-messages.js @@ -1,18 +1,18 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); +const { ObjectId } = mongoose.Types; const Message = require('../models/Message'); -require('dotenv').config({ path: '../../.env' }); - -const mongoUrl = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; +require('dotenv').config(); async function migrateMessages() { try { - console.log('[Migration] Connecting to MongoDB...'); - await mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - serverSelectionTimeoutMS: 5000, - connectTimeoutMS: 5000, - }); + // Подключение к MongoDB происходит через server/utils/mongoose.ts + console.log('[Migration] Checking MongoDB connection...'); + if (mongoose.connection.readyState !== 1) { + console.log('[Migration] Waiting for MongoDB connection...'); + await new Promise((resolve) => { + mongoose.connection.once('connected', resolve); + }); + } console.log('[Migration] Connected to MongoDB'); // Найти все сообщения @@ -54,7 +54,6 @@ async function migrateMessages() { console.log(' Expected:', expectedRecipient); // Конвертируем в ObjectId если нужно - const { ObjectId } = require('mongoose').Types; let recipientObjectId = expectedRecipient; try { if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) { diff --git a/server/routers/procurement/scripts/recreate-test-user.js b/server/routers/procurement/scripts/recreate-test-user.js index 80db85d..af2c530 100644 --- a/server/routers/procurement/scripts/recreate-test-user.js +++ b/server/routers/procurement/scripts/recreate-test-user.js @@ -1,57 +1,57 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); require('dotenv').config(); -// Импорт моделей - прямые пути без path.join и __dirname +// Импорт моделей const User = require('../models/User'); const Company = require('../models/Company'); const Request = require('../models/Request'); -const primaryUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement_db'; -const fallbackUri = - process.env.MONGODB_AUTH_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - -const connectWithFallback = async () => { - // Сначала пробуем FALLBACK (с аутентификацией) - try { - console.log('\n📡 Подключение к MongoDB (с аутентификацией)...'); - await mongoose.connect(fallbackUri, { useNewUrlParser: true, useUnifiedTopology: true }); - console.log('✅ Подключено к MongoDB'); +// Подключение к MongoDB происходит через server/utils/mongoose.ts +// Проверяем, подключено ли уже +const ensureConnection = async () => { + if (mongoose.connection.readyState === 1) { + console.log('✅ MongoDB уже подключено'); return; - } catch (fallbackError) { - console.log('❌ Ошибка подключения с аутентификацией:', fallbackError.message); - } - - // Если не получилось, пробуем без аутентификации - try { - console.log('\n📡 Подключение к MongoDB (без аутентификации)...'); - await mongoose.connect(primaryUri, { useNewUrlParser: true, useUnifiedTopology: true }); - console.log('✅ Подключено к MongoDB'); - } catch (primaryError) { - console.error('❌ Не удалось подключиться к MongoDB:', primaryError.message); - throw primaryError; } + + console.log('⏳ Ожидание подключения к MongoDB...'); + await new Promise((resolve) => { + if (mongoose.connection.readyState === 1) { + resolve(); + } else { + mongoose.connection.once('connected', resolve); + } + }); + console.log('✅ Подключено к MongoDB'); }; const recreateTestUser = async () => { try { - await connectWithFallback(); + await ensureConnection(); const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796'); const presetUserEmail = 'admin@test-company.ru'; + + const presetCompanyId2 = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06797'); + const presetUserEmail2 = 'manager@partner-company.ru'; - // Удалить старого тестового пользователя - console.log('🗑️ Удаление старого тестового пользователя...'); - const oldUser = await User.findOne({ email: presetUserEmail }); - if (oldUser) { - // Удалить связанную компанию - if (oldUser.companyId) { - await Company.findByIdAndDelete(oldUser.companyId); - console.log(' ✓ Старая компания удалена'); + // Удалить старых тестовых пользователей + console.log('🗑️ Удаление старых тестовых пользователей...'); + const testEmails = [presetUserEmail, presetUserEmail2]; + + for (const email of testEmails) { + const oldUser = await User.findOne({ email }); + if (oldUser) { + // Удалить связанную компанию + if (oldUser.companyId) { + await Company.findByIdAndDelete(oldUser.companyId); + console.log(` ✓ Старая компания для ${email} удалена`); + } + await User.findByIdAndDelete(oldUser._id); + console.log(` ✓ Старый пользователь ${email} удален`); + } else { + console.log(` ℹ️ Пользователь ${email} не найден`); } - await User.findByIdAndDelete(oldUser._id); - console.log(' ✓ Старый пользователь удален'); - } else { - console.log(' ℹ️ Старый пользователь не найден'); } // Создать новую компанию с правильной кодировкой UTF-8 @@ -82,8 +82,8 @@ const recreateTestUser = async () => { }); console.log(' ✓ Компания создана:', company.fullName); - // Создать нового пользователя с правильной кодировкой UTF-8 - console.log('\n👤 Создание тестового пользователя...'); + // Создать первого пользователя с правильной кодировкой UTF-8 + console.log('\n👤 Создание первого тестового пользователя...'); const user = await User.create({ email: presetUserEmail, password: 'SecurePass123!', @@ -95,18 +95,71 @@ const recreateTestUser = async () => { }); console.log(' ✓ Пользователь создан:', user.firstName, user.lastName); + // Создать вторую компанию + console.log('\n🏢 Создание второй тестовой компании...'); + const company2 = await Company.create({ + _id: presetCompanyId2, + fullName: 'ООО "Партнер"', + shortName: 'Партнер', + inn: '9876543210', + ogrn: '1089876543210', + legalForm: 'ООО', + industry: 'Торговля', + companySize: '11-50', + website: 'https://partner-company.ru', + phone: '+7 (495) 987-65-43', + email: 'info@partner-company.ru', + description: 'Надежный партнер для бизнеса', + legalAddress: 'г. Санкт-Петербург, пр. Невский, д. 100', + actualAddress: 'г. Санкт-Петербург, пр. Невский, д. 100', + foundedYear: 2018, + employeeCount: '11-50', + revenue: 'До 60 млн ₽', + rating: 4.3, + reviews: 5, + verified: true, + partnerGeography: ['spb', 'russia_all'], + slogan: 'Качество и надежность', + }); + console.log(' ✓ Компания создана:', company2.fullName); + + // Создать второго пользователя + console.log('\n👤 Создание второго тестового пользователя...'); + const user2 = await User.create({ + email: presetUserEmail2, + password: 'SecurePass123!', + firstName: 'Петр', + lastName: 'Петров', + position: 'Менеджер', + phone: '+7 (495) 987-65-43', + companyId: company2._id, + }); + console.log(' ✓ Пользователь создан:', user2.firstName, user2.lastName); + // Проверка что данные сохранены правильно console.log('\n✅ Проверка данных:'); + console.log('\n Пользователь 1:'); console.log(' Email:', user.email); console.log(' Имя:', user.firstName); console.log(' Фамилия:', user.lastName); console.log(' Компания:', company.fullName); console.log(' Должность:', user.position); + + console.log('\n Пользователь 2:'); + console.log(' Email:', user2.email); + console.log(' Имя:', user2.firstName); + console.log(' Фамилия:', user2.lastName); + console.log(' Компания:', company2.fullName); + console.log(' Должность:', user2.position); - console.log('\n✅ ГОТОВО! Тестовый пользователь создан с правильной кодировкой UTF-8'); + console.log('\n✅ ГОТОВО! Тестовые пользователи созданы с правильной кодировкой UTF-8'); console.log('\n📋 Данные для входа:'); + console.log('\n Пользователь 1:'); console.log(' Email: admin@test-company.ru'); console.log(' Пароль: SecurePass123!'); + console.log('\n Пользователь 2:'); + console.log(' Email: manager@partner-company.ru'); + console.log(' Пароль: SecurePass123!'); console.log(''); // Создать дополнительные тестовые компании для поиска diff --git a/server/routers/procurement/scripts/seed-activities.js b/server/routers/procurement/scripts/seed-activities.js index 9490f28..7e6e949 100644 --- a/server/routers/procurement/scripts/seed-activities.js +++ b/server/routers/procurement/scripts/seed-activities.js @@ -1,13 +1,11 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); require('dotenv').config(); -// Подключение моделей - прямые пути без path.join и __dirname +// Подключение моделей const Activity = require('../models/Activity'); const User = require('../models/User'); const Company = require('../models/Company'); -const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/procurement-platform'; - const activityTemplates = [ { type: 'request_received', @@ -53,8 +51,14 @@ const activityTemplates = [ async function seedActivities() { try { - console.log('🌱 Connecting to MongoDB...'); - await mongoose.connect(MONGODB_URI); + // Подключение к MongoDB происходит через server/utils/mongoose.ts + console.log('🌱 Checking MongoDB connection...'); + if (mongoose.connection.readyState !== 1) { + console.log('⏳ Waiting for MongoDB connection...'); + await new Promise((resolve) => { + mongoose.connection.once('connected', resolve); + }); + } console.log('✅ Connected to MongoDB'); // Найти тестового пользователя diff --git a/server/routers/procurement/scripts/seed-requests.js b/server/routers/procurement/scripts/seed-requests.js index f435260..59b4883 100644 --- a/server/routers/procurement/scripts/seed-requests.js +++ b/server/routers/procurement/scripts/seed-requests.js @@ -1,13 +1,17 @@ -const mongoose = require('mongoose'); +const mongoose = require('../../../utils/mongoose'); const Request = require('../models/Request'); const Company = require('../models/Company'); const User = require('../models/User'); -const mongoUri = process.env.MONGODB_URI || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin'; - async function seedRequests() { try { - await mongoose.connect(mongoUri); + // Подключение к MongoDB происходит через server/utils/mongoose.ts + if (mongoose.connection.readyState !== 1) { + console.log('⏳ Waiting for MongoDB connection...'); + await new Promise((resolve) => { + mongoose.connection.once('connected', resolve); + }); + } console.log('✅ Connected to MongoDB'); // Получаем все компании From 4c166a8d337a405acfeb387d397e7a61b112db7a Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Sun, 16 Nov 2025 23:55:33 +0300 Subject: [PATCH 135/147] rules --- rules.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 rules.md diff --git a/rules.md b/rules.md new file mode 100644 index 0000000..ece8fda --- /dev/null +++ b/rules.md @@ -0,0 +1,87 @@ +## Правила оформления студенческих бэкендов в `multi-stub` + +Этот документ описывает, как подключать новый студенческий бэкенд к общему серверу и как работать с JSON‑заглушками. Правила написаны так, чтобы их мог автоматически выполнять помощник Cursor. + +### 1. Общая структура проекта студента + +- **Размещение проекта** + - Каждый студенческий бэкенд живёт в своей подпапке в `server/routers/`. + - В корне подпапки должен быть основной файл роутера `index.js` (или `index.ts`), который экспортирует `express.Router()`. + - Подключение к общему серверу выполняется в `server/index.ts` через импорт и `app.use(, )`. + +- **Использование JSON‑заглушек** + - Если проект переносится из фронтенд‑репозитория и должен только отдавать данные, то в подпапке проекта должна быть папка `json/` со всеми нужными `.json` файлами. + - HTTP‑обработчики в роутере могут просто читать и возвращать содержимое этих файлов (например, через `require('./json/...')` или `import data from './json/...json'` с включённым `resolveJsonModule` / соответствующей конфигурацией bundler'а). + +### 2. Правила для Cursor при указании директории заглушек + +Когда пользователь явно указывает директорию с заглушками (например: `server/routers//json`), помощник Cursor должен последовательно выполнить следующие шаги. + +- **2.1. Проверка валидности импортов JSON‑файлов** + - Найти все `.js` / `.ts` файлы внутри подпапки проекта. + - В каждом таком файле найти импорты/require, которые ссылаются на `.json` файлы (относительные пути вроде `'./json/.../file.json'`). + - Для каждого такого импорта: + - **Проверить, что файл реально существует** по указанному пути относительно файла-импортёра. + - **Проверить расширение**: путь должен заканчиваться на `.json` (без опечаток). + - **Проверить регистр и точное совпадение имени файла** (важно для кросс‑платформенности, даже если локально используется Windows). + - Если найдены ошибки (файл не существует, опечатка в имени, неправильный относительный путь и т.п.): + - Сформировать понятный список проблем: в каком файле, какая строка/импорт и что именно не так. + - Предложить автоматически исправить пути (если по контексту можно однозначно угадать нужный `*.json` файл). + +- **2.2. Проверка подключения основного роутера проекта** + - Определить основной файл роутера проекта: + - По умолчанию это `server/routers//index.js` (или `index.ts`). + - Открыть `server/index.ts` и убедиться, что: + - Есть импорт роутера из соответствующей подпапки, например: + - `import Router from './routers/'` + - или `const Router = require('./routers/')` + - Имя переменной роутера **уникально** среди всех импортов роутеров (нет другого импорта с таким же именем). + - Есть вызов `app.use('', Router)`: + - `` должен быть осмысленным, совпадать с названием проекта или оговариваться пользователем. + - Если импорт или `app.use` отсутствуют: + - Сформировать предложение по добавлению корректного импорта и `app.use(...)`. + - Убедиться, что используемое имя роутера не конфликтует с уже существующими. + - Если обнаружен конфликт имён: + - Предложить переименовать новый роутер в уникальное имя и обновить соответствующие места в `server/index.ts`. + +### 3. Предложение «оживить» JSON‑заглушки + +После того как проверка импортов и подключения роутера завершена, помощник Cursor должен **задать пользователю вопрос**, не хочет ли он превратить заглушки в полноценный бэкенд. + +- **3.1. Формулировка предложения** + - Спросить у пользователя примерно так: + - «Обнаружены JSON‑заглушки в директории `<указанная-папка>`. Хотите, чтобы я попытался автоматически: + 1) построить модели данных (mongoose‑схемы) на основе структуры JSON; + 2) создать CRUD‑эндпоинты и/или более сложные маршруты, опираясь на существующие данные; + 3) заменить прямую отдачу `*.json` файлов на работу через базу данных?» + +- **3.2. Поведение при согласии пользователя** + - Проанализировать структуру JSON‑файлов: + - Определить основные сущности и поля. + - Выделить типы полей (строки, числа, даты, массивы, вложенные объекты и т.п.). + - На основе анализа предложить: + - Набор `mongoose`‑схем (`models`) с аккуратной сериализацией (виртуальное поле `id`, скрытие `_id` и `__v`). + - Набор маршрутов `express` для работы с этими моделями (минимум: чтение списков и элементов; по возможности — создание/обновление/удаление). + - Перед внесением изменений: + - Показать пользователю краткий план того, какие файлы будут созданы/изменены. + - Выполнить изменения только после явного подтверждения пользователя. + +### 4. Минимальные требования к новому студенческому бэкенду + +- **Обязательные элементы** + - Подпапка в `server/routers/`. + - Основной роутер `index.js` / `index.ts`, экспортирующий `express.Router()`. + - Подключение к общему серверу в `server/index.ts` (импорт + `app.use()` с уникальным именем роутера). + +- **Если используются JSON‑заглушки** + - Папка `json/` внутри проекта. + - Все пути в импортирующих файлах должны указывать на реально существующие `*.json` файлы. + - Не должно быть «магических» абсолютных путей; только относительные пути от файла до нужного JSON. + +- **Если проект «оживлён»** + - Папка `model/` с моделью(ями) данных (например, через `mongoose`). + - Роуты, которые вместо прямой отдачи файлов работают с моделями и, при необходимости, с внешними сервисами. + +Следуя этим правилам, можно подключать новые студенческие проекты в единый бэкенд, минимизировать типичные ошибки с путями к JSON и упростить автоматическое развитие заглушек до полноценного API. + + From f6f9163c3f9e7b367c5ce5f9375519ddd720dd59 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 17 Nov 2025 13:25:20 +0300 Subject: [PATCH 136/147] Update bcryptjs to version 3.0.3 and add smoke-tracker router to the server configuration. --- package-lock.json | 8 +- package.json | 2 +- server/index.ts | 2 + server/routers/smoke-tracker/API.md | 626 ++++++++++++++++++ server/routers/smoke-tracker/auth.js | 89 +++ server/routers/smoke-tracker/cigarettes.js | 75 +++ server/routers/smoke-tracker/const.js | 9 + server/routers/smoke-tracker/index.js | 13 + .../routers/smoke-tracker/middleware/auth.js | 26 + server/routers/smoke-tracker/model/auth.js | 33 + .../routers/smoke-tracker/model/cigarette.js | 38 ++ server/routers/smoke-tracker/model/user.js | 27 + .../smoke-tracker.postman_collection.json | 207 ++++++ server/routers/smoke-tracker/stats.js | 59 ++ server/routers/smoke-tracker/utils.js | 21 + 15 files changed, 1230 insertions(+), 5 deletions(-) create mode 100644 server/routers/smoke-tracker/API.md create mode 100644 server/routers/smoke-tracker/auth.js create mode 100644 server/routers/smoke-tracker/cigarettes.js create mode 100644 server/routers/smoke-tracker/const.js create mode 100644 server/routers/smoke-tracker/index.js create mode 100644 server/routers/smoke-tracker/middleware/auth.js create mode 100644 server/routers/smoke-tracker/model/auth.js create mode 100644 server/routers/smoke-tracker/model/cigarette.js create mode 100644 server/routers/smoke-tracker/model/user.js create mode 100644 server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json create mode 100644 server/routers/smoke-tracker/stats.js create mode 100644 server/routers/smoke-tracker/utils.js diff --git a/package-lock.json b/package-lock.json index a43c05c..cbf818f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", - "bcryptjs": "^3.0.2", + "bcryptjs": "^3.0.3", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -3723,9 +3723,9 @@ } }, "node_modules/bcryptjs": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", - "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", "license": "BSD-3-Clause", "bin": { "bcrypt": "bin/bcrypt" diff --git a/package.json b/package.json index a691838..cf42802 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "ai": "^4.1.13", "axios": "^1.7.7", "bcrypt": "^5.1.0", - "bcryptjs": "^3.0.2", + "bcryptjs": "^3.0.3", "body-parser": "^1.19.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", diff --git a/server/index.ts b/server/index.ts index 6ed51c4..46aef65 100644 --- a/server/index.ts +++ b/server/index.ts @@ -21,6 +21,7 @@ import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' import procurementRouter from './routers/procurement' +import smokeTrackerRouter from './routers/smoke-tracker' import { setIo } from './io' export const app = express() @@ -107,6 +108,7 @@ const initServer = async () => { app.use('/connectme', connectmeRouter) app.use('/questioneer', questioneerRouter) app.use('/procurement', procurementRouter) + app.use('/smoke-tracker', smokeTrackerRouter) app.use(errorHandler) // Создаем обычный HTTP сервер diff --git a/server/routers/smoke-tracker/API.md b/server/routers/smoke-tracker/API.md new file mode 100644 index 0000000..3d2e16d --- /dev/null +++ b/server/routers/smoke-tracker/API.md @@ -0,0 +1,626 @@ +# Smoke Tracker API — Документация для Frontend + +## Базовый URL + +``` +http://localhost:8044/smoke-tracker +``` + +В production окружении замените на соответствующий домен. + +--- + +## Оглавление + +1. [Авторизация](#авторизация) + - [Регистрация](#post-authsignup) + - [Вход](#post-authsignin) +2. [Логирование сигарет](#логирование-сигарет) + - [Записать сигарету](#post-cigarettes) + - [Получить список сигарет](#get-cigarettes) +3. [Статистика](#статистика) + - [Дневная статистика](#get-statsdaily) + +--- + +## Авторизация + +Все эндпоинты, кроме `/auth/signup` и `/auth/signin`, требуют JWT-токен в заголовке: + +``` +Authorization: Bearer +``` + +Токен возвращается при успешном входе (`/auth/signin`) и действителен **12 часов**. + +--- + +### `POST /auth/signup` + +**Описание**: Регистрация нового пользователя + +**Требуется авторизация**: ❌ Нет + +**Тело запроса** (JSON): + +```json +{ + "login": "string", // обязательно, уникальный логин + "password": "string" // обязательно +} +``` + +**Пример запроса**: + +```bash +curl -X POST http://localhost:8044/smoke-tracker/auth/signup \ + -H "Content-Type: application/json" \ + -d '{ + "login": "user123", + "password": "mySecurePassword" + }' +``` + +**Ответ при успехе** (200 OK): + +```json +{ + "success": true, + "body": { + "ok": true + } +} +``` + +**Возможные ошибки**: + +- **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля +- **500 Internal Server Error**: `"Пользователь с таким логином уже существует"` — логин занят + +--- + +### `POST /auth/signin` + +**Описание**: Вход в систему (получение JWT-токена) + +**Требуется авторизация**: ❌ Нет + +**Тело запроса** (JSON): + +```json +{ + "login": "string", // обязательно + "password": "string" // обязательно +} +``` + +**Пример запроса**: + +```bash +curl -X POST http://localhost:8044/smoke-tracker/auth/signin \ + -H "Content-Type: application/json" \ + -d '{ + "login": "user123", + "password": "mySecurePassword" + }' +``` + +**Ответ при успехе** (200 OK): + +```json +{ + "success": true, + "body": { + "user": { + "id": "507f1f77bcf86cd799439011", + "login": "user123", + "created": "2024-01-15T10:30:00.000Z" + }, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } +} +``` + +**Поля ответа**: + +- `user.id` — уникальный идентификатор пользователя +- `user.login` — логин пользователя +- `user.created` — дата создания аккаунта (ISO 8601) +- `token` — JWT-токен для авторизации (действителен 12 часов) + +**Возможные ошибки**: + +- **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля +- **500 Internal Server Error**: `"Неверный логин или пароль"` — неправильные учётные данные + +**Использование токена**: + +Сохраните токен в localStorage/sessionStorage/cookie и передавайте в заголовке всех последующих запросов: + +```javascript +// Пример для fetch API +const token = localStorage.getItem('smokeToken'); + +fetch('http://localhost:8044/smoke-tracker/cigarettes', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } +}); +``` + +--- + +## Логирование сигарет + +### `POST /cigarettes` + +**Описание**: Записать факт выкуренной сигареты + +**Требуется авторизация**: ✅ Да (Bearer token) + +**Тело запроса** (JSON): + +```json +{ + "smokedAt": "string (ISO 8601)", // необязательно, по умолчанию — текущее время + "note": "string" // необязательно, заметка/комментарий +} +``` + +**Пример запроса**: + +```bash +curl -X POST http://localhost:8044/smoke-tracker/cigarettes \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{ + "smokedAt": "2024-01-15T14:30:00.000Z", + "note": "После обеда" + }' +``` + +**Пример без указания времени** (будет текущее время): + +```bash +curl -X POST http://localhost:8044/smoke-tracker/cigarettes \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +**Ответ при успехе** (200 OK): + +```json +{ + "success": true, + "body": { + "id": "507f1f77bcf86cd799439012", + "userId": "507f1f77bcf86cd799439011", + "smokedAt": "2024-01-15T14:30:00.000Z", + "note": "После обеда", + "created": "2024-01-15T14:30:05.123Z" + } +} +``` + +**Поля ответа**: + +- `id` — уникальный идентификатор записи +- `userId` — ID пользователя +- `smokedAt` — дата и время курения (ISO 8601) +- `note` — заметка (если была указана) +- `created` — дата создания записи в БД + +**Возможные ошибки**: + +- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен +- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен +- **400 Bad Request**: `"Некорректный формат даты smokedAt"` — неверный формат даты + +--- + +### `GET /cigarettes` + +**Описание**: Получить список всех выкуренных сигарет текущего пользователя + +**Требуется авторизация**: ✅ Да (Bearer token) + +**Query-параметры** (все необязательные): + +| Параметр | Тип | Описание | Пример | +|----------|-----|----------|--------| +| `from` | string (ISO 8601) | Начало периода (включительно) | `2024-01-01T00:00:00.000Z` | +| `to` | string (ISO 8601) | Конец периода (включительно) | `2024-01-31T23:59:59.999Z` | + +**Пример запроса** (все сигареты): + +```bash +curl -X GET http://localhost:8044/smoke-tracker/cigarettes \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**Пример запроса** (с фильтрацией по датам): + +```bash +curl -X GET "http://localhost:8044/smoke-tracker/cigarettes?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**Ответ при успехе** (200 OK): + +```json +{ + "success": true, + "body": [ + { + "id": "507f1f77bcf86cd799439012", + "userId": "507f1f77bcf86cd799439011", + "smokedAt": "2024-01-15T10:30:00.000Z", + "note": "Утренняя", + "created": "2024-01-15T10:30:05.123Z" + }, + { + "id": "507f1f77bcf86cd799439013", + "userId": "507f1f77bcf86cd799439011", + "smokedAt": "2024-01-15T14:30:00.000Z", + "note": "После обеда", + "created": "2024-01-15T14:30:05.456Z" + } + ] +} +``` + +**Особенности**: + +- Записи отсортированы по `smokedAt` (от старых к новым) +- Если указаны `from` и/или `to`, будет применена фильтрация +- Пустой массив возвращается, если сигарет в периоде нет + +**Возможные ошибки**: + +- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен +- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен + +--- + +## Статистика + +### `GET /stats/daily` + +**Описание**: Получить дневную статистику по количеству сигарет для построения графика + +**Требуется авторизация**: ✅ Да (Bearer token) + +**Query-параметры** (все необязательные): + +| Параметр | Тип | Описание | Пример | По умолчанию | +|----------|-----|----------|--------|--------------| +| `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты | +| `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время | + +**Пример запроса** (последние 30 дней): + +```bash +curl -X GET http://localhost:8044/smoke-tracker/stats/daily \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**Пример запроса** (с указанием периода): + +```bash +curl -X GET "http://localhost:8044/smoke-tracker/stats/daily?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**Ответ при успехе** (200 OK): + +```json +{ + "success": true, + "body": [ + { + "date": "2024-01-15", + "count": 8 + }, + { + "date": "2024-01-16", + "count": 12 + }, + { + "date": "2024-01-17", + "count": 5 + } + ] +} +``` + +**Поля ответа**: + +- `date` — дата в формате `YYYY-MM-DD` +- `count` — количество сигарет, выкуренных в этот день + +**Особенности**: + +- Данные отсортированы по дате (от старых к новым) +- Дни без сигарет **не включаются** в ответ (фронтенду нужно самостоятельно заполнить пропуски нулями при построении графика) +- Агрегация происходит по дате из поля `smokedAt` (не `created`) + +**Пример использования для графика** (Chart.js): + +```javascript +const response = await fetch('http://localhost:8044/smoke-tracker/stats/daily', { + headers: { + 'Authorization': `Bearer ${token}` + } +}); + +const { body } = await response.json(); + +// Заполнение пропущенных дней нулями +const fillMissingDates = (data, from, to) => { + const result = []; + const current = new Date(from); + const end = new Date(to); + + while (current <= end) { + const dateStr = current.toISOString().split('T')[0]; + const existing = data.find(d => d.date === dateStr); + + result.push({ + date: dateStr, + count: existing ? existing.count : 0 + }); + + current.setDate(current.getDate() + 1); + } + + return result; +}; + +const filledData = fillMissingDates(body, '2024-01-01', '2024-01-31'); + +// Данные для графика +const chartData = { + labels: filledData.map(d => d.date), + datasets: [{ + label: 'Количество сигарет', + data: filledData.map(d => d.count), + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + }] +}; +``` + +**Возможные ошибки**: + +- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен +- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен + +--- + +## Общая структура ответов + +Все эндпоинты возвращают JSON в следующем формате: + +**Успешный ответ**: + +```json +{ + "success": true, + "body": { /* данные */ } +} +``` + +**Ответ с ошибкой**: + +```json +{ + "success": false, + "errors": "Описание ошибки" +} +``` + +или (при использовании глобального обработчика ошибок): + +```json +{ + "message": "Описание ошибки" +} +``` + +--- + +## Коды состояния HTTP + +| Код | Описание | +|-----|----------| +| **200 OK** | Запрос выполнен успешно | +| **400 Bad Request** | Некорректные данные в запросе | +| **401 Unauthorized** | Требуется авторизация или токен невалидный | +| **500 Internal Server Error** | Внутренняя ошибка сервера | + +--- + +## Примеры интеграции + +### React + Axios + +```javascript +import axios from 'axios'; + +const API_BASE_URL = 'http://localhost:8044/smoke-tracker'; + +// Создание экземпляра axios с базовыми настройками +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json' + } +}); + +// Интерцептор для добавления токена +api.interceptors.request.use(config => { + const token = localStorage.getItem('smokeToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Регистрация +export const signup = async (login, password) => { + const { data } = await api.post('/auth/signup', { login, password }); + return data; +}; + +// Вход +export const signin = async (login, password) => { + const { data } = await api.post('/auth/signin', { login, password }); + if (data.success) { + localStorage.setItem('smokeToken', data.body.token); + } + return data; +}; + +// Выход +export const signout = () => { + localStorage.removeItem('smokeToken'); +}; + +// Записать сигарету +export const logCigarette = async (smokedAt = null, note = '') => { + const { data } = await api.post('/cigarettes', { smokedAt, note }); + return data; +}; + +// Получить список сигарет +export const getCigarettes = async (from = null, to = null) => { + const params = {}; + if (from) params.from = from; + if (to) params.to = to; + + const { data } = await api.get('/cigarettes', { params }); + return data; +}; + +// Получить дневную статистику +export const getDailyStats = async (from = null, to = null) => { + const params = {}; + if (from) params.from = from; + if (to) params.to = to; + + const { data } = await api.get('/stats/daily', { params }); + return data; +}; +``` + +### Vanilla JavaScript + Fetch + +```javascript +const API_BASE_URL = 'http://localhost:8044/smoke-tracker'; + +// Получение токена +const getToken = () => localStorage.getItem('smokeToken'); + +// Базовый запрос +const apiRequest = async (endpoint, options = {}) => { + const token = getToken(); + + const headers = { + 'Content-Type': 'application/json', + ...options.headers + }; + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || error.errors || 'Ошибка запроса'); + } + + return response.json(); +}; + +// Регистрация +async function signup(login, password) { + return apiRequest('/auth/signup', { + method: 'POST', + body: JSON.stringify({ login, password }) + }); +} + +// Вход +async function signin(login, password) { + const data = await apiRequest('/auth/signin', { + method: 'POST', + body: JSON.stringify({ login, password }) + }); + + if (data.success) { + localStorage.setItem('smokeToken', data.body.token); + } + + return data; +} + +// Записать сигарету +async function logCigarette(note = '') { + return apiRequest('/cigarettes', { + method: 'POST', + body: JSON.stringify({ note }) + }); +} + +// Получить дневную статистику +async function getDailyStats() { + return apiRequest('/stats/daily'); +} +``` + +--- + +## Рекомендации по безопасности + +1. **Хранение токена**: + - Для веб-приложений: используйте `httpOnly` cookies или `sessionStorage` + - Избегайте `localStorage` при работе с чувствительными данными + - Для мобильных приложений: используйте безопасное хранилище (Keychain/Keystore) + +2. **HTTPS**: В production всегда используйте HTTPS для защиты токена при передаче + +3. **Обработка истечения токена**: + - Токен действителен 12 часов + - При получении ошибки 401 перенаправляйте пользователя на страницу входа + - Реализуйте механизм refresh token для бесшовного обновления + +4. **Валидация на фронтенде**: + - Проверяйте корректность email/логина перед отправкой + - Требуйте минимальную длину пароля (8+ символов) + - Показывайте индикатор силы пароля + +--- + +## Postman-коллекция + +Готовая коллекция для тестирования доступна в файле: + +``` +server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json +``` + +Импортируйте её в Postman для быстрого тестирования всех эндпоинтов. + +--- + +## Поддержка + +При возникновении вопросов или обнаружении проблем обращайтесь к разработчикам backend-команды. + diff --git a/server/routers/smoke-tracker/auth.js b/server/routers/smoke-tracker/auth.js new file mode 100644 index 0000000..b522812 --- /dev/null +++ b/server/routers/smoke-tracker/auth.js @@ -0,0 +1,89 @@ +const { Router } = require('express') +const hash = require('pbkdf2-password')() +const { promisify } = require('node:util') +const jwt = require('jsonwebtoken') + +const { getAnswer } = require('../../utils/common') + +const { SmokeAuthModel } = require('./model/auth') +const { SmokeUserModel } = require('./model/user') +const { SMOKE_TRACKER_TOKEN_KEY } = require('./const') +const { requiredValidate } = require('./utils') + +const router = Router() + +router.post( + '/signup', + requiredValidate('login', 'password'), + async (req, res, next) => { + const { login, password } = req.body + + const existing = await SmokeAuthModel.findOne({ login }) + + if (existing) { + throw new Error('Пользователь с таким логином уже существует') + } + + hash({ password }, async function (err, pass, salt, hashValue) { + if (err) return next(err) + + const user = await SmokeUserModel.create({ login }) + await SmokeAuthModel.create({ login, hash: hashValue, salt, userId: user.id }) + + res.json(getAnswer(null, { ok: true })) + }) + } +) + +function authenticate(login, pass, cb) { + SmokeAuthModel.findOne({ login }) + .populate('userId') + .exec() + .then((user) => { + if (!user) return cb(null, null) + + hash({ password: pass, salt: user.salt }, function (err, pass, salt, hashValue) { + if (err) return cb(err) + if (hashValue === user.hash) return cb(null, user) + cb(null, null) + }) + }) + .catch((err) => cb(err)) +} + +const auth = promisify(authenticate) + +router.post( + '/signin', + requiredValidate('login', 'password'), + async (req, res) => { + const { login, password } = req.body + + const user = await auth(login, password) + + if (!user) { + throw new Error('Неверный логин или пароль') + } + + const accessToken = jwt.sign( + { + ...JSON.parse(JSON.stringify(user.userId)), + }, + SMOKE_TRACKER_TOKEN_KEY, + { + expiresIn: '12h', + } + ) + + res.json( + getAnswer(null, { + user: user.userId, + token: accessToken, + }) + ) + } +) + +module.exports = router + + diff --git a/server/routers/smoke-tracker/cigarettes.js b/server/routers/smoke-tracker/cigarettes.js new file mode 100644 index 0000000..1cec10c --- /dev/null +++ b/server/routers/smoke-tracker/cigarettes.js @@ -0,0 +1,75 @@ +const { Router } = require('express') + +const { getAnswer } = require('../../utils/common') +const { CigaretteModel } = require('./model/cigarette') +const { authMiddleware } = require('./middleware/auth') + +const router = Router() + +// Все эндпоинты ниже требуют авторизации +router.use(authMiddleware) + +// Логирование одной сигареты +router.post('/', async (req, res, next) => { + try { + const { smokedAt, note } = req.body || {} + const user = req.user + + let date + if (smokedAt) { + const parsed = new Date(smokedAt) + if (Number.isNaN(parsed.getTime())) { + throw new Error('Некорректный формат даты smokedAt') + } + date = parsed + } else { + date = new Date() + } + + const item = await CigaretteModel.create({ + userId: user.id, + smokedAt: date, + note, + }) + + res.json(getAnswer(null, item)) + } catch (err) { + next(err) + } +}) + +// Получение списка сигарет пользователя (для отладки и таблиц) +router.get('/', async (req, res, next) => { + try { + const user = req.user + const { from, to } = req.query + + const filter = { userId: user.id } + + if (from || to) { + filter.smokedAt = {} + if (from) { + const fromDate = new Date(from) + if (!Number.isNaN(fromDate.getTime())) { + filter.smokedAt.$gte = fromDate + } + } + if (to) { + const toDate = new Date(to) + if (!Number.isNaN(toDate.getTime())) { + filter.smokedAt.$lte = toDate + } + } + } + + const items = await CigaretteModel.find(filter).sort({ smokedAt: 1 }) + + res.json(getAnswer(null, items)) + } catch (err) { + next(err) + } +}) + +module.exports = router + + diff --git a/server/routers/smoke-tracker/const.js b/server/routers/smoke-tracker/const.js new file mode 100644 index 0000000..1b089d8 --- /dev/null +++ b/server/routers/smoke-tracker/const.js @@ -0,0 +1,9 @@ +exports.SMOKE_TRACKER_USER_MODEL_NAME = 'SMOKE_TRACKER_USER' +exports.SMOKE_TRACKER_AUTH_MODEL_NAME = 'SMOKE_TRACKER_AUTH' +exports.SMOKE_TRACKER_CIGARETTE_MODEL_NAME = 'SMOKE_TRACKER_CIGARETTE' + +exports.SMOKE_TRACKER_TOKEN_KEY = + process.env.SMOKE_TRACKER_TOKEN_KEY || + 'smoke-tracker-secret-key-change-me' + + diff --git a/server/routers/smoke-tracker/index.js b/server/routers/smoke-tracker/index.js new file mode 100644 index 0000000..f3558c6 --- /dev/null +++ b/server/routers/smoke-tracker/index.js @@ -0,0 +1,13 @@ +const router = require('express').Router() + +const authRouter = require('./auth') +const cigarettesRouter = require('./cigarettes') +const statsRouter = require('./stats') + +router.use('/auth', authRouter) +router.use('/cigarettes', cigarettesRouter) +router.use('/stats', statsRouter) + +module.exports = router + + diff --git a/server/routers/smoke-tracker/middleware/auth.js b/server/routers/smoke-tracker/middleware/auth.js new file mode 100644 index 0000000..d762641 --- /dev/null +++ b/server/routers/smoke-tracker/middleware/auth.js @@ -0,0 +1,26 @@ +const jwt = require('jsonwebtoken') + +const { SMOKE_TRACKER_TOKEN_KEY } = require('../const') + +const authMiddleware = (req, res, next) => { + const authHeader = req.headers.authorization || '' + const token = authHeader.startsWith('Bearer ') + ? authHeader.slice(7) + : null + + if (!token) { + throw new Error('Требуется авторизация') + } + + try { + const decoded = jwt.verify(token, SMOKE_TRACKER_TOKEN_KEY) + req.user = decoded + next() + } catch (e) { + throw new Error('Неверный или истекший токен авторизации') + } +} + +module.exports.authMiddleware = authMiddleware + + diff --git a/server/routers/smoke-tracker/model/auth.js b/server/routers/smoke-tracker/model/auth.js new file mode 100644 index 0000000..7e701d5 --- /dev/null +++ b/server/routers/smoke-tracker/model/auth.js @@ -0,0 +1,33 @@ +const { Schema, model } = require('mongoose') + +const { + SMOKE_TRACKER_AUTH_MODEL_NAME, + SMOKE_TRACKER_USER_MODEL_NAME, +} = require('../const') + +const schema = new Schema({ + login: { type: String, required: true, unique: true }, + hash: { type: String, required: true }, + salt: { type: String, required: true }, + userId: { type: Schema.Types.ObjectId, ref: SMOKE_TRACKER_USER_MODEL_NAME }, + created: { + type: Date, + default: () => new Date().toISOString(), + }, +}) + +schema.set('toJSON', { + virtuals: true, + versionKey: false, + transform: function (doc, ret) { + delete ret._id + }, +}) + +schema.virtual('id').get(function () { + return this._id.toHexString() +}) + +exports.SmokeAuthModel = model(SMOKE_TRACKER_AUTH_MODEL_NAME, schema) + + diff --git a/server/routers/smoke-tracker/model/cigarette.js b/server/routers/smoke-tracker/model/cigarette.js new file mode 100644 index 0000000..768ac2d --- /dev/null +++ b/server/routers/smoke-tracker/model/cigarette.js @@ -0,0 +1,38 @@ +const { Schema, model } = require('mongoose') + +const { + SMOKE_TRACKER_CIGARETTE_MODEL_NAME, + SMOKE_TRACKER_USER_MODEL_NAME, +} = require('../const') + +const schema = new Schema({ + userId: { type: Schema.Types.ObjectId, ref: SMOKE_TRACKER_USER_MODEL_NAME, required: true }, + smokedAt: { + type: Date, + required: true, + default: () => new Date().toISOString(), + }, + note: { + type: String, + }, + created: { + type: Date, + default: () => new Date().toISOString(), + }, +}) + +schema.set('toJSON', { + virtuals: true, + versionKey: false, + transform: function (doc, ret) { + delete ret._id + }, +}) + +schema.virtual('id').get(function () { + return this._id.toHexString() +}) + +exports.CigaretteModel = model(SMOKE_TRACKER_CIGARETTE_MODEL_NAME, schema) + + diff --git a/server/routers/smoke-tracker/model/user.js b/server/routers/smoke-tracker/model/user.js new file mode 100644 index 0000000..221bbf7 --- /dev/null +++ b/server/routers/smoke-tracker/model/user.js @@ -0,0 +1,27 @@ +const { Schema, model } = require('mongoose') + +const { SMOKE_TRACKER_USER_MODEL_NAME } = require('../const') + +const schema = new Schema({ + login: { type: String, required: true, unique: true }, + created: { + type: Date, + default: () => new Date().toISOString(), + }, +}) + +schema.set('toJSON', { + virtuals: true, + versionKey: false, + transform: function (doc, ret) { + delete ret._id + }, +}) + +schema.virtual('id').get(function () { + return this._id.toHexString() +}) + +exports.SmokeUserModel = model(SMOKE_TRACKER_USER_MODEL_NAME, schema) + + diff --git a/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json b/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json new file mode 100644 index 0000000..7b522f6 --- /dev/null +++ b/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json @@ -0,0 +1,207 @@ +{ + "info": { + "_postman_id": "9d74101d-f788-4dbf-83b3-11c8f9789b73", + "name": "Smoke Tracker", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "smoke-tracker" + }, + "item": [ + { + "name": "Auth • Signup", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"login\": \"smoker-demo\",\n \"password\": \"secret123\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/smoke-tracker/auth/signup", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "auth", + "signup" + ] + }, + "description": "Регистрация нового пользователя. Повторный вызов с тем же логином вернёт ошибку." + }, + "response": [] + }, + { + "name": "Auth • Signin", + "event": [ + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "const json = pm.response.json();", + "if (json && json.body && json.body.token) {", + " pm.environment.set('smokeToken', json.body.token);", + "}" + ] + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"login\": \"smoker-demo\",\n \"password\": \"secret123\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/smoke-tracker/auth/signin", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "auth", + "signin" + ] + }, + "description": "Авторизация пользователя. Скрипт тестов сохранит JWT в переменную окружения smokeToken." + }, + "response": [] + }, + { + "name": "Cigarettes • Log entry", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + }, + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer {{smokeToken}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"smokedAt\": \"2025-01-01T09:30:00.000Z\",\n \"note\": \"Первая сигарета за день\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/smoke-tracker/cigarettes", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "cigarettes" + ] + }, + "description": "Создать запись о выкуренной сигарете. Если smokedAt не указан, сервер использует текущее время." + }, + "response": [] + }, + { + "name": "Cigarettes • List", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer {{smokeToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/smoke-tracker/cigarettes?from=2025-01-01T00:00:00.000Z&to=2025-01-07T23:59:59.999Z", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "cigarettes" + ], + "query": [ + { + "key": "from", + "value": "2025-01-01T00:00:00.000Z" + }, + { + "key": "to", + "value": "2025-01-07T23:59:59.999Z" + } + ] + }, + "description": "Список сигарет текущего пользователя. Параметры from/to необязательны." + }, + "response": [] + }, + { + "name": "Stats • Daily", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer {{smokeToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/smoke-tracker/stats/daily?from=2025-01-01&to=2025-01-31", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "stats", + "daily" + ], + "query": [ + { + "key": "from", + "value": "2025-01-01" + }, + { + "key": "to", + "value": "2025-01-31" + } + ] + }, + "description": "Агрегация по дням для графиков. Если from/to не заданы, используется последний месяц." + }, + "response": [] + } + ], + "event": [], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8044" + }, + { + "key": "smokeToken", + "value": "" + } + ] +} + diff --git a/server/routers/smoke-tracker/stats.js b/server/routers/smoke-tracker/stats.js new file mode 100644 index 0000000..56b4791 --- /dev/null +++ b/server/routers/smoke-tracker/stats.js @@ -0,0 +1,59 @@ +const { Router } = require('express') + +const { getAnswer } = require('../../utils/common') +const { CigaretteModel } = require('./model/cigarette') +const { authMiddleware } = require('./middleware/auth') + +const router = Router() + +// Все эндпоинты статистики требуют авторизации +router.use(authMiddleware) + +// Агрегация по дням: количество сигарет в день для построения графика +router.get('/daily', async (req, res, next) => { + try { + const user = req.user + const { from, to } = req.query + + const now = new Date() + const defaultFrom = new Date(now) + defaultFrom.setDate(defaultFrom.getDate() - 30) + + const fromDate = from ? new Date(from) : defaultFrom + const toDate = to ? new Date(to) : now + + const match = { + userId: user.id, + smokedAt: { + $gte: fromDate, + $lte: toDate, + }, + } + + const data = await CigaretteModel.aggregate([ + { $match: match }, + { + $group: { + _id: { + $dateToString: { format: '%Y-%m-%d', date: '$smokedAt' }, + }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const result = data.map((item) => ({ + date: item._id, + count: item.count, + })) + + res.json(getAnswer(null, result)) + } catch (err) { + next(err) + } +}) + +module.exports = router + + diff --git a/server/routers/smoke-tracker/utils.js b/server/routers/smoke-tracker/utils.js new file mode 100644 index 0000000..4c24b41 --- /dev/null +++ b/server/routers/smoke-tracker/utils.js @@ -0,0 +1,21 @@ +const requiredValidate = + (...fields) => + (req, res, next) => { + const errors = [] + + fields.forEach((field) => { + if (!req.body[field]) { + errors.push(field) + } + }) + + if (errors.length) { + throw new Error(`Не все поля заполнены: ${errors.join(', ')}`) + } else { + next() + } + } + +module.exports.requiredValidate = requiredValidate + + From dd75c54b3225ec1b516c8295f2b81eb109069c41 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 17 Nov 2025 14:04:46 +0300 Subject: [PATCH 137/147] Refactor userId handling in cigarettes and stats routes to use mongoose ObjectId for consistency; add debug logging for stats aggregation. --- server/routers/smoke-tracker/cigarettes.js | 5 +++-- server/routers/smoke-tracker/stats.js | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/server/routers/smoke-tracker/cigarettes.js b/server/routers/smoke-tracker/cigarettes.js index 1cec10c..0851dc0 100644 --- a/server/routers/smoke-tracker/cigarettes.js +++ b/server/routers/smoke-tracker/cigarettes.js @@ -1,4 +1,5 @@ const { Router } = require('express') +const mongoose = require('mongoose') const { getAnswer } = require('../../utils/common') const { CigaretteModel } = require('./model/cigarette') @@ -27,7 +28,7 @@ router.post('/', async (req, res, next) => { } const item = await CigaretteModel.create({ - userId: user.id, + userId: new mongoose.Types.ObjectId(user.id), smokedAt: date, note, }) @@ -44,7 +45,7 @@ router.get('/', async (req, res, next) => { const user = req.user const { from, to } = req.query - const filter = { userId: user.id } + const filter = { userId: new mongoose.Types.ObjectId(user.id) } if (from || to) { filter.smokedAt = {} diff --git a/server/routers/smoke-tracker/stats.js b/server/routers/smoke-tracker/stats.js index 56b4791..e377a08 100644 --- a/server/routers/smoke-tracker/stats.js +++ b/server/routers/smoke-tracker/stats.js @@ -1,4 +1,5 @@ const { Router } = require('express') +const mongoose = require('mongoose') const { getAnswer } = require('../../utils/common') const { CigaretteModel } = require('./model/cigarette') @@ -23,19 +24,24 @@ router.get('/daily', async (req, res, next) => { const toDate = to ? new Date(to) : now const match = { - userId: user.id, + userId: new mongoose.Types.ObjectId(user.id), smokedAt: { $gte: fromDate, $lte: toDate, }, } + // Отладка: проверяем, сколько записей попадает в фильтр + const totalCount = await CigaretteModel.countDocuments(match) + console.log('[STATS] Match filter:', JSON.stringify(match, null, 2)) + console.log('[STATS] Total cigarettes in range:', totalCount) + const data = await CigaretteModel.aggregate([ { $match: match }, { $group: { _id: { - $dateToString: { format: '%Y-%m-%d', date: '$smokedAt' }, + $dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' }, }, count: { $sum: 1 }, }, @@ -43,6 +49,8 @@ router.get('/daily', async (req, res, next) => { { $sort: { _id: 1 } }, ]) + console.log('[STATS] Aggregation result:', data) + const result = data.map((item) => ({ date: item._id, count: item.count, From f856d945966407fa0fe3d0978069092e9f931b68 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 17 Nov 2025 14:14:15 +0300 Subject: [PATCH 138/147] Add summary statistics endpoint to smoke-tracker API; update documentation to include new route --- server/routers/smoke-tracker/API.md | 203 ++++++++++++++++++ .../smoke-tracker.postman_collection.json | 37 ++++ server/routers/smoke-tracker/stats.js | 174 +++++++++++++++ 3 files changed, 414 insertions(+) diff --git a/server/routers/smoke-tracker/API.md b/server/routers/smoke-tracker/API.md index 3d2e16d..f4ab7ba 100644 --- a/server/routers/smoke-tracker/API.md +++ b/server/routers/smoke-tracker/API.md @@ -20,6 +20,7 @@ http://localhost:8044/smoke-tracker - [Получить список сигарет](#get-cigarettes) 3. [Статистика](#статистика) - [Дневная статистика](#get-statsdaily) + - [Сводная статистика](#get-statssummary) --- @@ -399,6 +400,208 @@ const chartData = { --- +### `GET /stats/summary` + +**Описание**: Получить расширенную статистику для текущего пользователя и общую по всем пользователям + +**Требуется авторизация**: ✅ Да (Bearer token) + +**Query-параметры** (все необязательные): + +| Параметр | Тип | Описание | Пример | По умолчанию | +|----------|-----|----------|--------|--------------| +| `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты | +| `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время | + +**Пример запроса**: + +```bash +curl -X GET http://localhost:8044/smoke-tracker/stats/summary \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +**Ответ при успехе** (200 OK): + +```json +{ + "success": true, + "body": { + "user": { + "daily": [ + { + "date": "2024-01-15", + "count": 8 + }, + { + "date": "2024-01-16", + "count": 12 + } + ], + "averagePerDay": 10.5, + "weekday": [ + { + "dayOfWeek": 2, + "dayName": "Понедельник", + "count": 25, + "average": "6.25" + }, + { + "dayOfWeek": 3, + "dayName": "Вторник", + "count": 30, + "average": "7.50" + } + ], + "total": 315, + "daysWithData": 30 + }, + "global": { + "daily": [ + { + "date": "2024-01-15", + "count": 45 + }, + { + "date": "2024-01-16", + "count": 52 + } + ], + "averagePerDay": 48.5, + "weekday": [ + { + "dayOfWeek": 2, + "dayName": "Понедельник", + "count": 120, + "average": "30.00" + }, + { + "dayOfWeek": 3, + "dayName": "Вторник", + "count": 135, + "average": "33.75" + } + ], + "total": 1455, + "daysWithData": 30, + "activeUsers": 5 + }, + "period": { + "from": "2024-01-01T00:00:00.000Z", + "to": "2024-01-31T23:59:59.999Z" + } + } +} +``` + +**Структура ответа**: + +**`user`** — статистика текущего пользователя: +- `daily` — массив с количеством сигарет по дням + - `date` — дата в формате YYYY-MM-DD + - `count` — количество сигарет +- `averagePerDay` — среднее количество сигарет в день (число с плавающей точкой) +- `weekday` — статистика по дням недели + - `dayOfWeek` — номер дня недели (1 = воскресенье, 2 = понедельник, ..., 7 = суббота) + - `dayName` — название дня недели + - `count` — общее количество сигарет в этот день недели за весь период + - `average` — среднее количество за один такой день недели (строка) +- `total` — общее количество сигарет за период +- `daysWithData` — количество дней, в которые были записи + +**`global`** — общая статистика по всем пользователям: +- `daily` — массив с суммарным количеством сигарет всех пользователей по дням +- `averagePerDay` — среднее количество сигарет в день (все пользователи) +- `weekday` — статистика по дням недели (все пользователи) +- `total` — общее количество сигарет всех пользователей за период +- `daysWithData` — количество дней с записями +- `activeUsers` — количество уникальных пользователей, записывавших сигареты в период + +**`period`** — информация о запрошенном периоде: +- `from` — начало периода (ISO 8601) +- `to` — конец периода (ISO 8601) + +**Особенности**: + +- Дни недели нумеруются по стандарту MongoDB: 1 = Воскресенье, 2 = Понедельник, ..., 7 = Суббота +- `average` для дней недели рассчитывается делением общего количества на количество таких дней в периоде +- Дни без записей **не включаются** в массив `daily` +- Глобальная статистика позволяет сравнить свои результаты с другими пользователями + +**Примеры использования**: + +```javascript +// Получение сводной статистики +const response = await fetch('http://localhost:8044/smoke-tracker/stats/summary', { + headers: { + 'Authorization': `Bearer ${token}` + } +}); + +const { body } = await response.json(); + +console.log(`Вы в среднем выкуриваете ${body.user.averagePerDay} сигарет в день`); +console.log(`Общее среднее по всем пользователям: ${body.global.averagePerDay} сигарет в день`); +console.log(`Активных пользователей в периоде: ${body.global.activeUsers}`); + +// Поиск самого "тяжёлого" дня недели +const maxWeekday = body.user.weekday.reduce((max, day) => + parseFloat(day.average) > parseFloat(max.average) ? day : max +); +console.log(`Больше всего вы курите в ${maxWeekday.dayName} (в среднем ${maxWeekday.average} сигарет)`); +``` + +**Визуализация данных по дням недели**: + +```javascript +// Данные для круговой диаграммы (Chart.js) +const weekdayChartData = { + labels: body.user.weekday.map(d => d.dayName), + datasets: [{ + label: 'Сигарет в день недели', + data: body.user.weekday.map(d => d.count), + backgroundColor: [ + 'rgba(255, 99, 132, 0.6)', + 'rgba(54, 162, 235, 0.6)', + 'rgba(255, 206, 86, 0.6)', + 'rgba(75, 192, 192, 0.6)', + 'rgba(153, 102, 255, 0.6)', + 'rgba(255, 159, 64, 0.6)', + 'rgba(199, 199, 199, 0.6)' + ] + }] +}; +``` + +**Сравнение с глобальной статистикой**: + +```javascript +// Сравнительный график (ваши данные vs общие данные) +const comparisonData = { + labels: body.user.weekday.map(d => d.dayName), + datasets: [ + { + label: 'Вы', + data: body.user.weekday.map(d => parseFloat(d.average)), + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + }, + { + label: 'Среднее по пользователям', + data: body.global.weekday.map(d => parseFloat(d.average)), + borderColor: 'rgb(54, 162, 235)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + } + ] +}; +``` + +**Возможные ошибки**: + +- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен +- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен + +--- + ## Общая структура ответов Все эндпоинты возвращают JSON в следующем формате: diff --git a/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json b/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json index 7b522f6..b8f117f 100644 --- a/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json +++ b/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json @@ -190,6 +190,43 @@ "description": "Агрегация по дням для графиков. Если from/to не заданы, используется последний месяц." }, "response": [] + }, + { + "name": "Stats • Summary", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer {{smokeToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/smoke-tracker/stats/summary?from=2025-01-01T00:00:00.000Z&to=2025-01-31T23:59:59.999Z", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "stats", + "summary" + ], + "query": [ + { + "key": "from", + "value": "2025-01-01T00:00:00.000Z" + }, + { + "key": "to", + "value": "2025-01-31T23:59:59.999Z" + } + ] + }, + "description": "Расширенная статистика: среднее в день, статистика по дням недели, сравнение с общими показателями всех пользователей." + }, + "response": [] } ], "event": [], diff --git a/server/routers/smoke-tracker/stats.js b/server/routers/smoke-tracker/stats.js index e377a08..3e960e0 100644 --- a/server/routers/smoke-tracker/stats.js +++ b/server/routers/smoke-tracker/stats.js @@ -62,6 +62,180 @@ router.get('/daily', async (req, res, next) => { } }) +// Сводная статистика: среднее в день, по дням недели, общее по всем пользователям +router.get('/summary', async (req, res, next) => { + try { + const user = req.user + const { from, to } = req.query + + const now = new Date() + const defaultFrom = new Date(now) + defaultFrom.setDate(defaultFrom.getDate() - 30) + + const fromDate = from ? new Date(from) : defaultFrom + const toDate = to ? new Date(to) : now + + // Фильтр для текущего пользователя + const userMatch = { + userId: new mongoose.Types.ObjectId(user.id), + smokedAt: { + $gte: fromDate, + $lte: toDate, + }, + } + + // Фильтр для всех пользователей (общая статистика) + const globalMatch = { + smokedAt: { + $gte: fromDate, + $lte: toDate, + }, + } + + // 1. Статистика по дням (для текущего пользователя) + const dailyStats = await CigaretteModel.aggregate([ + { $match: userMatch }, + { + $group: { + _id: { + $dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' }, + }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const dailyData = dailyStats.map((item) => ({ + date: item._id, + count: item.count, + })) + + // 2. Среднее количество в день (для текущего пользователя) + const totalCigarettes = dailyStats.reduce((sum, item) => sum + item.count, 0) + const daysWithData = dailyStats.length + const averagePerDay = daysWithData > 0 ? (totalCigarettes / daysWithData).toFixed(2) : 0 + + // 3. Статистика по дням недели (для текущего пользователя) + const weekdayStats = await CigaretteModel.aggregate([ + { $match: userMatch }, + { + $group: { + _id: { $dayOfWeek: '$smokedAt' }, // 1 = воскресенье, 2 = понедельник, ..., 7 = суббота + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const weekdayNames = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'] + const weekdayData = weekdayStats.map((item) => { + const dayIndex = item._id - 1 // MongoDB возвращает 1-7, приводим к 0-6 + return { + dayOfWeek: item._id, + dayName: weekdayNames[dayIndex], + count: item.count, + } + }) + + // Вычисляем среднее для каждого дня недели + const weekdayAverages = weekdayData.map((day) => { + // Считаем, сколько раз встречается этот день недели в периоде + const occurrences = Math.floor( + (toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24 * 7) + ) + 1 + return { + ...day, + average: occurrences > 0 ? (day.count / occurrences).toFixed(2) : day.count, + } + }) + + // 4. Общая статистика по всем пользователям + const globalDailyStats = await CigaretteModel.aggregate([ + { $match: globalMatch }, + { + $group: { + _id: { + $dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' }, + }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const globalDailyData = globalDailyStats.map((item) => ({ + date: item._id, + count: item.count, + })) + + const globalTotalCigarettes = globalDailyStats.reduce((sum, item) => sum + item.count, 0) + const globalDaysWithData = globalDailyStats.length + const globalAveragePerDay = + globalDaysWithData > 0 ? (globalTotalCigarettes / globalDaysWithData).toFixed(2) : 0 + + // Общая статистика по дням недели (все пользователи) + const globalWeekdayStats = await CigaretteModel.aggregate([ + { $match: globalMatch }, + { + $group: { + _id: { $dayOfWeek: '$smokedAt' }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const globalWeekdayData = globalWeekdayStats.map((item) => { + const dayIndex = item._id - 1 + return { + dayOfWeek: item._id, + dayName: weekdayNames[dayIndex], + count: item.count, + } + }) + + const globalWeekdayAverages = globalWeekdayData.map((day) => { + const occurrences = Math.floor( + (toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24 * 7) + ) + 1 + return { + ...day, + average: occurrences > 0 ? (day.count / occurrences).toFixed(2) : day.count, + } + }) + + // Количество активных пользователей в периоде + const activeUsers = await CigaretteModel.distinct('userId', globalMatch) + + const result = { + user: { + daily: dailyData, + averagePerDay: parseFloat(averagePerDay), + weekday: weekdayAverages, + total: totalCigarettes, + daysWithData, + }, + global: { + daily: globalDailyData, + averagePerDay: parseFloat(globalAveragePerDay), + weekday: globalWeekdayAverages, + total: globalTotalCigarettes, + daysWithData: globalDaysWithData, + activeUsers: activeUsers.length, + }, + period: { + from: fromDate.toISOString(), + to: toDate.toISOString(), + }, + } + + res.json(getAnswer(null, result)) + } catch (err) { + next(err) + } +}) + module.exports = router From 414383163e2a7ca1851e20f1af3799b276ad6b3e Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 17 Nov 2025 14:40:37 +0300 Subject: [PATCH 139/147] Enhance smoke-tracker API to include statistics for active users only; update documentation to reflect changes in user activity criteria and statistics calculations. --- server/routers/smoke-tracker/API.md | 16 +++++---- server/routers/smoke-tracker/stats.js | 50 +++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/server/routers/smoke-tracker/API.md b/server/routers/smoke-tracker/API.md index f4ab7ba..4d810dd 100644 --- a/server/routers/smoke-tracker/API.md +++ b/server/routers/smoke-tracker/API.md @@ -508,13 +508,17 @@ curl -X GET http://localhost:8044/smoke-tracker/stats/summary \ - `total` — общее количество сигарет за период - `daysWithData` — количество дней, в которые были записи -**`global`** — общая статистика по всем пользователям: -- `daily` — массив с суммарным количеством сигарет всех пользователей по дням -- `averagePerDay` — среднее количество сигарет в день (все пользователи) -- `weekday` — статистика по дням недели (все пользователи) -- `total` — общее количество сигарет всех пользователей за период +**`global`** — общая статистика по всем **активным** пользователям: +- `daily` — массив с суммарным количеством сигарет всех активных пользователей по дням +- `averagePerDay` — среднее количество сигарет в день (активные пользователи) +- `weekday` — статистика по дням недели (активные пользователи) +- `total` — общее количество сигарет всех активных пользователей за период - `daysWithData` — количество дней с записями -- `activeUsers` — количество уникальных пользователей, записывавших сигареты в период +- `activeUsers` — количество активных пользователей в период + +> **Примечание**: Активными считаются только пользователи, которые в среднем выкуривают **от 2 до 40 сигарет в день**. Это позволяет исключить из статистики: +> - Тестовые аккаунты и неактивных пользователей (< 2 сигарет/день) +> - Ошибочные или накликанные данные (> 40 сигарет/день) **`period`** — информация о запрошенном периоде: - `from` — начало периода (ISO 8601) diff --git a/server/routers/smoke-tracker/stats.js b/server/routers/smoke-tracker/stats.js index 3e960e0..2d135b7 100644 --- a/server/routers/smoke-tracker/stats.js +++ b/server/routers/smoke-tracker/stats.js @@ -150,9 +150,47 @@ router.get('/summary', async (req, res, next) => { } }) - // 4. Общая статистика по всем пользователям - const globalDailyStats = await CigaretteModel.aggregate([ + // 4. Определяем активных пользователей (от 2 до 40 сигарет в день в среднем) + const MIN_CIGARETTES_PER_DAY = 2 + const MAX_CIGARETTES_PER_DAY = 40 + const periodDays = Math.ceil((toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24)) + + // Получаем статистику по каждому пользователю + const userStats = await CigaretteModel.aggregate([ { $match: globalMatch }, + { + $group: { + _id: '$userId', + total: { $sum: 1 }, + }, + }, + ]) + + // Фильтруем активных пользователей (исключаем слишком низкие и слишком высокие значения) + const activeUserIds = userStats + .filter((stat) => { + const avgPerDay = stat.total / periodDays + return avgPerDay > MIN_CIGARETTES_PER_DAY && avgPerDay <= MAX_CIGARETTES_PER_DAY + }) + .map((stat) => stat._id) + + const filteredLow = userStats.filter((stat) => stat.total / periodDays <= MIN_CIGARETTES_PER_DAY).length + const filteredHigh = userStats.filter((stat) => stat.total / periodDays > MAX_CIGARETTES_PER_DAY).length + + console.log('[STATS] Total users:', userStats.length) + console.log('[STATS] Active users (2-40 cigs/day):', activeUserIds.length) + console.log('[STATS] Filtered out (too low):', filteredLow) + console.log('[STATS] Filtered out (too high):', filteredHigh) + + // Фильтр только для активных пользователей + const activeGlobalMatch = { + ...globalMatch, + userId: { $in: activeUserIds }, + } + + // Общая статистика по активным пользователям + const globalDailyStats = await CigaretteModel.aggregate([ + { $match: activeGlobalMatch }, { $group: { _id: { @@ -174,9 +212,9 @@ router.get('/summary', async (req, res, next) => { const globalAveragePerDay = globalDaysWithData > 0 ? (globalTotalCigarettes / globalDaysWithData).toFixed(2) : 0 - // Общая статистика по дням недели (все пользователи) + // Общая статистика по дням недели (активные пользователи) const globalWeekdayStats = await CigaretteModel.aggregate([ - { $match: globalMatch }, + { $match: activeGlobalMatch }, { $group: { _id: { $dayOfWeek: '$smokedAt' }, @@ -205,8 +243,8 @@ router.get('/summary', async (req, res, next) => { } }) - // Количество активных пользователей в периоде - const activeUsers = await CigaretteModel.distinct('userId', globalMatch) + // Количество активных пользователей + const activeUsers = activeUserIds const result = { user: { From 2480f7c376da5895b2e94b34470d400fd50d7df9 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Mon, 17 Nov 2025 20:13:20 +0300 Subject: [PATCH 140/147] Update smoke-tracker API documentation to reflect changes in JWT token expiration; modify auth.js to implement a permanent token without expiration. --- server/routers/smoke-tracker/API.md | 2 +- server/routers/smoke-tracker/auth.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/server/routers/smoke-tracker/API.md b/server/routers/smoke-tracker/API.md index 4d810dd..553c2ec 100644 --- a/server/routers/smoke-tracker/API.md +++ b/server/routers/smoke-tracker/API.md @@ -127,7 +127,7 @@ curl -X POST http://localhost:8044/smoke-tracker/auth/signin \ - `user.id` — уникальный идентификатор пользователя - `user.login` — логин пользователя - `user.created` — дата создания аккаунта (ISO 8601) -- `token` — JWT-токен для авторизации (действителен 12 часов) +- `token` — JWT-токен для авторизации (без ограничений по времени действия) **Возможные ошибки**: diff --git a/server/routers/smoke-tracker/auth.js b/server/routers/smoke-tracker/auth.js index b522812..32dbeed 100644 --- a/server/routers/smoke-tracker/auth.js +++ b/server/routers/smoke-tracker/auth.js @@ -69,10 +69,8 @@ router.post( { ...JSON.parse(JSON.stringify(user.userId)), }, - SMOKE_TRACKER_TOKEN_KEY, - { - expiresIn: '12h', - } + SMOKE_TRACKER_TOKEN_KEY + // Для этого проекта токен делаем бессрочным (без поля expiresIn) ) res.json( From fa860921da6b11d4543be790b748598f711cdb63 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Fri, 21 Nov 2025 16:19:47 +0300 Subject: [PATCH 141/147] update --- server/routers/assessment-tools/index.js | 17 ++ .../assessment-tools/models/Criteria.js | 39 ++++ .../routers/assessment-tools/models/Event.js | 44 ++++ .../routers/assessment-tools/models/Expert.js | 38 ++++ .../routers/assessment-tools/models/Rating.js | 59 +++++ .../routers/assessment-tools/models/Team.js | 47 ++++ .../routers/assessment-tools/models/index.js | 14 ++ .../assessment-tools/routes/criteria.js | 117 ++++++++++ .../routers/assessment-tools/routes/event.js | 108 +++++++++ .../assessment-tools/routes/experts.js | 104 +++++++++ .../assessment-tools/routes/ratings.js | 215 ++++++++++++++++++ .../routers/assessment-tools/routes/teams.js | 180 +++++++++++++++ .../scripts/recreate-test-user.js | 38 ++++ 13 files changed, 1020 insertions(+) create mode 100644 server/routers/assessment-tools/index.js create mode 100644 server/routers/assessment-tools/models/Criteria.js create mode 100644 server/routers/assessment-tools/models/Event.js create mode 100644 server/routers/assessment-tools/models/Expert.js create mode 100644 server/routers/assessment-tools/models/Rating.js create mode 100644 server/routers/assessment-tools/models/Team.js create mode 100644 server/routers/assessment-tools/models/index.js create mode 100644 server/routers/assessment-tools/routes/criteria.js create mode 100644 server/routers/assessment-tools/routes/event.js create mode 100644 server/routers/assessment-tools/routes/experts.js create mode 100644 server/routers/assessment-tools/routes/ratings.js create mode 100644 server/routers/assessment-tools/routes/teams.js create mode 100644 server/routers/assessment-tools/scripts/recreate-test-user.js diff --git a/server/routers/assessment-tools/index.js b/server/routers/assessment-tools/index.js new file mode 100644 index 0000000..2b93a66 --- /dev/null +++ b/server/routers/assessment-tools/index.js @@ -0,0 +1,17 @@ +const router = require('express').Router(); + +// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts) +const mongoose = require('../../utils/mongoose'); + +const timer = (time = 300) => (req, res, next) => setTimeout(next, time); + +router.use(timer()); + +// Подключение маршрутов - прямые пути без path.join и __dirname +router.use('/events', require('./routes/event')); +router.use('/teams', require('./routes/teams')); +router.use('/experts', require('./routes/experts')); +router.use('/criteria', require('./routes/criteria')); +router.use('/ratings', require('./routes/ratings')); + +module.exports = router; diff --git a/server/routers/assessment-tools/models/Criteria.js b/server/routers/assessment-tools/models/Criteria.js new file mode 100644 index 0000000..3aa2ba4 --- /dev/null +++ b/server/routers/assessment-tools/models/Criteria.js @@ -0,0 +1,39 @@ +const mongoose = require('../../../utils/mongoose'); + +const criterionItemSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + maxScore: { + type: Number, + default: 5, + min: 0, + max: 10 + } +}, { _id: false }); + +const criteriaSchema = new mongoose.Schema({ + blockName: { + type: String, + required: true + }, + criteria: [criterionItemSchema], + order: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('Criteria', criteriaSchema); + diff --git a/server/routers/assessment-tools/models/Event.js b/server/routers/assessment-tools/models/Event.js new file mode 100644 index 0000000..bb17b50 --- /dev/null +++ b/server/routers/assessment-tools/models/Event.js @@ -0,0 +1,44 @@ +const mongoose = require('../../../utils/mongoose'); + +const eventSchema = new mongoose.Schema({ + name: { + type: String, + required: true, + default: 'Новое мероприятие' + }, + description: { + type: String, + default: '' + }, + eventDate: { + type: Date, + required: true, + default: Date.now + }, + location: { + type: String, + default: '' + }, + status: { + type: String, + enum: ['draft', 'ready', 'active', 'completed'], + default: 'draft' + }, + votingEnabled: { + type: Boolean, + default: false + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('Event', eventSchema); + diff --git a/server/routers/assessment-tools/models/Expert.js b/server/routers/assessment-tools/models/Expert.js new file mode 100644 index 0000000..5a4ce67 --- /dev/null +++ b/server/routers/assessment-tools/models/Expert.js @@ -0,0 +1,38 @@ +const mongoose = require('../../../utils/mongoose'); +const crypto = require('crypto'); + +const expertSchema = new mongoose.Schema({ + fullName: { + type: String, + required: true + }, + token: { + type: String, + unique: true + }, + qrCodeUrl: { + type: String, + default: '' + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +// Generate unique token before saving +expertSchema.pre('save', function(next) { + if (!this.token) { + this.token = crypto.randomBytes(16).toString('hex'); + } + next(); +}); + +module.exports = mongoose.model('Expert', expertSchema); + diff --git a/server/routers/assessment-tools/models/Rating.js b/server/routers/assessment-tools/models/Rating.js new file mode 100644 index 0000000..13ed07f --- /dev/null +++ b/server/routers/assessment-tools/models/Rating.js @@ -0,0 +1,59 @@ +const mongoose = require('../../../utils/mongoose'); + +const ratingItemSchema = new mongoose.Schema({ + criteriaId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Criteria', + required: true + }, + criterionName: { + type: String, + required: true + }, + score: { + type: Number, + required: true, + min: 0, + max: 5 + } +}, { _id: false }); + +const ratingSchema = new mongoose.Schema({ + expertId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Expert', + required: true + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Team', + required: true + }, + ratings: [ratingItemSchema], + totalScore: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +// Calculate total score before saving +ratingSchema.pre('save', function(next) { + this.totalScore = this.ratings.reduce((sum, item) => sum + item.score, 0); + next(); +}); + +// Ensure unique combination of expert and team +ratingSchema.index({ expertId: 1, teamId: 1 }, { unique: true }); + +module.exports = mongoose.model('Rating', ratingSchema); + diff --git a/server/routers/assessment-tools/models/Team.js b/server/routers/assessment-tools/models/Team.js new file mode 100644 index 0000000..27aabc0 --- /dev/null +++ b/server/routers/assessment-tools/models/Team.js @@ -0,0 +1,47 @@ +const mongoose = require('../../../utils/mongoose'); + +const teamSchema = new mongoose.Schema({ + type: { + type: String, + enum: ['team', 'participant'], + required: true + }, + name: { + type: String, + required: true + }, + projectName: { + type: String, + default: '' + }, + caseDescription: { + type: String, + default: '' + }, + isActive: { + type: Boolean, + default: true + }, + votingStatus: { + type: String, + enum: ['not_evaluated', 'evaluating', 'evaluated'], + default: 'not_evaluated' + }, + isActiveForVoting: { + type: Boolean, + default: false + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}); + +module.exports = mongoose.model('Team', teamSchema); + diff --git a/server/routers/assessment-tools/models/index.js b/server/routers/assessment-tools/models/index.js new file mode 100644 index 0000000..6f05978 --- /dev/null +++ b/server/routers/assessment-tools/models/index.js @@ -0,0 +1,14 @@ +const Event = require('./Event'); +const Team = require('./Team'); +const Expert = require('./Expert'); +const Criteria = require('./Criteria'); +const Rating = require('./Rating'); + +module.exports = { + Event, + Team, + Expert, + Criteria, + Rating +}; + diff --git a/server/routers/assessment-tools/routes/criteria.js b/server/routers/assessment-tools/routes/criteria.js new file mode 100644 index 0000000..6c7b405 --- /dev/null +++ b/server/routers/assessment-tools/routes/criteria.js @@ -0,0 +1,117 @@ +const router = require('express').Router(); +const { Criteria } = require('../models'); + +// Критерии по умолчанию из hack.md +const DEFAULT_CRITERIA = [ + { + blockName: 'Оценка проекта', + criteria: [ + { name: 'Соответствие решения поставленной задаче', maxScore: 5 }, + { name: 'Оригинальность - использование нестандартных технических и проектных подходов', maxScore: 5 }, + { name: 'Работоспособность решения', maxScore: 1 }, + { name: 'Технологическая сложность решения', maxScore: 2 }, + { name: 'Объем функциональных возможностей решения', maxScore: 2 }, + { name: 'Аргументация способа выбранного решения', maxScore: 5 }, + { name: 'Качество предоставления информации', maxScore: 5 }, + { name: 'Наличие удобного UX/UI', maxScore: 5 }, + { name: 'Наличие не менее 5 AI-агентов', maxScore: 5 } + ], + order: 0 + } +]; + +// GET /api/criteria - получить все блоки критериев +router.get('/', async (req, res) => { + try { + const criteria = await Criteria.find().sort({ order: 1 }); + res.json(criteria); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/criteria/:id - получить блок критериев по ID +router.get('/:id', async (req, res) => { + try { + const criteria = await Criteria.findById(req.params.id); + if (!criteria) { + return res.status(404).json({ error: 'Criteria not found' }); + } + res.json(criteria); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/criteria - создать блок критериев +router.post('/', async (req, res) => { + try { + const { blockName, criteria, order } = req.body; + + if (!blockName || !criteria || !Array.isArray(criteria)) { + return res.status(400).json({ error: 'Block name and criteria array are required' }); + } + + const criteriaBlock = await Criteria.create({ + blockName, + criteria, + order: order !== undefined ? order : 0 + }); + + res.status(201).json(criteriaBlock); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/criteria/default - загрузить критерии по умолчанию из hack.md +router.post('/default', async (req, res) => { + try { + // Удаляем все существующие критерии + await Criteria.deleteMany({}); + + // Создаем критерии по умолчанию + const createdCriteria = await Criteria.insertMany(DEFAULT_CRITERIA); + + res.status(201).json(createdCriteria); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PUT /api/criteria/:id - редактировать блок +router.put('/:id', async (req, res) => { + try { + const { blockName, criteria, order } = req.body; + + const criteriaBlock = await Criteria.findById(req.params.id); + if (!criteriaBlock) { + return res.status(404).json({ error: 'Criteria not found' }); + } + + if (blockName !== undefined) criteriaBlock.blockName = blockName; + if (criteria !== undefined) criteriaBlock.criteria = criteria; + if (order !== undefined) criteriaBlock.order = order; + + await criteriaBlock.save(); + res.json(criteriaBlock); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/criteria/:id - удалить блок +router.delete('/:id', async (req, res) => { + try { + const criteria = await Criteria.findByIdAndDelete(req.params.id); + if (!criteria) { + return res.status(404).json({ error: 'Criteria not found' }); + } + res.json({ message: 'Criteria deleted successfully', criteria }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; + diff --git a/server/routers/assessment-tools/routes/event.js b/server/routers/assessment-tools/routes/event.js new file mode 100644 index 0000000..5fc614d --- /dev/null +++ b/server/routers/assessment-tools/routes/event.js @@ -0,0 +1,108 @@ +const router = require('express').Router(); +const { Event } = require('../models'); + +// GET /api/events - получить все мероприятия +router.get('/', async (req, res) => { + try { + const events = await Event.find().sort({ eventDate: -1 }); + res.json(events); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/events/:id - получить одно мероприятие +router.get('/:id', async (req, res) => { + try { + const event = await Event.findById(req.params.id); + + if (!event) { + return res.status(404).json({ error: 'Мероприятие не найдено' }); + } + + res.json(event); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/events - создать новое мероприятие +router.post('/', async (req, res) => { + try { + const { name, description, eventDate, location, status } = req.body; + + const event = await Event.create({ + name: name || 'Новое мероприятие', + description: description || '', + eventDate: eventDate || new Date(), + location: location || '', + status: status || 'draft', + votingEnabled: false + }); + + res.status(201).json(event); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PUT /api/events/:id - обновить мероприятие +router.put('/:id', async (req, res) => { + try { + const { name, description, eventDate, location, status } = req.body; + + const event = await Event.findById(req.params.id); + + if (!event) { + return res.status(404).json({ error: 'Мероприятие не найдено' }); + } + + if (name !== undefined) event.name = name; + if (description !== undefined) event.description = description; + if (eventDate !== undefined) event.eventDate = eventDate; + if (location !== undefined) event.location = location; + if (status !== undefined) event.status = status; + + await event.save(); + + res.json(event); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/events/:id - удалить мероприятие +router.delete('/:id', async (req, res) => { + try { + const event = await Event.findByIdAndDelete(req.params.id); + + if (!event) { + return res.status(404).json({ error: 'Мероприятие не найдено' }); + } + + res.json({ message: 'Мероприятие удалено', event }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/events/:id/toggle-voting - вкл/выкл оценку +router.patch('/:id/toggle-voting', async (req, res) => { + try { + const event = await Event.findById(req.params.id); + + if (!event) { + return res.status(404).json({ error: 'Мероприятие не найдено' }); + } + + event.votingEnabled = !event.votingEnabled; + await event.save(); + + res.json(event); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; + diff --git a/server/routers/assessment-tools/routes/experts.js b/server/routers/assessment-tools/routes/experts.js new file mode 100644 index 0000000..dbbf43f --- /dev/null +++ b/server/routers/assessment-tools/routes/experts.js @@ -0,0 +1,104 @@ +const router = require('express').Router(); +const { Expert } = require('../models'); + +// GET /api/experts - список экспертов +router.get('/', async (req, res) => { + try { + const experts = await Expert.find().sort({ createdAt: -1 }); + res.json(experts); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/experts/by-token/:token - получить данные эксперта по токену +router.get('/by-token/:token', async (req, res) => { + try { + const expert = await Expert.findOne({ token: req.params.token }); + if (!expert) { + return res.status(404).json({ error: 'Expert not found' }); + } + res.json(expert); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/experts/:id - получить эксперта по ID +router.get('/:id', async (req, res) => { + try { + const expert = await Expert.findById(req.params.id); + if (!expert) { + return res.status(404).json({ error: 'Expert not found' }); + } + res.json(expert); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/experts - создать эксперта (с генерацией уникальной ссылки) +router.post('/', async (req, res) => { + try { + const { fullName } = req.body; + + if (!fullName) { + return res.status(400).json({ error: 'Full name is required' }); + } + + // Создаем нового эксперта + const expert = new Expert({ + fullName + }); + + // Сохраняем эксперта (токен генерируется в pre-save хуке) + await expert.save(); + + // Формируем URL для QR кода ПОСЛЕ сохранения, когда токен уже сгенерирован + const baseUrl = req.protocol + '://' + req.get('host'); + expert.qrCodeUrl = `${baseUrl}/assessment-tools/expert/${expert.token}`; + + // Сохраняем еще раз с обновленным qrCodeUrl + await expert.save(); + + res.status(201).json(expert); + } catch (error) { + console.error('Error creating expert:', error); + res.status(500).json({ error: error.message }); + } +}); + +// PUT /api/experts/:id - редактировать эксперта +router.put('/:id', async (req, res) => { + try { + const { fullName } = req.body; + + const expert = await Expert.findById(req.params.id); + if (!expert) { + return res.status(404).json({ error: 'Expert not found' }); + } + + if (fullName !== undefined) expert.fullName = fullName; + + await expert.save(); + res.json(expert); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/experts/:id - удалить эксперта +router.delete('/:id', async (req, res) => { + try { + const expert = await Expert.findByIdAndDelete(req.params.id); + if (!expert) { + return res.status(404).json({ error: 'Expert not found' }); + } + res.json({ message: 'Expert deleted successfully', expert }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; + diff --git a/server/routers/assessment-tools/routes/ratings.js b/server/routers/assessment-tools/routes/ratings.js new file mode 100644 index 0000000..de1cc10 --- /dev/null +++ b/server/routers/assessment-tools/routes/ratings.js @@ -0,0 +1,215 @@ +const router = require('express').Router(); +const { Rating, Team, Expert, Criteria } = require('../models'); + +// GET /api/ratings - получить все оценки (с фильтрами) +router.get('/', async (req, res) => { + try { + const { expertId, teamId } = req.query; + const filter = {}; + + if (expertId) filter.expertId = expertId; + if (teamId) filter.teamId = teamId; + + const ratings = await Rating.find(filter) + .populate('expertId', 'fullName') + .populate('teamId', 'name type') + .sort({ createdAt: -1 }); + + res.json(ratings); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/ratings/team/:teamId - оценки конкретной команды +router.get('/team/:teamId', async (req, res) => { + try { + const ratings = await Rating.find({ teamId: req.params.teamId }) + .populate('expertId', 'fullName') + .populate('teamId', 'name type projectName'); + + res.json(ratings); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/ratings/expert/:expertId - оценки конкретного эксперта +router.get('/expert/:expertId', async (req, res) => { + try { + const ratings = await Rating.find({ expertId: req.params.expertId }) + .populate('teamId', 'name type projectName'); + + res.json(ratings); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/ratings/statistics - статистика с группировкой по командам +router.get('/statistics', async (req, res) => { + try { + const { type } = req.query; + + // Получаем все команды + const teamFilter = type ? { type, isActive: true } : { isActive: true }; + const teams = await Team.find(teamFilter); + + // Получаем все оценки + const ratings = await Rating.find() + .populate('expertId', 'fullName') + .populate('teamId', 'name type projectName'); + + // Группируем оценки по командам + const statistics = teams.map(team => { + const teamRatings = ratings.filter(r => r.teamId && r.teamId._id.toString() === team._id.toString()); + + // Считаем средние оценки по критериям + const criteriaStats = {}; + teamRatings.forEach(rating => { + rating.ratings.forEach(item => { + if (!criteriaStats[item.criterionName]) { + criteriaStats[item.criterionName] = { + name: item.criterionName, + scores: [], + average: 0 + }; + } + criteriaStats[item.criterionName].scores.push(item.score); + }); + }); + + // Вычисляем средние значения + Object.keys(criteriaStats).forEach(key => { + const scores = criteriaStats[key].scores; + criteriaStats[key].average = scores.reduce((sum, s) => sum + s, 0) / scores.length; + }); + + // Считаем общий балл команды (среднее от всех экспертов) + const totalScore = teamRatings.length > 0 + ? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length + : 0; + + return { + team: { + _id: team._id, + name: team.name, + type: team.type, + projectName: team.projectName + }, + ratings: teamRatings.map(r => ({ + expert: r.expertId ? r.expertId.fullName : 'Unknown', + criteria: r.ratings, + totalScore: r.totalScore + })), + criteriaStats: Object.values(criteriaStats), + totalScore: totalScore, + ratingsCount: teamRatings.length + }; + }); + + res.json(statistics); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/ratings/top3 - топ-3 команды/участники +router.get('/top3', async (req, res) => { + try { + const { type } = req.query; + + // Получаем статистику + const teamFilter = type ? { type, isActive: true } : { isActive: true }; + const teams = await Team.find(teamFilter); + + const ratings = await Rating.find() + .populate('teamId', 'name type projectName'); + + // Группируем и считаем средние баллы + const teamScores = teams.map(team => { + const teamRatings = ratings.filter(r => r.teamId && r.teamId._id.toString() === team._id.toString()); + + const totalScore = teamRatings.length > 0 + ? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length + : 0; + + return { + team: { + _id: team._id, + name: team.name, + type: team.type, + projectName: team.projectName + }, + totalScore: totalScore, + ratingsCount: teamRatings.length + }; + }); + + // Сортируем по баллам и берем топ-3 + const top3 = teamScores + .filter(t => t.ratingsCount > 0) + .sort((a, b) => b.totalScore - a.totalScore) + .slice(0, 3); + + res.json(top3); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/ratings - создать/обновить оценку эксперта +router.post('/', async (req, res) => { + try { + const { expertId, teamId, ratings } = req.body; + + if (!expertId || !teamId || !ratings || !Array.isArray(ratings)) { + return res.status(400).json({ error: 'Expert ID, team ID, and ratings array are required' }); + } + + // Проверяем существование эксперта и команды + const expert = await Expert.findById(expertId); + const team = await Team.findById(teamId); + + if (!expert) { + return res.status(404).json({ error: 'Expert not found' }); + } + + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + + // Проверяем, активна ли команда + if (!team.isActive) { + return res.status(400).json({ error: 'Team voting is disabled' }); + } + + // Ищем существующую оценку + let rating = await Rating.findOne({ expertId, teamId }); + + if (rating) { + // Обновляем существующую оценку + rating.ratings = ratings; + await rating.save(); + } else { + // Создаем новую оценку + rating = await Rating.create({ + expertId, + teamId, + ratings + }); + } + + // Возвращаем с populate + rating = await Rating.findById(rating._id) + .populate('expertId', 'fullName') + .populate('teamId', 'name type projectName'); + + res.status(rating ? 200 : 201).json(rating); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; + diff --git a/server/routers/assessment-tools/routes/teams.js b/server/routers/assessment-tools/routes/teams.js new file mode 100644 index 0000000..746925a --- /dev/null +++ b/server/routers/assessment-tools/routes/teams.js @@ -0,0 +1,180 @@ +const router = require('express').Router(); +const { Team } = require('../models'); + +// GET /api/teams - список всех команд +router.get('/', async (req, res) => { + try { + const { type } = req.query; + const filter = type ? { type } : {}; + const teams = await Team.find(filter).sort({ createdAt: -1 }); + res.json(teams); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/teams/active/voting - получить активную для оценки команду (ДОЛЖЕН БЫТЬ ПЕРЕД /:id) +router.get('/active/voting', async (req, res) => { + try { + const team = await Team.findOne({ isActiveForVoting: true }); + res.json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/teams/stop-all-voting/global - остановить все оценивания (ДОЛЖЕН БЫТЬ ПЕРЕД /:id) +router.patch('/stop-all-voting/global', async (req, res) => { + try { + // Находим все команды, которые сейчас оцениваются + const result = await Team.updateMany( + { isActiveForVoting: true }, + { + isActiveForVoting: false, + votingStatus: 'evaluated' + } + ); + + res.json({ + message: 'All voting stopped successfully', + modifiedCount: result.modifiedCount + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET /api/teams/:id - получить команду по ID +router.get('/:id', async (req, res) => { + try { + const team = await Team.findById(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + res.json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// POST /api/teams - создать команду/участника +router.post('/', async (req, res) => { + try { + const { type, name, projectName, caseDescription } = req.body; + + if (!type || !name) { + return res.status(400).json({ error: 'Type and name are required' }); + } + + const team = await Team.create({ + type, + name, + projectName: projectName || '', + caseDescription: caseDescription || '', + isActive: true + }); + + res.status(201).json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PUT /api/teams/:id - редактировать команду +router.put('/:id', async (req, res) => { + try { + const { type, name, projectName, caseDescription } = req.body; + + const team = await Team.findById(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + + if (type !== undefined) team.type = type; + if (name !== undefined) team.name = name; + if (projectName !== undefined) team.projectName = projectName; + if (caseDescription !== undefined) team.caseDescription = caseDescription; + + await team.save(); + res.json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// DELETE /api/teams/:id - удалить команду +router.delete('/:id', async (req, res) => { + try { + const team = await Team.findByIdAndDelete(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + res.json({ message: 'Team deleted successfully', team }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/teams/:id/activate-for-voting - активировать команду для оценки +router.patch('/:id/activate-for-voting', async (req, res) => { + try { + // Деактивируем все команды и сохраняем их статус + const previouslyActive = await Team.findOne({ isActiveForVoting: true }); + if (previouslyActive) { + previouslyActive.isActiveForVoting = false; + previouslyActive.votingStatus = 'evaluated'; + await previouslyActive.save(); + } + + // Активируем выбранную команду + const team = await Team.findById(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + + team.isActiveForVoting = true; + team.votingStatus = 'evaluating'; + await team.save(); + + res.json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/teams/:id/stop-voting - остановить оценивание конкретной команды +router.patch('/:id/stop-voting', async (req, res) => { + try { + const team = await Team.findById(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + + team.isActiveForVoting = false; + team.votingStatus = 'evaluated'; + await team.save(); + + res.json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// PATCH /api/teams/:id/toggle-active - остановить оценку команды +router.patch('/:id/toggle-active', async (req, res) => { + try { + const team = await Team.findById(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + + team.isActive = !team.isActive; + await team.save(); + + res.json(team); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/routers/assessment-tools/scripts/recreate-test-user.js b/server/routers/assessment-tools/scripts/recreate-test-user.js new file mode 100644 index 0000000..b1362de --- /dev/null +++ b/server/routers/assessment-tools/scripts/recreate-test-user.js @@ -0,0 +1,38 @@ +// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts) +const mongoose = require('../../../utils/mongoose'); +const { Event } = require('../models'); + +async function recreateTestUser() { + try { + // Ждем, пока подключение будет готово + if (mongoose.connection.readyState !== 1) { + await new Promise(resolve => { + mongoose.connection.once('connected', resolve); + }); + } + + console.log('Connected to MongoDB'); + + // Создаем тестовое мероприятие если его нет + let event = await Event.findOne(); + if (!event) { + event = await Event.create({ + name: 'Tatar san', + status: 'draft', + votingEnabled: false + }); + console.log('Test event created:', event.name); + } + + console.log('Database initialized successfully'); + + await mongoose.disconnect(); + console.log('Disconnected from MongoDB'); + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +recreateTestUser(); + From 1d4521b8036d9007d59d61759b6cca4db5ea6b44 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Fri, 21 Nov 2025 16:53:13 +0300 Subject: [PATCH 142/147] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assessment-tools/models/Criteria.js | 7 +++- .../routers/assessment-tools/models/Event.js | 2 +- .../routers/assessment-tools/models/Expert.js | 7 +++- .../routers/assessment-tools/models/Rating.js | 7 +++- .../routers/assessment-tools/models/Team.js | 7 +++- .../assessment-tools/routes/criteria.js | 30 +++++++++---- .../assessment-tools/routes/experts.js | 12 ++++-- .../assessment-tools/routes/ratings.js | 32 +++++++++----- .../routers/assessment-tools/routes/teams.js | 42 ++++++++++++------- .../scripts/recreate-test-user.js | 16 ++++--- 10 files changed, 111 insertions(+), 51 deletions(-) diff --git a/server/routers/assessment-tools/models/Criteria.js b/server/routers/assessment-tools/models/Criteria.js index 3aa2ba4..35961e2 100644 --- a/server/routers/assessment-tools/models/Criteria.js +++ b/server/routers/assessment-tools/models/Criteria.js @@ -1,4 +1,4 @@ -const mongoose = require('../../../utils/mongoose'); +const mongoose = require('mongoose'); const criterionItemSchema = new mongoose.Schema({ name: { @@ -14,6 +14,11 @@ const criterionItemSchema = new mongoose.Schema({ }, { _id: false }); const criteriaSchema = new mongoose.Schema({ + eventId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Event', + required: true + }, blockName: { type: String, required: true diff --git a/server/routers/assessment-tools/models/Event.js b/server/routers/assessment-tools/models/Event.js index bb17b50..6d504a8 100644 --- a/server/routers/assessment-tools/models/Event.js +++ b/server/routers/assessment-tools/models/Event.js @@ -1,4 +1,4 @@ -const mongoose = require('../../../utils/mongoose'); +const mongoose = require('mongoose'); const eventSchema = new mongoose.Schema({ name: { diff --git a/server/routers/assessment-tools/models/Expert.js b/server/routers/assessment-tools/models/Expert.js index 5a4ce67..a774160 100644 --- a/server/routers/assessment-tools/models/Expert.js +++ b/server/routers/assessment-tools/models/Expert.js @@ -1,7 +1,12 @@ -const mongoose = require('../../../utils/mongoose'); +const mongoose = require('mongoose'); const crypto = require('crypto'); const expertSchema = new mongoose.Schema({ + eventId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Event', + required: true + }, fullName: { type: String, required: true diff --git a/server/routers/assessment-tools/models/Rating.js b/server/routers/assessment-tools/models/Rating.js index 13ed07f..b5a2539 100644 --- a/server/routers/assessment-tools/models/Rating.js +++ b/server/routers/assessment-tools/models/Rating.js @@ -1,4 +1,4 @@ -const mongoose = require('../../../utils/mongoose'); +const mongoose = require('mongoose'); const ratingItemSchema = new mongoose.Schema({ criteriaId: { @@ -19,6 +19,11 @@ const ratingItemSchema = new mongoose.Schema({ }, { _id: false }); const ratingSchema = new mongoose.Schema({ + eventId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Event', + required: true + }, expertId: { type: mongoose.Schema.Types.ObjectId, ref: 'Expert', diff --git a/server/routers/assessment-tools/models/Team.js b/server/routers/assessment-tools/models/Team.js index 27aabc0..abb2a6c 100644 --- a/server/routers/assessment-tools/models/Team.js +++ b/server/routers/assessment-tools/models/Team.js @@ -1,6 +1,11 @@ -const mongoose = require('../../../utils/mongoose'); +const mongoose = require('mongoose'); const teamSchema = new mongoose.Schema({ + eventId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Event', + required: true + }, type: { type: String, enum: ['team', 'participant'], diff --git a/server/routers/assessment-tools/routes/criteria.js b/server/routers/assessment-tools/routes/criteria.js index 6c7b405..41c6dbf 100644 --- a/server/routers/assessment-tools/routes/criteria.js +++ b/server/routers/assessment-tools/routes/criteria.js @@ -23,7 +23,10 @@ const DEFAULT_CRITERIA = [ // GET /api/criteria - получить все блоки критериев router.get('/', async (req, res) => { try { - const criteria = await Criteria.find().sort({ order: 1 }); + const { eventId } = req.query; + const filter = {}; + if (eventId) filter.eventId = eventId; + const criteria = await Criteria.find(filter).sort({ order: 1 }); res.json(criteria); } catch (error) { res.status(500).json({ error: error.message }); @@ -46,13 +49,14 @@ router.get('/:id', async (req, res) => { // POST /api/criteria - создать блок критериев router.post('/', async (req, res) => { try { - const { blockName, criteria, order } = req.body; + const { eventId, blockName, criteria, order } = req.body; - if (!blockName || !criteria || !Array.isArray(criteria)) { - return res.status(400).json({ error: 'Block name and criteria array are required' }); + if (!eventId || !blockName || !criteria || !Array.isArray(criteria)) { + return res.status(400).json({ error: 'EventId, block name and criteria array are required' }); } const criteriaBlock = await Criteria.create({ + eventId, blockName, criteria, order: order !== undefined ? order : 0 @@ -67,11 +71,21 @@ router.post('/', async (req, res) => { // POST /api/criteria/default - загрузить критерии по умолчанию из hack.md router.post('/default', async (req, res) => { try { - // Удаляем все существующие критерии - await Criteria.deleteMany({}); + const { eventId } = req.body; - // Создаем критерии по умолчанию - const createdCriteria = await Criteria.insertMany(DEFAULT_CRITERIA); + if (!eventId) { + return res.status(400).json({ error: 'EventId is required' }); + } + + // Удаляем все существующие критерии для этого мероприятия + await Criteria.deleteMany({ eventId }); + + // Создаем критерии по умолчанию с eventId + const criteriaWithEventId = DEFAULT_CRITERIA.map(c => ({ + ...c, + eventId + })); + const createdCriteria = await Criteria.insertMany(criteriaWithEventId); res.status(201).json(createdCriteria); } catch (error) { diff --git a/server/routers/assessment-tools/routes/experts.js b/server/routers/assessment-tools/routes/experts.js index dbbf43f..189c438 100644 --- a/server/routers/assessment-tools/routes/experts.js +++ b/server/routers/assessment-tools/routes/experts.js @@ -4,7 +4,10 @@ const { Expert } = require('../models'); // GET /api/experts - список экспертов router.get('/', async (req, res) => { try { - const experts = await Expert.find().sort({ createdAt: -1 }); + const { eventId } = req.query; + const filter = {}; + if (eventId) filter.eventId = eventId; + const experts = await Expert.find(filter).sort({ createdAt: -1 }); res.json(experts); } catch (error) { res.status(500).json({ error: error.message }); @@ -40,14 +43,15 @@ router.get('/:id', async (req, res) => { // POST /api/experts - создать эксперта (с генерацией уникальной ссылки) router.post('/', async (req, res) => { try { - const { fullName } = req.body; + const { eventId, fullName } = req.body; - if (!fullName) { - return res.status(400).json({ error: 'Full name is required' }); + if (!eventId || !fullName) { + return res.status(400).json({ error: 'EventId and full name are required' }); } // Создаем нового эксперта const expert = new Expert({ + eventId, fullName }); diff --git a/server/routers/assessment-tools/routes/ratings.js b/server/routers/assessment-tools/routes/ratings.js index de1cc10..2bbb5b0 100644 --- a/server/routers/assessment-tools/routes/ratings.js +++ b/server/routers/assessment-tools/routes/ratings.js @@ -4,11 +4,12 @@ const { Rating, Team, Expert, Criteria } = require('../models'); // GET /api/ratings - получить все оценки (с фильтрами) router.get('/', async (req, res) => { try { - const { expertId, teamId } = req.query; + const { expertId, teamId, eventId } = req.query; const filter = {}; if (expertId) filter.expertId = expertId; if (teamId) filter.teamId = teamId; + if (eventId) filter.eventId = eventId; const ratings = await Rating.find(filter) .populate('expertId', 'fullName') @@ -49,14 +50,18 @@ router.get('/expert/:expertId', async (req, res) => { // GET /api/ratings/statistics - статистика с группировкой по командам router.get('/statistics', async (req, res) => { try { - const { type } = req.query; + const { type, eventId } = req.query; // Получаем все команды - const teamFilter = type ? { type, isActive: true } : { isActive: true }; + const teamFilter = { isActive: true }; + if (type) teamFilter.type = type; + if (eventId) teamFilter.eventId = eventId; const teams = await Team.find(teamFilter); // Получаем все оценки - const ratings = await Rating.find() + const ratingFilter = {}; + if (eventId) ratingFilter.eventId = eventId; + const ratings = await Rating.find(ratingFilter) .populate('expertId', 'fullName') .populate('teamId', 'name type projectName'); @@ -117,13 +122,17 @@ router.get('/statistics', async (req, res) => { // GET /api/ratings/top3 - топ-3 команды/участники router.get('/top3', async (req, res) => { try { - const { type } = req.query; + const { type, eventId } = req.query; // Получаем статистику - const teamFilter = type ? { type, isActive: true } : { isActive: true }; + const teamFilter = { isActive: true }; + if (type) teamFilter.type = type; + if (eventId) teamFilter.eventId = eventId; const teams = await Team.find(teamFilter); - const ratings = await Rating.find() + const ratingFilter = {}; + if (eventId) ratingFilter.eventId = eventId; + const ratings = await Rating.find(ratingFilter) .populate('teamId', 'name type projectName'); // Группируем и считаем средние баллы @@ -161,10 +170,10 @@ router.get('/top3', async (req, res) => { // POST /api/ratings - создать/обновить оценку эксперта router.post('/', async (req, res) => { try { - const { expertId, teamId, ratings } = req.body; + const { eventId, expertId, teamId, ratings } = req.body; - if (!expertId || !teamId || !ratings || !Array.isArray(ratings)) { - return res.status(400).json({ error: 'Expert ID, team ID, and ratings array are required' }); + if (!eventId || !expertId || !teamId || !ratings || !Array.isArray(ratings)) { + return res.status(400).json({ error: 'EventId, expert ID, team ID, and ratings array are required' }); } // Проверяем существование эксперта и команды @@ -185,7 +194,7 @@ router.post('/', async (req, res) => { } // Ищем существующую оценку - let rating = await Rating.findOne({ expertId, teamId }); + let rating = await Rating.findOne({ eventId, expertId, teamId }); if (rating) { // Обновляем существующую оценку @@ -194,6 +203,7 @@ router.post('/', async (req, res) => { } else { // Создаем новую оценку rating = await Rating.create({ + eventId, expertId, teamId, ratings diff --git a/server/routers/assessment-tools/routes/teams.js b/server/routers/assessment-tools/routes/teams.js index 746925a..284db0e 100644 --- a/server/routers/assessment-tools/routes/teams.js +++ b/server/routers/assessment-tools/routes/teams.js @@ -4,8 +4,10 @@ const { Team } = require('../models'); // GET /api/teams - список всех команд router.get('/', async (req, res) => { try { - const { type } = req.query; - const filter = type ? { type } : {}; + const { type, eventId } = req.query; + const filter = {}; + if (type) filter.type = type; + if (eventId) filter.eventId = eventId; const teams = await Team.find(filter).sort({ createdAt: -1 }); res.json(teams); } catch (error) { @@ -16,7 +18,10 @@ router.get('/', async (req, res) => { // GET /api/teams/active/voting - получить активную для оценки команду (ДОЛЖЕН БЫТЬ ПЕРЕД /:id) router.get('/active/voting', async (req, res) => { try { - const team = await Team.findOne({ isActiveForVoting: true }); + const { eventId } = req.query; + const filter = { isActiveForVoting: true }; + if (eventId) filter.eventId = eventId; + const team = await Team.findOne(filter); res.json(team); } catch (error) { res.status(500).json({ error: error.message }); @@ -26,9 +31,13 @@ router.get('/active/voting', async (req, res) => { // PATCH /api/teams/stop-all-voting/global - остановить все оценивания (ДОЛЖЕН БЫТЬ ПЕРЕД /:id) router.patch('/stop-all-voting/global', async (req, res) => { try { + const { eventId } = req.body; // Находим все команды, которые сейчас оцениваются + const filter = { isActiveForVoting: true }; + if (eventId) filter.eventId = eventId; + const result = await Team.updateMany( - { isActiveForVoting: true }, + filter, { isActiveForVoting: false, votingStatus: 'evaluated' @@ -60,13 +69,14 @@ router.get('/:id', async (req, res) => { // POST /api/teams - создать команду/участника router.post('/', async (req, res) => { try { - const { type, name, projectName, caseDescription } = req.body; + const { eventId, type, name, projectName, caseDescription } = req.body; - if (!type || !name) { - return res.status(400).json({ error: 'Type and name are required' }); + if (!eventId || !type || !name) { + return res.status(400).json({ error: 'EventId, type and name are required' }); } const team = await Team.create({ + eventId, type, name, projectName: projectName || '', @@ -118,8 +128,17 @@ router.delete('/:id', async (req, res) => { // PATCH /api/teams/:id/activate-for-voting - активировать команду для оценки router.patch('/:id/activate-for-voting', async (req, res) => { try { - // Деактивируем все команды и сохраняем их статус - const previouslyActive = await Team.findOne({ isActiveForVoting: true }); + // Получаем команду для активации + const team = await Team.findById(req.params.id); + if (!team) { + return res.status(404).json({ error: 'Team not found' }); + } + + // Деактивируем все команды этого мероприятия + const previouslyActive = await Team.findOne({ + isActiveForVoting: true, + eventId: team.eventId + }); if (previouslyActive) { previouslyActive.isActiveForVoting = false; previouslyActive.votingStatus = 'evaluated'; @@ -127,11 +146,6 @@ router.patch('/:id/activate-for-voting', async (req, res) => { } // Активируем выбранную команду - const team = await Team.findById(req.params.id); - if (!team) { - return res.status(404).json({ error: 'Team not found' }); - } - team.isActiveForVoting = true; team.votingStatus = 'evaluating'; await team.save(); diff --git a/server/routers/assessment-tools/scripts/recreate-test-user.js b/server/routers/assessment-tools/scripts/recreate-test-user.js index b1362de..6002d66 100644 --- a/server/routers/assessment-tools/scripts/recreate-test-user.js +++ b/server/routers/assessment-tools/scripts/recreate-test-user.js @@ -1,14 +1,13 @@ -// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts) +// Импортировать mongoose из общего модуля (подключение происходит автоматически) const mongoose = require('../../../utils/mongoose'); -const { Event } = require('../models'); +const Event = require('../models/Event'); async function recreateTestUser() { try { - // Ждем, пока подключение будет готово + // Проверяем подключение к MongoDB if (mongoose.connection.readyState !== 1) { - await new Promise(resolve => { - mongoose.connection.once('connected', resolve); - }); + console.log('Waiting for MongoDB connection...'); + await new Promise(resolve => setTimeout(resolve, 1000)); } console.log('Connected to MongoDB'); @@ -22,12 +21,11 @@ async function recreateTestUser() { votingEnabled: false }); console.log('Test event created:', event.name); + } else { + console.log('Event already exists:', event.name); } console.log('Database initialized successfully'); - - await mongoose.disconnect(); - console.log('Disconnected from MongoDB'); } catch (error) { console.error('Error:', error); process.exit(1); From 449aef6f54b6b5b14b8bacde399153ad24eb0590 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Fri, 21 Nov 2025 18:43:04 +0300 Subject: [PATCH 143/147] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/index.ts | 2 ++ server/routers/assessment-tools/index.js | 1 + 2 files changed, 3 insertions(+) diff --git a/server/index.ts b/server/index.ts index 46aef65..e24449d 100644 --- a/server/index.ts +++ b/server/index.ts @@ -22,6 +22,7 @@ import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' import procurementRouter from './routers/procurement' import smokeTrackerRouter from './routers/smoke-tracker' +import assessmentToolsRouter from './routers/assessment-tools' import { setIo } from './io' export const app = express() @@ -109,6 +110,7 @@ const initServer = async () => { app.use('/questioneer', questioneerRouter) app.use('/procurement', procurementRouter) app.use('/smoke-tracker', smokeTrackerRouter) + app.use('/assessment-tools', assessmentToolsRouter) app.use(errorHandler) // Создаем обычный HTTP сервер diff --git a/server/routers/assessment-tools/index.js b/server/routers/assessment-tools/index.js index 2b93a66..8104b39 100644 --- a/server/routers/assessment-tools/index.js +++ b/server/routers/assessment-tools/index.js @@ -15,3 +15,4 @@ router.use('/criteria', require('./routes/criteria')); router.use('/ratings', require('./routes/ratings')); module.exports = router; +module.exports.default = router; From 599170df2cb291984bafe676855873c24d706a52 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Fri, 21 Nov 2025 22:37:14 +0300 Subject: [PATCH 144/147] update --- .../assessment-tools/models/Criteria.js | 6 ++++ .../assessment-tools/routes/criteria.js | 29 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/server/routers/assessment-tools/models/Criteria.js b/server/routers/assessment-tools/models/Criteria.js index 35961e2..6d80c71 100644 --- a/server/routers/assessment-tools/models/Criteria.js +++ b/server/routers/assessment-tools/models/Criteria.js @@ -23,6 +23,12 @@ const criteriaSchema = new mongoose.Schema({ type: String, required: true }, + criteriaType: { + type: String, + enum: ['team', 'participant', 'all'], + default: 'all', + required: true + }, criteria: [criterionItemSchema], order: { type: Number, diff --git a/server/routers/assessment-tools/routes/criteria.js b/server/routers/assessment-tools/routes/criteria.js index 41c6dbf..49458f2 100644 --- a/server/routers/assessment-tools/routes/criteria.js +++ b/server/routers/assessment-tools/routes/criteria.js @@ -4,7 +4,8 @@ const { Criteria } = require('../models'); // Критерии по умолчанию из hack.md const DEFAULT_CRITERIA = [ { - blockName: 'Оценка проекта', + blockName: 'Оценка проекта команды', + criteriaType: 'team', criteria: [ { name: 'Соответствие решения поставленной задаче', maxScore: 5 }, { name: 'Оригинальность - использование нестандартных технических и проектных подходов', maxScore: 5 }, @@ -17,15 +18,33 @@ const DEFAULT_CRITERIA = [ { name: 'Наличие не менее 5 AI-агентов', maxScore: 5 } ], order: 0 + }, + { + blockName: 'Оценка выступления участника', + criteriaType: 'participant', + criteria: [ + { name: 'Качество презентации и донесения идеи', maxScore: 5 }, + { name: 'Понимание технологии и решения', maxScore: 5 }, + { name: 'Аргументация выбранного подхода', maxScore: 5 }, + { name: 'Ответы на вопросы жюри', maxScore: 5 }, + { name: 'Коммуникативные навыки', maxScore: 5 } + ], + order: 1 } ]; // GET /api/criteria - получить все блоки критериев router.get('/', async (req, res) => { try { - const { eventId } = req.query; + const { eventId, criteriaType } = req.query; const filter = {}; if (eventId) filter.eventId = eventId; + if (criteriaType && criteriaType !== 'all') { + filter.$or = [ + { criteriaType: criteriaType }, + { criteriaType: 'all' } + ]; + } const criteria = await Criteria.find(filter).sort({ order: 1 }); res.json(criteria); } catch (error) { @@ -49,7 +68,7 @@ router.get('/:id', async (req, res) => { // POST /api/criteria - создать блок критериев router.post('/', async (req, res) => { try { - const { eventId, blockName, criteria, order } = req.body; + const { eventId, blockName, criteriaType, criteria, order } = req.body; if (!eventId || !blockName || !criteria || !Array.isArray(criteria)) { return res.status(400).json({ error: 'EventId, block name and criteria array are required' }); @@ -58,6 +77,7 @@ router.post('/', async (req, res) => { const criteriaBlock = await Criteria.create({ eventId, blockName, + criteriaType: criteriaType || 'all', criteria, order: order !== undefined ? order : 0 }); @@ -96,7 +116,7 @@ router.post('/default', async (req, res) => { // PUT /api/criteria/:id - редактировать блок router.put('/:id', async (req, res) => { try { - const { blockName, criteria, order } = req.body; + const { blockName, criteriaType, criteria, order } = req.body; const criteriaBlock = await Criteria.findById(req.params.id); if (!criteriaBlock) { @@ -104,6 +124,7 @@ router.put('/:id', async (req, res) => { } if (blockName !== undefined) criteriaBlock.blockName = blockName; + if (criteriaType !== undefined) criteriaBlock.criteriaType = criteriaType; if (criteria !== undefined) criteriaBlock.criteria = criteria; if (order !== undefined) criteriaBlock.order = order; From 4c35decfd7c51012c0912df9195600ec1b470d24 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Fri, 21 Nov 2025 23:47:56 +0300 Subject: [PATCH 145/147] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BA=D1=80=D0=B8=D1=82=D0=B5=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assessment-tools/routes/ratings.js | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/server/routers/assessment-tools/routes/ratings.js b/server/routers/assessment-tools/routes/ratings.js index 2bbb5b0..f58b899 100644 --- a/server/routers/assessment-tools/routes/ratings.js +++ b/server/routers/assessment-tools/routes/ratings.js @@ -119,49 +119,64 @@ router.get('/statistics', async (req, res) => { } }); -// GET /api/ratings/top3 - топ-3 команды/участники +// GET /api/ratings/top3 - топ-3 команды и топ-3 участники отдельно +// ВАЖНО: всегда возвращаем объект вида { teams: Top3Item[], participants: Top3Item[] }, +// чтобы фронтенд мог безопасно работать с data.teams / data.participants router.get('/top3', async (req, res) => { try { const { type, eventId } = req.query; - - // Получаем статистику + + // Получаем все активные команды/участников const teamFilter = { isActive: true }; - if (type) teamFilter.type = type; if (eventId) teamFilter.eventId = eventId; const teams = await Team.find(teamFilter); - + const ratingFilter = {}; if (eventId) ratingFilter.eventId = eventId; - const ratings = await Rating.find(ratingFilter) - .populate('teamId', 'name type projectName'); - - // Группируем и считаем средние баллы - const teamScores = teams.map(team => { - const teamRatings = ratings.filter(r => r.teamId && r.teamId._id.toString() === team._id.toString()); - - const totalScore = teamRatings.length > 0 - ? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length - : 0; - - return { - team: { - _id: team._id, - name: team.name, - type: team.type, - projectName: team.projectName - }, - totalScore: totalScore, - ratingsCount: teamRatings.length - }; - }); - - // Сортируем по баллам и берем топ-3 - const top3 = teamScores - .filter(t => t.ratingsCount > 0) - .sort((a, b) => b.totalScore - a.totalScore) - .slice(0, 3); - - res.json(top3); + const ratings = await Rating.find(ratingFilter).populate('teamId', 'name type projectName'); + + const calculateTop3 = (sourceTeams) => { + const teamScores = sourceTeams.map((team) => { + const teamRatings = ratings.filter( + (r) => r.teamId && r.teamId._id.toString() === team._id.toString() + ); + + const totalScore = + teamRatings.length > 0 + ? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length + : 0; + + return { + team: { + _id: team._id, + name: team.name, + type: team.type, + projectName: team.projectName + }, + totalScore, + ratingsCount: teamRatings.length + }; + }); + + return teamScores + .filter((t) => t.ratingsCount > 0) + .sort((a, b) => b.totalScore - a.totalScore) + .slice(0, 3); + }; + + const teamEntities = teams.filter((t) => t.type === 'team'); + const participantEntities = teams.filter((t) => t.type === 'participant'); + + const teamTop3 = calculateTop3(teamEntities); + const participantTop3 = calculateTop3(participantEntities); + + // Параметр type управляет только содержимым, но не форматом ответа + const response = { + teams: !type || type === 'team' ? teamTop3 : [], + participants: !type || type === 'participant' ? participantTop3 : [] + }; + + res.json(response); } catch (error) { res.status(500).json({ error: error.message }); } From d477a0a5f12b5df5c03af09e129675bfb0461f70 Mon Sep 17 00:00:00 2001 From: innoavvlasov Date: Sat, 22 Nov 2025 00:05:51 +0300 Subject: [PATCH 146/147] fix --- server/routers/assessment-tools/routes/experts.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/routers/assessment-tools/routes/experts.js b/server/routers/assessment-tools/routes/experts.js index 189c438..849c45f 100644 --- a/server/routers/assessment-tools/routes/experts.js +++ b/server/routers/assessment-tools/routes/experts.js @@ -59,7 +59,16 @@ router.post('/', async (req, res) => { await expert.save(); // Формируем URL для QR кода ПОСЛЕ сохранения, когда токен уже сгенерирован - const baseUrl = req.protocol + '://' + req.get('host'); + // Приоритеты: + // 1) Явная переменная окружения FRONTEND_BASE_URL (например, https://platform.brojs.ru) + // 2) Проксируемые заголовки x-forwarded-proto / x-forwarded-host + // 3) Локальные req.protocol + req.get('host') + const forwardedProto = req.get('x-forwarded-proto'); + const forwardedHost = req.get('x-forwarded-host'); + const protocol = forwardedProto || req.protocol; + const host = forwardedHost || req.get('host'); + const baseUrl = process.env.FRONTEND_BASE_URL || `${protocol}://${host}`; + expert.qrCodeUrl = `${baseUrl}/assessment-tools/expert/${expert.token}`; // Сохраняем еще раз с обновленным qrCodeUrl From 7066252bcb4e89cfc6ed9309093d472fd9730d97 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Alexandrovich Date: Fri, 5 Dec 2025 16:51:44 +0300 Subject: [PATCH 147/147] Update Jest configuration to include TypeScript support and add new code quality checks workflow; translate comments to Russian and adjust paths in test files. --- .gitea/workflows/check.yaml | 28 + jest.config.js | 145 ++--- package-lock.json | 531 ++++++++++++++---- package.json | 2 + .../__tests__/__snapshots__/todo.test.js.snap | 1 - server/__tests__/todo.test.js | 2 +- .../features/image/image.controller.js | 1 + .../routes/__tests__/buyProducts.test.js | 1 + server/routers/procurement/routes/auth.js | 2 + server/routers/procurement/routes/buy.js | 1 + .../routers/procurement/routes/buyProducts.js | 1 + .../questioneer/public/static/js/common.js | 1 + .../questioneer/public/static/js/create.js | 30 +- tsconfig.json | 194 +++---- 14 files changed, 646 insertions(+), 294 deletions(-) create mode 100644 .gitea/workflows/check.yaml diff --git a/.gitea/workflows/check.yaml b/.gitea/workflows/check.yaml new file mode 100644 index 0000000..25fcc22 --- /dev/null +++ b/.gitea/workflows/check.yaml @@ -0,0 +1,28 @@ +name: Code Quality Checks +run-name: Проверка кода (lint & typecheck) от ${{ gitea.actor }} +on: [push] + +jobs: + lint-and-typecheck: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run eslint -- --quiet + + - name: Run TypeScript type check + run: npx tsc --noEmit + + - name: Run tests + run: npm test -- --quiet \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index c53c59a..e1d5760 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,43 +1,43 @@ /** - * For a detailed explanation regarding each configuration property, visit: + * Для подробного объяснения каждого свойства конфигурации, посетите: * https://jestjs.io/docs/configuration */ /** @type {import('jest').Config} */ const config = { - // All imported modules in your tests should be mocked automatically + // Все импортированные модули в тестах должны быть автоматически замоканы // automock: false, - // Stop running tests after `n` failures + // Остановить выполнение тестов после `n` неудач // bail: 0, - // The directory where Jest should store its cached dependency information + // Директория, где Jest должен хранить кэшированную информацию о зависимостях // cacheDirectory: "C:\\Users\\alex\\AppData\\Local\\Temp\\jest", - // Automatically clear mock calls, instances, contexts and results before every test + // Автоматически очищать вызовы моков, экземпляры, контексты и результаты перед каждым тестом clearMocks: true, - // Indicates whether the coverage information should be collected while executing the test + // Указывает, должна ли собираться информация о покрытии во время выполнения тестов collectCoverage: true, - // An array of glob patterns indicating a set of files for which coverage information should be collected + // Массив glob-паттернов, указывающих набор файлов, для которых должна собираться информация о покрытии collectCoverageFrom: [ "/server/routers/**/*.js" ], - // The directory where Jest should output its coverage files + // Директория, куда Jest должен выводить файлы покрытия coverageDirectory: "coverage", - // An array of regexp pattern strings used to skip coverage collection + // Массив строк regexp-паттернов, используемых для пропуска сбора покрытия coveragePathIgnorePatterns: [ "\\\\node_modules\\\\", "/server/routers/old" ], - // Indicates which provider should be used to instrument code for coverage + // Указывает, какой провайдер должен использоваться для инструментирования кода для покрытия coverageProvider: "v8", - // A list of reporter names that Jest uses when writing coverage reports + // Список имен репортеров, которые Jest использует при записи отчетов о покрытии // coverageReporters: [ // "json", // "text", @@ -45,156 +45,159 @@ const config = { // "clover" // ], - // An object that configures minimum threshold enforcement for coverage results + // Объект, который настраивает принудительное применение минимальных порогов для результатов покрытия // coverageThreshold: undefined, - // A path to a custom dependency extractor + // Путь к пользовательскому извлекателю зависимостей // dependencyExtractor: undefined, - // Make calling deprecated APIs throw helpful error messages + // Заставить вызовы устаревших API выбрасывать полезные сообщения об ошибках // errorOnDeprecated: false, - // The default configuration for fake timers + // Конфигурация по умолчанию для поддельных таймеров // fakeTimers: { // "enableGlobally": false // }, - // Force coverage collection from ignored files using an array of glob patterns + // Принудительно собирать покрытие из игнорируемых файлов, используя массив glob-паттернов // forceCoverageMatch: [], - // A path to a module which exports an async function that is triggered once before all test suites + // Путь к модулю, который экспортирует асинхронную функцию, вызываемую один раз перед всеми наборами тестов // globalSetup: undefined, - // A path to a module which exports an async function that is triggered once after all test suites + // Путь к модулю, который экспортирует асинхронную функцию, вызываемую один раз после всех наборов тестов // globalTeardown: undefined, - // A set of global variables that need to be available in all test environments + // Набор глобальных переменных, которые должны быть доступны во всех тестовых окружениях // globals: {}, - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // Максимальное количество воркеров, используемых для запуска тестов. Может быть указано в % или числом. Например, maxWorkers: 10% будет использовать 10% от количества CPU + 1 в качестве максимального числа воркеров. maxWorkers: 2 будет использовать максимум 2 воркера. // maxWorkers: "50%", - // An array of directory names to be searched recursively up from the requiring module's location + // Массив имен директорий, которые должны быть рекурсивно найдены вверх от местоположения требуемого модуля // moduleDirectories: [ // "node_modules" // ], - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "mjs", - // "cjs", - // "jsx", - // "ts", - // "tsx", - // "json", - // "node" - // ], + // Массив расширений файлов, которые используют ваши модули + moduleFileExtensions: [ + "js", + "mjs", + "cjs", + "jsx", + "ts", + "tsx", + "json", + "node" + ], - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // Карта из регулярных выражений в имена модулей или массивы имен модулей, которые позволяют заглушить ресурсы одним модулем // moduleNameMapper: {}, - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // Массив строк regexp-паттернов, сопоставляемых со всеми путями модулей перед тем, как они будут считаться 'видимыми' для загрузчика модулей // modulePathIgnorePatterns: [], - // Activates notifications for test results + // Активирует уведомления для результатов тестов // notify: false, - // An enum that specifies notification mode. Requires { notify: true } + // Перечисление, которое указывает режим уведомлений. Требует { notify: true } // notifyMode: "failure-change", - // A preset that is used as a base for Jest's configuration - // preset: undefined, + // Пресет, который используется в качестве основы для конфигурации Jest + preset: 'ts-jest', - // Run tests from one or more projects + // Запускать тесты из одного или нескольких проектов // projects: undefined, - // Use this configuration option to add custom reporters to Jest + // Используйте эту опцию конфигурации для добавления пользовательских репортеров в Jest // reporters: undefined, - // Automatically reset mock state before every test + // Автоматически сбрасывать состояние моков перед каждым тестом // resetMocks: false, - // Reset the module registry before running each individual test + // Сбрасывать реестр модулей перед запуском каждого отдельного теста // resetModules: false, - // A path to a custom resolver + // Путь к пользовательскому резолверу // resolver: undefined, - // Automatically restore mock state and implementation before every test + // Автоматически восстанавливать состояние моков и реализацию перед каждым тестом // restoreMocks: false, - // The root directory that Jest should scan for tests and modules within + // Корневая директория, которую Jest должен сканировать для поиска тестов и модулей // rootDir: undefined, - // A list of paths to directories that Jest should use to search for files in + // Список путей к директориям, которые Jest должен использовать для поиска файлов // roots: [ // "" // ], - // Allows you to use a custom runner instead of Jest's default test runner + // Позволяет использовать пользовательский раннер вместо стандартного тестового раннера Jest // runner: "jest-runner", - // The paths to modules that run some code to configure or set up the testing environment before each test + // Пути к модулям, которые выполняют некоторый код для настройки или подготовки тестового окружения перед каждым тестом // setupFiles: [], - // A list of paths to modules that run some code to configure or set up the testing framework before each test + // Список путей к модулям, которые выполняют некоторый код для настройки или подготовки тестового фреймворка перед каждым тестом // setupFilesAfterEnv: [], - // The number of seconds after which a test is considered as slow and reported as such in the results. + // Количество секунд, после которого тест считается медленным и сообщается как таковой в результатах. // slowTestThreshold: 5, - // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // Список путей к модулям сериализаторов снимков, которые Jest должен использовать для тестирования снимков // snapshotSerializers: [], - // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + // Тестовое окружение, которое будет использоваться для тестирования + testEnvironment: "node", - // Options that will be passed to the testEnvironment + // Опции, которые будут переданы в testEnvironment // testEnvironmentOptions: {}, - // Adds a location field to test results + // Добавляет поле местоположения к результатам тестов // testLocationInResults: false, - // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], + // Glob-паттерны, которые Jest использует для обнаружения тестовых файлов + testMatch: [ + "**/__tests__/**/*.[jt]s?(x)", + "**/?(*.)+(spec|test).[tj]s?(x)" + ], - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // Массив строк regexp-паттернов, которые сопоставляются со всеми тестовыми путями, сопоставленные тесты пропускаются // testPathIgnorePatterns: [ // "\\\\node_modules\\\\" // ], - // The regexp pattern or array of patterns that Jest uses to detect test files + // Regexp-паттерн или массив паттернов, которые Jest использует для обнаружения тестовых файлов // testRegex: [], - // This option allows the use of a custom results processor + // Эта опция позволяет использовать пользовательский процессор результатов // testResultsProcessor: undefined, - // This option allows use of a custom test runner + // Эта опция позволяет использовать пользовательский тестовый раннер // testRunner: "jest-circus/runner", - // A map from regular expressions to paths to transformers - // transform: undefined, + // Карта из регулярных выражений в пути к трансформерам + transform: { + '^.+\\.ts$': 'ts-jest', + '^.+\\.tsx$': 'ts-jest', + }, - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // Массив строк regexp-паттернов, которые сопоставляются со всеми путями исходных файлов, сопоставленные файлы будут пропускать трансформацию // transformIgnorePatterns: [ // "\\\\node_modules\\\\", // "\\.pnp\\.[^\\\\]+$" // ], - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // Массив строк regexp-паттернов, которые сопоставляются со всеми модулями перед тем, как загрузчик модулей автоматически вернет мок для них // unmockedModulePathPatterns: undefined, - // Indicates whether each individual test should be reported during the run + // Указывает, должен ли каждый отдельный тест сообщаться во время выполнения verbose: true, - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // Массив regexp-паттернов, которые сопоставляются со всеми путями исходных файлов перед повторным запуском тестов в режиме наблюдения // watchPathIgnorePatterns: [], - // Whether to use watchman for file crawling + // Использовать ли watchman для обхода файлов // watchman: true, }; diff --git a/package-lock.json b/package-lock.json index cbf818f..64ef6f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/jest": "^30.0.0", "@types/node": "22.10.2", "eslint": "^9.17.0", "globals": "^15.14.0", @@ -49,6 +50,7 @@ "mockingoose": "^2.16.2", "nodemon": "3.1.9", "supertest": "^7.0.0", + "ts-jest": "^29.4.6", "ts-node-dev": "2.0.0", "typescript": "5.7.3" } @@ -225,14 +227,15 @@ "license": "ISC" }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.25.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -442,9 +445,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -475,100 +478,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/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/@babel/highlight/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/@babel/parser": { "version": "7.25.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", @@ -1638,6 +1547,16 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -1699,6 +1618,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", @@ -1715,6 +1644,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -3103,6 +3056,230 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3818,6 +3995,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -5882,6 +6072,28 @@ "graphql": "14 - 16" } }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7688,6 +7900,13 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8245,6 +8464,13 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/nice-grpc": { "version": "2.1.12", "resolved": "https://registry.npmjs.org/nice-grpc/-/nice-grpc-2.1.12.tgz", @@ -8800,9 +9026,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "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" }, @@ -9431,9 +9657,9 @@ "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9789,7 +10015,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10255,6 +10481,72 @@ "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", "license": "MIT" }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -10449,6 +10741,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -10749,6 +11055,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "devOptional": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index cf42802..4dca919 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/jest": "^30.0.0", "@types/node": "22.10.2", "eslint": "^9.17.0", "globals": "^15.14.0", @@ -61,6 +62,7 @@ "mockingoose": "^2.16.2", "nodemon": "3.1.9", "supertest": "^7.0.0", + "ts-jest": "^29.4.6", "ts-node-dev": "2.0.0", "typescript": "5.7.3" } diff --git a/server/__tests__/__snapshots__/todo.test.js.snap b/server/__tests__/__snapshots__/todo.test.js.snap index 2573ff1..46ffa2a 100644 --- a/server/__tests__/__snapshots__/todo.test.js.snap +++ b/server/__tests__/__snapshots__/todo.test.js.snap @@ -4,7 +4,6 @@ exports[`todo list app get list 1`] = ` { "body": [ { - "_id": "670f69b5796ce7a9069da2f7", "created": "2024-10-16T07:22:29.042Z", "id": "670f69b5796ce7a9069da2f7", "items": [], diff --git a/server/__tests__/todo.test.js b/server/__tests__/todo.test.js index 8ee1701..8972cb3 100644 --- a/server/__tests__/todo.test.js +++ b/server/__tests__/todo.test.js @@ -2,7 +2,7 @@ const { describe, it, expect } = require('@jest/globals') const request = require('supertest') const express = require('express') const mockingoose = require('mockingoose') -const { ListModel } = require('../data/model/todo/list') +const { ListModel } = require('../routers/todo/model/todo/list') const todo = require('../routers/todo/routes') diff --git a/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js b/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js index 49b4489..f7b2451 100644 --- a/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js +++ b/server/routers/kfu-m-24-1/back-new/features/image/image.controller.js @@ -48,6 +48,7 @@ exports.generate = async (req, res) => { } ); const content = chatResp.data.choices[0].message.content; + // eslint-disable-next-line no-useless-escape const match = content.match(/ { diff --git a/server/routers/procurement/routes/auth.js b/server/routers/procurement/routes/auth.js index ec54893..254a152 100644 --- a/server/routers/procurement/routes/auth.js +++ b/server/routers/procurement/routes/auth.js @@ -136,6 +136,7 @@ const waitForDatabaseConnection = async () => { } try { + // eslint-disable-next-line no-undef const connection = await connectDB(); if (!connection) { break; @@ -218,6 +219,7 @@ const initializeTestUser = async () => { console.error('Error initializing test data:', error.message); if (error?.code === 13 || /auth/i.test(error?.message || '')) { try { + // eslint-disable-next-line no-undef await connectDB(); } catch (connectError) { if (process.env.DEV === 'true') { diff --git a/server/routers/procurement/routes/buy.js b/server/routers/procurement/routes/buy.js index 69f22f2..58b1d66 100644 --- a/server/routers/procurement/routes/buy.js +++ b/server/routers/procurement/routes/buy.js @@ -202,6 +202,7 @@ router.get('/docs/:id/file', async (req, res) => { } const mimeType = mimeTypes[doc.type] || 'application/octet-stream' + // eslint-disable-next-line no-useless-escape const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_') res.setHeader('Content-Type', mimeType) diff --git a/server/routers/procurement/routes/buyProducts.js b/server/routers/procurement/routes/buyProducts.js index b844b18..af0912c 100644 --- a/server/routers/procurement/routes/buyProducts.js +++ b/server/routers/procurement/routes/buyProducts.js @@ -37,6 +37,7 @@ const storage = multer.diskStorage({ const originalExtension = path.extname(fixedName) || ''; const baseName = path .basename(fixedName, originalExtension) + // eslint-disable-next-line no-control-regex .replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу cb(null, `${Date.now()}_${baseName}${originalExtension}`); }, diff --git a/server/routers/questioneer/public/static/js/common.js b/server/routers/questioneer/public/static/js/common.js index 7007de8..9a6a943 100644 --- a/server/routers/questioneer/public/static/js/common.js +++ b/server/routers/questioneer/public/static/js/common.js @@ -187,6 +187,7 @@ function showConfirm(message, callback, title) { function generateQRCode(data, size) { const typeNumber = 0; // Автоматическое определение const errorCorrectionLevel = 'L'; // Низкий уровень коррекции ошибок + // eslint-disable-next-line no-undef const qr = qrcode(typeNumber, errorCorrectionLevel); qr.addData(data); qr.make(); diff --git a/server/routers/questioneer/public/static/js/create.js b/server/routers/questioneer/public/static/js/create.js index e979449..6bb97a0 100644 --- a/server/routers/questioneer/public/static/js/create.js +++ b/server/routers/questioneer/public/static/js/create.js @@ -344,21 +344,21 @@ $(document).ready(function() { // Инициализируем атрибуты required updateRequiredAttributes(); -}); - -// Обработчик удаления вопроса -$(document).on('click', '.remove-question', function() { - $(this).closest('.question-item').remove(); - updateQuestionNumbers(); - // Вызываем функцию обновления атрибутов required - updateRequiredAttributes(); -}); - -// Обработчик удаления опции -$(document).on('click', '.remove-option', function() { - $(this).closest('.option-item').remove(); + // Обработчик удаления вопроса + $(document).on('click', '.remove-question', function() { + $(this).closest('.question-item').remove(); + updateQuestionNumbers(); + + // Вызываем функцию обновления атрибутов required + updateRequiredAttributes(); + }); - // Вызываем функцию обновления атрибутов required - updateRequiredAttributes(); + // Обработчик удаления опции + $(document).on('click', '.remove-option', function() { + $(this).closest('.option-item').remove(); + + // Вызываем функцию обновления атрибутов required + updateRequiredAttributes(); + }); }); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 72ecfc4..b1980b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,109 +1,109 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Посетите https://aka.ms/tsconfig чтобы узнать больше об этом файле */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Проекты */ + // "incremental": true, /* Сохранять .tsbuildinfo файлы для инкрементальной компиляции проектов. */ + // "composite": true, /* Включить ограничения, которые позволяют использовать проект TypeScript со ссылками на проекты. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Указать путь к файлу инкрементальной компиляции .tsbuildinfo. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Отключить предпочтение исходных файлов вместо файлов объявлений при ссылке на составные проекты. */ + // "disableSolutionSearching": true, /* Исключить проект из проверки ссылок нескольких проектов при редактировании. */ + // "disableReferencedProjectLoad": true, /* Уменьшить количество проектов, загружаемых автоматически TypeScript. */ - /* Language and Environment */ - "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Язык и окружение */ + "target": "es2018", /* Установить версию языка JavaScript для сгенерированного JavaScript и включить совместимые объявления библиотек. */ + // "lib": [], /* Указать набор объединенных файлов объявлений библиотек, описывающих целевое окружение выполнения. */ + // "jsx": "preserve", /* Указать, какой JSX код генерируется. */ + // "experimentalDecorators": true, /* Включить экспериментальную поддержку устаревших экспериментальных декораторов. */ + // "emitDecoratorMetadata": true, /* Генерировать метаданные типов дизайна для декорированных объявлений в исходных файлах. */ + // "jsxFactory": "", /* Указать функцию фабрики JSX, используемую при таргетинге на React JSX, например 'React.createElement' или 'h'. */ + // "jsxFragmentFactory": "", /* Указать ссылку на JSX Fragment, используемую для фрагментов при таргетинге на React JSX, например 'React.Fragment' или 'Fragment'. */ + // "jsxImportSource": "", /* Указать спецификатор модуля, используемый для импорта функций фабрики JSX при использовании 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Указать объект, вызываемый для 'createElement'. Применяется только при таргетинге на 'react' JSX. */ + // "noLib": true, /* Отключить включение любых файлов библиотек, включая lib.d.ts по умолчанию. */ + // "useDefineForClassFields": true, /* Генерировать поля классов, совместимые со стандартом ECMAScript. */ + // "moduleDetection": "auto", /* Управлять методом, используемым для обнаружения JS файлов формата модулей. */ - /* Modules */ - "module": "NodeNext", /* Specify what module code is generated. */ - "rootDir": ".", /* Specify the root folder within your source files. */ - "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Модули */ + "module": "NodeNext", /* Указать, какой код модулей генерируется. */ + "rootDir": ".", /* Указать корневую папку в ваших исходных файлах. */ + "moduleResolution": "nodenext", /* Указать, как TypeScript ищет файл по заданному спецификатору модуля. */ + // "baseUrl": "./", /* Указать базовую директорию для разрешения неотносительных имен модулей. */ + // "paths": {}, /* Указать набор записей, которые переопределяют импорты для дополнительных мест поиска. */ + // "rootDirs": [], /* Разрешить обработку нескольких папок как одной при разрешении модулей. */ + // "typeRoots": [], /* Указать несколько папок, которые действуют как './node_modules/@types'. */ + // "types": [], /* Указать имена пакетов типов, которые должны быть включены без ссылки в исходном файле. */ + // "allowUmdGlobalAccess": true, /* Разрешить доступ к UMD глобальным переменным из модулей. */ + // "moduleSuffixes": [], /* Список суффиксов имен файлов для поиска при разрешении модуля. */ + // "allowImportingTsExtensions": true, /* Разрешить импорты с расширениями файлов TypeScript. Требует '--moduleResolution bundler' и либо '--noEmit', либо '--emitDeclarationOnly'. */ + "resolvePackageJsonExports": true, /* Использовать поле 'exports' из package.json при разрешении импортов пакетов. */ + // "resolvePackageJsonImports": true, /* Использовать поле 'imports' из package.json при разрешении импортов. */ + // "customConditions": [], /* Условия для установки в дополнение к специфичным для резолвера значениям по умолчанию при разрешении импортов. */ + "resolveJsonModule": true, /* Включить импорт .json файлов. */ + // "allowArbitraryExtensions": true, /* Включить импорт файлов с любым расширением, при условии наличия файла объявлений. */ + // "noResolve": true, /* Запретить 'import's, 'require's или ''s от расширения количества файлов, которые TypeScript должен добавить в проект. */ - /* JavaScript Support */ - "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Поддержка JavaScript */ + "allowJs": true, /* Разрешить файлам JavaScript быть частью вашей программы. Используйте опцию 'checkJS' для получения ошибок из этих файлов. */ + // "checkJs": true, /* Включить сообщения об ошибках в файлах JavaScript с проверкой типов. */ + // "maxNodeModuleJsDepth": 1, /* Указать максимальную глубину папок, используемую для проверки JavaScript файлов из 'node_modules'. Применимо только с 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Генерация */ + // "declaration": true, /* Генерировать .d.ts файлы из файлов TypeScript и JavaScript в вашем проекте. */ + // "declarationMap": true, /* Создавать sourcemaps для d.ts файлов. */ + // "emitDeclarationOnly": true, /* Выводить только d.ts файлы, а не JavaScript файлы. */ + // "sourceMap": true, /* Создавать файлы source map для сгенерированных JavaScript файлов. */ + // "inlineSourceMap": true, /* Включать файлы sourcemap внутри сгенерированного JavaScript. */ + // "outFile": "./", /* Указать файл, который объединяет все выходные данные в один JavaScript файл. Если 'declaration' равно true, также обозначает файл, который объединяет весь .d.ts вывод. */ + "outDir": "./dist", /* Указать выходную папку для всех сгенерированных файлов. */ + // "removeComments": true, /* Отключить генерацию комментариев. */ + // "noEmit": true, /* Отключить генерацию файлов из компиляции. */ + // "importHelpers": true, /* Разрешить импорт вспомогательных функций из tslib один раз на проект, вместо включения их в каждый файл. */ + // "importsNotUsedAsValues": "remove", /* Указать поведение генерации/проверки для импортов, которые используются только для типов. */ + // "downlevelIteration": true, /* Генерировать более совместимый, но многословный и менее производительный JavaScript для итерации. */ + // "sourceRoot": "", /* Указать корневой путь для отладчиков для поиска эталонного исходного кода. */ + // "mapRoot": "", /* Указать местоположение, где отладчик должен найти map файлы вместо сгенерированных местоположений. */ + // "inlineSources": true, /* Включать исходный код в sourcemaps внутри сгенерированного JavaScript. */ + // "emitBOM": true, /* Генерировать UTF-8 Byte Order Mark (BOM) в начале выходных файлов. */ + // "newLine": "crlf", /* Установить символ новой строки для генерации файлов. */ + // "stripInternal": true, /* Отключить генерацию объявлений, которые имеют '@internal' в их JSDoc комментариях. */ + // "noEmitHelpers": true, /* Отключить генерацию пользовательских вспомогательных функций, таких как '__extends' в скомпилированном выводе. */ + // "noEmitOnError": true, /* Отключить генерацию файлов, если сообщается о любых ошибках проверки типов. */ + // "preserveConstEnums": true, /* Отключить стирание объявлений 'const enum' в сгенерированном коде. */ + // "declarationDir": "./", /* Указать выходную директорию для сгенерированных файлов объявлений. */ + // "preserveValueImports": true, /* Сохранять неиспользуемые импортированные значения в выводе JavaScript, которые в противном случае были бы удалены. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Ограничения взаимодействия */ + "isolatedModules": true, /* Обеспечить, чтобы каждый файл мог быть безопасно транслирован без зависимости от других импортов. */ + // "verbatimModuleSyntax": true, /* Не преобразовывать или опускать любые импорты или экспорты, не помеченные как только для типов, обеспечивая их запись в формате выходного файла на основе настройки 'module'. */ + // "allowSyntheticDefaultImports": true, /* Разрешить 'import x from y' когда модуль не имеет экспорта по умолчанию. */ + "esModuleInterop": true, /* Генерировать дополнительный JavaScript для упрощения поддержки импорта модулей CommonJS. Это включает 'allowSyntheticDefaultImports' для совместимости типов. */ + // "preserveSymlinks": true, /* Отключить разрешение символических ссылок к их реальному пути. Соответствует тому же флагу в node. */ + "forceConsistentCasingInFileNames": true, /* Обеспечить правильный регистр в импортах. */ - /* Type Checking */ - "strict": false, /* Enable all strict type-checking options. */ - "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + /* Проверка типов */ + "strict": false, /* Включить все строгие опции проверки типов. */ + "noImplicitAny": false, /* Включить сообщения об ошибках для выражений и объявлений с подразумеваемым типом 'any'. */ + // "strictNullChecks": true, /* При проверке типов учитывать 'null' и 'undefined'. */ + // "strictFunctionTypes": true, /* При присваивании функций проверять, чтобы параметры и возвращаемые значения были совместимы по подтипам. */ + // "strictBindCallApply": true, /* Проверять, что аргументы для методов 'bind', 'call' и 'apply' соответствуют исходной функции. */ + // "strictPropertyInitialization": true, /* Проверять свойства классов, которые объявлены, но не установлены в конструкторе. */ + // "noImplicitThis": true, /* Включить сообщения об ошибках, когда 'this' получает тип 'any'. */ + // "useUnknownInCatchVariables": true, /* Переменные предложения catch по умолчанию как 'unknown' вместо 'any'. */ + // "alwaysStrict": true, /* Обеспечить, чтобы 'use strict' всегда генерировался. */ + // "noUnusedLocals": true, /* Включить сообщения об ошибках, когда локальные переменные не читаются. */ + // "noUnusedParameters": true, /* Вызывать ошибку, когда параметр функции не читается. */ + // "exactOptionalPropertyTypes": true, /* Интерпретировать типы необязательных свойств как написано, а не добавлять 'undefined'. */ + // "noImplicitReturns": true, /* Включить сообщения об ошибках для путей кода, которые не возвращают явно в функции. */ + // "noFallthroughCasesInSwitch": true, /* Включить сообщения об ошибках для случаев провала в операторах switch. */ + // "noUncheckedIndexedAccess": true, /* Добавлять 'undefined' к типу при доступе с использованием индекса. */ + // "noImplicitOverride": true, /* Обеспечить, чтобы переопределяющие члены в производных классах были помечены модификатором override. */ + // "noPropertyAccessFromIndexSignature": true, /* Принуждает использовать индексированные аксессоры для ключей, объявленных с использованием индексированного типа. */ + // "allowUnusedLabels": true, /* Отключить сообщения об ошибках для неиспользуемых меток. */ + // "allowUnreachableCode": true, /* Отключить сообщения об ошибках для недостижимого кода. */ + /* Полнота */ + // "skipDefaultLibCheck": true, /* Пропускать проверку типов .d.ts файлов, которые включены в TypeScript. */ + "skipLibCheck": true /* Пропускать проверку типов всех .d.ts файлов. */ }, "exclude": [ "node_modules",