Compare commits

..

1 Commits

Author SHA1 Message Date
90f16928a1 fix: change get for orders
All checks were successful
platform/multy-stub/pipeline/head This commit looks good
2025-01-12 10:38:13 +03:00
1497 changed files with 1806 additions and 43853 deletions

Binary file not shown.

View File

@@ -1,9 +0,0 @@
# 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

View File

@@ -1,138 +0,0 @@
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

View File

@@ -1,38 +1,16 @@
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
FROM node:20
RUN mkdir -p /usr/src/app/server/log/
WORKDIR /usr/src/app/
# Копирование только package.json/package-lock.json для продакшн зависимостей
COPY ./server /usr/src/app/server
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 ci --production
# Копирование собранного приложения из билдера
COPY --from=builder /usr/src/app/dist /usr/src/app/dist
COPY --from=builder /usr/src/app/server /usr/src/app/server
# RUN npm i --omit=dev
RUN npm ci
EXPOSE 8044
CMD ["npm", "run", "up:prod"]

View File

@@ -1,12 +1,6 @@
#!/bin/sh
docker stop ms-mongo
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
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

25
docker-compose.yaml Normal file
View File

@@ -0,0 +1,25 @@
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

View File

@@ -1,30 +0,0 @@
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

View File

@@ -4,7 +4,7 @@ import pluginJs from "@eslint/js";
export default [
{ ignores: ['server/routers/old/*'] },
{ files: ["**/*.js"], languageOptions: { } },
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
{

View File

@@ -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,
// Остановить выполнение тестов после `n` неудач
// Stop running tests after `n` failures
// bail: 0,
// Директория, где Jest должен хранить кэшированную информацию о зависимостях
// The directory where Jest should store its cached dependency information
// 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,
// Массив glob-паттернов, указывающих набор файлов, для которых должна собираться информация о покрытии
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: [
"<rootDir>/server/routers/**/*.js"
],
// Директория, куда Jest должен выводить файлы покрытия
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// Массив строк regexp-паттернов, используемых для пропуска сбора покрытия
// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: [
"\\\\node_modules\\\\",
"<rootDir>/server/routers/old"
],
// Указывает, какой провайдер должен использоваться для инструментирования кода для покрытия
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// Список имен репортеров, которые Jest использует при записи отчетов о покрытии
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
@@ -45,159 +45,156 @@ const config = {
// "clover"
// ],
// Объект, который настраивает принудительное применение минимальных порогов для результатов покрытия
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// Путь к пользовательскому извлекателю зависимостей
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Заставить вызовы устаревших API выбрасывать полезные сообщения об ошибках
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Конфигурация по умолчанию для поддельных таймеров
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Принудительно собирать покрытие из игнорируемых файлов, используя массив glob-паттернов
// Force coverage collection from ignored files using an array of glob patterns
// 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: {},
// Максимальное количество воркеров, используемых для запуска тестов. Может быть указано в % или числом. Например, maxWorkers: 10% будет использовать 10% от количества CPU + 1 в качестве максимального числа воркеров. maxWorkers: 2 будет использовать максимум 2 воркера.
// 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: "50%",
// Массив имен директорий, которые должны быть рекурсивно найдены вверх от местоположения требуемого модуля
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// Массив расширений файлов, которые используют ваши модули
moduleFileExtensions: [
"js",
"mjs",
"cjs",
"jsx",
"ts",
"tsx",
"json",
"node"
],
// An array of file extensions your modules use
// 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: {},
// Массив строк regexp-паттернов, сопоставляемых со всеми путями модулей перед тем, как они будут считаться 'видимыми' для загрузчика модулей
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Активирует уведомления для результатов тестов
// Activates notifications for test results
// notify: false,
// Перечисление, которое указывает режим уведомлений. Требует { notify: true }
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// Пресет, который используется в качестве основы для конфигурации Jest
preset: 'ts-jest',
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Запускать тесты из одного или нескольких проектов
// Run tests from one or more projects
// projects: undefined,
// Используйте эту опцию конфигурации для добавления пользовательских репортеров в Jest
// Use this configuration option to add custom reporters to 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,
// Корневая директория, которую Jest должен сканировать для поиска тестов и модулей
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// Список путей к директориям, которые Jest должен использовать для поиска файлов
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Позволяет использовать пользовательский раннер вместо стандартного тестового раннера Jest
// Allows you to use a custom runner instead of Jest's default test runner
// 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,
// Список путей к модулям сериализаторов снимков, которые Jest должен использовать для тестирования снимков
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// Тестовое окружение, которое будет использоваться для тестирования
testEnvironment: "node",
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Опции, которые будут переданы в testEnvironment
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Добавляет поле местоположения к результатам тестов
// Adds a location field to test results
// testLocationInResults: false,
// Glob-паттерны, которые Jest использует для обнаружения тестовых файлов
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// Массив строк regexp-паттернов, которые сопоставляются со всеми тестовыми путями, сопоставленные тесты пропускаются
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Regexp-паттерн или массив паттернов, которые Jest использует для обнаружения тестовых файлов
// The regexp pattern or array of patterns that Jest uses to detect test files
// 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",
// Карта из регулярных выражений в пути к трансформерам
transform: {
'^.+\\.ts$': 'ts-jest',
'^.+\\.tsx$': 'ts-jest',
},
// A map from regular expressions to paths to transformers
// transform: undefined,
// Массив строк regexp-паттернов, которые сопоставляются со всеми путями исходных файлов, сопоставленные файлы будут пропускать трансформацию
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// Массив строк regexp-паттернов, которые сопоставляются со всеми модулями перед тем, как загрузчик модулей автоматически вернет мок для них
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Указывает, должен ли каждый отдельный тест сообщаться во время выполнения
// Indicates whether each individual test should be reported during the run
verbose: true,
// Массив regexp-паттернов, которые сопоставляются со всеми путями исходных файлов перед повторным запуском тестов в режиме наблюдения
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Использовать ли watchman для обхода файлов
// Whether to use watchman for file crawling
// watchman: true,
};

5138
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
{
"name": "multi-stub",
"version": "2.0.0",
"version": "1.0.1",
"description": "",
"main": "server/index.ts",
"type": "commonjs",
"main": "index.js",
"scripts": {
"start": "cross-env NODE_ENV=\"development\" ts-node-dev .",
"build": "tsc",
"up:prod": "node dist/server/index.js",
"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",
"eslint": "npx eslint ./server",
"eslint:fix": "npx eslint ./server --fix",
"test": "jest"
@@ -21,49 +23,34 @@
"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",
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.7",
"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",
"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.1",
"multer": "^1.4.5-lts.1",
"mongodb": "^6.12.0",
"mongoose": "^8.9.2",
"morgan": "^1.10.0",
"pbkdf2-password": "^1.2.1",
"rotating-file-stream": "^3.2.5",
"socket.io": "^4.8.1",
"zod": "^3.24.3"
"uuid": "^11.0.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",
"ts-jest": "^29.4.6",
"ts-node-dev": "2.0.0",
"typescript": "5.7.3"
"supertest": "^7.0.0"
}
}

View File

@@ -1,87 +0,0 @@
## Правила оформления студенческих бэкендов в `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.

View File

@@ -4,6 +4,7 @@ exports[`todo list app get list 1`] = `
{
"body": [
{
"_id": "670f69b5796ce7a9069da2f7",
"created": "2024-10-16T07:22:29.042Z",
"id": "670f69b5796ce7a9069da2f7",
"items": [],

View File

@@ -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('../routers/todo/model/todo/list')
const { ListModel } = require('../data/model/todo/list')
const todo = require('../routers/todo/routes')

2
server/data/const.js Normal file
View File

@@ -0,0 +1,2 @@
exports.TODO_LIST_MODEL_NAME = 'TODO_LIST'
exports.TODO_ITEM_MODEL_NAME = 'TODO_ITEM'

View File

@@ -0,0 +1,23 @@
const { Schema, model } = require('mongoose')
const { TODO_ITEM_MODEL_NAME } = require('../../const')
const schema = new Schema({
title: String,
done: { type: Boolean, default: false },
closed: Date,
created: {
type: Date, default: () => new Date().toISOString(),
},
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
})
schema.virtual('id').get(function () {
return this._id.toHexString()
})
exports.ItemModel = model(TODO_ITEM_MODEL_NAME, schema)

View File

@@ -1,22 +1,18 @@
const { Schema, model } = require('mongoose')
const { TODO_LIST_MODEL_NAME, TODO_ITEM_MODEL_NAME, TODO_AUTH_USER_MODEL_NAME } = require('../../const')
const { TODO_LIST_MODEL_NAME, TODO_ITEM_MODEL_NAME } = require('../../const')
const schema = new Schema({
title: String,
created: {
type: Date, default: () => new Date().toISOString(),
},
createdBy: { type: Schema.Types.ObjectId, ref: TODO_AUTH_USER_MODEL_NAME },
items: [{ type: Schema.Types.ObjectId, ref: TODO_ITEM_MODEL_NAME }],
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id
}
})
schema.virtual('id').get(function () {

13
server/error.js Normal file
View File

@@ -0,0 +1,13 @@
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 || 'Что-то пошло не так',
})
}

View File

@@ -1,28 +0,0 @@
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 || 'Что-то пошло не так',
})
}

95
server/index.js Normal file
View File

@@ -0,0 +1,95 @@
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("/todo", require("./routers/todo/routes"))
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(require("./error"))
server.listen(config.port, () =>
console.log(`Listening on http://localhost:${config.port}`)
)

