Compare commits
262 Commits
kfu-m-24-1
...
feature/wo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
919714089a | ||
|
|
7066252bcb | ||
| d477a0a5f1 | |||
| 4c35decfd7 | |||
| 599170df2c | |||
| 449aef6f54 | |||
| 1d4521b803 | |||
| fa860921da | |||
|
|
2480f7c376 | ||
|
|
414383163e | ||
|
|
f856d94596 | ||
|
|
dd75c54b32 | ||
|
|
f6f9163c3f | ||
|
|
4c166a8d33 | ||
| 284be82e1e | |||
| 41b5cb6fae | |||
| c4664edd7e | |||
| 69eddf47db | |||
| 71f3f353ab | |||
| 0d1dcf21c1 | |||
| 35493a09b5 | |||
| 390d97e6d5 | |||
| eca5cba858 | |||
| 6c190b80fb | |||
| a6065dd95c | |||
| 99127c42e2 | |||
| 599ccd1582 | |||
| 2b5e5564c8 | |||
| 7937be469b | |||
| 9f72d5885e | |||
| f65fd175ca | |||
|
|
d049c29f93 | ||
|
|
351ea75072 | ||
|
|
34163788f3 | ||
|
|
4ef4dd3c1b | ||
|
|
80498a0ff0 | ||
|
|
00386cc135 | ||
|
|
f5faae7907 | ||
|
|
659f9fd684 | ||
|
|
256de78e64 | ||
|
|
1500486cd8 | ||
|
|
63a825f153 | ||
|
|
1383e360a1 | ||
|
|
ca01d1c538 | ||
|
|
a315c8d4ef | ||
|
|
5ac9559b8f | ||
|
|
7b9e7d0a99 | ||
|
|
63b25928ff | ||
|
|
7d3b563759 | ||
|
|
baba20c028 | ||
|
|
87a9b8b02d | ||
|
|
cc41fa73cd | ||
|
|
ba923b9f91 | ||
|
|
cede47157e | ||
|
|
279c4fc86d | ||
|
|
c2f8d6ecee | ||
|
|
b4858efa73 | ||
|
|
6154932d9e | ||
|
|
d4cc85f644 | ||
|
|
82e8b785c4 | ||
|
|
5785e50cc5 | ||
|
|
de101348fc | ||
|
|
f442544912 | ||
|
|
d09dbcb697 | ||
|
|
f25bae1a08 | ||
|
|
800b60fb6d | ||
|
|
36558dfb85 | ||
|
|
c11bcd5d26 | ||
|
|
8450cc2d4d | ||
|
|
b1a9ee1403 | ||
|
|
80b9d9c8c8 | ||
|
|
db6665736a | ||
|
|
81980fa011 | ||
|
|
ac5f3eee96 | ||
|
|
9d87f7479c | ||
|
|
3639524fc7 | ||
|
|
f66114b22f | ||
|
|
8090de8031 | ||
|
|
081d663711 | ||
|
|
4fe16e5aa8 | ||
|
|
1fd5495570 | ||
|
|
9d68ee735a | ||
|
|
076e51c53a | ||
|
|
409a315a25 | ||
|
|
7a3264d43d | ||
|
|
effa320fa8 | ||
|
|
cc2a66367d | ||
|
|
989b5b010e | ||
|
|
f0e7ba94d2 | ||
|
|
3739fc8449 | ||
|
|
a74d191b30 | ||
|
|
a391cc88c9 | ||
|
|
12f8e63390 | ||
|
|
37238a1385 | ||
|
|
48cd044131 | ||
|
|
5665c4bf1e | ||
|
|
ad35d47ff5 | ||
|
|
f13cdd82df | ||
|
|
d6ebe10421 | ||
|
|
6e59e801b0 | ||
|
|
5dafd60299 | ||
|
|
825d7f1dd2 | ||
|
|
a3ea53c2f0 | ||
|
|
f37f34d803 | ||
|
|
bd0b11dc4a | ||
|
|
b36106cc8c | ||
|
|
07d35c4516 | ||
|
|
471cbacb66 | ||
|
|
229b181972 | ||
|
|
72615c7b98 | ||
|
|
45cafbee91 | ||
|
|
580651094f | ||
|
|
0ee92e98b2 | ||
|
|
3d8d9ee171 | ||
|
|
bde67dc7c3 | ||
|
|
a7be793608 | ||
|
|
ca81e19d14 | ||
|
|
7bd82fedce | ||
|
|
1aeb62d490 | ||
|
|
5886270e29 | ||
|
|
8f544d5c99 | ||
|
|
8dd8ec8930 | ||
|
|
3af82f7478 | ||
|
|
39a62818e9 | ||
|
|
24ff712306 | ||
|
|
ec6b30e220 | ||
|
|
548dbfcc9d | ||
|
|
09174abaa4 | ||
|
|
7ecb73ac6e | ||
|
|
8ade320440 | ||
|
|
bffa3fa2a3 | ||
|
|
4cf29c97b9 | ||
|
|
9377771531 | ||
|
|
0a96a87f94 | ||
|
|
5c14212429 | ||
|
|
e49d38657d | ||
|
|
1c7d1fc1ae | ||
|
|
7503d076e8 | ||
|
|
04f70aaa45 | ||
|
|
7b2b7b477f | ||
|
|
da7e25d339 | ||
|
|
b9f6e4d7aa | ||
|
|
396633932b | ||
|
|
46ad6ea9f3 | ||
|
|
1fa09ecac3 | ||
|
|
18b33ae10a | ||
|
|
e4e00184a5 | ||
|
|
9177765e8c | ||
|
|
0c0c62fe1b | ||
|
|
a0c9c5bab1 | ||
|
|
01b6e4ae72 | ||
|
|
2e36ee6e8b | ||
|
|
18cfa427d2 | ||
|
|
904a227adb | ||
|
|
23e532b770 | ||
|
|
f658e1f828 | ||
|
|
0500497fc1 | ||
|
|
ea691536ac | ||
|
|
c251a640b6 | ||
|
|
8031938b2f | ||
|
|
ca4bfdade4 | ||
|
|
b5f6f6d30f | ||
|
|
36107afbc2 | ||
|
|
539b1d2277 | ||
|
|
a9490da5a6 | ||
|
|
845e57d688 | ||
|
|
6835c84cc4 | ||
|
|
337e3ee2bf | ||
|
|
72d298ef2f | ||
|
|
d410164941 | ||
|
|
6b5ae7bce1 | ||
|
|
d80c4efb49 | ||
|
|
ddcf27b022 | ||
|
|
26c53e7455 | ||
|
|
0fbbe33e8a | ||
|
|
687508d26f | ||
|
|
f89729dbeb | ||
|
|
d90fee82d5 | ||
|
|
bde6ab4c7a | ||
|
|
2d0b97be44 | ||
|
|
3c22354130 | ||
|
|
ab555cd70e | ||
|
|
95bcaf3c5e | ||
|
|
48167530fd | ||
|
|
f909d90b6f | ||
|
|
e7d114a9d9 | ||
|
|
b83e0d603c | ||
|
|
7f57b2a4d3 | ||
|
|
c8f7e47181 | ||
|
|
e5d6b7cecd | ||
|
|
8a1868482c | ||
|
|
1bf68cea08 | ||
|
|
110e8300a1 | ||
| f3566361fb | |||
| a63a229b64 | |||
| 8944508308 | |||
| 775f24cffa | |||
|
|
78b72b0edc | ||
|
|
333fe79c8b | ||
|
|
9d10c8501a | ||
|
|
d64ece382a | ||
|
|
f91f821f86 | ||
|
|
b5301f948a | ||
|
|
dd589790c2 | ||
|
|
1fcc5ed70d | ||
|
|
41dbe81001 | ||
|
|
7b685ad99e | ||
| 2f1e1dc040 | |||
|
|
70e8a6877c | ||
|
|
87fd3121f9 | ||
|
|
4f9434163e | ||
|
|
350d452a7b | ||
| 9a0669df13 | |||
|
|
c0883fc2bc | ||
| 566bce4663 | |||
| c828718498 | |||
|
|
69c280b266 | ||
| 6794b01ac8 | |||
| 1cb586f55a | |||
|
|
df21879c0d | ||
|
|
30c9c86c93 | ||
|
|
2925d0f17b | ||
|
|
752dabd015 | ||
|
|
815f11d5bc | ||
| 02eb0e60b7 | |||
| a64ac93935 | |||
| 66a48d1c7e | |||
| 26c66f16b4 | |||
| 02e50bb2f9 | |||
| fadc62c8f0 | |||
| 4759f6f7ee | |||
| 14f2164a82 | |||
| 14ef1f9bad | |||
| dc99318ff0 | |||
| d2fc5f4d5c | |||
| 938bd48fff | |||
| 96f819dc91 | |||
| 25eee8adf5 | |||
| d2b2a29d3d | |||
| 1cf71261d1 | |||
| 88552eb04f | |||
| ab92c99321 | |||
| 02963de893 | |||
| 48550416d9 | |||
| 878c5ffd68 | |||
|
|
6e37fe93f7 | ||
|
|
72a2667549 | ||
|
|
39db7b4d26 | ||
| ff25c0ecb9 | |||
|
|
f1a93bffb5 | ||
| aa231d4f43 | |||
|
|
f254d57db4 | ||
| 106f835934 | |||
| f9b30a4cfd | |||
| 5e4a99529d | |||
| 4d585002d7 | |||
| b073fe3fdf | |||
| 312cc229d8 | |||
| 11b1d670d0 | |||
| 771f75ef08 | |||
|
|
edf9b2c82b | ||
| a88d3657bf |
9
.env.example
Normal file
9
.env.example
Normal file
@@ -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
|
||||
138
.gitea/workflows/check.yaml
Normal file
138
.gitea/workflows/check.yaml
Normal file
@@ -0,0 +1,138 @@
|
||||
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
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: npx tsc --noEmit
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
smoke-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint-and-typecheck
|
||||
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: Start MongoDB
|
||||
run: |
|
||||
docker run -d \
|
||||
--name mongodb-smoke-test \
|
||||
-p 27017:27017 \
|
||||
-e MONGO_INITDB_ROOT_USERNAME=admin \
|
||||
-e MONGO_INITDB_ROOT_PASSWORD=password \
|
||||
mongo:8.0.3
|
||||
|
||||
- name: Wait for MongoDB to be ready
|
||||
run: |
|
||||
timeout=30
|
||||
elapsed=0
|
||||
while ! docker exec mongodb-smoke-test mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "MongoDB не запустился за $timeout секунд"
|
||||
exit 1
|
||||
fi
|
||||
echo "Ожидание запуска MongoDB... ($elapsed/$timeout)"
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
echo "MongoDB готов"
|
||||
|
||||
- name: Start application
|
||||
env:
|
||||
MONGO_ADDR: mongodb://admin:password@localhost:27017/test_db?authSource=admin
|
||||
PORT: 8044
|
||||
NODE_ENV: test
|
||||
run: |
|
||||
npm start > app.log 2>&1 &
|
||||
echo $! > app.pid
|
||||
echo "Приложение запущено с PID: $(cat app.pid)"
|
||||
|
||||
- name: Wait for application to start
|
||||
run: |
|
||||
timeout=30
|
||||
elapsed=0
|
||||
while ! node -e "require('http').get('http://localhost:8044', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" 2>/dev/null; do
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "Приложение не запустилось за $timeout секунд"
|
||||
cat app.log
|
||||
exit 1
|
||||
fi
|
||||
echo "Ожидание запуска приложения... ($elapsed/$timeout)"
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
echo "Приложение запущено"
|
||||
|
||||
- name: Check application stability (30 seconds)
|
||||
run: |
|
||||
duration=30
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $duration ]; do
|
||||
if ! kill -0 $(cat app.pid) 2>/dev/null; then
|
||||
echo "❌ Приложение упало через $elapsed секунд"
|
||||
cat app.log
|
||||
exit 1
|
||||
fi
|
||||
if ! node -e "require('http').get('http://localhost:8044', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" 2>/dev/null; then
|
||||
echo "❌ Приложение не отвечает через $elapsed секунд"
|
||||
cat app.log
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Приложение работает ($elapsed/$duration секунд)"
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
done
|
||||
echo "✅ Приложение стабильно работает в течение $duration секунд"
|
||||
|
||||
- name: Stop application
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f app.pid ]; then
|
||||
pid=$(cat app.pid)
|
||||
if kill -0 $pid 2>/dev/null; then
|
||||
echo "Остановка приложения (PID: $pid)"
|
||||
kill -TERM $pid || true
|
||||
sleep 3
|
||||
if kill -0 $pid 2>/dev/null; then
|
||||
echo "Принудительная остановка приложения"
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
rm -f app.pid
|
||||
fi
|
||||
|
||||
- name: Stop MongoDB
|
||||
if: always()
|
||||
run: |
|
||||
docker stop mongodb-smoke-test || true
|
||||
docker rm mongodb-smoke-test || true
|
||||
34
Dockerfile
34
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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
volumes:
|
||||
ms_volume8:
|
||||
ms_logs:
|
||||
|
||||
services:
|
||||
mongoDb:
|
||||
image: mongo:8.0.3
|
||||
volumes:
|
||||
- ms_volume8:/data/db
|
||||
restart: always
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
multy-stubs:
|
||||
# build: .
|
||||
image: bro.js/ms/bh:$TAG
|
||||
restart: always
|
||||
volumes:
|
||||
- ms_logs:/usr/src/app/server/log
|
||||
ports:
|
||||
- 8044:8044
|
||||
environment:
|
||||
- TZ=Europe/Moscow
|
||||
- MONGO_ADDR=mongodb
|
||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
version: "3"
|
||||
|
||||
volumes:
|
||||
ms_volume8:
|
||||
ms_logs:
|
||||
|
||||
services:
|
||||
multy-stubs:
|
||||
image: bro.js/ms/bh:$TAG
|
||||
restart: always
|
||||
volumes:
|
||||
- ms_logs:/usr/src/app/server/log
|
||||
ports:
|
||||
- 8044:8044
|
||||
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
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
145
jest.config.js
145
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: [
|
||||
"<rootDir>/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\\\\",
|
||||
"<rootDir>/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: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
|
||||
3497
package-lock.json
generated
3497
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -1,15 +1,13 @@
|
||||
{
|
||||
"name": "multi-stub",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"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"
|
||||
@@ -23,9 +21,13 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
@@ -35,25 +37,33 @@
|
||||
"express": "5.0.1",
|
||||
"express-jwt": "^8.5.1",
|
||||
"express-session": "^1.18.1",
|
||||
"gigachat": "^0.0.16",
|
||||
"jsdom": "^25.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"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",
|
||||
"socket.io": "^4.8.1",
|
||||
"uuid": "^11.0.3"
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "22.10.2",
|
||||
"eslint": "^9.17.0",
|
||||
"globals": "^15.14.0",
|
||||
"jest": "^29.7.0",
|
||||
"mockingoose": "^2.16.2",
|
||||
"nodemon": "3.1.9",
|
||||
"supertest": "^7.0.0"
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node-dev": "2.0.0",
|
||||
"typescript": "5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
87
rules.md
Normal file
87
rules.md
Normal file
@@ -0,0 +1,87 @@
|
||||
## Правила оформления студенческих бэкендов в `multi-stub`
|
||||
|
||||
Этот документ описывает, как подключать новый студенческий бэкенд к общему серверу и как работать с JSON‑заглушками. Правила написаны так, чтобы их мог автоматически выполнять помощник Cursor.
|
||||
|
||||
### 1. Общая структура проекта студента
|
||||
|
||||
- **Размещение проекта**
|
||||
- Каждый студенческий бэкенд живёт в своей подпапке в `server/routers/<project-name>`.
|
||||
- В корне подпапки должен быть основной файл роутера `index.js` (или `index.ts`), который экспортирует `express.Router()`.
|
||||
- Подключение к общему серверу выполняется в `server/index.ts` через импорт и `app.use(<mountPath>, <router>)`.
|
||||
|
||||
- **Использование JSON‑заглушек**
|
||||
- Если проект переносится из фронтенд‑репозитория и должен только отдавать данные, то в подпапке проекта должна быть папка `json/` со всеми нужными `.json` файлами.
|
||||
- HTTP‑обработчики в роутере могут просто читать и возвращать содержимое этих файлов (например, через `require('./json/...')` или `import data from './json/...json'` с включённым `resolveJsonModule` / соответствующей конфигурацией bundler'а).
|
||||
|
||||
### 2. Правила для Cursor при указании директории заглушек
|
||||
|
||||
Когда пользователь явно указывает директорию с заглушками (например: `server/routers/<project-name>/json`), помощник Cursor должен последовательно выполнить следующие шаги.
|
||||
|
||||
- **2.1. Проверка валидности импортов JSON‑файлов**
|
||||
- Найти все `.js` / `.ts` файлы внутри подпапки проекта.
|
||||
- В каждом таком файле найти импорты/require, которые ссылаются на `.json` файлы (относительные пути вроде `'./json/.../file.json'`).
|
||||
- Для каждого такого импорта:
|
||||
- **Проверить, что файл реально существует** по указанному пути относительно файла-импортёра.
|
||||
- **Проверить расширение**: путь должен заканчиваться на `.json` (без опечаток).
|
||||
- **Проверить регистр и точное совпадение имени файла** (важно для кросс‑платформенности, даже если локально используется Windows).
|
||||
- Если найдены ошибки (файл не существует, опечатка в имени, неправильный относительный путь и т.п.):
|
||||
- Сформировать понятный список проблем: в каком файле, какая строка/импорт и что именно не так.
|
||||
- Предложить автоматически исправить пути (если по контексту можно однозначно угадать нужный `*.json` файл).
|
||||
|
||||
- **2.2. Проверка подключения основного роутера проекта**
|
||||
- Определить основной файл роутера проекта:
|
||||
- По умолчанию это `server/routers/<project-name>/index.js` (или `index.ts`).
|
||||
- Открыть `server/index.ts` и убедиться, что:
|
||||
- Есть импорт роутера из соответствующей подпапки, например:
|
||||
- `import <SomeUniqueName>Router from './routers/<project-name>'`
|
||||
- или `const <SomeUniqueName>Router = require('./routers/<project-name>')`
|
||||
- Имя переменной роутера **уникально** среди всех импортов роутеров (нет другого импорта с таким же именем).
|
||||
- Есть вызов `app.use('<mount-path>', <SomeUniqueName>Router)`:
|
||||
- `<mount-path>` должен быть осмысленным, совпадать с названием проекта или оговариваться пользователем.
|
||||
- Если импорт или `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/<project-name>`.
|
||||
- Основной роутер `index.js` / `index.ts`, экспортирующий `express.Router()`.
|
||||
- Подключение к общему серверу в `server/index.ts` (импорт + `app.use()` с уникальным именем роутера).
|
||||
|
||||
- **Если используются JSON‑заглушки**
|
||||
- Папка `json/` внутри проекта.
|
||||
- Все пути в импортирующих файлах должны указывать на реально существующие `*.json` файлы.
|
||||
- Не должно быть «магических» абсолютных путей; только относительные пути от файла до нужного JSON.
|
||||
|
||||
- **Если проект «оживлён»**
|
||||
- Папка `model/` с моделью(ями) данных (например, через `mongoose`).
|
||||
- Роуты, которые вместо прямой отдачи файлов работают с моделями и, при необходимости, с внешними сервисами.
|
||||
|
||||
Следуя этим правилам, можно подключать новые студенческие проекты в единый бэкенд, минимизировать типичные ошибки с путями к JSON и упростить автоматическое развитие заглушек до полноценного API.
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ exports[`todo list app get list 1`] = `
|
||||
{
|
||||
"body": [
|
||||
{
|
||||
"_id": "670f69b5796ce7a9069da2f7",
|
||||
"created": "2024-10-16T07:22:29.042Z",
|
||||
"id": "670f69b5796ce7a9069da2f7",
|
||||
"items": [],
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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 || 'Что-то пошло не так',
|
||||
})
|
||||
}
|
||||
28
server/error.ts
Normal file
28
server/error.ts
Normal file
@@ -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 || 'Что-то пошло не так',
|
||||
})
|
||||
}
|
||||
@@ -1,98 +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(require("./error"))
|
||||
|
||||
server.listen(config.port, () =>
|
||||
console.log(`Listening on http://localhost:${config.port}`)
|
||||
)
|
||||
157
server/index.ts
Normal file
157
server/index.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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 procurementRouter from './routers/procurement'
|
||||
import smokeTrackerRouter from './routers/smoke-tracker'
|
||||
import assessmentToolsRouter from './routers/assessment-tools'
|
||||
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 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('/procurement', procurementRouter)
|
||||
app.use('/smoke-tracker', smokeTrackerRouter)
|
||||
app.use('/assessment-tools', assessmentToolsRouter)
|
||||
app.use(errorHandler)
|
||||
|
||||
// Создаем обычный HTTP сервер
|
||||
const server = app.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)
|
||||
})
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
initServer().catch(console.error)
|
||||
13
server/io.js
13
server/io.js
@@ -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
|
||||
13
server/io.ts
Normal file
13
server/io.ts
Normal file
@@ -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
|
||||
16
server/models/ErrorLog.ts
Normal file
16
server/models/ErrorLog.ts
Normal file
@@ -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)
|
||||
55
server/models/questionnaire.ts
Normal file
55
server/models/questionnaire.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
// Типы вопросов
|
||||
export const QUESTION_TYPES = {
|
||||
SINGLE_CHOICE: 'single_choice', // Один вариант
|
||||
MULTIPLE_CHOICE: 'multiple_choice', // Несколько вариантов
|
||||
TEXT: 'text', // Текстовый ответ
|
||||
RATING: 'rating', // Оценка по шкале
|
||||
TAG_CLOUD: 'tag_cloud' // Облако тегов
|
||||
};
|
||||
|
||||
// Типы отображения
|
||||
export const DISPLAY_TYPES = {
|
||||
DEFAULT: 'default',
|
||||
TAG_CLOUD: 'tag_cloud',
|
||||
VOTING: 'voting',
|
||||
POLL: 'poll'
|
||||
};
|
||||
|
||||
// Схема варианта ответа
|
||||
const optionSchema = new mongoose.Schema({
|
||||
text: { type: String, required: true },
|
||||
count: { type: Number, default: 0 } // счетчик голосов
|
||||
});
|
||||
|
||||
// Схема вопроса
|
||||
const questionSchema = new mongoose.Schema({
|
||||
text: { type: String, required: true },
|
||||
type: {
|
||||
type: String,
|
||||
enum: Object.values(QUESTION_TYPES),
|
||||
required: true
|
||||
},
|
||||
options: [optionSchema],
|
||||
required: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
// Схема опроса
|
||||
const questionnaireSchema = new mongoose.Schema({
|
||||
title: { type: String, required: true },
|
||||
description: { type: String },
|
||||
questions: [questionSchema],
|
||||
displayType: {
|
||||
type: String,
|
||||
enum: Object.values(DISPLAY_TYPES),
|
||||
default: DISPLAY_TYPES.DEFAULT
|
||||
},
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
updatedAt: { type: Date, default: Date.now },
|
||||
adminLink: { type: String, required: true }, // ссылка для редактирования
|
||||
publicLink: { type: String, required: true } // ссылка для голосования
|
||||
});
|
||||
|
||||
export const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema);
|
||||
|
||||
@@ -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(`
|
||||
<h1>multy stub is working v${pkg.version}</h1>
|
||||
<ul>
|
||||
${folders.map((f) => `<li>${f}</li>`).join('')}
|
||||
</ul>
|
||||
|
||||
<h2>models</h2>
|
||||
<ul>${
|
||||
(await Promise.all(
|
||||
(await mongoose.modelNames()).map(async (name) => {
|
||||
const count = await mongoose.model(name).countDocuments()
|
||||
return `<li>${name} - ${count}</li>`
|
||||
}
|
||||
)
|
||||
)).map(t => t).join(' ')
|
||||
}</ul>
|
||||
`)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
18
server/routers/assessment-tools/index.js
Normal file
18
server/routers/assessment-tools/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
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;
|
||||
module.exports.default = router;
|
||||
50
server/routers/assessment-tools/models/Criteria.js
Normal file
50
server/routers/assessment-tools/models/Criteria.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const mongoose = require('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({
|
||||
eventId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Event',
|
||||
required: true
|
||||
},
|
||||
blockName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
criteriaType: {
|
||||
type: String,
|
||||
enum: ['team', 'participant', 'all'],
|
||||
default: 'all',
|
||||
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);
|
||||
|
||||
44
server/routers/assessment-tools/models/Event.js
Normal file
44
server/routers/assessment-tools/models/Event.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const mongoose = require('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);
|
||||
|
||||
43
server/routers/assessment-tools/models/Expert.js
Normal file
43
server/routers/assessment-tools/models/Expert.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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
|
||||
},
|
||||
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);
|
||||
|
||||
64
server/routers/assessment-tools/models/Rating.js
Normal file
64
server/routers/assessment-tools/models/Rating.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const mongoose = require('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({
|
||||
eventId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Event',
|
||||
required: true
|
||||
},
|
||||
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);
|
||||
|
||||
52
server/routers/assessment-tools/models/Team.js
Normal file
52
server/routers/assessment-tools/models/Team.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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'],
|
||||
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);
|
||||
|
||||
14
server/routers/assessment-tools/models/index.js
Normal file
14
server/routers/assessment-tools/models/index.js
Normal file
@@ -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
|
||||
};
|
||||
|
||||
152
server/routers/assessment-tools/routes/criteria.js
Normal file
152
server/routers/assessment-tools/routes/criteria.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const router = require('express').Router();
|
||||
const { Criteria } = require('../models');
|
||||
|
||||
// Критерии по умолчанию из hack.md
|
||||
const DEFAULT_CRITERIA = [
|
||||
{
|
||||
blockName: 'Оценка проекта команды',
|
||||
criteriaType: 'team',
|
||||
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
|
||||
},
|
||||
{
|
||||
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, 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) {
|
||||
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 { 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' });
|
||||
}
|
||||
|
||||
const criteriaBlock = await Criteria.create({
|
||||
eventId,
|
||||
blockName,
|
||||
criteriaType: criteriaType || 'all',
|
||||
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 {
|
||||
const { eventId } = req.body;
|
||||
|
||||
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) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/criteria/:id - редактировать блок
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const { blockName, criteriaType, 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 (criteriaType !== undefined) criteriaBlock.criteriaType = criteriaType;
|
||||
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;
|
||||
|
||||
108
server/routers/assessment-tools/routes/event.js
Normal file
108
server/routers/assessment-tools/routes/event.js
Normal file
@@ -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;
|
||||
|
||||
117
server/routers/assessment-tools/routes/experts.js
Normal file
117
server/routers/assessment-tools/routes/experts.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const router = require('express').Router();
|
||||
const { Expert } = require('../models');
|
||||
|
||||
// GET /api/experts - список экспертов
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 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 { eventId, fullName } = req.body;
|
||||
|
||||
if (!eventId || !fullName) {
|
||||
return res.status(400).json({ error: 'EventId and full name are required' });
|
||||
}
|
||||
|
||||
// Создаем нового эксперта
|
||||
const expert = new Expert({
|
||||
eventId,
|
||||
fullName
|
||||
});
|
||||
|
||||
// Сохраняем эксперта (токен генерируется в pre-save хуке)
|
||||
await expert.save();
|
||||
|
||||
// Формируем URL для QR кода ПОСЛЕ сохранения, когда токен уже сгенерирован
|
||||
// Приоритеты:
|
||||
// 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
|
||||
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;
|
||||
|
||||
240
server/routers/assessment-tools/routes/ratings.js
Normal file
240
server/routers/assessment-tools/routes/ratings.js
Normal file
@@ -0,0 +1,240 @@
|
||||
const router = require('express').Router();
|
||||
const { Rating, Team, Expert, Criteria } = require('../models');
|
||||
|
||||
// GET /api/ratings - получить все оценки (с фильтрами)
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
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')
|
||||
.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, 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('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 команды и топ-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 (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 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 });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/ratings - создать/обновить оценку эксперта
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const { eventId, expertId, teamId, ratings } = req.body;
|
||||
|
||||
if (!eventId || !expertId || !teamId || !ratings || !Array.isArray(ratings)) {
|
||||
return res.status(400).json({ error: 'EventId, 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({ eventId, expertId, teamId });
|
||||
|
||||
if (rating) {
|
||||
// Обновляем существующую оценку
|
||||
rating.ratings = ratings;
|
||||
await rating.save();
|
||||
} else {
|
||||
// Создаем новую оценку
|
||||
rating = await Rating.create({
|
||||
eventId,
|
||||
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;
|
||||
|
||||
194
server/routers/assessment-tools/routes/teams.js
Normal file
194
server/routers/assessment-tools/routes/teams.js
Normal file
@@ -0,0 +1,194 @@
|
||||
const router = require('express').Router();
|
||||
const { Team } = require('../models');
|
||||
|
||||
// GET /api/teams - список всех команд
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
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) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/teams/active/voting - получить активную для оценки команду (ДОЛЖЕН БЫТЬ ПЕРЕД /:id)
|
||||
router.get('/active/voting', async (req, res) => {
|
||||
try {
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
// 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(
|
||||
filter,
|
||||
{
|
||||
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 { eventId, type, name, projectName, caseDescription } = req.body;
|
||||
|
||||
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 || '',
|
||||
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 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';
|
||||
await previouslyActive.save();
|
||||
}
|
||||
|
||||
// Активируем выбранную команду
|
||||
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;
|
||||
@@ -0,0 +1,36 @@
|
||||
// Импортировать mongoose из общего модуля (подключение происходит автоматически)
|
||||
const mongoose = require('../../../utils/mongoose');
|
||||
const Event = require('../models/Event');
|
||||
|
||||
async function recreateTestUser() {
|
||||
try {
|
||||
// Проверяем подключение к MongoDB
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
console.log('Waiting for MongoDB connection...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
console.log('Event already exists:', event.name);
|
||||
}
|
||||
|
||||
console.log('Database initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
recreateTestUser();
|
||||
|
||||
2
server/routers/dogsitters-finder/const.js
Normal file
2
server/routers/dogsitters-finder/const.js
Normal file
@@ -0,0 +1,2 @@
|
||||
exports.DSF_AUTH_USER_MODEL_NAME = 'DSF_AUTH_USER'
|
||||
exports.DSF_INTERACTION_MODEL_NAME = 'DSF_INTERACTION'
|
||||
@@ -7,29 +7,29 @@ router.get("/users", (request, response) => {
|
||||
router.post("/auth", (request, response) => {
|
||||
const { phoneNumber, password } = request.body;
|
||||
console.log(phoneNumber, password);
|
||||
if (phoneNumber === "89999999999") {
|
||||
response.send(require("./json/auth/dogsitter.success.json"));
|
||||
} else if (phoneNumber === "89555555555") {
|
||||
response.status(400).send(require("./json/auth/error.json"));
|
||||
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
|
||||
response.send(require("./json/auth/success.json"));
|
||||
} else {
|
||||
response.send(require("./json/auth/owner.success.json"));
|
||||
response.status(401).send(require("./json/auth/error.json"));
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/auth/2fa", (request, response) => {
|
||||
const { code } = request.body;
|
||||
if (code === "0000") {
|
||||
response.send(require("./json/2fa/success.json"));
|
||||
const { phoneNumber, code } = request.body;
|
||||
if (code === "0000" && phoneNumber === "89999999999") {
|
||||
response.send(require("./json/2fa/dogsitter.success.json"));
|
||||
} else if (code === "0000" && phoneNumber === "89559999999") {
|
||||
response.send(require("./json/2fa/owner.success.json"));
|
||||
} else {
|
||||
response.status(400).send(require("./json/2fa/error.json"));
|
||||
response.status(401).send(require("./json/2fa/error.json"));
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/register", (request, response) => {
|
||||
const { firstName, secondName, phoneNumber, password, role } = request.body;
|
||||
console.log(phoneNumber, password, role);
|
||||
if (phoneNumber === "89283244141" || phoneNumber === "89872855893") {
|
||||
response.status(400).send(require("./json/register/error.json"));
|
||||
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
|
||||
response.status(401).send(require("./json/register/error.json"));
|
||||
} else if (role === "dogsitter") {
|
||||
response.send(require("./json/register/dogsitter.success.json"));
|
||||
} else {
|
||||
@@ -37,4 +37,192 @@ router.post("/register", (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
router.get("/auth/session", (request, response) => {
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
return response.status(401).json({ error: "Authorization header missing" });
|
||||
}
|
||||
|
||||
// Берём сам токен из заголовка
|
||||
const token = authHeader.split(" ")[1];
|
||||
|
||||
if (!token) {
|
||||
return response.status(401).json({ error: "Bearer token missing" });
|
||||
}
|
||||
|
||||
const jwt = require("jsonwebtoken");
|
||||
const secretKey = "secret";
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, secretKey);
|
||||
|
||||
if (decoded.role === "dogsitter") {
|
||||
response.send(require("./json/role/dogsitter.success.json"));
|
||||
} else {
|
||||
response.send(require("./json/role/owner.success.json"));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("token e:", e);
|
||||
return response.status(403).json({ error: "Invalid token" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Проверка взаимодействия между пользователем и догситтером
|
||||
router.get("/interactions/check", (req, res) => {
|
||||
const { owner_id, dogsitter_id } = req.query;
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
|
||||
const interactions = usersFile.interactions || [];
|
||||
|
||||
const exists = interactions.some(
|
||||
(interaction) =>
|
||||
interaction.owner_id === Number(owner_id) &&
|
||||
interaction.dogsitter_id === Number(dogsitter_id)
|
||||
);
|
||||
|
||||
res.json({ exists });
|
||||
});
|
||||
|
||||
// Добавление нового взаимодействия
|
||||
router.post("/interactions", (req, res) => {
|
||||
const { owner_id, dogsitter_id, interaction_type } = req.body;
|
||||
|
||||
if (!owner_id || !dogsitter_id || !interaction_type) {
|
||||
return res.status(400).json({ error: "Missing required fields" });
|
||||
}
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
|
||||
if (!usersFile.interactions) {
|
||||
usersFile.interactions = [];
|
||||
}
|
||||
|
||||
// Проверяем, существует ли уже такое взаимодействие
|
||||
const exists = usersFile.interactions.some(
|
||||
(interaction) =>
|
||||
interaction.owner_id === Number(owner_id) &&
|
||||
interaction.dogsitter_id === Number(dogsitter_id)
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
usersFile.interactions.push({
|
||||
owner_id: Number(owner_id),
|
||||
dogsitter_id: Number(dogsitter_id),
|
||||
interaction_type,
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
usersFilePath,
|
||||
JSON.stringify(usersFile, null, 2),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Добавлено взаимодействие: owner_id=${owner_id}, dogsitter_id=${dogsitter_id}`
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
router.get("/dogsitter-viewing", (req, res) => {
|
||||
const { id } = req.query;
|
||||
console.log(`Получен запрос для dogsitter с ID: ${id}`);
|
||||
|
||||
const usersFile = require("./json/users/users.json");
|
||||
const users = usersFile.data; // Извлекаем массив из свойства "data"
|
||||
|
||||
const user = users.find((user) => user.id === Number(id));
|
||||
|
||||
if (user) {
|
||||
res.json(user); // Возвращаем найденного пользователя
|
||||
} else {
|
||||
res.status(404).json({ error: "User not found" }); // Если пользователь не найден
|
||||
}
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
router.post('/dogsitter-viewing/rating/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { rating } = req.body;
|
||||
|
||||
if (!rating || rating < 1 || rating > 5) {
|
||||
return res.status(400).json({ error: 'Некорректная оценка' });
|
||||
}
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
const users = usersFile.data;
|
||||
|
||||
const userIndex = users.findIndex(user => user.id === Number(id));
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({ error: 'Догситтер не найден' });
|
||||
}
|
||||
|
||||
if (!users[userIndex].ratings) {
|
||||
users[userIndex].ratings = [];
|
||||
}
|
||||
users[userIndex].ratings.push(rating);
|
||||
|
||||
if (users[userIndex].ratings.length > 100) {
|
||||
users[userIndex].ratings.shift();
|
||||
}
|
||||
|
||||
const total = users[userIndex].ratings.reduce((sum, r) => sum + r, 0);
|
||||
users[userIndex].rating = parseFloat((total / users[userIndex].ratings.length).toFixed(2));
|
||||
|
||||
fs.writeFileSync(usersFilePath, JSON.stringify({ data: users }, null, 2), 'utf8');
|
||||
|
||||
console.log(`Обновлен рейтинг догситтера ${id}: ${users[userIndex].rating}`);
|
||||
|
||||
res.json({ rating: users[userIndex].rating, ratings: users[userIndex].ratings });
|
||||
});
|
||||
|
||||
|
||||
router.patch('/users/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
console.log('Полученные данные для обновления:', updateData);
|
||||
|
||||
|
||||
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||
|
||||
delete require.cache[require.resolve(usersFilePath)];
|
||||
const usersFile = require(usersFilePath);
|
||||
const users = usersFile.data;
|
||||
|
||||
const userIndex = users.findIndex((user) => user.id === Number(id));
|
||||
if (userIndex === -1) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
users[userIndex] = { ...users[userIndex], ...updateData };
|
||||
|
||||
fs.writeFileSync(
|
||||
usersFilePath,
|
||||
JSON.stringify({ data: users }, null, 2),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
console.log('Обновлённые данные пользователя:', users[userIndex]);
|
||||
|
||||
res.json(users[userIndex]);
|
||||
});
|
||||
|
||||
|
||||
module.exports = router
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.7q66wTNyLZp3TGFYF_JdU-yhlWViJulTxP_PCQzO4OI"
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Invalid code."
|
||||
"message": "Invalid code",
|
||||
"statusCode": 401
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.sI9839YXveTpEWhdpr5QbCYllt6hHYO7NsrQDcrXZIQ"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Two-factor authentication passed."
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 1,
|
||||
"phoneNumber": 89283244141,
|
||||
"firstName": "Вася",
|
||||
"secondName": "Пупкин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
||||
"price": 1500,
|
||||
"aboutMe": "Я люблю собак"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"error": "Пользователь не найден"
|
||||
}
|
||||
"message": "Неверный логин или пароль",
|
||||
"error": "Unauthorized",
|
||||
"statusCode": 401
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 3,
|
||||
"phoneNumber": 89872855893,
|
||||
"firstName": "Гадий",
|
||||
"secondName": "Петрович",
|
||||
"role": "owner"
|
||||
}
|
||||
}
|
||||
5
server/routers/dogsitters-finder/json/auth/success.json
Normal file
5
server/routers/dogsitters-finder/json/auth/success.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Первый фактор аутентификации пройден",
|
||||
"statusCode": 200
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 5,
|
||||
"phoneNumber": 89555555555,
|
||||
"firstName": "Масяня",
|
||||
"secondName": "Карлова",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
||||
"price": 100,
|
||||
"aboutMe": "Все на свете - собаки"
|
||||
}
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.T9V3-f3rD1deA5a2J-tYNw0cACEpzKHbhMPkc7gh8c0"
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"error": "Пользователь с таким номером телефона уже существует"
|
||||
"message": "Такой пользователь уже был зарегистрирован",
|
||||
"error": "Unauthorized",
|
||||
"statusCode": 401
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
{
|
||||
"data": {
|
||||
"id": 6,
|
||||
"phoneNumber": 89888888888,
|
||||
"firstName": "Генадий",
|
||||
"secondName": "Паровозов",
|
||||
"role": "owner"
|
||||
}
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Niwicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.qgOhk9tNcaMRbarRWISTgvGx5Eq_X8fcA5lhdVs2tQI"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": 1,
|
||||
"role": "dogsitter"
|
||||
}
|
||||
5
server/routers/dogsitters-finder/json/role/error.json
Normal file
5
server/routers/dogsitters-finder/json/role/error.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"message": "Неверный jwt token",
|
||||
"error": "Forbidden",
|
||||
"statusCode": 403
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": 3,
|
||||
"role": "owner"
|
||||
}
|
||||
@@ -1,39 +1,69 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"phone_number": 89283244141,
|
||||
"first_name": "Вася",
|
||||
"second_name": "Пупкин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
||||
"price": 1500,
|
||||
"about_me": "Я люблю собак"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"phone_number": 89272844541,
|
||||
"first_name": "Ваня",
|
||||
"second_name": "Пуськин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
|
||||
"price": 1000000,
|
||||
"about_me": "Я не люблю собак. И вообще я котоман."
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"phone_number": 89872855893,
|
||||
"first_name": "Гадий",
|
||||
"second_name": "Петрович",
|
||||
"role": "owner"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"phone_number": 89872844591,
|
||||
"first_name": "Галкин",
|
||||
"second_name": "Максим",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
|
||||
"price": 1000000,
|
||||
"about_me": "Миллион алых роз"
|
||||
}
|
||||
]
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"phone_number": "89999999999",
|
||||
"first_name": "Вася",
|
||||
"second_name": "Пупкин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, Пушкина, 12",
|
||||
"price": "1500",
|
||||
"about_me": "Я люблю собак!",
|
||||
"rating": 5,
|
||||
"ratings": [
|
||||
5,
|
||||
5
|
||||
],
|
||||
"tg": "jullllllie"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"phone_number": 89272844541,
|
||||
"first_name": "Ваня",
|
||||
"second_name": "Пуськин",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
|
||||
"price": 2000,
|
||||
"about_me": "Я не люблю собак. И вообще я котоман.",
|
||||
"rating": 4,
|
||||
"ratings": [
|
||||
4,
|
||||
4
|
||||
],
|
||||
"tg": "vanya006"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"phone_number": 89559999999,
|
||||
"first_name": "Гадий",
|
||||
"second_name": "Петрович",
|
||||
"role": "owner"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"phone_number": 89872844591,
|
||||
"first_name": "Галкин",
|
||||
"second_name": "Максим",
|
||||
"role": "dogsitter",
|
||||
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
|
||||
"price": 1750,
|
||||
"about_me": "Миллион алых роз",
|
||||
"rating": 4.5,
|
||||
"ratings": [
|
||||
4,
|
||||
5
|
||||
],
|
||||
"tg": "maks100500"
|
||||
}
|
||||
],
|
||||
"interactions": [
|
||||
{
|
||||
"owner_id": 3,
|
||||
"dogsitter_id": 4
|
||||
},
|
||||
{
|
||||
"owner_id": 1,
|
||||
"dogsitter_id": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
24
server/routers/dogsitters-finder/model/interaction.js
Normal file
24
server/routers/dogsitters-finder/model/interaction.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { Schema, model } = require("mongoose");
|
||||
|
||||
const { DSF_AUTH_USER_MODEL_NAME, DSF_INTERACTION_MODEL_NAME } = require("../../const");
|
||||
|
||||
const interactionSchema = new Schema({
|
||||
owner_id: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: DSF_AUTH_USER_MODEL_NAME,
|
||||
required: true
|
||||
},
|
||||
dogsitter_id: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: DSF_AUTH_USER_MODEL_NAME,
|
||||
required: true
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
interactionSchema.index({ owner_id: 1, dogsitter_id: 1 });
|
||||
|
||||
module.exports.Interaction = model(DSF_INTERACTION_MODEL_NAME, interactionSchema);
|
||||
83
server/routers/dogsitters-finder/model/user.js
Normal file
83
server/routers/dogsitters-finder/model/user.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const { Schema, model } = require("mongoose");
|
||||
|
||||
const { DSF_AUTH_USER_MODEL_NAME } = require("../../const");
|
||||
|
||||
const userSchema = new Schema({
|
||||
phone_number: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
match: /^\+?\d{10,15}$/
|
||||
},
|
||||
first_name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
second_name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ["dogsitter", "owner"],
|
||||
required: true
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
required: function() {
|
||||
return this.role === "dogsitter";
|
||||
}
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
required: function() {
|
||||
return this.role === "dogsitter";
|
||||
}
|
||||
},
|
||||
about_me: {
|
||||
type: String,
|
||||
maxlength: 500
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 5,
|
||||
default: 0
|
||||
},
|
||||
ratings: {
|
||||
type: [Number],
|
||||
default: [],
|
||||
validate: {
|
||||
validator: function(arr) {
|
||||
return arr.every(v => v >= 0 && v <= 5);
|
||||
},
|
||||
message: "Рейтинг должен быть в диапазоне от 0 до 5!"
|
||||
}
|
||||
},
|
||||
tg: {
|
||||
type: String,
|
||||
match: /^[a-zA-Z0-9_]{5,32}$/
|
||||
},
|
||||
created: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
userSchema.virtual("id").get(function() {
|
||||
return this._id.toHexString();
|
||||
});
|
||||
|
||||
userSchema.set("toJSON", {
|
||||
virtuals: true,
|
||||
versionKey: false,
|
||||
transform: function(doc, ret) {
|
||||
delete ret._id;
|
||||
delete ret.__v;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.User = model(DSF_AUTH_USER_MODEL_NAME, userSchema);
|
||||
149
server/routers/dogsitters-finder/routes.js
Normal file
149
server/routers/dogsitters-finder/routes.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const { Router } = require('express')
|
||||
const { expressjwt } = require('express-jwt')
|
||||
|
||||
const { getAnswer } = require('../../utils/common')
|
||||
const { User, Interaction } = require('./model')
|
||||
const { TOKEN_KEY } = require('./const')
|
||||
const { requiredValidate } = require('./utils')
|
||||
|
||||
const router = Router()
|
||||
|
||||
// Получение списка пользователей
|
||||
router.get('/users', async (req, res) => {
|
||||
|
||||
const users = await User.find()
|
||||
.select('-__v -ratings -phone_number')
|
||||
.lean()
|
||||
|
||||
console.log('get users successfull')
|
||||
|
||||
res.send(getAnswer(null, users))
|
||||
})
|
||||
|
||||
// Получение конкретного пользователя
|
||||
router.get('/dogsitter-viewing', async (req, res) => {
|
||||
const { userId } = req.params
|
||||
|
||||
const user = await User.findById(userId)
|
||||
.select('-__v -ratings')
|
||||
.lean()
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).send(getAnswer(new Error('Пользователь не найден')))
|
||||
}
|
||||
|
||||
res.send(getAnswer(null, user))
|
||||
})
|
||||
|
||||
router.use(expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }))
|
||||
|
||||
// Добавление оценки пользователю
|
||||
router.post('/dogsitter-viewing/rating', requiredValidate('value'), async (req, res) => {
|
||||
const { userId } = req.params
|
||||
const { value } = req.body
|
||||
const authUserId = req.auth.id
|
||||
|
||||
try {
|
||||
const user = await User.findById(userId)
|
||||
if (!user) throw new Error('Пользователь не найден')
|
||||
if (user.role !== 'dogsitter') throw new Error('Нельзя оценивать этого пользователя')
|
||||
if (user.id === authUserId) throw new Error('Нельзя оценивать самого себя')
|
||||
|
||||
user.ratings.push(Number(value))
|
||||
user.rating = user.ratings.reduce((a, b) => a + b, 0) / user.ratings.length
|
||||
|
||||
const updatedUser = await user.save()
|
||||
|
||||
res.send(getAnswer(null, {
|
||||
id: updatedUser.id,
|
||||
rating: updatedUser.rating.toFixed(1),
|
||||
totalRatings: updatedUser.ratings.length
|
||||
}))
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).send(getAnswer(error))
|
||||
}
|
||||
})
|
||||
|
||||
// Обновление информации пользователя
|
||||
router.patch('/users', async (req, res) => {
|
||||
const { userId } = req.params
|
||||
const updates = req.body
|
||||
|
||||
try {
|
||||
const user = await User.findByIdAndUpdate(userId, updates, { new: true })
|
||||
.select('-__v -ratings')
|
||||
|
||||
if (!user) throw new Error('Пользователь не найден')
|
||||
res.send(getAnswer(null, user))
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).send(getAnswer(error))
|
||||
}
|
||||
})
|
||||
|
||||
// Создание объекта взаимодействия
|
||||
router.post('/interactions',
|
||||
expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }),
|
||||
requiredValidate('dogsitter_id'),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { dogsitter_id } = req.body
|
||||
const owner_id = req.auth.id // ID из JWT токена
|
||||
|
||||
// Проверка существования пользователей
|
||||
const [owner, dogsitter] = await Promise.all([
|
||||
User.findById(owner_id),
|
||||
User.findById(dogsitter_id)
|
||||
])
|
||||
|
||||
if (!owner || owner.role !== 'owner') {
|
||||
throw new Error('Владелец не найден или имеет неверную роль')
|
||||
}
|
||||
|
||||
if (!dogsitter || dogsitter.role !== 'dogsitter') {
|
||||
throw new Error('Догситтер не найден или имеет неверную роль')
|
||||
}
|
||||
|
||||
// Создание взаимодействия
|
||||
const interaction = await Interaction.create({
|
||||
owner_id,
|
||||
dogsitter_id
|
||||
})
|
||||
|
||||
res.send(getAnswer(null, {
|
||||
id: interaction.id,
|
||||
timestamp: interaction.timestamp
|
||||
}))
|
||||
|
||||
} catch (error) {
|
||||
res.status(400).send(getAnswer(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
router.get('/interactions/check', async (req, res) => {
|
||||
const { owner_id, dogsitter_id } = req.query;
|
||||
|
||||
if (!owner_id || !dogsitter_id) {
|
||||
return res.status(400).send(getAnswer('Missing owner_id or dogsitter_id'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Поиск взаимодействий по owner_id и dogsitter_id
|
||||
const interactions = await Interaction.find({ owner_id, dogsitter_id })
|
||||
.select('-__v') // Выбираем только нужные поля
|
||||
.lean();
|
||||
|
||||
if (interactions.length === 0) {
|
||||
return res.status(404).send(getAnswer('No interactions found'));
|
||||
}
|
||||
|
||||
res.send(getAnswer(null, interactions));
|
||||
} catch (error) {
|
||||
console.error('Error checking interactions:', error);
|
||||
res.status(500).send(getAnswer('Internal Server Error'));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router
|
||||
@@ -1,111 +1,117 @@
|
||||
const router = require('express').Router()
|
||||
const {MasterModel} = require('./model/master')
|
||||
const mongoose = require("mongoose")
|
||||
const {OrderModel} = require("./model/order")
|
||||
const router = require("express").Router();
|
||||
const { MasterModel } = require("./model/master");
|
||||
const mongoose = require("mongoose");
|
||||
const { OrderModel } = require("./model/order");
|
||||
|
||||
router.post("/masters/list", async (req, res, next) => {
|
||||
try {
|
||||
const { startDate, endDate } = req.body;
|
||||
|
||||
router.get("/masters", async (req, res, next) => {
|
||||
try {
|
||||
const masters = await MasterModel.find({});
|
||||
const orders = await OrderModel.find({});
|
||||
|
||||
const mastersWithOrders = masters.map((master) => {
|
||||
const masterOrders = orders.filter((order) => {
|
||||
return (
|
||||
order?.master && order.master.toString() === master._id.toString()
|
||||
);
|
||||
});
|
||||
|
||||
const schedule = masterOrders.map((order) => ({
|
||||
id: order._id,
|
||||
startWashTime: order.startWashTime,
|
||||
endWashTime: order.endWashTime,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: master._id,
|
||||
name: master.name,
|
||||
schedule: schedule,
|
||||
phone: master.phone,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).send({ success: true, body: mastersWithOrders });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/masters/:id', async (req, res,next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)){
|
||||
throw new Error('ID is required')
|
||||
if (!startDate || !endDate) {
|
||||
throw new Error("Missing startDate or endDate");
|
||||
}
|
||||
|
||||
try {
|
||||
const master = await MasterModel.findByIdAndDelete(id, {
|
||||
new: true,
|
||||
});
|
||||
if (!master) {
|
||||
throw new Error('master not found')
|
||||
}
|
||||
res.status(200).send({success: true, body: master})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const masters = await MasterModel.find({});
|
||||
|
||||
const orders = await OrderModel.find({
|
||||
$or: [
|
||||
{ startWashTime: { $lt: end }, endWashTime: { $gt: start } }
|
||||
]
|
||||
});
|
||||
|
||||
router.post('/masters', async (req, res,next) => {
|
||||
|
||||
const {name, phone} = req.body
|
||||
|
||||
if (!name || !phone ){
|
||||
throw new Error('Enter name and phone')
|
||||
}
|
||||
try {
|
||||
const master = await MasterModel.create({name, phone})
|
||||
res.status(200).send({success: true, body: master})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
router.patch('/masters/:id', async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error('ID is required')
|
||||
}
|
||||
|
||||
const { name, phone } = req.body;
|
||||
|
||||
if (!name && !phone) {
|
||||
throw new Error('Enter name and phone')
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const updateData = {};
|
||||
if (name) updateData.name = name;
|
||||
if (phone) updateData.phone = phone;
|
||||
|
||||
const master = await MasterModel.findByIdAndUpdate(
|
||||
id,
|
||||
updateData,
|
||||
{ new: true }
|
||||
const mastersWithOrders = masters.map((master) => {
|
||||
const masterOrders = orders.filter((order) => {
|
||||
return (
|
||||
order?.master && order.master.toString() === master._id.toString()
|
||||
);
|
||||
});
|
||||
|
||||
if (!master) {
|
||||
throw new Error('master not found')
|
||||
}
|
||||
const schedule = masterOrders.map((order) => ({
|
||||
id: order._id,
|
||||
startWashTime: order.startWashTime,
|
||||
endWashTime: order.endWashTime,
|
||||
}));
|
||||
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
return {
|
||||
id: master._id,
|
||||
name: master.name,
|
||||
schedule: schedule,
|
||||
phone: master.phone,
|
||||
};
|
||||
});
|
||||
|
||||
res.status(200).send({ success: true, body: mastersWithOrders });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router
|
||||
router.delete("/masters/:id", async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error("ID is required");
|
||||
}
|
||||
|
||||
try {
|
||||
const master = await MasterModel.findByIdAndDelete(id, {
|
||||
new: true,
|
||||
});
|
||||
if (!master) {
|
||||
throw new Error("master not found");
|
||||
}
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/masters", async (req, res, next) => {
|
||||
const { name, phone } = req.body;
|
||||
|
||||
if (!name || !phone) {
|
||||
throw new Error("Enter name and phone");
|
||||
}
|
||||
try {
|
||||
const master = await MasterModel.create({ name, phone });
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.patch("/masters/:id", async (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error("ID is required");
|
||||
}
|
||||
|
||||
const { name, phone } = req.body;
|
||||
|
||||
if (!name && !phone) {
|
||||
throw new Error("Enter name and phone");
|
||||
}
|
||||
|
||||
try {
|
||||
const updateData = {};
|
||||
if (name) updateData.name = name;
|
||||
if (phone) updateData.phone = phone;
|
||||
|
||||
const master = await MasterModel.findByIdAndUpdate(id, updateData, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (!master) {
|
||||
throw new Error("master not found");
|
||||
}
|
||||
|
||||
res.status(200).send({ success: true, body: master });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
23
server/routers/dry-wash/get-token.js
Normal file
23
server/routers/dry-wash/get-token.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const getGigaToken = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||
const data = await response.json()
|
||||
return data.features['dry-wash-bh'].GIGA_TOKEN.value
|
||||
}
|
||||
|
||||
const getSystemPrompt = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||
const data = await response.json()
|
||||
return data.features['dry-wash-bh'].SYSTEM_PROMPT.value
|
||||
}
|
||||
|
||||
const getGigaChatModel = async () => {
|
||||
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||
const data = await response.json()
|
||||
return data.features['dry-wash-bh'].GIGA_CHAT_MODEL.value
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGigaToken,
|
||||
getSystemPrompt,
|
||||
getGigaChatModel
|
||||
}
|
||||
29
server/routers/dry-wash/model/order.car-img.js
Normal file
29
server/routers/dry-wash/model/order.car-img.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { Schema, model } = require('mongoose')
|
||||
|
||||
const schema = new Schema({
|
||||
image: String,
|
||||
imageRating: String,
|
||||
imageDescription: String,
|
||||
orderId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'dry-wash-order'
|
||||
},
|
||||
created: {
|
||||
type: Date,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
schema.set('toJSON', {
|
||||
virtuals: true,
|
||||
versionKey: false,
|
||||
transform(_doc, ret) {
|
||||
delete ret._id
|
||||
}
|
||||
})
|
||||
|
||||
schema.virtual('id').get(function () {
|
||||
return this._id.toHexString()
|
||||
})
|
||||
|
||||
exports.OrderCarImgModel = model('dry-wash-order-car-image', schema)
|
||||
@@ -15,7 +15,7 @@ const schema = new Schema({
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
carColor: String,
|
||||
carColor: Schema.Types.Mixed,
|
||||
startWashTime: {
|
||||
type: Date,
|
||||
required: true
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
const mongoose = require("mongoose")
|
||||
const router = require('express').Router()
|
||||
const multer = require('multer')
|
||||
const { MasterModel } = require('./model/master')
|
||||
const { OrderModel } = require('./model/order')
|
||||
const { OrderCarImgModel } = require('./model/order.car-img')
|
||||
const { orderStatus } = require('./model/const')
|
||||
const { getGigaToken, getSystemPrompt, getGigaChatModel } = require('./get-token')
|
||||
|
||||
const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value)
|
||||
const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value)
|
||||
const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100
|
||||
const isValidCarColor = (value) => value.length < 50 && /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
|
||||
const isValidCarColor = (value) => {
|
||||
if (typeof value === 'number') {
|
||||
return value >= 0 && value <= 7
|
||||
} else if (typeof value === 'string') {
|
||||
return /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value)
|
||||
|
||||
const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/
|
||||
@@ -26,6 +36,9 @@ const isValidLocation = (value) => {
|
||||
const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value)
|
||||
const isValidOrderNotes = (value) => value.length < 500
|
||||
|
||||
const allowedMimeTypes = ['image/jpeg', 'image/png']
|
||||
const sizeLimitInMegaBytes = 15
|
||||
|
||||
const VALIDATION_MESSAGES = {
|
||||
order: {
|
||||
notFound: 'Order not found'
|
||||
@@ -60,6 +73,13 @@ const VALIDATION_MESSAGES = {
|
||||
carColor: {
|
||||
invalid: 'Invalid car color'
|
||||
},
|
||||
carImg: {
|
||||
required: 'Car image file is required',
|
||||
invalid: {
|
||||
type: `Invalid car image file type. Allowed types: ${allowedMimeTypes}`,
|
||||
size: `Invalid car image file size. Limit is ${sizeLimitInMegaBytes}MB`
|
||||
}
|
||||
},
|
||||
washingBegin: {
|
||||
required: 'Begin time of washing is required',
|
||||
invalid: 'Invalid begin time of washing'
|
||||
@@ -143,17 +163,21 @@ router.post('/create', async (req, res, next) => {
|
||||
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
const { id } = req.params
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
||||
}
|
||||
|
||||
try {
|
||||
const order = await OrderModel.findById(id)
|
||||
|
||||
if (!order) {
|
||||
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
||||
}
|
||||
|
||||
res.status(200).send({ success: true, body: order })
|
||||
const imgProps = await OrderCarImgModel.findOne({ orderId: order.id })
|
||||
|
||||
res.status(200).send({ success: true, body: { ...order.toObject(), ...imgProps?.toObject() } })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
@@ -248,4 +272,191 @@ router.delete('/:id', async (req, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
const storage = multer.memoryStorage()
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: sizeLimitInMegaBytes * 1024 * 1024 },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true)
|
||||
} else {
|
||||
cb(new Error(VALIDATION_MESSAGES.carImg.invalid.type), false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
const axios = require('axios')
|
||||
|
||||
const GIGA_CHAT_OAUTH = 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth'
|
||||
const GIGA_CHAT_API = 'https://gigachat.devices.sberbank.ru/api/v1'
|
||||
|
||||
const getToken = async (req, res) => {
|
||||
const gigaToken = await getGigaToken()
|
||||
|
||||
const rqUID = uuidv4()
|
||||
const body = new URLSearchParams({
|
||||
scope: "GIGACHAT_API_PERS",
|
||||
})
|
||||
|
||||
const response = await fetch(GIGA_CHAT_OAUTH, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Basic ${gigaToken}`,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
RqUID: rqUID,
|
||||
},
|
||||
body,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
console.error("Ошибка запроса: ", errorData)
|
||||
return res.status(response.status).json(errorData)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
const uploadImage = async (file, accessToken) => {
|
||||
const formData = new FormData()
|
||||
const blob = new Blob([file.buffer], { type: file.mimetype })
|
||||
formData.append('file', blob, file.originalname)
|
||||
formData.append('purpose', 'general')
|
||||
|
||||
const config = {
|
||||
maxBodyLength: Infinity,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${GIGA_CHAT_API}/files`, formData, config)
|
||||
return response.data.id
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS_MAP = ['white', 'black', 'silver', 'gray', 'beige-brown', 'red', 'blue', 'green']
|
||||
|
||||
const getColorName = (colorKey) => {
|
||||
if (typeof colorKey === 'number' && COLORS_MAP[colorKey]) {
|
||||
return COLORS_MAP[colorKey]
|
||||
}
|
||||
return colorKey
|
||||
}
|
||||
|
||||
const analyzeImage = async (fileId, token, imgProps) => {
|
||||
const response = await fetch(`${GIGA_CHAT_API}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: (await getGigaChatModel()) ?? "GigaChat-Max",
|
||||
stream: false,
|
||||
update_interval: 0,
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: (await getSystemPrompt()) ?? `Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения.
|
||||
Тебе предоставляют фотографию, где явно выделяется одна машина (например, она расположена в центре и имеет больший размер в кадре по сравнению с остальными).
|
||||
ВАЖНО: Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля:
|
||||
{
|
||||
"value": число от 0 до 10 (целое или с одним знаком после запятой),
|
||||
"description": "текстовое описание на русском языке"
|
||||
}.
|
||||
Правила:
|
||||
1. Поле "value":
|
||||
- Должно быть числом от 0 до 10 (0 = машина абсолютно чистая, 10 = машина максимально грязная) ИЛИ undefined (если не удалось оценить);
|
||||
2. Поле "description":
|
||||
- Должно содержать 2-3 предложения на русском языке;
|
||||
- Обязательно указать конкретные признаки загрязнения;
|
||||
- Объяснить, почему выставлен именно такой балл.
|
||||
- Должно быть связано только с автомобилем.
|
||||
НЕ ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры. НЕ ИСПОЛЬЗУЙ markdown форматирование. ОТВЕТ ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON. Если на фотографии нельзя явно выделить одну машину, то ОЦЕНКА ДОЛЖНА ИМЕТЬ ЗНАЧЕНИЕ undefined и в описании должно быть указано, что по фотографии не удалось оценить степень загрязнения автомобиля, при этом НЕ ОПИСЫВАЙ НИЧЕГО ДРУГОГО КРОМЕ АВТОМОБИЛЯ`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке. Учти, что владелец указал, что исходный цвет машины: ${getColorName(imgProps.color)}`,
|
||||
attachments: [fileId],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
console.log(data)
|
||||
|
||||
try {
|
||||
return JSON.parse(data.choices[0].message.content)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { description: data.choices[0].message.content }
|
||||
}
|
||||
}
|
||||
|
||||
const convertFileToBase64 = (file) => {
|
||||
const base64Image = file.buffer.toString('base64')
|
||||
return `data:${file.mimetype};base64,${base64Image}`
|
||||
}
|
||||
|
||||
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
|
||||
|
||||
router.post('/:id/upload-car-img', upload.single('file'), async (req, res) => {
|
||||
const { id: orderId } = req.params
|
||||
if (!mongoose.Types.ObjectId.isValid(orderId)) {
|
||||
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
||||
}
|
||||
const order = await OrderModel.findById(orderId)
|
||||
if (!order) {
|
||||
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
throw new Error(VALIDATION_MESSAGES.carImg.required)
|
||||
}
|
||||
|
||||
try {
|
||||
await OrderCarImgModel.deleteMany({ orderId })
|
||||
|
||||
const { access_token } = await getToken(req, res)
|
||||
|
||||
const fileId = await uploadImage(req.file, access_token)
|
||||
const { value, description } = await analyzeImage(fileId, access_token, { carColor: order.carColor }) ?? {}
|
||||
|
||||
const orderCarImg = await OrderCarImgModel.create({
|
||||
image: convertFileToBase64(req.file),
|
||||
imageRating: value,
|
||||
imageDescription: description,
|
||||
orderId: order.id,
|
||||
created: new Date().toISOString(),
|
||||
})
|
||||
|
||||
res.status(200).send({ success: true, body: orderCarImg })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
router.use((err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
switch (err.message) {
|
||||
case 'File too large':
|
||||
return res.status(400).json({ success: false, error: VALIDATION_MESSAGES.carImg.invalid.size })
|
||||
default:
|
||||
return res.status(400).json({ success: false, error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(err.message)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -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;
|
||||
21
server/routers/edateam-legacy/index.ts
Normal file
21
server/routers/edateam-legacy/index.ts
Normal file
@@ -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;
|
||||
@@ -8,22 +8,44 @@ router.get("/update-like", (request, response) => {
|
||||
response.send(require("./json/gamepage/success.json"));
|
||||
});
|
||||
|
||||
router.get("/add-to-cart", (request, response) => {
|
||||
response.send(require("./json/home-page-data/games-in-cart.json"));
|
||||
});
|
||||
|
||||
router.get("/categories", (request, response) => {
|
||||
response.send(require("./json/home-page-data/all-games.json"));
|
||||
});
|
||||
|
||||
router.get("/shopping-cart", (request, response) => {
|
||||
response.send(require("./json/shopping-cart/success.json"));
|
||||
router.get("/favourites", (request, response) => {
|
||||
response.send(require("./json/home-page-data/all-games.json"));
|
||||
});
|
||||
|
||||
router.get("/home", (request, response) => {
|
||||
response.send(require("./json/home-page-data/success.json"));
|
||||
// router.get("/shopping-cart", (request, response) => {
|
||||
// response.send(require("./json/shopping-cart/success.json"));
|
||||
// });
|
||||
|
||||
router.get("/shopping-cart", (request, response) => {
|
||||
response.send(require("./json/home-page-data/games-in-cart.json"));
|
||||
});
|
||||
|
||||
// Добавляем поддержку разных ответов для /home
|
||||
router.get("/home", (req, res) => {
|
||||
if (stubs.home === "success") {
|
||||
res.send(require("./json/home-page-data/success.json"));
|
||||
} else if (stubs.home === "empty") {
|
||||
res.send({ data: [] }); // Отправляем пустой массив
|
||||
} else {
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/all-games", (request, response) => {
|
||||
response.send(require("./json/home-page-data/all-games.json"));
|
||||
});
|
||||
|
||||
const stubs = {
|
||||
home: "success",
|
||||
};
|
||||
|
||||
// // Маршрут для обновления лайков
|
||||
// router.post("/update-like", (request, response) => {
|
||||
@@ -38,7 +60,6 @@ router.get("/all-games", (request, response) => {
|
||||
// });
|
||||
// });
|
||||
|
||||
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
|
||||
@@ -49,7 +70,7 @@ const commentsFilePath = path.join(__dirname, "./json/gamepage/success.json");
|
||||
async function readComments() {
|
||||
const data = await fs.readFile(commentsFilePath, "utf-8");
|
||||
const parsedData = JSON.parse(data);
|
||||
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
|
||||
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
|
||||
return parsedData;
|
||||
}
|
||||
// Write to JSON file
|
||||
@@ -88,5 +109,149 @@ router.post("/update-like", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Путь к JSON-файлу с корзиной
|
||||
const cartFilePath = path.join(
|
||||
__dirname,
|
||||
"./json/home-page-data/games-in-cart.json"
|
||||
);
|
||||
|
||||
// Функция для чтения JSON-файла
|
||||
async function readCart() {
|
||||
const data = await fs.readFile(cartFilePath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
// Функция для записи в JSON-файл
|
||||
async function writeCart(data) {
|
||||
await fs.writeFile(cartFilePath, JSON.stringify(data, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
// Маршрут для добавления/удаления товара в корзине
|
||||
router.post("/add-to-cart", async (req, res) => {
|
||||
const { id, action } = req.body;
|
||||
|
||||
// Проверка наличия id и action
|
||||
if (id === undefined || action === undefined) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "Invalid id or action" });
|
||||
}
|
||||
|
||||
try {
|
||||
const cartData = await readCart();
|
||||
let ids = cartData.data.ids;
|
||||
|
||||
if (action === "add") {
|
||||
// Если action "add", добавляем товар, если его нет в корзине
|
||||
if (!ids?.includes(id)) {
|
||||
ids.push(id);
|
||||
}
|
||||
} else if (action === "remove") {
|
||||
// Если action "remove", удаляем товар, если он есть в корзине
|
||||
if (ids?.includes(id)) {
|
||||
ids = ids.filter((item) => item !== id);
|
||||
}
|
||||
} else {
|
||||
// Если action невалиден
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "Invalid action" });
|
||||
}
|
||||
|
||||
// Записываем обновленные данные обратно в файл
|
||||
cartData.data.ids = ids;
|
||||
await writeCart(cartData);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: "Cart updated successfully",
|
||||
data: cartData.data, // Возвращаем обновленные данные
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating cart:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
const createElement = (key, value, buttonTitle, basePath) => `
|
||||
<label>
|
||||
<input name="${key}" type="radio" ${
|
||||
stubs[key] === value ? "checked" : ""
|
||||
} onclick="fetch('${basePath}/admin/set/${key}/${value}')"/>
|
||||
${buttonTitle || value}
|
||||
</label>
|
||||
`;
|
||||
|
||||
router.get("/admin/home", (request, response) => {
|
||||
const basePath = request.baseUrl; // Получаем базовый путь маршрутизатора
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /home</legend>
|
||||
${createElement("home", "success", "Отдать успешный ответ", basePath)}
|
||||
${createElement("home", "empty", "Отдать пустой массив", basePath)}
|
||||
${createElement("home", "error", "Отдать ошибку", basePath)}
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/game-page", (request, response) => {
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /game-page</legend>
|
||||
${createElement(
|
||||
"game-page",
|
||||
"success",
|
||||
"Отдать успешный ответ"
|
||||
)}
|
||||
${createElement("game-page", "empty", "Отдать пустой массив")}
|
||||
${createElement("game-page", "error", "Отдать ошибку")}
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/categories", (request, response) => {
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /categories</legend>
|
||||
${createElement(
|
||||
"categories",
|
||||
"success",
|
||||
"Отдать успешный ответ"
|
||||
)}
|
||||
${createElement("categories", "empty", "Отдать пустой массив")}
|
||||
${createElement("categories", "error", "Отдать ошибку")}
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/favourites", (request, response) => {
|
||||
response.send(`
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Настройка данных для /favourites</legend>
|
||||
${createElement(
|
||||
"favourites",
|
||||
"success",
|
||||
"Отдать успешный ответ"
|
||||
)}
|
||||
${createElement("favourites", "empty", "Отдать пустой массив")}
|
||||
${createElement("favourites", "error", "Отдать ошибку")}
|
||||
</fieldset>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
router.get("/admin/set/:key/:value", (request, response) => {
|
||||
const { key, value } = request.params;
|
||||
stubs[key] = value;
|
||||
response.send("Настройки обновлены!");
|
||||
});
|
||||
@@ -5,28 +5,28 @@
|
||||
{
|
||||
"username": "Пользователь1",
|
||||
"text": "Текст комментария 1",
|
||||
"likes": 11,
|
||||
"likes": 13,
|
||||
"rating": 8,
|
||||
"date": "2025-03-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"username": "Пользователь2",
|
||||
"text": "Текст комментария 2",
|
||||
"likes": 7,
|
||||
"likes": 10,
|
||||
"rating": 7,
|
||||
"date": "2025-01-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"username": "Пользователь3",
|
||||
"text": "Текст комментария 3",
|
||||
"likes": 2,
|
||||
"likes": 4,
|
||||
"rating": 3,
|
||||
"date": "2025-02-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"username": "Пользователь4",
|
||||
"text": "Текст комментария 4",
|
||||
"likes": 15,
|
||||
"likes": 18,
|
||||
"rating": 2,
|
||||
"date": "2025-12-01T10:00:00Z"
|
||||
}
|
||||
|
||||
@@ -3,41 +3,43 @@
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Elden Ring",
|
||||
"image": "game17",
|
||||
"price": 3295,
|
||||
"old_price": 3599,
|
||||
"imgPath": "img_top_17",
|
||||
"description": "Крупномасштабная RPG, действие которой происходит в обширном открытом мире c богатой мифологией и множеством опасных врагов.",
|
||||
"category": "RPG"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "The Witcher 3: Wild Hunt",
|
||||
"image": "game1",
|
||||
"price": 990,
|
||||
"old_price": 1200,
|
||||
"os": "windows",
|
||||
"imgPath": "img_top_1",
|
||||
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
|
||||
"category": "RPG"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id": 17,
|
||||
"title": "Red Dead Redemption 2",
|
||||
"image": "game2",
|
||||
"price": 980,
|
||||
"old_price": 3800,
|
||||
"os": "windows",
|
||||
"imgPath": "img_top_2",
|
||||
"description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.",
|
||||
"category": "Adventures"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Forza Horizon 5",
|
||||
"image": "game3",
|
||||
"price": 1900,
|
||||
"os": "windows",
|
||||
"imgPath": "img_top_3",
|
||||
"description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.",
|
||||
"category": "Race"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
@@ -45,72 +47,66 @@
|
||||
"image": "game4",
|
||||
"price": 1200,
|
||||
"old_price": 2500,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_4",
|
||||
"description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.",
|
||||
"category": "Shooters"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Counter-Strike 2",
|
||||
"image": "game5",
|
||||
"price": 479,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_5",
|
||||
"description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.",
|
||||
"category": "Shooters"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Grand Theft Auto V",
|
||||
"image": "game6",
|
||||
"price": 700,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_6",
|
||||
"description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.",
|
||||
"category": "Adventures"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Assassin’s Creed IV: Black Flag",
|
||||
"image": "game7",
|
||||
"price": 1100,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_7",
|
||||
"description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.",
|
||||
"category": "Adventures"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Spider-Man",
|
||||
"image": "game8",
|
||||
"price": 3800,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_8",
|
||||
"description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.",
|
||||
"category": "Action"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "Assassin’s Creed Mirage",
|
||||
"image": "game9",
|
||||
"price": 1600,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_9",
|
||||
"description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.",
|
||||
"category": "Action"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
@@ -118,79 +114,72 @@
|
||||
"image": "game10",
|
||||
"price": 800,
|
||||
"old_price": 2200,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_10",
|
||||
"description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.",
|
||||
"category": "RPG"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "ARK: Survival Evolved",
|
||||
"image": "game11",
|
||||
"price": 790,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_11",
|
||||
"description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.",
|
||||
"category": "Simulators"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "FIFA 23",
|
||||
"image": "game12",
|
||||
"price": 3900,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_12",
|
||||
"description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.",
|
||||
"category": "Sports"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"title": "Dirt 5",
|
||||
"image": "game13",
|
||||
"price": 2300,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_13",
|
||||
"description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.",
|
||||
"category": "Race"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"title": "Cyberpunk 2077",
|
||||
"image": "game14",
|
||||
"price": 3400,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_14",
|
||||
"description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.",
|
||||
"category": "RPG"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Age of Empires IV",
|
||||
"image": "game15",
|
||||
"price": 3200,
|
||||
"os": "windows",
|
||||
|
||||
"imgPath": "img_top_15",
|
||||
"description": "Классическая стратегия в реальном времени с историческими кампаниями.",
|
||||
"category": "Strategies"
|
||||
,"fav1": "star1",
|
||||
"fav2": "star2"
|
||||
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"title": "Civilization VI",
|
||||
"image": "game16",
|
||||
"price": 4200,
|
||||
"os": "windows",
|
||||
"imgPath": "img_top_16",
|
||||
"description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.",
|
||||
"category": "Strategies"
|
||||
|
||||
@@ -105,23 +105,27 @@
|
||||
{
|
||||
"image": "news1",
|
||||
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
|
||||
"imgPath": "img_news_1"
|
||||
"imgPath": "img_news_1",
|
||||
"link": "https://gamemag.ru/news/185583/delta-force-hawk-ops-gameplay-showcase-havoc-warfare"
|
||||
},
|
||||
{
|
||||
"image": "news2",
|
||||
"text": "Первый трейлер Assassin’s Creed Shadows — с темнокожим самураем в феодальной Японии",
|
||||
"imgPath": "img_news_2"
|
||||
"imgPath": "img_news_2",
|
||||
"link": "https://stopgame.ru/newsdata/62686/pervyy_trailer_assassin_s_creed_shadows_s_temnokozhim_samuraem_v_feodalnoy_yaponii"
|
||||
},
|
||||
{
|
||||
"image": "news3",
|
||||
"text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony",
|
||||
"imgPath": "img_news_3"
|
||||
"imgPath": "img_news_3",
|
||||
"link": "https://stopgame.ru/newsdata/62706/prizrak_cusimy_vyshel_na_pk_i_uzhe_stavit_rekordy_dlya_sony"
|
||||
},
|
||||
{
|
||||
"image": "news4",
|
||||
"text": "Авторы Skull and Bones расширяют планы на второй сезо",
|
||||
"imgPath": "img_news_4"
|
||||
"text": "Авторы Skull and Bones расширяют планы на второй сезон",
|
||||
"imgPath": "img_news_4",
|
||||
"link": "https://stopgame.ru/newsdata/62711/avtory_skull_and_bones_rasshiryayut_plany_na_vtoroy_sezon"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1
server/routers/kfu-m-24-1/back-new/.env
Normal file
1
server/routers/kfu-m-24-1/back-new/.env
Normal file
@@ -0,0 +1 @@
|
||||
GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw==
|
||||
2
server/routers/kfu-m-24-1/back-new/.gitignore
vendored
Normal file
2
server/routers/kfu-m-24-1/back-new/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.env
|
||||
21
server/routers/kfu-m-24-1/back-new/README.md
Normal file
21
server/routers/kfu-m-24-1/back-new/README.md
Normal file
@@ -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` 生成图片(返回模拟图片链接)
|
||||
24
server/routers/kfu-m-24-1/back-new/app.js
Normal file
24
server/routers/kfu-m-24-1/back-new/app.js
Normal file
@@ -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;
|
||||
5
server/routers/kfu-m-24-1/back-new/features.config.js
Normal file
5
server/routers/kfu-m-24-1/back-new/features.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
auth: true,
|
||||
user: true,
|
||||
image: true, // 关闭为 false
|
||||
};
|
||||
@@ -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: {}
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = content.match(/<img src=\"(.*?)\"/);
|
||||
if (!match) {
|
||||
return res.status(500).json({ error: 'No image generated' });
|
||||
}
|
||||
const imageId = match[1];
|
||||
const imageResp = await axios.get(
|
||||
`https://gigachat.devices.sberbank.ru/api/v1/files/${imageId}/content`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'RqUID': uuidv4(),
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
);
|
||||
res.set('Content-Type', 'image/jpeg');
|
||||
res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' })));
|
||||
res.send(imageResp.data);
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
console.error('AI生成图片出错:');
|
||||
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);
|
||||
}
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const ctrl = require('./image.controller');
|
||||
|
||||
router.get('/prompt', ctrl.generate);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,12 @@
|
||||
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: {}
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
5455
server/routers/kfu-m-24-1/back-new/package-lock.json
generated
Normal file
5455
server/routers/kfu-m-24-1/back-new/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
server/routers/kfu-m-24-1/back-new/package.json
Normal file
21
server/routers/kfu-m-24-1/back-new/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
5
server/routers/kfu-m-24-1/back-new/server.js
Normal file
5
server/routers/kfu-m-24-1/back-new/server.js
Normal file
@@ -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}`);
|
||||
});
|
||||
8
server/routers/kfu-m-24-1/back-new/shared/hateoas.js
Normal file
8
server/routers/kfu-m-24-1/back-new/shared/hateoas.js
Normal file
@@ -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;
|
||||
20
server/routers/kfu-m-24-1/back-new/shared/usersDb.js
Normal file
20
server/routers/kfu-m-24-1/back-new/shared/usersDb.js
Normal file
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -607,8 +607,7 @@ function createGigachat(options = {}) {
|
||||
}
|
||||
var gigachat = createGigachat();
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
module.exports = {
|
||||
createGigachat,
|
||||
gigachat
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ router.use(async (req, res, next) => {
|
||||
process.env.GIGACHAT_ACCESS_TOKEN = json.access_token;
|
||||
process.env.GIGACHAT_EXPIRES_AT = json.expires_at;
|
||||
console.log(JSON.stringify(response.data));
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
113
server/routers/procurement/index.js
Normal file
113
server/routers/procurement/index.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
|
||||
// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts)
|
||||
const mongoose = require('../../utils/mongoose');
|
||||
|
||||
// Загрузить переменные окружения
|
||||
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');
|
||||
}
|
||||
|
||||
// Импортировать маршруты - прямые пути без path.join и __dirname
|
||||
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 requestsRoutes = require('./routes/requests');
|
||||
const homeRoutes = require('./routes/home');
|
||||
const activityRoutes = require('./routes/activity');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Проверить подключение к MongoDB (подключение происходит в server/utils/mongoose.ts)
|
||||
const dbConnected = mongoose.connection.readyState === 1;
|
||||
|
||||
// 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 uploadsRoot = 'server/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',
|
||||
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('/buy-products', buyProductsRoutes);
|
||||
app.use('/experience', experienceRoutes);
|
||||
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) => {
|
||||
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;
|
||||
42
server/routers/procurement/middleware/auth.js
Normal file
42
server/routers/procurement/middleware/auth.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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 ', '');
|
||||
|
||||
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.userId = decoded.userId;
|
||||
req.companyId = decoded.companyId;
|
||||
req.user = decoded;
|
||||
log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId);
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('[Auth] Token verification failed:', error.message);
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
|
||||
const generateToken = (userId, companyId, firstName = '', lastName = '', companyName = '') => {
|
||||
log('[Auth] Generating token for userId:', userId, 'companyId:', companyId);
|
||||
return jwt.sign(
|
||||
{ userId, companyId, firstName, lastName, companyName },
|
||||
process.env.JWT_SECRET || 'your-secret-key',
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = { verifyToken, generateToken };
|
||||
61
server/routers/procurement/models/Activity.js
Normal file
61
server/routers/procurement/models/Activity.js
Normal file
@@ -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);
|
||||
|
||||
43
server/routers/procurement/models/BuyDocument.js
Normal file
43
server/routers/procurement/models/BuyDocument.js
Normal file
@@ -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);
|
||||
|
||||
87
server/routers/procurement/models/BuyProduct.js
Normal file
87
server/routers/procurement/models/BuyProduct.js
Normal file
@@ -0,0 +1,87 @@
|
||||
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,
|
||||
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: [fileSchema],
|
||||
acceptedBy: [{
|
||||
companyId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Company'
|
||||
},
|
||||
acceptedAt: {
|
||||
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);
|
||||
76
server/routers/procurement/models/Company.js
Normal file
76
server/routers/procurement/models/Company.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const companySchema = new mongoose.Schema({
|
||||
fullName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
shortName: String,
|
||||
inn: {
|
||||
type: String,
|
||||
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],
|
||||
verified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
metrics: {
|
||||
type: {
|
||||
profileViews: { type: Number, default: 0 }
|
||||
},
|
||||
default: {}
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
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 });
|
||||
|
||||
module.exports = mongoose.model('Company', companySchema);
|
||||
46
server/routers/procurement/models/Experience.js
Normal file
46
server/routers/procurement/models/Experience.js
Normal file
@@ -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);
|
||||
|
||||
37
server/routers/procurement/models/Message.js
Normal file
37
server/routers/procurement/models/Message.js
Normal file
@@ -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);
|
||||
57
server/routers/procurement/models/Product.js
Normal file
57
server/routers/procurement/models/Product.js
Normal file
@@ -0,0 +1,57 @@
|
||||
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: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
price: String,
|
||||
unit: String,
|
||||
minOrder: String,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
index: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// Индекс для поиска
|
||||
productSchema.index({ companyId: 1, type: 1 });
|
||||
productSchema.index({ name: 'text', description: 'text' });
|
||||
|
||||
// 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);
|
||||
82
server/routers/procurement/models/Request.js
Normal file
82
server/routers/procurement/models/Request.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const requestSchema = new mongoose.Schema({
|
||||
senderCompanyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
recipientCompanyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
subject: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: ''
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
files: [{
|
||||
id: { type: String },
|
||||
name: { type: String },
|
||||
url: { type: String },
|
||||
type: { type: String },
|
||||
size: { type: Number },
|
||||
storagePath: { type: String },
|
||||
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
|
||||
},
|
||||
responseFiles: [{
|
||||
id: { type: String },
|
||||
name: { type: String },
|
||||
url: { type: String },
|
||||
type: { type: String },
|
||||
size: { type: Number },
|
||||
storagePath: { type: String },
|
||||
uploadedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
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 });
|
||||
requestSchema.index({ subject: 1, createdAt: -1 });
|
||||
|
||||
module.exports = mongoose.model('Request', requestSchema);
|
||||
58
server/routers/procurement/models/Review.js
Normal file
58
server/routers/procurement/models/Review.js
Normal file
@@ -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);
|
||||
73
server/routers/procurement/models/User.js
Normal file
73
server/routers/procurement/models/User.js
Normal file
@@ -0,0 +1,73 @@
|
||||
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
|
||||
}
|
||||
}, {
|
||||
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();
|
||||
|
||||
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);
|
||||
240
server/routers/procurement/routes/__tests__/buyProducts.test.js
Normal file
240
server/routers/procurement/routes/__tests__/buyProducts.test.js
Normal file
@@ -0,0 +1,240 @@
|
||||
const express = require('express')
|
||||
const mongoose = require('mongoose')
|
||||
const request = require('supertest')
|
||||
const { describe, it, beforeAll, expect } = require('@jest/globals')
|
||||
|
||||
// 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
101
server/routers/procurement/routes/activity.js
Normal file
101
server/routers/procurement/routes/activity.js
Normal file
@@ -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;
|
||||
|
||||
517
server/routers/procurement/routes/auth.js
Normal file
517
server/routers/procurement/routes/auth.js
Normal file
@@ -0,0 +1,517 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
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('../../../utils/mongoose');
|
||||
const { Types } = mongoose;
|
||||
|
||||
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 = '') => {
|
||||
if (process.env.DEV === 'true') {
|
||||
if (data) {
|
||||
console.log(message, data);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
// eslint-disable-next-line no-undef
|
||||
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 {
|
||||
await waitForDatabaseConnection();
|
||||
|
||||
let company = await Company.findById(PRESET_COMPANY_ID);
|
||||
if (!company) {
|
||||
company = await Company.create({
|
||||
_id: PRESET_COMPANY_ID,
|
||||
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: 'Качество и инновация'
|
||||
});
|
||||
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',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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: PRESET_COMPANY_ID
|
||||
});
|
||||
log('✅ Test user initialized');
|
||||
} 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) {
|
||||
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') {
|
||||
console.error('Failed to re-connect after auth error:', connectError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// Проверка обязательных полей
|
||||
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' });
|
||||
}
|
||||
|
||||
// Создать компанию
|
||||
let company;
|
||||
try {
|
||||
company = new Company({
|
||||
fullName,
|
||||
shortName: fullName.substring(0, 20),
|
||||
inn,
|
||||
ogrn,
|
||||
legalForm,
|
||||
industry,
|
||||
companySize,
|
||||
website,
|
||||
verified: false,
|
||||
rating: 0,
|
||||
description: '',
|
||||
slogan: '',
|
||||
partnerGeography: ['moscow', 'russia_all']
|
||||
});
|
||||
const savedCompany = await company.save();
|
||||
company = savedCompany;
|
||||
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 });
|
||||
}
|
||||
|
||||
// Создать пользователя
|
||||
try {
|
||||
const newUser = await User.create({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
position: position || '',
|
||||
phone: phone || '',
|
||||
companyId: company._id
|
||||
});
|
||||
|
||||
log('✅ User created:', newUser._id);
|
||||
|
||||
const token = generateToken(newUser._id.toString(), newUser.companyId.toString(), newUser.firstName, newUser.lastName, company.fullName);
|
||||
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 {
|
||||
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) {
|
||||
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 isMatch = await user.comparePassword(password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
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 = 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);
|
||||
|
||||
res.json({
|
||||
tokens: {
|
||||
accessToken: token,
|
||||
refreshToken: token
|
||||
},
|
||||
user: {
|
||||
id: user._id.toString(),
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
position: user.position,
|
||||
companyId: user.companyId.toString()
|
||||
},
|
||||
company: companyData ? {
|
||||
id: companyData._id.toString(),
|
||||
name: companyData.fullName,
|
||||
inn: companyData.inn
|
||||
} : null
|
||||
});
|
||||
} 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.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);
|
||||
}
|
||||
|
||||
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);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user