View File

@@ -1,157 +0,0 @@
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 Normal file
View File

@@ -0,0 +1,13 @@
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

View File

@@ -1,13 +0,0 @@
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

View File

@@ -1,16 +0,0 @@
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)

View File

@@ -1,55 +0,0 @@
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);

33
server/root.js Normal file
View File

@@ -0,0 +1,33 @@
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

View File

@@ -1,18 +0,0 @@
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;

View File

@@ -1,50 +0,0 @@
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);

View File

@@ -1,44 +0,0 @@
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);

View File

@@ -1,43 +0,0 @@
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);

View File

@@ -1,64 +0,0 @@
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);

View File

@@ -1,52 +0,0 @@
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);

View File

@@ -1,14 +0,0 @@
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
};

View File

@@ -1,152 +0,0 @@
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;

View File

@@ -1,108 +0,0 @@
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;

View File

@@ -1,117 +0,0 @@
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;

View File

@@ -1,240 +0,0 @@
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;

View File

@@ -1,194 +0,0 @@
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;

View File

@@ -1,36 +0,0 @@
// Импортировать 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();

View File

@@ -1,8 +0,0 @@
const { Router } = require('express')
const router = Router()
router.get('/cities', (request, response) => {
response.send(require('./json/cities.json'))
})
module.exports = router

View File

@@ -1,85 +0,0 @@
{
"data": [
{
"id": 1,
"title": "Моска"
},
{
"id": 2,
"title": "Санкт-петербург"
},
{
"id": 3,
"title": "Новосибирска"
},
{
"id": 4,
"title": "Екатеринбург"
},
{
"id": 5,
"title": "Казань"
},
{
"id": 6,
"title": "Нижний новгород"
},
{
"id": 7,
"title": "Челябинск"
},
{
"id": 8,
"title": "Самара"
},
{
"id": 9,
"title": "Омск"
},
{
"id": 10,
"title": "Ростов-на-дону"
},
{
"id": 11,
"title": "Уфа"
},
{
"id": 12,
"title": "Красноярск"
},
{
"id": 13,
"title": "Пермь"
},
{
"id": 14,
"title": "Воронеж"
},
{
"id": 15,
"title": "Волгоград"
},
{
"id": 16,
"title": "Краснодар"
},
{
"id": 17,
"title": "Тюмень"
},
{
"id": 18,
"title": "Ижевск"
},
{
"id": 19,
"title": "Барнаул"
},
{
"id": 20,
"title": "Владивосток"
}
],
"count": 20
}

View File

@@ -1,2 +0,0 @@
exports.DSF_AUTH_USER_MODEL_NAME = 'DSF_AUTH_USER'
exports.DSF_INTERACTION_MODEL_NAME = 'DSF_INTERACTION'

View File

@@ -1,228 +1,35 @@
const router = require("express").Router();
const router = require('express').Router();
router.get("/users", (request, response) => {
response.send(require("./json/users/users.json"));
});
response.send(require("./json/users/users.json"))
})
router.post("/auth", (request, response) => {
const { phoneNumber, password } = request.body;
console.log(phoneNumber, password);
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
response.send(require("./json/auth/success.json"));
} else {
response.status(401).send(require("./json/auth/error.json"));
}
});
router.post("/auth/2fa", (request, response) => {
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(401).send(require("./json/2fa/error.json"));
}
});
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"))
}
else {
response.send(require("./json/auth/owner.success.json"))
}
})
router.post("/register", (request, response) => {
const { firstName, secondName, phoneNumber, password, role } = request.body;
console.log(phoneNumber, password, role);
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 {
response.send(require("./json/register/owner.success.json"));
}
});
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"));
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"))
}
} catch (e) {
console.log("token e:", e);
return response.status(403).json({ error: "Invalid token" });
}
});
else if (role === 'dogsitter') {
response.send(require("./json/register/dogsitter.success.json"))
}
else {
response.send(require("./json/register/owner.success.json"))
}
})
// Проверка взаимодействия между пользователем и догситтером
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
module.exports = router;

View File

@@ -1,3 +0,0 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.7q66wTNyLZp3TGFYF_JdU-yhlWViJulTxP_PCQzO4OI"
}

View File

@@ -1,5 +0,0 @@
{
"status": "error",
"message": "Invalid code",
"statusCode": 401
}

View File

@@ -1,3 +0,0 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.sI9839YXveTpEWhdpr5QbCYllt6hHYO7NsrQDcrXZIQ"
}

View File

@@ -0,0 +1,12 @@
{
"data": {
"id": 1,
"phoneNumber": 89283244141,
"firstName": "Вася",
"secondName": "Пупкин",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
"price": 1500,
"aboutMe": "Я люблю собак"
}
}

View File

@@ -1,5 +1,3 @@
{
"message": "Неверный логин или пароль",
"error": "Unauthorized",
"statusCode": 401
}
"error": "Пользователь не найден"
}

View File

@@ -0,0 +1,9 @@
{
"data": {
"id": 3,
"phoneNumber": 89872855893,
"firstName": "Гадий",
"secondName": "Петрович",
"role": "owner"
}
}

View File

@@ -1,5 +0,0 @@
{
"status": "success",
"message": "Первый фактор аутентификации пройден",
"statusCode": 200
}

View File

@@ -1,3 +1,12 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.T9V3-f3rD1deA5a2J-tYNw0cACEpzKHbhMPkc7gh8c0"
"data": {
"id": 5,
"phoneNumber": 89555555555,
"firstName": "Масяня",
"secondName": "Карлова",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
"price": 100,
"aboutMe": "Все на свете - собаки"
}
}

View File

@@ -1,5 +1,3 @@
{
"message": "Такой пользователь уже был зарегистрирован",
"error": "Unauthorized",
"statusCode": 401
"error": "Пользователь с таким номером телефона уже существует"
}

View File

@@ -1,3 +1,9 @@
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Niwicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.qgOhk9tNcaMRbarRWISTgvGx5Eq_X8fcA5lhdVs2tQI"
"data": {
"id": 6,
"phoneNumber": 89888888888,
"firstName": "Генадий",
"secondName": "Паровозов",
"role": "owner"
}
}

View File

@@ -1,4 +0,0 @@
{
"id": 1,
"role": "dogsitter"
}

View File

@@ -1,5 +0,0 @@
{
"message": "Неверный jwt token",
"error": "Forbidden",
"statusCode": 403
}

View File

@@ -1,4 +0,0 @@
{
"id": 3,
"role": "owner"
}

View File

@@ -1,69 +1,39 @@
{
"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
}
]
}
[
{
"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": "Миллион алых роз"
}
]

View File

@@ -1,24 +0,0 @@
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);

View File

@@ -1,83 +0,0 @@
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);

View File

@@ -1,149 +0,0 @@
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

View File

@@ -1,117 +1,87 @@
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")
router.post("/masters/list", async (req, res, next) => {
try {
const { startDate, endDate } = req.body;
if (!startDate || !endDate) {
throw new Error("Missing startDate or endDate");
router.get('/masters', async (req, res,next) => {
try {
const master = await MasterModel.find({})
res.status(200).send({success: true, body: master})
} 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')
}
const start = new Date(startDate);
const end = new Date(endDate);
const masters = await MasterModel.find({});
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 orders = await OrderModel.find({
$or: [
{ startWashTime: { $lt: end }, endWashTime: { $gt: start } }
]
});
const mastersWithOrders = masters.map((master) => {
const masterOrders = orders.filter((order) => {
return (
order?.master && order.master.toString() === master._id.toString()
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 schedule = masterOrders.map((order) => ({
id: order._id,
startWashTime: order.startWashTime,
endWashTime: order.endWashTime,
}));
if (!master) {
throw new Error('master not found')
}
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");
}
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);
}
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;
module.exports = router

View File

@@ -1,21 +1,9 @@
const router = require('express').Router()
const { OrderModel } = require('./model/order')
router.post('/orders', async (req, res, next) => {
const {startDate, endDate} = req.body
if (!startDate || !endDate) {
throw new Error('startDate and endDate are required')
}
const orders = await OrderModel.find({
$or: [
{startWashTime: { $gte: new Date(startDate), $lte: new Date(endDate) }},
{endWashTime: { $gte: new Date(startDate), $lte: new Date(endDate) }},
]
})
res.status(200).send({ success: true, body: orders })
router.post('/orders', (req, res) => {
res
.status(200)
.send(require(`./json/arm-orders/success.json`))
})
module.exports = router

View File

@@ -1,23 +0,0 @@
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
}

View File

@@ -1,12 +1,10 @@
const router = require('express').Router()
const armMasterRouter = require('./arm-master')
const armOrdersRouter = require('./arm-orders')
const orderRouter = require('./order')
router.use('/arm', armMasterRouter)
router.use('/arm', armOrdersRouter)
router.use('/order', orderRouter)
module.exports = router

View File

@@ -2,59 +2,24 @@
"success": true,
"body": [
{
"phone": "+79876543210",
"carNumber": "А123ВЕ16",
"carBody": 1,
"carColor": "#ffff00",
"startWashTime": "2025-05-12T08:21:00.000Z",
"endWashTime": "2025-05-12T08:22:00.000Z",
"location": "55.792799704829854,49.11034340707925 Республика Татарстан (Татарстан), Казань, улица Чернышевского",
"id": "order1",
"carNumber": "A123BC",
"startWashTime": "2024-11-24T10:30:00.000Z",
"endWashTime": "2024-11-24T16:30:00.000Z",
"orderDate": "2024-11-24T08:41:46.366Z",
"status": "progress",
"notes": "",
"created": "2025-01-18T17:43:21.488Z",
"updated": "2025-01-18T17:43:21.492Z"
"phone": "79001234563",
"location": "Казань, ул. Баумана, 1"
},
{
"phone": "89876543210",
"carNumber": "К456МН23",
"carBody": 2,
"carColor": "#ffffff",
"startWashTime": "2025-01-12T08:21:00Z",
"endWashTime": "2025-01-12T08:22:00Z",
"location": "55.808430668108585,49.198608125449255 Республика Татарстан (Татарстан), Казань, улица Академика Губкина, 50/1",
"status": "pending",
"notes": "заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки",
"created": "2025-01-18T17:46:10.388Z",
"updated": "2025-01-18T17:46:10.395Z",
"id": "678be8e211e62f4a61790cca"
},
{
"phone": "4098765432105",
"carNumber": "О789РС777",
"carBody": 3,
"carColor": "красный",
"startWashTime": "2025-08-12T08:21:00.000Z",
"endWashTime": "2025-08-12T08:22:00.000Z",
"location": "55.78720449830353,49.12111640202319 Республика Татарстан (Татарстан), Казань, улица Пушкина, 5/43",
"status": "cancelled",
"notes": "Заказ отменен по запросу самого клиента",
"created": "2025-01-18T17:47:46.294Z",
"updated": "2025-01-18T17:47:46.295Z",
"id": "678be8e211e62f4a61790ccb"
},
{
"phone": "+79876543210",
"carNumber": "Т123УХ716",
"carBody": 99,
"carColor": "чайная роза",
"startWashTime": "2025-01-11T11:21:00.000Z",
"endWashTime": "2025-01-12T11:22:00.000Z",
"location": "55.77063673480112,49.22182909159608 Республика Татарстан (Татарстан), Казань, Советский район, микрорайон Азино-2",
"id": "order2",
"carNumber": "A245BC",
"startWashTime": "2024-11-24T11:30:00.000Z",
"endWashTime": "2024-11-24T17:30:00.000Z",
"orderDate": "2024-11-24T07:40:46.366Z",
"status": "progress",
"notes": "Клиент остался доволен, предложить в следующий раз акцию",
"created": "2025-01-18T17:55:05.691Z",
"updated": "2025-01-18T17:55:05.695Z",
"id": "678be8e211e62f4a61790ccc"
"phone": "79001234567",
"location": "Казань, ул. Баумана, 43"
}
]
}

View File

@@ -1,11 +0,0 @@
const orderStatus = {
CANCELLED: 'cancelled',
PROGRESS: 'progress',
PENDING: 'pending',
WORKING: 'working',
COMPLETE: 'complete',
}
module.exports = {
orderStatus
}

View File

@@ -11,9 +11,6 @@ const schema = new Schema({
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform(_doc, ret) {
delete ret._id;
}
})
schema.virtual('id').get(function () {

View File

@@ -1,29 +0,0 @@
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)

View File

@@ -1,75 +1,26 @@
const { Schema, model } = require('mongoose')
const { orderStatus } = require('./const')
const { OrderNumberModel } = require('./order.number')
const schema = new Schema({
phone: {
type: String,
required: true
},
carNumber: {
type: String,
required: true
},
carBody: {
type: Number,
required: true
},
carColor: Schema.Types.Mixed,
startWashTime: {
type: Date,
required: true
},
endWashTime: {
type: Date,
required: true
},
location: {
type: String,
required: true
},
orderNumber: {
type: String,
unique: true
},
status: {
type: String,
required: true,
enum: Object.values(orderStatus)
},
master: {
type: Schema.Types.ObjectId,
ref: 'dry-wash-master'
},
notes: String,
startWashTime: {type: String, required: true},
endWashTime: {type: String, required: true},
orderDate: {type: String, required: true},
location: {type: String, required: true},
phone: {type: String, required: true},
status: {type: String, required: true},
carNumber: {type: String, required: true},
created: {
type: Date,
default: () => new Date().toISOString(),
type: Date, default: () => new Date().toISOString(),
},
updated: {
type: Date,
default: () => new Date().toISOString(),
type: Date, default: () => new Date().toISOString(),
},
})
schema.pre('save', async function (next) {
if (this.isNew) {
const counter = await OrderNumberModel.findOneAndUpdate(
{ _id: 'orderNumber' },
{ $inc: { sequenceValue: 1 } },
{ new: true, upsert: true }
)
this.orderNumber = counter.sequenceValue.toString()
}
next()
master: {type: Schema.Types.ObjectId, ref: 'dry-wash-master'},
notes: String,
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform(_doc, ret) {
delete ret._id
}
})
schema.virtual('id').get(function () {

View File

@@ -1,14 +0,0 @@
const { Schema, model } = require('mongoose')
const schema = new Schema({
_id: {
type: String,
required: true,
},
sequenceValue: {
type: Number,
default: 0
}
})
exports.OrderNumberModel = model('dry-wash-order-number', schema)

View File

@@ -1,462 +0,0 @@
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) => {
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})?)$/
const longitudeRe = /^(-?(?:1[0-7]|[1-9])?\d(?:\.\d{1,18})?|180(?:\.0{1,18})?)$/
const addressRe = /^[а-я0-9\s,.'-/()]*$/i
const isValidLocation = (value) => {
if (value.length > 200) {
return false
}
const [coordinates, address] = value.split(' ')
const [latitude, longitude] = coordinates.split(',')
return latitudeRe.test(latitude) && longitudeRe.test(longitude) && addressRe.test(address)
}
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'
},
orderId: {
invalid: 'Valid order ID is required',
},
orderStatus: {
invalid: 'Invalid order status'
},
orderNotes: {
invalid: 'Invalid order notes'
},
master: {
notFound: 'Master not found'
},
masterId: {
invalid: 'Invalid master ID',
},
phoneNumber: {
required: 'Phone number is required',
invalid: 'Invalid phone number'
},
carNumber: {
required: 'Car number is required',
invalid: 'Invalid car number'
},
carBody: {
required: 'Car body type is required',
invalid: 'Invalid car body type'
},
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'
},
washingEnd: {
required: 'End time of washing is required',
invalid: 'Invalid end time of washing'
},
washingLocation: {
required: 'Location of washing is required',
invalid: 'Invalid location of washing'
},
}
router.post('/create', async (req, res, next) => {
const bodyErrors = []
const { customer } = req.body
if (!customer.phone) {
bodyErrors.push(VALIDATION_MESSAGES.phoneNumber.required)
} else if (!isValidPhoneNumber(customer.phone)) {
bodyErrors.push(VALIDATION_MESSAGES.phoneNumber.invalid)
}
const { car } = req.body
if (!car.number) {
bodyErrors.push(VALIDATION_MESSAGES.carNumber.required)
} else if (!isValidCarNumber(car.number)) {
bodyErrors.push(VALIDATION_MESSAGES.carNumber.invalid)
}
if (!car.body) {
bodyErrors.push(VALIDATION_MESSAGES.carBody.required)
} else if (!isValidCarBodyType(car.body)) {
bodyErrors.push(VALIDATION_MESSAGES.carBody.invalid)
}
if (!isValidCarColor(car.color)) {
bodyErrors.push(VALIDATION_MESSAGES.carColor.invalid)
}
const { washing } = req.body
if (!washing.begin) {
bodyErrors.push(VALIDATION_MESSAGES.washingBegin.required)
} else if (!isValidISODate(washing.begin)) {
bodyErrors.push(VALIDATION_MESSAGES.washingBegin.invalid)
}
if (!washing.end) {
bodyErrors.push(VALIDATION_MESSAGES.washingEnd.required)
} else if (!isValidISODate(washing.end)) {
bodyErrors.push(VALIDATION_MESSAGES.washingEnd.invalid)
}
if (!washing.location) {
bodyErrors.push(VALIDATION_MESSAGES.washingLocation.required)
} else if (!isValidLocation(washing.location)) {
bodyErrors.push(VALIDATION_MESSAGES.washingLocation.invalid)
}
if (bodyErrors.length > 0) {
throw new Error(bodyErrors.join(', '))
}
try {
const order = await OrderModel.create({
phone: customer.phone,
carNumber: car.number,
carBody: car.body,
carColor: car.color,
startWashTime: washing.begin,
endWashTime: washing.end,
location: washing.location,
status: orderStatus.PROGRESS,
notes: '',
created: new Date().toISOString(),
})
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
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)
}
const imgProps = await OrderCarImgModel.findOne({ orderId: order.id })
res.status(200).send({ success: true, body: { ...order.toObject(), ...imgProps?.toObject() } })
} catch (error) {
next(error)
}
})
router.patch('/:id', async (req, res, next) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
const bodyErrors = []
const { status } = req.body
if (status) {
if (!isValidOrderStatus(status)) {
bodyErrors.push(VALIDATION_MESSAGES.orderStatus.invalid)
}
}
const { master: masterId } = req.body
if (masterId) {
if (!mongoose.Types.ObjectId.isValid(masterId)) {
bodyErrors.push(VALIDATION_MESSAGES.masterId.invalid)
} else {
try {
const master = await MasterModel.findById(masterId)
if (!master) {
bodyErrors.push(VALIDATION_MESSAGES.master.notFound)
}
} catch (error) {
next(error)
}
}
}
const { notes } = req.body
if (notes) {
if (!isValidOrderNotes(notes)) {
bodyErrors.push(VALIDATION_MESSAGES.orderNotes.invalid)
}
}
if (bodyErrors.length > 0) {
throw new Error(bodyErrors.join(', '))
}
try {
const updateData = {}
if (status) {
updateData.status = status
}
if (masterId) {
updateData.master = masterId
}
if (notes) {
updateData.notes = notes
}
updateData.updated = new Date().toISOString()
const order = await OrderModel.findByIdAndUpdate(
id,
updateData,
{ new: true }
)
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
router.delete('/: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.findByIdAndDelete(id, {
new: true,
})
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
res.status(200).send({ success: true, body: order })
} catch (error) {
next(error)
}
})
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

View File

@@ -0,0 +1,15 @@
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;

View File

@@ -1,21 +0,0 @@
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;

View File

@@ -1,12 +0,0 @@
const router = require("express").Router();
router.get('/game-links', (request, response) => {
response.send(require('./json/game-links/success.json'))
})
router.get('/4u2k-links', (request, response) => {
response.send(require('./json/4u2k-links/success.json'))
})
;
module.exports = router;

View File

@@ -1,31 +0,0 @@
{
"data":[
{
"type": "video",
"links": {
"l1": "https://www.youtube.com/embed/DsQMLrPdLf8?si=l9X57nHqaSYlxDFf",
"l2": "https://www.youtube.com/embed/Dk8AAU_UdVk?si=N8NdYMUCfawdsJGE",
"l3": "https://www.youtube.com/embed/HKfDfWrCwEA?si=qPugjiKR8V9eZ-yG",
"l4": "https://www.youtube.com/embed/tD-6xHAHrQ4?si=ZFe41gSK8d5gqahW"
}
},
{
"type": "podcast",
"links": {
"l1": "https://www.youtube.com/embed/RtVs87Nd1MQ?si=i4giUCtbp4Ouqv2W",
"l2": "https://www.youtube.com/embed/DfTU5LA_kw8?si=m7fI5Ie9yIGDFCrU",
"l3": "https://www.youtube.com/embed/Sp-1fX1Q15I?si=xyealVly9IBMW7Xi",
"l4": "https://www.youtube.com/embed/rLYFJYfluRs?si=MjW1beQ-Q9-TAehF"
}
},
{
"type": "entertainment",
"links": {
"l1": "https://www.youtube.com/embed/DiuuglRCchQ?si=8wTVXKbV-mbHuSjW",
"l2": "https://www.youtube.com/embed/zmZcIX5PEyo?si=Hbrv32kl0fqcmtV9",
"l3": "https://www.youtube.com/embed/Te-TZUjmzFQ?si=fNG16eruoFEY2KNq",
"l4": "https://www.youtube.com/embed/si-MQ5qg3zE?si=67mfO6gV80n1ULqo"
}
}
]
}

View File

@@ -1,20 +0,0 @@
{
"data":[
{
"title": "ABC",
"description": "Мой брат Колян сбацал про меня байку на англицком и несколько фишек кинул для шухера. Англицкий ты вроде знаешь, впряжешься за меня, а?",
"link": "https://www.oxfordonlineenglish.com/english-level-test/reading"
},
{
"title": "Алё, меня слышно?",
"description": "Мой кент на мобилу текст записал с иностранкой. Понимаешь, о чём тут говорят?",
"link": "https://test-english.com/listening/"
},
{
"title": "Анонимное тестирование",
"description": "Ты язык-то нормально знаешь? Проверься, никто угарать не будет",
"link": "https://www.ego4u.com/en/cram-up/tests"
}
]
}

View File

@@ -1,24 +0,0 @@
const Router = require('express').Router
const router = Router()
router.post('/auth/login', (req, res) => {
if (req.body.email === 'qwerty@mail.ru') {
res.status(200).send(require('./json/login/login-success.json'))
} else {
res.status(401).send(require('./json/login/login-error.json'));
}
})
router.post('/auth/register', (req, res) => {
res.status(400).send(require('./json/registration/registration-error.json'))
// res.status(201).send(require('./json/registration/registration-error.json'))
})
router.post('/auth/reset-password', (req, res) => {
res.status(200).send(require('./json/reset-password/reset-password-success.json'))
// res.status(404).send(require('./json/reset-password/reset-password-error.json'))
})
module.exports = router;

View File

@@ -1,11 +0,0 @@
{
"success": false,
"body": null,
"errors": [
{
"code": "AUTH_INVALID_CREDENTIALS",
"message": "Неверное имя пользователя или пароль"
}
],
"warnings": []
}

View File

@@ -1,8 +0,0 @@
{
"success": true,
"body": {
"token": "AUTH_TOKEN"
},
"errors": [],
"warnings": []
}

View File

@@ -1,11 +0,0 @@
{
"success": false,
"body": null,
"errors": [
{
"code": "REGISTRATION_EMAIL_TAKEN",
"message": "Электронная почта уже используется"
}
],
"warnings": []
}

View File

@@ -1,9 +0,0 @@
{
"success": true,
"body": {
"userId": "12345",
"token": "AUTH_TOKEN"
},
"errors": [],
"warnings": []
}

View File

@@ -1,11 +0,0 @@
{
"success": false,
"body": null,
"errors": [
{
"code": "RESET_PASSWORD_EMAIL_NOT_FOUND",
"message": "Адрес электронной почты не зарегистрирован"
}
],
"warnings": []
}

View File

@@ -1,8 +0,0 @@
{
"success": true,
"body": {
"message": "Отправлено электронное письмо для сброса пароля"
},
"errors": [],
"warnings": []
}

View File

@@ -1,7 +1,5 @@
const router = require('express').Router();
router.use('/performer', require('./dashboard-performer'))
router.use('/auth', require('./auth'))
router.use('/landing', require('./landing'))
module.exports = router;

View File

@@ -1,29 +0,0 @@
const Router = require('express').Router;
const router = Router()
const values = {
'blocks': 'success',
'application': 'success'
}
const timer = (_req, _res, next) => {
setTimeout(() => next(), 500)
}
router.use(timer)
router.get(
'/blocks',
(req, res) =>
res.send(require(`./json/blocks-${values['blocks']}.json`))
)
router.post(
'/application',
(req, res) => {
res.send(require(`./json/application-${values['application']}.json`))
}
)
module.exports = router

View File

@@ -1,7 +0,0 @@
{
"success": false,
"body": { },
"errors": [
"Что-то пошло не так"
]
}

View File

@@ -1,4 +0,0 @@
{
"success": true,
"body": { }
}

View File

@@ -1,9 +0,0 @@
{
"success": false,
"body": {
"blocks": []
},
"errors": [
"Что-то пошло не так"
]
}

View File

@@ -1,22 +0,0 @@
{
"success": true,
"body": {
"blocks": [
{
"titleKey":"block1.title",
"textKey":"block1.subtitle",
"imageName":"truck1"
},
{
"titleKey":"block2.title",
"textKey":"block2.subtitle",
"imageName":"truck2"
},
{
"titleKey":"block3.title",
"textKey":"block3.subtitle",
"imageName":"truck3"
}
]
}
}

View File

@@ -1,257 +0,0 @@
const router = require("express").Router();
router.get("/game-page", (request, response) => {
response.send(require("./json/gamepage/success.json"));
});
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("/favourites", (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("/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) => {
// const { username, likes } = request.body;
// // Эмулируем успешное обновление лайков
// console.log(`Лайки для пользователя ${username} обновлены до ${likes}`);
// response.status(200).json({
// success: true,
// message: `Лайки для пользователя ${username} обновлены до ${likes}`,
// });
// });
const fs = require("fs").promises;
const path = require("path");
// Path to JSON file
const commentsFilePath = path.join(__dirname, "./json/gamepage/success.json");
// Read JSON file
async function readComments() {
const data = await fs.readFile(commentsFilePath, "utf-8");
const parsedData = JSON.parse(data);
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
return parsedData;
}
// Write to JSON file
async function writeComments(data) {
await fs.writeFile(commentsFilePath, JSON.stringify(data, null, 2), "utf-8");
}
// Update likes route
router.post("/update-like", async (req, res) => {
const { username, likes } = req.body;
if (!username || likes === undefined) {
return res.status(400).json({ success: false, message: "Invalid input" });
}
try {
const data = await readComments();
const comment = data.data.comments.find((c) => c.username === username);
if (comment) {
comment.likes = likes;
await writeComments(data); // Сохраняем обновленные данные в файл
// Возвращаем актуализированные данные
res.status(200).json({
success: true,
message: "Likes updated successfully",
data: data.data, // Возвращаем актуализированные данные
});
} else {
res.status(404).json({ success: false, message: "Comment not found" });
}
} catch (error) {
console.error("Error updating likes:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
// Путь к 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("Настройки обновлены!");
});

View File

@@ -1,35 +0,0 @@
{
"success": true,
"data": {
"comments": [
{
"username": ользователь1",
"text": "Текст комментария 1",
"likes": 13,
"rating": 8,
"date": "2025-03-01T10:00:00Z"
},
{
"username": ользователь2",
"text": "Текст комментария 2",
"likes": 10,
"rating": 7,
"date": "2025-01-01T10:00:00Z"
},
{
"username": ользователь3",
"text": "Текст комментария 3",
"likes": 4,
"rating": 3,
"date": "2025-02-01T10:00:00Z"
},
{
"username": ользователь4",
"text": "Текст комментария 4",
"likes": 18,
"rating": 2,
"date": "2025-12-01T10:00:00Z"
}
]
}
}

View File

@@ -1,188 +0,0 @@
{
"success": true,
"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,
"imgPath": "img_top_1",
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
"category": "RPG"
},
{
"id": 17,
"title": "Red Dead Redemption 2",
"image": "game2",
"price": 980,
"old_price": 3800,
"imgPath": "img_top_2",
"description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.",
"category": "Adventures"
},
{
"id": 3,
"title": "Forza Horizon 5",
"image": "game3",
"price": 1900,
"imgPath": "img_top_3",
"description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.",
"category": "Race"
},
{
"id": 4,
"title": "Atomic Heart",
"image": "game4",
"price": 1200,
"old_price": 2500,
"imgPath": "img_top_4",
"description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.",
"category": "Shooters"
},
{
"id": 5,
"title": "Counter-Strike 2",
"image": "game5",
"price": 479,
"imgPath": "img_top_5",
"description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.",
"category": "Shooters"
},
{
"id": 6,
"title": "Grand Theft Auto V",
"image": "game6",
"price": 700,
"imgPath": "img_top_6",
"description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.",
"category": "Adventures"
},
{
"id": 7,
"title": "Assassins Creed IV: Black Flag",
"image": "game7",
"price": 1100,
"imgPath": "img_top_7",
"description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.",
"category": "Adventures"
},
{
"id": 8,
"title": "Spider-Man",
"image": "game8",
"price": 3800,
"imgPath": "img_top_8",
"description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.",
"category": "Action"
},
{
"id": 9,
"title": "Assassins Creed Mirage",
"image": "game9",
"price": 1600,
"imgPath": "img_top_9",
"description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.",
"category": "Action"
},
{
"id": 10,
"title": "Assassins Creed Valhalla",
"image": "game10",
"price": 800,
"old_price": 2200,
"imgPath": "img_top_10",
"description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.",
"category": "RPG"
},
{
"id": 11,
"title": "ARK: Survival Evolved",
"image": "game11",
"price": 790,
"imgPath": "img_top_11",
"description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.",
"category": "Simulators"
},
{
"id": 12,
"title": "FIFA 23",
"image": "game12",
"price": 3900,
"imgPath": "img_top_12",
"description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.",
"category": "Sports"
},
{
"id": 13,
"title": "Dirt 5",
"image": "game13",
"price": 2300,
"imgPath": "img_top_13",
"description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.",
"category": "Race"
},
{
"id": 14,
"title": "Cyberpunk 2077",
"image": "game14",
"price": 3400,
"imgPath": "img_top_14",
"description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.",
"category": "RPG"
},
{
"id": 15,
"title": "Age of Empires IV",
"image": "game15",
"price": 3200,
"imgPath": "img_top_15",
"description": "Классическая стратегия в реальном времени с историческими кампаниями.",
"category": "Strategies"
},
{
"id": 16,
"title": "Civilization VI",
"image": "game16",
"price": 4200,
"imgPath": "img_top_16",
"description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.",
"category": "Strategies"
}
]
}

View File

@@ -1,16 +0,0 @@
{
"success": true,
"data": {
"ids": [
3,
13,
1,
10,
4,
9,
15,
6,
7
]
}
}

View File

@@ -1,131 +0,0 @@
{
"success": true,
"data": {
"topSail": [
{
"id": 1,
"image": "game1",
"price": 1500,
"imgPath": "img_top_1"
},
{
"id": 2,
"image": "game2",
"price": 980,
"imgPath": "img_top_2"
},
{
"id": 3,
"image": "game3",
"price": 1900,
"imgPath": "img_top_3"
},
{
"id": 4,
"image": "game4",
"price": 1200,
"imgPath": "img_top_4"
},
{
"id": 5,
"image": "game5",
"price": 479,
"imgPath": "img_top_5"
},
{
"id": 6,
"image": "game6",
"price": 700,
"imgPath": "img_top_6"
},
{
"id": 7,
"image": "game7",
"price": 1100,
"imgPath": "img_top_7"
},
{
"id": 8,
"image": "game8",
"price": 3800,
"imgPath": "img_top_8"
}
],
"categories": [
{
"image": "category1",
"text": "гонки",
"imgPath": "img_categories_1",
"category": "Race"
},
{
"image": "category2",
"text": "глубокий сюжет",
"imgPath": "img_categories_2",
"category": ""
},
{
"image": "category3",
"text": "симуляторы",
"imgPath": "img_categories_3",
"category": "Simulators"
},
{
"image": "category4",
"text": "открытый мир",
"imgPath": "img_categories_4",
"category": "RPG"
},
{
"image": "category5",
"text": "экшен",
"imgPath": "img_categories_5",
"category": "Action"
},
{
"image": "category6",
"text": "стратегии",
"imgPath": "img_categories_6",
"category": "Strategies"
},
{
"image": "category7",
"text": "шутеры",
"imgPath": "img_categories_7",
"category": "Shooters"
},
{
"image": "category8",
"text": "приключения",
"imgPath": "img_categories_8",
"category": "Adventures"
}
],
"news": [
{
"image": "news1",
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
"imgPath": "img_news_1",
"link": "https://gamemag.ru/news/185583/delta-force-hawk-ops-gameplay-showcase-havoc-warfare"
},
{
"image": "news2",
"text": "Первый трейлер Assassins Creed Shadows — с темнокожим самураем в феодальной Японии",
"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",
"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",
"link": "https://stopgame.ru/newsdata/62711/avtory_skull_and_bones_rasshiryayut_plany_na_vtoroy_sezon"
}
]
}
}

View File

@@ -1,37 +0,0 @@
{
"success":true,
"data":
[
{
"id": 1,
"title": "Mortal Kombat 11",
"image": "mortal",
"alt": "Игра Мортал Комбат 11, картинка",
"releaseDate": "23 апр. 2019",
"description": "MORTAL KOMBAT 11 ULTIMATE ВКЛЮЧАЕТ В СЕБЯ БАЗОВУЮ ИГРУ МК11, КОМВАТ РАСК 1, ДОПОЛНЕНИЕ «ПОСЛЕДСТВИЯ» И НЕДАВНО ДОБАВЛЕННЫЙ НАБОР «КОМБАТ 2».",
"price": 300
},
{
"id": 2,
"title": "EA SPORTS™ FIFA 23",
"image": "fifa",
"alt": "Игра Фифа, картинка",
"releaseDate": "30 сен. 2022",
"description": "В FIFA 23 всемирная игра становится еще лучше с технологией HyperMotion2, мужским и женским FIFA World Cup™, женскими командами, кроссплатформенной игрой и множеством прочих возможностей.",
"price": 300
},
{
"id": 3,
"title": "Ведьмак: Дикая Охота",
"image": "ved",
"alt": "Игра Ведьмак, картинка",
"releaseDate": "18 мая 2015",
"description": "Вы — Геральт из Ривии, наемный убийца чудовищ. Вы путешествуете по миру, в котором бушует война и на каждом шагу подстерегают чудовища. Вам предстоит выполнить заказ и найти Цири — Дитя Предназначения, живое оружие, способное изменить облик этого мира.",
"price": 300
}
]
}

View File

@@ -1,3 +0,0 @@
exports.KAZAN_EXPLORE_RESULTS_MODEL_NAME = 'KAZAN_EXPLORE_RESULTS'
exports.TOKEN_KEY = "kazan-explore_top_secret_key_hbfhqf9jq9prg"

View File

@@ -1,310 +1,103 @@
const router = require('express').Router();
const { expressjwt } = require('express-jwt')
const axios = require('axios');
const jwt = require('jsonwebtoken')
const { ResultsModel } = require('./model/results')
const { TOKEN_KEY } = require('./const')
// First page
router.get('/getInfoAboutKazan', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require('./json/first/info-about-kazan/success.json');
const translatedData = data[lang] || data['ru'];
response.send(translatedData);
} catch (error) {
response.status(500).send({ message: 'Internal server error' });
}
});
router.get('/getServices', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/first/services/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
router.get('/getNews', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/first/news/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
// Sport page
router.get('/getFirstText', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require('./json/sport/first-text/success.json');
const translatedData = data[lang] || data['ru'];
response.send(translatedData);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
});
router.get('/getSecondText', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require('./json/sport/second-text/success.json');
const translatedData = data[lang] || data['ru'];
response.send(translatedData);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
});
router.get('/getSportData', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/sport/sport-list/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
router.get('/getSportQuiz', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/sport/quiz/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
// Places page
router.get('/getPlacesData', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/places/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
// Transport page
router.get('/getInfoAboutTransportPage', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require('./json/transport/info-about-page/success.json');
const translatedData = data[lang] || data['ru'];
response.send(translatedData);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
router.get('/getBus', (request, response) => {
response.send(require('./json/transport/bus-numbers/success.json'))
})
router.get('/getTral', (request, response) => {
response.send(require('./json/transport/tral-numbers/success.json'))
})
router.get('/getEvents', (request, response) => {
response.send(require('./json/transport/events-calendar/success.json'))
})
router.get('/getTripSchedule', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/transport/trip-schedule/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
// History page
router.get('/getHistoryText', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/history/text/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
router.get('/getHistoryList', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/history/list/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
// Education page
router.get('/getInfoAboutEducation', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require('./json/education/text/success.json');
const translatedData = data[lang] || data['ru'];
response.send(translatedData);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
router.get('/getEducationList', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require(`./json/education/cards/${lang}/success.json`);
response.send(data);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
router.get('/getInfoAboutKFU', (request, response) => {
const lang = request.query.lang || 'ru';
try {
const data = require('./json/education/kfu/success.json');
const translatedData = data[lang] || data['ru'];
response.send(translatedData);
} catch (error) {
response.status(404).send({ message: 'Language not found' });
}
})
// Login
router.post('/entrance', (request, response) => {
const { email, password } = request.body.entranceData;
try {
const users = require('./json/users-information/success.json');
const user = users.data.find(user => user.email === email && user.password === password);
if (!user) {
return response.status(401).send('Неверные учетные данные');
}
const responseObject = {
email: user.email,
}
return response.json(responseObject);
} catch (error) {
console.error('Ошибка чтения файла:', error);
response.status(500).send('Внутренняя ошибка сервера');
}
})
router.post('/registration', async (request, response) => {
const { email, password, confirmPassword } = request.body.registerData;
try {
if (password !== confirmPassword) {
return response.status(400).send('Пароли не совпадают!');
}
const users = require('./json/users-information/success.json');
const existingUser = users.data.find(user => user.email === email);
if (existingUser) {
return response.status(400).send('Пользователь с такой почтой уже существует!');
}
return response.json({ email: email });
} catch (error) {
console.error('Ошибка регистрации пользователя:', error);
response.status(500).send('Внутренняя ошибка сервера');
}
});
router.post('/signin', async (req, res) => {
const { user } = req.body
if (!user || !user.token) {
return res.status(404).json({error : "No user found"});
}
const valRes = await axios.get('https://antd-table-v2-backend.onrender.com/api/auth/check',
{
headers: {
'authorization': `Bearer ${user.token}`
}
}
)
if (valRes.status !== 200) {
return res.status(401).json({error : "User authorization error"});
}
const accessToken = jwt.sign({
...JSON.parse(JSON.stringify(user._id)),
}, TOKEN_KEY, {
expiresIn: '12h'
})
user.token = accessToken;
res.json(user)
})
router.use(
expressjwt({
secret: TOKEN_KEY,
algorithms: ['HS256'],
getToken: function fromHeaderOrQuerystring(req) {
if (req.headers.authorization && req.headers.authorization.split(" ")[0] === "Bearer")
return req.headers.authorization.split(" ")[1];
else if (req.query && req.query.token)
return req.query.token;
return null;
}
})
)
router.get('/getQuizResults/:userId', async (request, response) => {
const { userId } = request.params;
try {
const results = await ResultsModel.findOne({ userId: userId }).exec();
if (!results)
return response.status(404).send({ message: 'Quiz results not found' });
response.send(results.items);
} catch (error) {
response.status(500).send({ message: 'An error occurred while fetching quiz results' });
}
});
router.post('/addQuizResult', async (request, response) => {
const { userId, quizId, result } = request.body;
if (!userId || !quizId || !result)
return response.status(400).send({ message: 'Invalid input data' });
try {
let userResults = await ResultsModel.findOne({ userId: userId }).exec();
if (!userResults) {
userResults = new ResultsModel({ userId, items: [] });
}
const itemToOverride = userResults.items.find(item => item.quizId === quizId)
if (!itemToOverride) {
userResults.items.push({ quizId, result });
}
else {
itemToOverride.result = result;
}
await userResults.save();
response.status(200).send({ message: 'Quiz result added successfully' });
} catch (error) {
response.status(500).send({ message: 'An error occurred while adding quiz result' });
}
});
module.exports = router;
const router = require('express').Router();
//const loginMiddleware = require('../middleware/login-middleware');
// First page
router.get('/getInfoAboutKazan', (request, response) => {
response.send(require('./json/first/info-about-kazan/success.json'))
})
router.get('/getNews', (request, response) => {
response.send(require('./json/first/news/success.json'))
})
// Sport page
router.get('/getSportData', (request, response) => {
response.send(require('./json/sport/sport-data.json'))
})
// Places page
router.get('/getPlacesData', (request, response) => {
response.send(require('./json/places/places-data.json'))
})
// Transport page
router.get('/getInfoAboutTransportPage', (request, response) => {
response.send(require('./json/transport/info-about-page.json'))
})
router.get('/getBus', (request, response) => {
response.send(require('./json/transport/bus-numbers.json'))
})
router.get('/getTral', (request, response) => {
response.send(require('./json/transport/tral-numbers.json'))
})
router.get('/getEvents', (request, response) => {
response.send(require('./json/transport/events-calendar.json'))
})
router.get('/getTripSchedule', (request, response) => {
response.send(require('./json/transport/trip-schedule/success.json'))
})
router.get('/getInfoAboutInstitutions', (request, response) => {
response.send(require('./json/transport/info-about-institutions.json'))
})
// Education page
router.get('/getInfoAboutKFU', (request, response) => {
response.send(require('./json/education/info-about-kfu.json'))
})
// Login
router.post('/entrance', (request, response) => {
const { email, password } = request.body.entranceData;
try {
const users = require('../json/users-information/success.json');
const user = users.data.find(user => user.email === email && user.password === password);
if (!user) {
return response.status(401).send('Неверные учетные данные');
}
const responseObject = {
email: user.email,
}
if (user.cardId){
responseObject.cardId = user.cardId || "";
}
return response.json(responseObject);
} catch (error) {
console.error('Ошибка чтения файла:', error);
response.status(500).send('Внутренняя ошибка сервера');
}
})
router.post('/registration', async (request, response) => {
const { email, password, confirmPassword } = request.body.registerData;
try {
if (password !== confirmPassword) {
return response.status(400).send('Пароли не совпадают!');
}
const users = require('../json/users-information/success.json');
const existingUser = users.data.find(user => user.email === email);
if (existingUser) {
return response.status(400).send('Пользователь с такой почтой уже существует!');
}
return response.json({ email: email});
} catch (error) {
console.error('Ошибка регистрации пользователя:', error);
response.status(500).send('Внутренняя ошибка сервера');
}
});
module.exports = router;

View File

@@ -1,26 +0,0 @@
[
{
"id": "1",
"image": "img1",
"title": "Preschool and School Education",
"text": "Kazan has more than 300 preschool institutions that provide comprehensive development for children from an early age. School education in the city is of a high standard, as evidenced by the results of graduation exams and competitions. Many programs are implemented in Kazan to support talented students, including specialized schools and advanced subject studies."
},
{
"id": "2",
"image": "img2",
"title": "Secondary and Higher Education",
"text": "Kazan is home to prestigious secondary specialized educational institutions that train mid-level specialists for various industries. The city is a major educational hub with over 20 higher educational institutions, including some of Russia's leading universities. Kazan's universities offer a wide range of educational programs that meet modern labor market demands."
},
{
"id": "3",
"image": "img3",
"title": "Science",
"text": "Kazan is one of Russia's leading scientific centers, hosting numerous research institutes and academic institutions. The city organizes major scientific conferences and forums that attract scientists from around the world. Kazan researchers have achieved significant success in various fields, including chemistry, physics, medicine, and information technology."
},
{
"id": "4",
"image": "img4",
"title": "Innovations",
"text": "Kazan is a leader in innovations in Russia. The city is home to major tech companies and startups that develop and implement innovative solutions. Currently, Tatarstan hosts the largest industrial and manufacturing special economic zone in Russia, 'Alabuga,' 4 industrial parks, the 'Himgrad' technopolis, 14 technology parks, and an IT park."
}
]

View File

@@ -1,27 +0,0 @@
[
{
"id": "1",
"image": "img1",
"title": "Дошкольное и школьное образование",
"text": "В Казани насчитывается более 300 дошкольных учреждений, обеспечивающих всестороннее развитие детей с раннего возраста. Школьное образование в городе отличается высоким уровнем, о чем свидетельствуют результаты выпускных экзаменов и олимпиад. В Казани реализуется множество программ по поддержке талантливых школьников, включая специализированные школы и углубленное изучение предметов."
},
{
"id": "2",
"image": "img2",
"title": "Среднее и высшее образование",
"text": "В Казани расположены престижные средние специальные учебные заведения, готовящие специалистов среднего звена для различных отраслей. Город является крупным образовательным центром с более чем 20 высшими учебными заведениями, в том числе ведущими университетами России. Казанские вузы предлагают широкий спектр образовательных программ, отвечающих современным требованиям рынка труда."
},
{
"id": "3",
"image": "img3",
"title": "Наука",
"text": "Казань является одним из ведущих научных центров России, где сосредоточены многочисленные научно-исследовательские институты и академические учреждения. В городе проводятся крупные научные конференции и форумы, привлекающие ученых со всего мира. Казанские ученые добились значительных успехов в различных областях, включая химию, физику, медицину и информационные технологии."
},
{
"id": "4",
"image": "img4",
"title": "Инновации",
"text": "Казань является одним из лидеров в сфере инноваций в России. В городе работают крупные технологические компании и стартапы, разрабатывающие и внедряющие инновационные решения. В настоящее время в Татарстане действуют: крупнейшая в России особая экономическая зона промышленно-производственного типа «Алабуга», 4 индустриальных парка, технополис «Химград», 14 технопарков, IT-парк."
}
]

Some files were not shown because too many files have changed in this diff Show More