Compare commits

..

3 Commits

Author SHA1 Message Date
Elvira.Kulikova
7b9183fd6b fix bugs
Some checks failed
platform/multy-stub/pipeline/pr-master There was a failure building this commit
platform/multy-stub/pipeline/head There was a failure building this commit
2025-03-19 17:36:40 +03:00
Elvira.Kulikova
1a63f53fef Merge branch 'master' into freetracker_customer 2025-03-18 17:19:49 +03:00
Elvira.Kulikova
909662e92e trips
Some checks failed
ms-devops/pipeline/pr-master There was a failure building this commit
2025-02-07 19:24:13 +03:00
91 changed files with 1225 additions and 16749 deletions

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,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,
{

2837
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.2.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,13 +23,9 @@
"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",
@@ -37,20 +35,17 @@
"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",
"mongodb": "^6.12.0",
"mongoose": "^8.9.2",
"mongoose-sequence": "^6.0.1",
"morgan": "^1.10.1",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"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",
@@ -60,8 +55,6 @@
"jest": "^29.7.0",
"mockingoose": "^2.16.2",
"nodemon": "3.1.9",
"supertest": "^7.0.0",
"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.

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

99
server/index.js Normal file
View File

@@ -0,0 +1,99 @@
const express = require("express")
const bodyParser = require("body-parser")
const cookieParser = require("cookie-parser")
const session = require("express-session")
const morgan = require("morgan")
const path = require("path")
const rfs = require("rotating-file-stream")
const app = express()
require("dotenv").config()
exports.app = app
const accessLogStream = rfs.createStream("access.log", {
size: "10M",
interval: "1d",
compress: "gzip",
path: path.join(__dirname, "log"),
})
const errorLogStream = rfs.createStream("error.log", {
size: "10M",
interval: "1d",
compress: "gzip",
path: path.join(__dirname, "log"),
})
const config = require("../.serverrc")
const { setIo } = require("./io")
app.use(cookieParser())
app.use(
morgan("combined", {
stream: accessLogStream,
skip: function (req, res) {
return res.statusCode >= 400
},
})
)
// log all requests to access.log
app.use(
morgan("combined", {
stream: errorLogStream,
skip: function (req, res) {
console.log('statusCode', res.statusCode, res.statusCode <= 400)
return res.statusCode < 400
},
})
)
const server = setIo(app)
const sess = {
secret: "super-secret-key",
resave: true,
saveUninitialized: true,
cookie: {},
}
if (app.get("env") === "production") {
app.set("trust proxy", 1)
sess.cookie.secure = true
}
app.use(session(sess))
app.use(
bodyParser.json({
limit: "50mb",
})
)
app.use(
bodyParser.urlencoded({
limit: "50mb",
extended: true,
})
)
app.use(require("./root"))
/**
* Добавляйте сюда свои routers.
*/
app.use("/kfu-m-24-1", require("./routers/kfu-m-24-1"))
app.use("/epja-2024-1", require("./routers/epja-2024-1"))
app.use("/v1/todo", require("./routers/todo"))
app.use("/dogsitters-finder", require("./routers/dogsitters-finder"))
app.use("/kazan-explore", require("./routers/kazan-explore"))
app.use("/edateam", require("./routers/edateam-legacy"))
app.use("/dry-wash", require("./routers/dry-wash"))
app.use("/freetracker", require("./routers/freetracker"))
app.use("/dhs-testing", require("./routers/dhs-testing"))
app.use("/gamehub", require("./routers/gamehub"))
app.use("/esc", require("./routers/esc"))
app.use('/connectme', require('./routers/connectme'))
app.use('/questioneer', require('./routers/questioneer'))
app.use(require("./error"))
server.listen(config.port, () =>
console.log(`Listening on http://localhost:${config.port}`)
)

View File

@@ -1,155 +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 { 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(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,7 +1,7 @@
const mongoose = require('mongoose');
// Типы вопросов
export const QUESTION_TYPES = {
const QUESTION_TYPES = {
SINGLE_CHOICE: 'single_choice', // Один вариант
MULTIPLE_CHOICE: 'multiple_choice', // Несколько вариантов
TEXT: 'text', // Текстовый ответ
@@ -10,7 +10,7 @@ export const QUESTION_TYPES = {
};
// Типы отображения
export const DISPLAY_TYPES = {
const DISPLAY_TYPES = {
DEFAULT: 'default',
TAG_CLOUD: 'tag_cloud',
VOTING: 'voting',
@@ -51,5 +51,10 @@ const questionnaireSchema = new mongoose.Schema({
publicLink: { type: String, required: true } // ссылка для голосования
});
export const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema);
const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema);
module.exports = {
Questionnaire,
QUESTION_TYPES,
DISPLAY_TYPES
};

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

@@ -342,16 +342,7 @@ const uploadImage = async (file, accessToken) => {
}
}
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 analyzeImage = async (fileId, token) => {
const response = await fetch(`${GIGA_CHAT_API}/chat/completions`, {
method: "POST",
headers: {
@@ -366,26 +357,11 @@ const analyzeImage = async (fileId, token, imgProps) => {
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 и в описании должно быть указано, что по фотографии не удалось оценить степень загрязнения автомобиля, при этом НЕ ОПИСЫВАЙ НИЧЕГО ДРУГОГО КРОМЕ АВТОМОБИЛЯ`,
content: (await getSystemPrompt()) ?? `Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения. ВАЖНО: Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля: { "value": число от 0 до 10 (целое или с одним знаком после запятой), "description": "текстовое описание на русском языке" } Правила: 1. Поле "value": - Должно быть числом от 0 до 10 - 0 = машина абсолютно чистая - 10 = машина максимально грязная 2. Поле "description": - Должно содержать 2-3 предложения на русском языке - Обязательно указать конкретные признаки загрязнения - Объяснить почему выставлен именно такой балл НЕ ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры. НЕ ИСПОЛЬЗУЙ markdown форматирование. ОТВЕТ ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON. Если на фотографии нет одной машины, то оценка должна быть 0 и в описании должно быть указано, почему не удалось оценить.`,
},
{
role: "user",
content: `Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке. Учти, что владелец указал, что исходный цвет машины: ${getColorName(imgProps.color)}`,
content: 'Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке',
attachments: [fileId],
},
],
@@ -430,7 +406,7 @@ router.post('/:id/upload-car-img', upload.single('file'), async (req, res) => {
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 { value, description } = await analyzeImage(fileId, access_token) ?? {}
const orderCarImg = await OrderCarImgModel.create({
image: convertFileToBase64(req.file),

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

@@ -0,0 +1,22 @@
const Router = require('express').Router;
const router = Router()
const timer = (_req, _res, next) => {
setTimeout(() => next(), 500)
}
router.use(timer)
router.get(
'/trips',
(req, res) =>
res.send(require(`./json/trips-success.json`))
)
router.get('/cars/:id', (req, res) => {
res.send(require(`./json/cars-success.json`))
})
module.exports = router

View File

@@ -0,0 +1,28 @@
{
"success": true,
"errors": [],
"warnings":[],
"body": [
{
"id": 1,
"car": "Mersedes",
"driver": "Иванов Иван Иванович",
"telephone": "+7 9600376666",
"upgradeNum": "Челябинск"
},
{
"id": 2,
"car": "Mersedes",
"driver": "Иванов Иван Иванович",
"telephone": "+7 9600376666",
"upgradeNum": "Челябинск"
},
{
"id": 3,
"car": "Mersedes",
"driver": "Иванов Иван Иванович",
"telephone": "+7 9600376666",
"upgradeNum": "Челябинск"
}
]
}

View File

@@ -0,0 +1,807 @@
{
"success": true,
"errors": [],
"warnings":[],
"body": [
{
"key": 0,
"trip": "Владисвосток-Москва",
"count": 99,
"date_start": "Mon, 29 Jun 2020 14:13:16 GMT",
"date_end": "Wed, 15 Jul 2020 09:45:23 GMT",
"created_at": "Thu, 01 Dec 2022 15:55:14 GMT"
},
{
"key": 1,
"trip": "Казань-Москва",
"count": 85,
"date_start": "Mon, 17 Oct 2022 09:46:21 GMT",
"date_end": "Fri, 28 Oct 2022 16:30:12 GMT",
"created_at": "Wed, 14 Jun 2023 03:06:52 GMT"
},
{
"key": 2,
"trip": "Казань-Сызрань",
"count": 48,
"date_start": "Thu, 01 Sep 2022 04:28:45 GMT",
"date_end": "Mon, 12 Sep 2022 11:15:33 GMT",
"created_at": "Mon, 07 Oct 2024 01:37:16 GMT"
},
{
"key": 3,
"trip": "Владивосток-Владимир",
"count": 72,
"date_start": "Wed, 26 Jun 2024 16:22:43 GMT",
"date_end": "Tue, 09 Jul 2024 13:45:28 GMT",
"created_at": "Sun, 11 Dec 2022 16:12:07 GMT"
},
{
"key": 4,
"trip": "Владивосток-Вологда",
"count": 29,
"date_start": "Mon, 29 Jun 2020 17:37:33 GMT",
"date_end": "Thu, 16 Jul 2020 08:22:15 GMT",
"created_at": "Thu, 26 Mar 2020 01:18:31 GMT"
},
{
"key": 5,
"trip": "Москва-Вологда",
"count": 78,
"date_start": "Fri, 05 May 2023 03:42:20 GMT",
"date_end": "Wed, 17 May 2023 19:30:45 GMT",
"created_at": "Tue, 21 Sep 2021 13:54:18 GMT"
},
{
"key": 6,
"trip": "Казань-Вологда",
"count": 36,
"date_start": "Sun, 02 Oct 2022 14:35:34 GMT",
"date_end": "Sat, 15 Oct 2022 10:20:18 GMT",
"created_at": "Sun, 11 Sep 2022 12:27:29 GMT"
},
{
"key": 7,
"trip": "Казань-Тольятти",
"count": 7,
"date_start": "Tue, 16 Feb 2021 16:17:32 GMT",
"date_end": "Mon, 01 Mar 2021 14:45:22 GMT",
"created_at": "Tue, 02 Mar 2021 02:15:25 GMT"
},
{
"key": 8,
"trip": "Чита-Тольятти",
"count": 29,
"date_start": "Mon, 16 Sep 2024 17:48:59 GMT",
"date_end": "Sun, 29 Sep 2024 09:30:15 GMT",
"created_at": "Thu, 02 Jul 2020 17:12:02 GMT"
},
{
"key": 9,
"trip": "Чита-Тюмень",
"count": 94,
"date_start": "Wed, 04 May 2022 01:36:51 GMT",
"date_end": "Tue, 17 May 2022 18:20:33 GMT",
"created_at": "Mon, 28 Oct 2024 04:00:12 GMT"
},
{
"key": 10,
"trip": "Москва-Тюмень",
"count": 12,
"date_start": "Thu, 24 Oct 2024 05:48:10 GMT",
"date_end": "Wed, 06 Nov 2024 16:25:40 GMT",
"created_at": "Sun, 30 Aug 2020 05:14:05 GMT"
},
{
"key": 11,
"trip": "Казань-Тольятти",
"count": 49,
"date_start": "Mon, 22 Apr 2024 15:59:19 GMT",
"date_end": "Sun, 05 May 2024 12:30:45 GMT",
"created_at": "Tue, 22 Dec 2020 09:51:24 GMT"
},
{
"key": 12,
"trip": "Владивосток-Владимир",
"count": 40,
"date_start": "Thu, 30 May 2024 01:48:52 GMT",
"date_end": "Wed, 12 Jun 2024 20:15:33 GMT",
"created_at": "Sat, 28 Jan 2023 14:11:25 GMT"
},
{
"key": 13,
"trip": "Самара-Тюмень",
"count": 47,
"date_start": "Sun, 22 Nov 2020 13:11:01 GMT",
"date_end": "Sat, 05 Dec 2020 09:45:28 GMT",
"created_at": "Mon, 17 Jan 2022 16:24:57 GMT"
},
{
"key": 14,
"trip": "Казань-Вологда",
"count": 32,
"date_start": "Thu, 08 Dec 2022 03:45:34 GMT",
"date_end": "Wed, 21 Dec 2022 15:20:18 GMT",
"created_at": "Wed, 05 Jun 2024 08:59:22 GMT"
},
{
"key": 15,
"trip": "Самара-Казань",
"count": 61,
"date_start": "Sat, 02 May 2020 19:18:10 GMT",
"date_end": "Fri, 15 May 2020 16:30:45 GMT",
"created_at": "Sat, 12 Aug 2023 01:13:15 GMT"
},
{
"key": 16,
"trip": "Тюмень-Казань",
"count": 99,
"date_start": "Tue, 22 Dec 2020 08:05:09 GMT",
"date_end": "Mon, 04 Jan 2021 14:25:33 GMT",
"created_at": "Sun, 15 Jan 2023 09:27:02 GMT"
},
{
"key": 17,
"trip": "Казань-Тольятти",
"count": 29,
"date_start": "Thu, 15 Dec 2022 07:04:50 GMT",
"date_end": "Wed, 28 Dec 2022 11:40:15 GMT",
"created_at": "Wed, 22 Mar 2023 23:11:56 GMT"
},
{
"key": 18,
"trip": "Тюмень-Вологда",
"count": 72,
"date_start": "Wed, 28 Dec 2022 11:36:57 GMT",
"date_end": "Tue, 10 Jan 2023 18:20:45 GMT",
"created_at": "Sun, 06 Mar 2022 12:22:23 GMT"
},
{
"key": 19,
"trip": "Тюмень-Ижевск",
"count": 44,
"date_start": "Tue, 30 Jun 2020 05:32:53 GMT",
"date_end": "Mon, 13 Jul 2020 12:15:30 GMT",
"created_at": "Mon, 22 Apr 2024 11:49:06 GMT"
},
{
"key": 20,
"trip": "Самара-Тюмень",
"count": 86,
"date_start": "Sat, 29 Aug 2020 02:28:28 GMT",
"date_end": "Fri, 11 Sep 2020 09:40:18 GMT",
"created_at": "Sat, 23 Mar 2024 02:57:58 GMT"
},
{
"key": 21,
"trip": "Москва-Вологда",
"count": 67,
"date_start": "Wed, 28 Aug 2024 14:19:06 GMT",
"date_end": "Tue, 10 Sep 2024 21:30:45 GMT",
"created_at": "Fri, 29 Apr 2022 20:59:06 GMT"
},
{
"key": 22,
"trip": "Чита-Тольятти",
"count": 5,
"date_start": "Thu, 07 Oct 2021 03:54:24 GMT",
"date_end": "Wed, 20 Oct 2021 10:25:33 GMT",
"created_at": "Mon, 01 Mar 2021 20:24:58 GMT"
},
{
"key": 23,
"trip": "Москва-Ижевск",
"count": 52,
"date_start": "Sun, 09 May 2021 05:00:06 GMT",
"date_end": "Sat, 22 May 2021 11:45:20 GMT",
"created_at": "Sun, 25 Aug 2024 07:43:58 GMT"
},
{
"key": 24,
"trip": "Москва-Пермь",
"count": 56,
"date_start": "Thu, 22 Apr 2021 01:45:23 GMT",
"date_end": "Wed, 05 May 2021 08:30:15 GMT",
"created_at": "Tue, 14 Apr 2020 14:34:44 GMT"
},
{
"key": 25,
"trip": "Владивосток-Владимир",
"count": 84,
"date_start": "Sun, 30 May 2021 06:20:13 GMT",
"date_end": "Sat, 12 Jun 2021 13:15:40 GMT",
"created_at": "Sat, 02 Oct 2021 16:16:48 GMT"
},
{
"key": 26,
"trip": "Тюмень-Ижевск",
"count": 64,
"date_start": "Mon, 18 Mar 2024 03:50:05 GMT",
"date_end": "Sun, 31 Mar 2024 10:35:25 GMT",
"created_at": "Thu, 06 Aug 2020 10:22:30 GMT"
},
{
"key": 27,
"trip": "Самара-Тюмень",
"count": 52,
"date_start": "Sun, 20 Mar 2022 06:26:26 GMT",
"date_end": "Sat, 02 Apr 2022 13:20:15 GMT",
"created_at": "Wed, 15 May 2024 16:25:33 GMT"
},
{
"key": 28,
"trip": "Самара-Казань",
"count": 69,
"date_start": "Wed, 16 Sep 2020 23:18:39 GMT",
"date_end": "Tue, 29 Sep 2020 06:05:30 GMT",
"created_at": "Sun, 27 Aug 2023 10:13:51 GMT"
},
{
"key": 29,
"trip": "Владивосток-Вологда",
"count": 39,
"date_start": "Sat, 13 Jul 2024 16:41:31 GMT",
"date_end": "Fri, 26 Jul 2024 23:30:18 GMT",
"created_at": "Sun, 04 Apr 2021 10:16:17 GMT"
},
{
"key": 30,
"trip": "Москва-Тюмень",
"count": 15,
"date_start": "Mon, 12 Feb 2024 21:46:30 GMT",
"date_end": "Sun, 25 Feb 2024 04:35:22 GMT",
"created_at": "Mon, 25 Oct 2021 21:06:29 GMT"
},
{
"key": 31,
"trip": "Москва-Тюмень",
"count": 98,
"date_start": "Tue, 01 Oct 2024 12:54:27 GMT",
"date_end": "Mon, 14 Oct 2024 19:40:15 GMT",
"created_at": "Wed, 09 Mar 2022 03:46:32 GMT"
},
{
"key": 32,
"trip": "N",
"count": 44,
"date_start": "Wed, 08 Apr 2020 13:09:07 GMT",
"date_end": "Tue, 21 Apr 2020 20:00:33 GMT",
"created_at": "Wed, 01 Jan 2025 19:48:47 GMT"
},
{
"key": 33,
"trip": "Q",
"count": 23,
"date_start": "Wed, 09 Feb 2022 00:13:32 GMT",
"date_end": "Tue, 22 Feb 2022 07:05:20 GMT",
"created_at": "Sun, 19 Mar 2023 13:20:24 GMT"
},
{
"key": 34,
"trip": "Чита-Тюмень",
"count": 67,
"date_start": "Mon, 21 Oct 2024 14:57:27 GMT",
"date_end": "Sun, 03 Nov 2024 21:45:15 GMT",
"created_at": "Tue, 15 Nov 2022 17:33:15 GMT"
},
{
"key": 35,
"trip": "S",
"count": 28,
"date_start": "Wed, 18 Mar 2020 10:03:48 GMT",
"date_end": "Tue, 31 Mar 2020 16:50:33 GMT",
"created_at": "Thu, 23 May 2024 20:59:48 GMT"
},
{
"key": 36,
"trip": "Казань-Вологда",
"count": 82,
"date_start": "Sat, 24 Apr 2021 08:35:13 GMT",
"date_end": "Fri, 07 May 2021 15:25:40 GMT",
"created_at": "Tue, 24 Oct 2023 03:16:38 GMT"
},
{
"key": 37,
"trip": "Казань-Тольятти",
"count": 43,
"date_start": "Wed, 05 Oct 2022 20:10:30 GMT",
"date_end": "Tue, 18 Oct 2022 03:00:18 GMT",
"created_at": "Tue, 11 Jun 2024 22:41:02 GMT"
},
{
"key": 38,
"trip": "Москва-Тюмень",
"count": 56,
"date_start": "Thu, 06 Jan 2022 02:47:53 GMT",
"date_end": "Wed, 19 Jan 2022 09:35:25 GMT",
"created_at": "Mon, 06 Sep 2021 05:40:10 GMT"
},
{
"key": 39,
"trip": "Чита-Тольятти",
"count": 84,
"date_start": "Wed, 05 Jan 2022 06:42:08 GMT",
"date_end": "Tue, 18 Jan 2022 13:30:40 GMT",
"created_at": "Mon, 13 Apr 2020 02:45:40 GMT"
},
{
"key": 40,
"trip": "Самара-Казань",
"count": 51,
"date_start": "Mon, 12 Feb 2024 09:03:02 GMT",
"date_end": "Sun, 25 Feb 2024 15:50:33 GMT",
"created_at": "Tue, 14 May 2024 13:03:26 GMT"
},
{
"key": 41,
"trip": "Москва-Пермь",
"count": 58,
"date_start": "Fri, 19 May 2023 23:10:06 GMT",
"date_end": "Thu, 01 Jun 2023 06:00:20 GMT",
"created_at": "Sun, 05 Dec 2021 07:35:56 GMT"
},
{
"key": 42,
"trip": "Владивосток-Вологда",
"count": 46,
"date_start": "Thu, 18 Mar 2021 14:42:08 GMT",
"date_end": "Wed, 31 Mar 2021 21:30:45 GMT",
"created_at": "Fri, 02 Jul 2021 01:45:38 GMT"
},
{
"key": 43,
"trip": "Тюмень-Вологда",
"count": 92,
"date_start": "Sat, 01 Feb 2025 10:11:00 GMT",
"date_end": "Fri, 14 Feb 2025 17:00:33 GMT",
"created_at": "Mon, 05 Jul 2021 06:14:03 GMT"
},
{
"key": 44,
"trip": "E",
"count": 96,
"date_start": "Mon, 24 Feb 2020 19:09:06 GMT",
"date_end": "Sun, 08 Mar 2020 02:00:20 GMT",
"created_at": "Sun, 12 Apr 2020 04:59:22 GMT"
},
{
"key": 45,
"trip": "E",
"count": 3,
"date_start": "Sun, 18 Jun 2023 07:26:40 GMT",
"date_end": "Sat, 01 Jul 2023 14:15:25 GMT",
"created_at": "Sun, 18 Aug 2024 06:19:01 GMT"
},
{
"key": 46,
"trip": "E",
"count": 63,
"date_start": "Sat, 10 Apr 2021 13:23:55 GMT",
"date_end": "Fri, 23 Apr 2021 20:10:40 GMT",
"created_at": "Thu, 02 Apr 2020 16:26:15 GMT"
},
{
"key": 47,
"trip": "Москва-Тюмень",
"count": 16,
"date_start": "Wed, 23 Aug 2023 00:01:58 GMT",
"date_end": "Tue, 05 Sep 2023 06:50:33 GMT",
"created_at": "Fri, 16 Feb 2024 01:39:30 GMT"
},
{
"key": 48,
"trip": "Владивосток-Владимир",
"count": 36,
"date_start": "Tue, 25 Apr 2023 23:09:56 GMT",
"date_end": "Mon, 08 May 2023 06:00:20 GMT",
"created_at": "Sat, 17 Dec 2022 13:52:04 GMT"
},
{
"key": 49,
"trip": "Самара-Казань",
"count": 62,
"date_start": "Fri, 04 Oct 2024 11:01:56 GMT",
"date_end": "Thu, 17 Oct 2024 17:50:33 GMT",
"created_at": "Thu, 15 Feb 2024 02:39:03 GMT"
},
{
"key": 50,
"trip": "Москва-Тюмень",
"count": 65,
"date_start": "Thu, 18 Jun 2020 22:40:19 GMT",
"date_end": "Wed, 01 Jul 2020 05:30:15 GMT",
"created_at": "Mon, 28 Jun 2021 12:55:18 GMT"
},
{
"key": 51,
"trip": "Тюмень-Вологда",
"count": 17,
"date_start": "Tue, 08 Sep 2020 08:53:55 GMT",
"date_end": "Mon, 21 Sep 2020 15:40:30 GMT",
"created_at": "Thu, 19 Mar 2020 00:27:17 GMT"
},
{
"key": 52,
"trip": "Казань-Москва",
"count": 28,
"date_start": "Wed, 07 Sep 2022 20:41:37 GMT",
"date_end": "Tue, 20 Sep 2022 03:30:25 GMT",
"created_at": "Wed, 20 Apr 2022 04:36:43 GMT"
},
{
"key": 53,
"trip": "U",
"count": 36,
"date_start": "Mon, 22 May 2023 20:35:52 GMT",
"date_end": "Sun, 04 Jun 2023 03:25:40 GMT",
"created_at": "Tue, 20 Oct 2020 13:11:01 GMT"
},
{
"key": 54,
"trip": "S",
"count": 57,
"date_start": "Mon, 01 Jan 2024 12:53:55 GMT",
"date_end": "Sun, 14 Jan 2024 19:40:30 GMT",
"created_at": "Fri, 31 Jan 2025 10:15:04 GMT"
},
{
"key": 55,
"trip": "Казань-Сызрань",
"count": 27,
"date_start": "Thu, 03 Nov 2022 13:51:00 GMT",
"date_end": "Wed, 16 Nov 2022 20:40:15 GMT",
"created_at": "Wed, 03 Jun 2020 07:10:46 GMT"
},
{
"key": 56,
"trip": "N",
"count": 58,
"date_start": "Tue, 03 Nov 2020 10:27:49 GMT",
"date_end": "Mon, 16 Nov 2020 17:15:33 GMT",
"created_at": "Sat, 01 May 2021 08:40:29 GMT"
},
{
"key": 57,
"trip": "Владисвосток-Москва",
"count": 29,
"date_start": "Sun, 05 Jan 2020 00:10:45 GMT",
"date_end": "Sat, 18 Jan 2020 07:00:20 GMT",
"created_at": "Thu, 29 Jul 2021 08:04:38 GMT"
},
{
"key": 58,
"trip": "Казань-Тольятти",
"count": 22,
"date_start": "Wed, 05 Apr 2023 14:15:27 GMT",
"date_end": "Tue, 18 Apr 2023 21:05:15 GMT",
"created_at": "Fri, 06 Dec 2024 03:20:18 GMT"
},
{
"key": 59,
"trip": "Москва-Пермь",
"count": 8,
"date_start": "Fri, 18 Nov 2022 21:56:47 GMT",
"date_end": "Thu, 01 Dec 2022 04:45:30 GMT",
"created_at": "Tue, 07 Jul 2020 16:18:05 GMT"
},
{
"key": 60,
"trip": "Владисвосток-Москва",
"count": 29,
"date_start": "Sat, 06 Apr 2024 10:22:54 GMT",
"date_end": "Fri, 19 Apr 2024 17:10:40 GMT",
"created_at": "Wed, 21 Oct 2020 18:04:55 GMT"
},
{
"key": 61,
"trip": "Q",
"count": 55,
"date_start": "Mon, 28 Aug 2023 18:16:49 GMT",
"date_end": "Sun, 10 Sep 2023 01:05:33 GMT",
"created_at": "Mon, 29 Jul 2024 02:42:50 GMT"
},
{
"key": 62,
"trip": "Казань-Москва",
"count": 64,
"date_start": "Mon, 11 Dec 2023 13:50:59 GMT",
"date_end": "Sun, 24 Dec 2023 20:40:25 GMT",
"created_at": "Wed, 13 Jan 2021 18:49:04 GMT"
},
{
"key": 63,
"trip": "N",
"count": 36,
"date_start": "Tue, 29 Sep 2020 17:47:49 GMT",
"date_end": "Mon, 12 Oct 2020 00:35:30 GMT",
"created_at": "Sat, 05 Dec 2020 01:30:25 GMT"
},
{
"key": 64,
"trip": "Z",
"count": 36,
"date_start": "Tue, 26 May 2020 16:40:52 GMT",
"date_end": "Mon, 08 Jun 2020 23:30:25 GMT",
"created_at": "Thu, 25 Feb 2021 23:11:39 GMT"
},
{
"key": 65,
"trip": "Чита-Тольятти",
"count": 61,
"date_start": "Thu, 21 Sep 2023 19:54:17 GMT",
"date_end": "Wed, 04 Oct 2023 02:45:15 GMT",
"created_at": "Thu, 30 Mar 2023 15:33:19 GMT"
},
{
"key": 66,
"trip": "Владивосток-Вологда",
"count": 42,
"date_start": "Tue, 14 Apr 2020 15:50:34 GMT",
"date_end": "Mon, 27 Apr 2020 22:40:20 GMT",
"created_at": "Thu, 15 Apr 2021 02:35:05 GMT"
},
{
"key": 67,
"trip": "U",
"count": 75,
"date_start": "Thu, 10 Aug 2023 00:45:04 GMT",
"date_end": "Wed, 23 Aug 2023 07:35:30 GMT",
"created_at": "Wed, 09 Jun 2021 13:29:50 GMT"
},
{
"key": 68,
"trip": "Тюмень-Ижевск",
"count": 73,
"date_start": "Sat, 19 Mar 2022 00:00:59 GMT",
"date_end": "Fri, 01 Apr 2022 06:50:40 GMT",
"created_at": "Tue, 18 Apr 2023 11:39:45 GMT"
},
{
"key": 69,
"trip": "Казань-Тольятти",
"count": 27,
"date_start": "Thu, 15 Aug 2024 04:16:12 GMT",
"date_end": "Wed, 28 Aug 2024 11:05:25 GMT",
"created_at": "Wed, 02 Jun 2021 10:41:59 GMT"
},
{
"key": 70,
"trip": "Казань-Тольятти",
"count": 85,
"date_start": "Sun, 27 Sep 2020 23:18:20 GMT",
"date_end": "Sat, 10 Oct 2020 06:05:15 GMT",
"created_at": "Tue, 13 Apr 2021 06:32:10 GMT"
},
{
"key": 71,
"trip": "Москва-Тюмень",
"count": 85,
"date_start": "Mon, 23 Sep 2024 22:49:15 GMT",
"date_end": "Sun, 06 Oct 2024 05:40:30 GMT",
"created_at": "Sat, 09 Dec 2023 04:25:00 GMT"
},
{
"key": 72,
"trip": "Владисвосток-Москва",
"count": 54,
"date_start": "Fri, 05 Mar 2021 19:03:42 GMT",
"date_end": "Thu, 18 Mar 2021 01:50:25 GMT",
"created_at": "Sat, 07 Dec 2024 02:38:40 GMT"
},
{
"key": 73,
"trip": "Москва-Пермь",
"count": 93,
"date_start": "Wed, 16 Feb 2022 08:53:29 GMT",
"date_end": "Tue, 01 Mar 2022 15:40:15 GMT",
"created_at": "Mon, 18 Jan 2021 12:32:30 GMT"
},
{
"key": 74,
"trip": "Казань-Тольятти",
"count": 11,
"date_start": "Wed, 27 Jan 2021 14:00:04 GMT",
"date_end": "Tue, 09 Feb 2021 20:50:30 GMT",
"created_at": "Wed, 22 Apr 2020 14:49:30 GMT"
},
{
"key": 75,
"trip": "Казань-Москва",
"count": 92,
"date_start": "Wed, 23 Aug 2023 11:38:30 GMT",
"date_end": "Tue, 05 Sep 2023 18:25:15 GMT",
"created_at": "Sat, 21 May 2022 15:04:36 GMT"
},
{
"key": 76,
"trip": "Москва-Пермь",
"count": 7,
"date_start": "Tue, 31 May 2022 05:01:43 GMT",
"date_end": "Mon, 13 Jun 2022 11:50:30 GMT",
"created_at": "Tue, 22 Oct 2024 05:05:05 GMT"
},
{
"key": 77,
"trip": "Самара-Тюмень",
"count": 71,
"date_start": "Thu, 25 Jan 2024 10:34:25 GMT",
"date_end": "Wed, 07 Feb 2024 17:20:15 GMT",
"created_at": "Tue, 10 Mar 2020 22:40:26 GMT"
},
{
"key": 78,
"trip": "Москва-Пермь",
"count": 70,
"date_start": "Thu, 05 Jan 2023 11:57:02 GMT",
"date_end": "Wed, 18 Jan 2023 18:45:30 GMT",
"created_at": "Wed, 27 Sep 2023 08:44:01 GMT"
},
{
"key": 79,
"trip": "Тюмень-Казань",
"count": 68,
"date_start": "Fri, 18 Mar 2022 04:00:41 GMT",
"date_end": "Thu, 31 Mar 2022 10:50:25 GMT",
"created_at": "Tue, 25 Apr 2023 02:53:24 GMT"
},
{
"key": 80,
"trip": "Москва-Владивосток",
"count": 32,
"date_start": "Thu, 09 Mar 2023 20:34:19 GMT",
"date_end": "Wed, 22 Mar 2023 03:25:15 GMT",
"created_at": "Sun, 05 May 2024 13:34:05 GMT"
},
{
"key": 81,
"trip": "Москва-Владивосток",
"count": 32,
"date_start": "Fri, 09 Dec 2022 00:39:37 GMT",
"date_end": "Thu, 22 Dec 2022 07:30:25 GMT",
"created_at": "Thu, 03 Oct 2024 18:33:04 GMT"
},
{
"key": 82,
"trip": "E",
"count": 57,
"date_start": "Mon, 21 Mar 2022 09:02:45 GMT",
"date_end": "Sun, 03 Apr 2022 15:50:30 GMT",
"created_at": "Sun, 16 Oct 2022 07:03:06 GMT"
},
{
"key": 83,
"trip": "P",
"count": 5,
"date_start": "Tue, 05 Mar 2024 13:06:40 GMT",
"date_end": "Mon, 18 Mar 2024 19:55:25 GMT",
"created_at": "Sun, 18 Apr 2021 02:55:13 GMT"
},
{
"key": 84,
"trip": "Казань-Сызрань",
"count": 42,
"date_start": "Sun, 11 Aug 2024 02:05:46 GMT",
"date_end": "Sat, 24 Aug 2024 08:55:30 GMT",
"created_at": "Sun, 18 Oct 2020 18:39:33 GMT"
},
{
"key": 85,
"trip": "Казань-Тольятти",
"count": 48,
"date_start": "Fri, 07 Aug 2020 09:21:46 GMT",
"date_end": "Thu, 20 Aug 2020 16:10:30 GMT",
"created_at": "Wed, 22 Jul 2020 21:59:15 GMT"
},
{
"key": 86,
"trip": "Самара-Казань",
"count": 43,
"date_start": "Thu, 03 Dec 2020 21:51:58 GMT",
"date_end": "Wed, 16 Dec 2020 04:40:25 GMT",
"created_at": "Mon, 07 Nov 2022 18:42:21 GMT"
},
{
"key": 87,
"trip": "Москва-Тюмень",
"count": 50,
"date_start": "Mon, 20 Apr 2020 17:12:02 GMT",
"date_end": "Sun, 03 May 2020 00:00:30 GMT",
"created_at": "Fri, 10 Feb 2023 02:41:47 GMT"
},
{
"key": 88,
"trip": "Москва-Вологда",
"count": 83,
"date_start": "Mon, 22 Jan 2024 07:15:58 GMT",
"date_end": "Sun, 04 Feb 2024 14:05:25 GMT",
"created_at": "Mon, 27 Jan 2020 21:25:21 GMT"
},
{
"key": 89,
"trip": "Владивосток-Владимир",
"count": 27,
"date_start": "Wed, 08 Nov 2023 00:45:46 GMT",
"date_end": "Tue, 21 Nov 2023 07:35:30 GMT",
"created_at": "Sun, 03 Apr 2022 08:12:20 GMT"
},
{
"key": 90,
"trip": "S",
"count": 75,
"date_start": "Fri, 06 Aug 2021 21:40:09 GMT",
"date_end": "Thu, 19 Aug 2021 04:30:25 GMT",
"created_at": "Mon, 27 Dec 2021 10:19:20 GMT"
},
{
"key": 91,
"trip": "U",
"count": 30,
"date_start": "Sun, 08 Mar 2020 17:12:41 GMT",
"date_end": "Sat, 21 Mar 2020 00:00:30 GMT",
"created_at": "Mon, 22 Aug 2022 15:00:22 GMT"
},
{
"key": 92,
"trip": "Москва-Ижевск",
"count": 46,
"date_start": "Sat, 23 May 2020 10:41:01 GMT",
"date_end": "Fri, 05 Jun 2020 17:30:25 GMT",
"created_at": "Wed, 06 Jul 2022 06:39:05 GMT"
},
{
"key": 93,
"trip": "U",
"count": 16,
"date_start": "Fri, 26 May 2023 09:51:29 GMT",
"date_end": "Thu, 08 Jun 2023 16:40:15 GMT",
"created_at": "Sat, 26 Feb 2022 08:38:00 GMT"
},
{
"key": 94,
"trip": "Q",
"count": 4,
"date_start": "Wed, 08 Jul 2020 11:02:39 GMT",
"date_end": "Tue, 21 Jul 2020 17:50:25 GMT",
"created_at": "Fri, 24 Dec 2021 23:43:01 GMT"
},
{
"key": 95,
"trip": "Москва-Вологда",
"count": 70,
"date_start": "Mon, 10 Oct 2022 00:06:40 GMT",
"date_end": "Sun, 23 Oct 2022 06:55:30 GMT",
"created_at": "Sun, 28 Jul 2024 09:47:35 GMT"
},
{
"key": 96,
"trip": "N",
"count": 80,
"date_start": "Thu, 03 Oct 2024 02:27:30 GMT",
"date_end": "Wed, 16 Oct 2024 09:15:25 GMT",
"created_at": "Sun, 12 Feb 2023 12:07:51 GMT"
},
{
"key": 97,
"trip": "Тюмень-Вологда",
"count": 40,
"date_start": "Sat, 23 Mar 2024 21:42:40 GMT",
"date_end": "Fri, 05 Apr 2024 04:30:30 GMT",
"created_at": "Fri, 17 Mar 2023 15:47:32 GMT"
},
{
"key": 98,
"trip": "Москва-Пермь",
"count": 38,
"date_start": "Thu, 14 Jul 2022 14:03:39 GMT",
"date_end": "Wed, 27 Jul 2022 20:50:25 GMT",
"created_at": "Sat, 30 Nov 2024 07:09:52 GMT"
},
{
"key": 99,
"trip": "Тюмень-Казань",
"count": 74,
"date_start": "Thu, 19 Mar 2020 04:40:50 GMT",
"date_end": "Wed, 01 Apr 2020 11:30:30 GMT",
"created_at": "Thu, 20 Aug 2020 13:58:54 GMT"
}
]
}

View File

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

View File

@@ -1 +0,0 @@
GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw==

View File

@@ -1,2 +0,0 @@
node_modules/
.env

View File

@@ -1,21 +0,0 @@
# back-new
非Python实现的后端Node.js + Express
## 启动方法
1. 安装依赖:
```bash
npm install
```
2. 启动服务:
```bash
npm start
```
默认端口:`3002`
## 支持接口
- POST `/api/auth/login` 用户登录
- POST `/api/auth/register` 用户注册
- GET `/gigachat/prompt?prompt=xxx` 生成图片(返回模拟图片链接)

View File

@@ -1,24 +0,0 @@
const express = require('express');
const cors = require('cors');
const featuresConfig = require('./features.config');
const imageRoutes = require('./features/image/image.routes');
const app = express();
app.use(cors());
app.use(express.json());
if (featuresConfig.auth) {
app.use('/api/auth', require('./features/auth/auth.routes'));
}
if (featuresConfig.user) {
app.use('/api/user', require('./features/user/user.routes'));
}
if (featuresConfig.image) {
app.use('/gigachat', imageRoutes);
}
app.get('/api/', (req, res) => {
res.json({ message: 'API root' });
});
module.exports = app;

View File

@@ -1,5 +0,0 @@
module.exports = {
auth: true,
user: true,
image: true, // 关闭为 false
};

View File

@@ -1,95 +0,0 @@
const usersDb = require('../../shared/usersDb');
const makeLinks = require('../../shared/hateoas');
exports.login = (req, res) => {
const { username, password, email } = req.body;
const user = usersDb.findUser(username, email, password);
if (user) {
res.json({
data: {
user: {
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
},
token: 'token-' + user.id,
message: 'Login successful'
},
_links: makeLinks('/api/auth', {
self: '/login',
profile: '/profile/',
logout: '/logout'
}),
_meta: {}
});
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
};
exports.register = (req, res) => {
const { username, password, email, firstName, lastName } = req.body;
if (usersDb.exists(username, email)) {
return res.status(409).json({ error: 'User already exists' });
}
const newUser = usersDb.addUser({ username, password, email, firstName, lastName });
res.json({
data: {
user: {
id: newUser.id,
username,
email,
firstName,
lastName
},
token: 'token-' + newUser.id,
message: 'Register successful'
},
_links: makeLinks('/api/auth', {
self: '/register',
login: '/login',
profile: '/profile/'
}),
_meta: {}
});
};
exports.profile = (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = auth.replace('Bearer ', '');
const id = parseInt(token.replace('token-', ''));
const user = usersDb.findById(id);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
res.json({
data: {
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
},
_links: makeLinks('/api/auth', {
self: '/profile/',
logout: '/logout'
}),
_meta: {}
});
};
exports.logout = (req, res) => {
res.json({
message: 'Logout successful',
_links: makeLinks('/api/auth', {
self: '/logout',
login: '/login'
}),
_meta: {}
});
};

View File

@@ -1,10 +0,0 @@
const express = require('express');
const router = express.Router();
const ctrl = require('./auth.controller');
router.post('/login', ctrl.login);
router.post('/register', ctrl.register);
router.get('/profile/', ctrl.profile);
router.post('/logout', ctrl.logout);
module.exports = router;

View File

@@ -1,81 +0,0 @@
const axios = require('axios');
const makeLinks = require('../../shared/hateoas');
const path = require('path');
const qs = require('qs');
const { v4: uuidv4 } = require('uuid');
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
exports.generate = async (req, res) => {
const { prompt } = req.query;
if (!prompt) {
return res.status(400).json({ error: 'Prompt parameter is required' });
}
try {
const apiKey = process.env.GIGACHAT_API_KEY;
const tokenResp = await axios.post(
'https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
{
'scope':' GIGACHAT_API_PERS',
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': `Basic ${apiKey}`,
'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e'
},
}
);
const accessToken = tokenResp.data.access_token;
const chatResp = await axios.post(
'https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
{
model: "GigaChat",
messages: [
{ role: "system", content: "Ты — Василий Кандинский" },
{ role: "user", content: prompt }
],
stream: false,
function_call: 'auto'
},
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'RqUID': uuidv4(),
}
}
);
const content = chatResp.data.choices[0].message.content;
const match = content.match(/<img src=\"(.*?)\"/);
if (!match) {
return res.status(500).json({ error: 'No image generated' });
}
const imageId = match[1];
const imageResp = await axios.get(
`https://gigachat.devices.sberbank.ru/api/v1/files/${imageId}/content`,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'RqUID': uuidv4(),
},
responseType: 'arraybuffer'
}
);
res.set('Content-Type', 'image/jpeg');
res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' })));
res.send(imageResp.data);
} catch (err) {
if (err.response) {
console.error('AI生成图片出错:');
console.error('status:', err.response.status);
console.error('headers:', err.response.headers);
console.error('data:', err.response.data);
console.error('config:', err.config);
} else {
console.error('AI生成图片出错:', err.message);
}
res.status(500).json({ error: err.message });
}
};

View File

@@ -1,7 +0,0 @@
const express = require('express');
const router = express.Router();
const ctrl = require('./image.controller');
router.get('/prompt', ctrl.generate);
module.exports = router;

View File

@@ -1,12 +0,0 @@
const usersDb = require('../../shared/usersDb');
const makeLinks = require('../../shared/hateoas');
exports.list = (req, res) => {
res.json({
data: usersDb.getAll(),
_links: makeLinks('/api/user', {
self: '/list',
}),
_meta: {}
});
};

View File

@@ -1,7 +0,0 @@
const express = require('express');
const router = express.Router();
const ctrl = require('./user.controller');
router.get('/list', ctrl.list);
module.exports = router;

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
{
"name": "back-new",
"version": "1.0.0",
"description": "非Python实现的后端兼容前端接口",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "jest"
},
"dependencies": {
"axios": "^1.10.0",
"cors": "^2.8.5",
"dotenv": "^17.0.0",
"express": "^4.21.2",
"qs": "^6.14.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"jest": "^30.0.3"
}
}

View File

@@ -1,5 +0,0 @@
const app = require('./app');
const PORT = process.env.PORT || 3002;
app.listen(PORT, () => {
console.log(`Mock backend running on https://dev.bro.js.ru/ms/back-new/${PORT}`);
});

View File

@@ -1,8 +0,0 @@
function makeLinks(base, links) {
const result = {};
for (const [rel, path] of Object.entries(links)) {
result[rel] = { href: base + path };
}
return result;
}
module.exports = makeLinks;

View File

@@ -1,20 +0,0 @@
let users = [
{ id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User' }
];
let nextId = 2;
exports.findUser = (username, email, password) =>
users.find(u => (u.username === username || u.email === email) && u.password === password);
exports.findById = (id) => users.find(u => u.id === id);
exports.addUser = ({ username, password, email, firstName, lastName }) => {
const newUser = { id: nextId++, username, password, email, firstName, lastName };
users.push(newUser);
return newUser;
};
exports.exists = (username, email) =>
users.some(u => u.username === username || u.email === email);
exports.getAll = () => users;

View File

@@ -1,113 +0,0 @@
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const fs = require('fs');
// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts)
const mongoose = require('../../utils/mongoose');
// Загрузить переменные окружения
dotenv.config();
// Включить логирование при разработке: установите DEV=true в .env или при запуске
// export DEV=true && npm start (для Linux/Mac)
// set DEV=true && npm start (для Windows)
// По умолчанию логи отключены. Все console.log функции отключаются если DEV !== 'true'
if (process.env.DEV === 'true') {
console.log(' DEBUG MODE ENABLED - All logs are visible');
}
// Импортировать маршруты - прямые пути без path.join и __dirname
const authRoutes = require('./routes/auth');
const companiesRoutes = require('./routes/companies');
const messagesRoutes = require('./routes/messages');
const searchRoutes = require('./routes/search');
const buyRoutes = require('./routes/buy');
const experienceRoutes = require('./routes/experience');
const productsRoutes = require('./routes/products');
const reviewsRoutes = require('./routes/reviews');
const buyProductsRoutes = require('./routes/buyProducts');
const requestsRoutes = require('./routes/requests');
const homeRoutes = require('./routes/home');
const activityRoutes = require('./routes/activity');
const app = express();
// Проверить подключение к MongoDB (подключение происходит в server/utils/mongoose.ts)
const dbConnected = mongoose.connection.readyState === 1;
// Middleware
app.use(cors());
app.use(express.json({ charset: 'utf-8' }));
app.use(express.urlencoded({ extended: true, charset: 'utf-8' }));
// Set UTF-8 encoding for all responses
app.use((req, res, next) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
next();
});
// CORS headers
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// Задержка для имитации сети (опционально)
const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms);
app.use(delay());
// Статика для загруженных файлов
const uploadsRoot = 'server/remote-assets/uploads';
if (!fs.existsSync(uploadsRoot)) {
fs.mkdirSync(uploadsRoot, { recursive: true });
}
app.use('/uploads', express.static(uploadsRoot));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
api: 'running',
database: dbConnected ? 'mongodb' : 'mock',
timestamp: new Date().toISOString()
});
});
// Маршруты
app.use('/auth', authRoutes);
app.use('/companies', companiesRoutes);
app.use('/messages', messagesRoutes);
app.use('/search', searchRoutes);
app.use('/buy', buyRoutes);
app.use('/buy-products', buyProductsRoutes);
app.use('/experience', experienceRoutes);
app.use('/products', productsRoutes);
app.use('/reviews', reviewsRoutes);
app.use('/requests', requestsRoutes);
app.use('/home', homeRoutes);
app.use('/activities', activityRoutes);
// Обработка ошибок
app.use((err, req, res, next) => {
console.error('API Error:', err);
res.status(err.status || 500).json({
error: err.message || 'Internal server error'
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({
error: 'Not found'
});
});
// Экспортировать для использования в brojs
module.exports = app;

View File

@@ -1,42 +0,0 @@
const jwt = require('jsonwebtoken');
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
req.userId = decoded.userId;
req.companyId = decoded.companyId;
req.user = decoded;
log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId);
next();
} catch (error) {
console.error('[Auth] Token verification failed:', error.message);
return res.status(401).json({ error: 'Invalid token' });
}
};
const generateToken = (userId, companyId, firstName = '', lastName = '', companyName = '') => {
log('[Auth] Generating token for userId:', userId, 'companyId:', companyId);
return jwt.sign(
{ userId, companyId, firstName, lastName, companyName },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '7d' }
);
};
module.exports = { verifyToken, generateToken };

View File

@@ -1,61 +0,0 @@
const mongoose = require('mongoose');
const activitySchema = new mongoose.Schema({
companyId: {
type: String,
required: true,
index: true
},
userId: {
type: String,
required: true
},
type: {
type: String,
enum: [
'message_received',
'message_sent',
'request_received',
'request_sent',
'request_response',
'product_accepted',
'review_received',
'profile_updated',
'product_added',
'buy_product_added'
],
required: true
},
title: {
type: String,
required: true
},
description: {
type: String
},
relatedCompanyId: {
type: String
},
relatedCompanyName: {
type: String
},
metadata: {
type: mongoose.Schema.Types.Mixed
},
read: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now,
index: true
}
});
// Индексы для оптимизации
activitySchema.index({ companyId: 1, createdAt: -1 });
activitySchema.index({ companyId: 1, read: 1, createdAt: -1 });
module.exports = mongoose.model('Activity', activitySchema);

View File

@@ -1,43 +0,0 @@
const mongoose = require('mongoose');
const buyDocumentSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true,
index: true
},
ownerCompanyId: {
type: String,
required: true,
index: true
},
name: {
type: String,
required: true
},
type: {
type: String,
required: true
},
size: {
type: Number,
required: true
},
filePath: {
type: String,
required: true
},
acceptedBy: {
type: [String],
default: []
},
createdAt: {
type: Date,
default: Date.now,
index: true
}
});
module.exports = mongoose.model('BuyDocument', buyDocumentSchema);

View File

@@ -1,87 +0,0 @@
const mongoose = require('mongoose');
// Явно определяем схему для файлов
const fileSchema = new mongoose.Schema({
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
url: {
type: String,
required: true
},
type: {
type: String,
required: true
},
size: {
type: Number,
required: true
},
storagePath: String,
uploadedAt: {
type: Date,
default: Date.now
}
}, { _id: false });
const buyProductSchema = new mongoose.Schema({
companyId: {
type: String,
required: true,
index: true
},
name: {
type: String,
required: true
},
description: {
type: String,
required: true,
minlength: 10,
maxlength: 1000
},
quantity: {
type: String,
required: true
},
unit: {
type: String,
default: 'шт'
},
files: [fileSchema],
acceptedBy: [{
companyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company'
},
acceptedAt: {
type: Date,
default: Date.now
}
}],
status: {
type: String,
enum: ['draft', 'published'],
default: 'published'
},
createdAt: {
type: Date,
default: Date.now,
index: true
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Индексы для оптимизации поиска
buyProductSchema.index({ companyId: 1, createdAt: -1 });
buyProductSchema.index({ name: 'text', description: 'text' });
module.exports = mongoose.model('BuyProduct', buyProductSchema);

View File

@@ -1,76 +0,0 @@
const mongoose = require('mongoose');
const companySchema = new mongoose.Schema({
fullName: {
type: String,
required: true
},
shortName: String,
inn: {
type: String,
sparse: true
},
ogrn: String,
legalForm: String,
industry: String,
companySize: String,
website: String,
phone: String,
email: String,
slogan: String,
description: String,
foundedYear: Number,
employeeCount: String,
revenue: String,
legalAddress: String,
actualAddress: String,
bankDetails: String,
logo: String,
rating: {
type: Number,
default: 0,
min: 0,
max: 5
},
reviews: {
type: Number,
default: 0
},
ownerId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
platformGoals: [String],
productsOffered: String,
productsNeeded: String,
partnerIndustries: [String],
partnerGeography: [String],
verified: {
type: Boolean,
default: false
},
metrics: {
type: {
profileViews: { type: Number, default: 0 }
},
default: {}
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
collection: 'companies',
minimize: false
});
// Индексы для поиска
companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' });
companySchema.index({ industry: 1 });
companySchema.index({ rating: -1 });
module.exports = mongoose.model('Company', companySchema);

View File

@@ -1,46 +0,0 @@
const mongoose = require('mongoose');
const experienceSchema = new mongoose.Schema({
companyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true,
index: true
},
confirmed: {
type: Boolean,
default: false
},
customer: {
type: String,
required: true
},
subject: {
type: String,
required: true
},
volume: {
type: String
},
contact: {
type: String
},
comment: {
type: String
},
createdAt: {
type: Date,
default: Date.now,
index: true
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Индексы для оптимизации поиска
experienceSchema.index({ companyId: 1, createdAt: -1 });
module.exports = mongoose.model('Experience', experienceSchema);

View File

@@ -1,37 +0,0 @@
const mongoose = require('mongoose');
const messageSchema = new mongoose.Schema({
threadId: {
type: String,
required: true,
index: true
},
senderCompanyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true
},
recipientCompanyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true
},
text: {
type: String,
required: true
},
read: {
type: Boolean,
default: false
},
timestamp: {
type: Date,
default: Date.now,
index: true
}
});
// Индекс для быстрого поиска сообщений потока
messageSchema.index({ threadId: 1, timestamp: -1 });
module.exports = mongoose.model('Message', messageSchema);

View File

@@ -1,57 +0,0 @@
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
category: {
type: String,
required: true
},
description: {
type: String,
required: true,
minlength: 20,
maxlength: 500
},
type: {
type: String,
enum: ['sell', 'buy'],
required: true
},
productUrl: String,
companyId: {
type: String,
required: true,
index: true
},
price: String,
unit: String,
minOrder: String,
createdAt: {
type: Date,
default: Date.now,
index: true
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Индекс для поиска
productSchema.index({ companyId: 1, type: 1 });
productSchema.index({ name: 'text', description: 'text' });
// Transform _id to id in JSON output
productSchema.set('toJSON', {
transform: (doc, ret) => {
ret.id = ret._id;
delete ret._id;
delete ret.__v;
return ret;
}
});
module.exports = mongoose.model('Product', productSchema);

View File

@@ -1,82 +0,0 @@
const mongoose = require('mongoose');
const requestSchema = new mongoose.Schema({
senderCompanyId: {
type: String,
required: true,
index: true
},
recipientCompanyId: {
type: String,
required: true,
index: true
},
subject: {
type: String,
required: false,
trim: true,
default: ''
},
text: {
type: String,
required: true
},
files: [{
id: { type: String },
name: { type: String },
url: { type: String },
type: { type: String },
size: { type: Number },
storagePath: { type: String },
uploadedAt: {
type: Date,
default: Date.now
}
}],
productId: {
type: String,
ref: 'BuyProduct'
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'],
default: 'pending'
},
response: {
type: String,
default: null
},
responseFiles: [{
id: { type: String },
name: { type: String },
url: { type: String },
type: { type: String },
size: { type: Number },
storagePath: { type: String },
uploadedAt: {
type: Date,
default: Date.now
}
}],
respondedAt: {
type: Date,
default: null
},
createdAt: {
type: Date,
default: Date.now,
index: true
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Индексы для оптимизации поиска
requestSchema.index({ senderCompanyId: 1, createdAt: -1 });
requestSchema.index({ recipientCompanyId: 1, createdAt: -1 });
requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 });
requestSchema.index({ subject: 1, createdAt: -1 });
module.exports = mongoose.model('Request', requestSchema);

View File

@@ -1,58 +0,0 @@
const mongoose = require('mongoose');
const reviewSchema = new mongoose.Schema({
companyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true,
index: true
},
authorCompanyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company',
required: true
},
authorName: {
type: String,
required: true
},
authorCompany: {
type: String,
required: true
},
rating: {
type: Number,
required: true,
min: 1,
max: 5
},
comment: {
type: String,
required: true,
minlength: 10,
maxlength: 1000
},
date: {
type: Date,
default: Date.now
},
verified: {
type: Boolean,
default: true
},
createdAt: {
type: Date,
default: Date.now,
index: true
},
updatedAt: {
type: Date,
default: Date.now
}
});
// Индексы для оптимизации поиска
reviewSchema.index({ companyId: 1, createdAt: -1 });
reviewSchema.index({ authorCompanyId: 1 });
module.exports = mongoose.model('Review', reviewSchema);

View File

@@ -1,73 +0,0 @@
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
type: String,
required: true,
minlength: 8
},
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
position: String,
phone: String,
companyId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Company'
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
collection: 'users',
minimize: false,
toObject: { versionKey: false }
});
userSchema.set('toObject', { virtuals: false, versionKey: false });
// Хешировать пароль перед сохранением
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Метод для сравнения паролей
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
// Скрыть пароль при преобразовании в JSON
userSchema.methods.toJSON = function() {
const obj = this.toObject();
delete obj.password;
return obj;
};
module.exports = mongoose.model('User', userSchema);

View File

@@ -1,239 +0,0 @@
const express = require('express')
const mongoose = require('mongoose')
const request = require('supertest')
// Mock auth middleware
const mockAuthMiddleware = (req, res, next) => {
req.user = {
companyId: 'test-company-id',
id: 'test-user-id',
}
next()
}
describe('Buy Products Routes', () => {
let app
let router
beforeAll(() => {
app = express()
app.use(express.json())
// Create a test router with mock middleware
router = express.Router()
// Mock endpoints for testing structure
router.get('/company/:companyId', mockAuthMiddleware, (req, res) => {
res.json([])
})
router.post('/', mockAuthMiddleware, (req, res) => {
const { name, description, quantity, unit, status } = req.body
if (!name || !description || !quantity) {
return res.status(400).json({
error: 'name, description, and quantity are required',
})
}
if (description.trim().length < 10) {
return res.status(400).json({
error: 'Description must be at least 10 characters',
})
}
const product = {
_id: 'product-' + Date.now(),
companyId: req.user.companyId,
name: name.trim(),
description: description.trim(),
quantity: quantity.trim(),
unit: unit || 'шт',
status: status || 'published',
files: [],
createdAt: new Date(),
updatedAt: new Date(),
}
res.status(201).json(product)
})
app.use('/buy-products', router)
})
describe('GET /buy-products/company/:companyId', () => {
it('should return products list for a company', async () => {
const res = await request(app)
.get('/buy-products/company/test-company-id')
.expect(200)
expect(Array.isArray(res.body)).toBe(true)
})
it('should require authentication', async () => {
// This test would fail without proper auth middleware
const res = await request(app)
.get('/buy-products/company/test-company-id')
expect(res.status).toBeLessThan(500)
})
})
describe('POST /buy-products', () => {
it('should create a new product with valid data', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
unit: 'шт',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body).toHaveProperty('_id')
expect(res.body.name).toBe('Test Product')
expect(res.body.description).toBe(productData.description)
expect(res.body.status).toBe('published')
})
it('should reject product without name', async () => {
const productData = {
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('required')
})
it('should reject product without description', async () => {
const productData = {
name: 'Test Product',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('required')
})
it('should reject product without quantity', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('required')
})
it('should reject product with description less than 10 characters', async () => {
const productData = {
name: 'Test Product',
description: 'short',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(400)
expect(res.body.error).toContain('10 characters')
})
it('should set default unit to "шт" if not provided', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.unit).toBe('шт')
})
it('should use provided unit', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
unit: 'кг',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.unit).toBe('кг')
})
it('should set status to "published" by default', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.status).toBe('published')
})
})
describe('Data validation', () => {
it('should trim whitespace from product data', async () => {
const productData = {
name: ' Test Product ',
description: ' This is a test product description ',
quantity: ' 10 ',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.name).toBe('Test Product')
expect(res.body.description).toBe('This is a test product description')
expect(res.body.quantity).toBe('10')
})
it('should include companyId from auth token', async () => {
const productData = {
name: 'Test Product',
description: 'This is a test product description',
quantity: '10',
}
const res = await request(app)
.post('/buy-products')
.send(productData)
.expect(201)
expect(res.body.companyId).toBe('test-company-id')
})
})
})

View File

@@ -1,101 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Activity = require('../models/Activity');
const User = require('../models/User');
// Получить последние активности компании
router.get('/', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({ activities: [] });
}
const companyId = user.companyId.toString();
const limit = parseInt(req.query.limit) || 10;
const activities = await Activity.find({ companyId })
.sort({ createdAt: -1 })
.limit(limit)
.lean();
res.json({ activities });
} catch (error) {
console.error('Error getting activities:', error);
res.status(500).json({ error: error.message });
}
});
// Отметить активность как прочитанную
router.patch('/:id/read', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.status(403).json({ error: 'Access denied' });
}
const companyId = user.companyId.toString();
const activityId = req.params.id;
const activity = await Activity.findOne({
_id: activityId,
companyId
});
if (!activity) {
return res.status(404).json({ error: 'Activity not found' });
}
activity.read = true;
await activity.save();
res.json({ success: true, activity });
} catch (error) {
console.error('Error updating activity:', error);
res.status(500).json({ error: error.message });
}
});
// Отметить все активности как прочитанные
router.post('/mark-all-read', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.status(403).json({ error: 'Access denied' });
}
const companyId = user.companyId.toString();
await Activity.updateMany(
{ companyId, read: false },
{ $set: { read: true } }
);
res.json({ success: true });
} catch (error) {
console.error('Error marking all as read:', error);
res.status(500).json({ error: error.message });
}
});
// Создать активность (вспомогательная функция)
router.createActivity = async (data) => {
try {
const activity = new Activity(data);
await activity.save();
return activity;
} catch (error) {
console.error('Error creating activity:', error);
throw error;
}
};
module.exports = router;

View File

@@ -1,515 +0,0 @@
const express = require('express');
const router = express.Router();
const { generateToken, verifyToken } = require('../middleware/auth');
const User = require('../models/User');
const Company = require('../models/Company');
const Request = require('../models/Request');
const BuyProduct = require('../models/BuyProduct');
const Message = require('../models/Message');
const Review = require('../models/Review');
const mongoose = require('../../../utils/mongoose');
const { Types } = mongoose;
const PRESET_COMPANY_ID = new Types.ObjectId('68fe2ccda3526c303ca06796');
const PRESET_USER_EMAIL = 'admin@test-company.ru';
const changePasswordFlow = async (userId, currentPassword, newPassword) => {
if (!currentPassword || !newPassword) {
return { status: 400, body: { error: 'Current password and new password are required' } };
}
if (typeof newPassword !== 'string' || newPassword.trim().length < 8) {
return { status: 400, body: { error: 'New password must be at least 8 characters long' } };
}
const user = await User.findById(userId);
if (!user) {
return { status: 404, body: { error: 'User not found' } };
}
const isMatch = await user.comparePassword(currentPassword);
if (!isMatch) {
return { status: 400, body: { error: 'Current password is incorrect' } };
}
user.password = newPassword;
user.updatedAt = new Date();
await user.save();
return { status: 200, body: { message: 'Password updated successfully' } };
};
const deleteAccountFlow = async (userId, password) => {
if (!password) {
return { status: 400, body: { error: 'Password is required to delete account' } };
}
const user = await User.findById(userId);
if (!user) {
return { status: 404, body: { error: 'User not found' } };
}
const validPassword = await user.comparePassword(password);
if (!validPassword) {
return { status: 400, body: { error: 'Password is incorrect' } };
}
const companyId = user.companyId ? user.companyId.toString() : null;
const companyObjectId = companyId && Types.ObjectId.isValid(companyId) ? new Types.ObjectId(companyId) : null;
const cleanupTasks = [];
if (companyId) {
cleanupTasks.push(Request.deleteMany({
$or: [{ senderCompanyId: companyId }, { recipientCompanyId: companyId }],
}));
cleanupTasks.push(BuyProduct.deleteMany({ companyId }));
if (companyObjectId) {
cleanupTasks.push(Message.deleteMany({
$or: [
{ senderCompanyId: companyObjectId },
{ recipientCompanyId: companyObjectId },
],
}));
cleanupTasks.push(Review.deleteMany({
$or: [
{ companyId: companyObjectId },
{ authorCompanyId: companyObjectId },
],
}));
}
cleanupTasks.push(Company.findByIdAndDelete(companyId));
}
cleanupTasks.push(User.findByIdAndDelete(user._id));
await Promise.all(cleanupTasks);
return { status: 200, body: { message: 'Account deleted successfully' } };
};
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
const waitForDatabaseConnection = async () => {
const isAuthFailure = (error) => {
if (!error) return false;
if (error.code === 13 || error.code === 18) return true;
return /auth/i.test(String(error.message || ''));
};
const verifyAuth = async () => {
try {
await mongoose.connection.db.admin().command({ listDatabases: 1 });
return true;
} catch (error) {
if (isAuthFailure(error)) {
return false;
}
throw error;
}
};
for (let attempt = 0; attempt < 3; attempt++) {
if (mongoose.connection.readyState === 1) {
const authed = await verifyAuth();
if (authed) {
return;
}
await mongoose.connection.close().catch(() => {});
}
try {
const connection = await connectDB();
if (!connection) {
break;
}
const authed = await verifyAuth();
if (authed) {
return;
}
await mongoose.connection.close().catch(() => {});
} catch (error) {
if (!isAuthFailure(error)) {
throw error;
}
}
}
throw new Error('Unable to authenticate with MongoDB');
};
// Инициализация тестового пользователя
const initializeTestUser = async () => {
try {
await waitForDatabaseConnection();
let company = await Company.findById(PRESET_COMPANY_ID);
if (!company) {
company = await Company.create({
_id: PRESET_COMPANY_ID,
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
inn: '7707083893',
ogrn: '1027700132195',
legalForm: 'ООО',
industry: 'Производство',
companySize: '50-100',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://test-company.ru',
verified: true,
rating: 4.5,
description: 'Ведущая компания в области производства',
slogan: 'Качество и инновация'
});
log('✅ Test company initialized');
} else {
await Company.updateOne(
{ _id: PRESET_COMPANY_ID },
{
$set: {
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
industry: 'Производство',
companySize: '50-100',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://test-company.ru',
},
}
);
}
let existingUser = await User.findOne({ email: PRESET_USER_EMAIL });
if (!existingUser) {
existingUser = await User.create({
email: PRESET_USER_EMAIL,
password: 'SecurePass123!',
firstName: 'Иван',
lastName: 'Петров',
position: 'Генеральный директор',
companyId: PRESET_COMPANY_ID
});
log('✅ Test user initialized');
} else if (!existingUser.companyId || existingUser.companyId.toString() !== PRESET_COMPANY_ID.toString()) {
existingUser.companyId = PRESET_COMPANY_ID;
existingUser.updatedAt = new Date();
await existingUser.save();
log(' Test user company reference was fixed');
}
} catch (error) {
console.error('Error initializing test data:', error.message);
if (error?.code === 13 || /auth/i.test(error?.message || '')) {
try {
await connectDB();
} catch (connectError) {
if (process.env.DEV === 'true') {
console.error('Failed to re-connect after auth error:', connectError.message);
}
}
}
}
};
initializeTestUser();
// Регистрация
router.post('/register', async (req, res) => {
try {
await waitForDatabaseConnection();
const { email, password, firstName, lastName, position, phone, fullName, inn, ogrn, legalForm, industry, companySize, website } = req.body;
// Проверка обязательных полей
if (!email || !password || !firstName || !lastName || !fullName) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Проверка существования пользователя
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({ error: 'User already exists' });
}
// Создать компанию
let company;
try {
company = new Company({
fullName,
shortName: fullName.substring(0, 20),
inn,
ogrn,
legalForm,
industry,
companySize,
website,
verified: false,
rating: 0,
description: '',
slogan: '',
partnerGeography: ['moscow', 'russia_all']
});
const savedCompany = await company.save();
company = savedCompany;
log('✅ Company saved:', company._id, 'Result:', savedCompany ? 'Success' : 'Failed');
} catch (err) {
console.error('Company save error:', err);
return res.status(400).json({ error: 'Failed to create company: ' + err.message });
}
// Создать пользователя
try {
const newUser = await User.create({
email,
password,
firstName,
lastName,
position: position || '',
phone: phone || '',
companyId: company._id
});
log('✅ User created:', newUser._id);
const token = generateToken(newUser._id.toString(), newUser.companyId.toString(), newUser.firstName, newUser.lastName, company.fullName);
return res.status(201).json({
tokens: {
accessToken: token,
refreshToken: token
},
user: {
id: newUser._id.toString(),
email: newUser.email,
firstName: newUser.firstName,
lastName: newUser.lastName,
position: newUser.position,
companyId: newUser.companyId.toString()
},
company: {
id: company._id.toString(),
name: company.fullName,
inn: company.inn
}
});
} catch (err) {
console.error('User creation error:', err);
return res.status(400).json({ error: 'Failed to create user: ' + err.message });
}
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: error.message });
}
});
// Вход
router.post('/login', async (req, res) => {
try {
if (process.env.DEV === 'true') {
console.log('[Auth] /login called');
}
await waitForDatabaseConnection();
if (process.env.DEV === 'true') {
console.log('[Auth] DB ready, running login query');
}
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
if (
user.email === PRESET_USER_EMAIL &&
(!user.companyId || user.companyId.toString() !== PRESET_COMPANY_ID.toString())
) {
await User.updateOne(
{ _id: user._id },
{ $set: { companyId: PRESET_COMPANY_ID, updatedAt: new Date() } }
);
user.companyId = PRESET_COMPANY_ID;
}
// Получить компанию до использования в generateToken
let companyData = null;
try {
companyData = user.companyId ? await Company.findById(user.companyId) : null;
} catch (err) {
console.error('Failed to fetch company:', err.message);
}
if (user.email === PRESET_USER_EMAIL) {
try {
companyData = await Company.findByIdAndUpdate(
PRESET_COMPANY_ID,
{
$set: {
fullName: 'ООО "Тестовая Компания"',
shortName: 'ООО "Тест"',
inn: '7707083893',
ogrn: '1027700132195',
legalForm: 'ООО',
industry: 'Производство',
companySize: '50-100',
partnerGeography: ['moscow', 'russia_all'],
website: 'https://test-company.ru',
verified: true,
rating: 4.5,
description: 'Ведущая компания в области производства',
slogan: 'Качество и инновация',
updatedAt: new Date(),
},
},
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
} catch (err) {
console.error('Failed to ensure preset company:', err.message);
}
}
const token = generateToken(user._id.toString(), user.companyId.toString(), user.firstName, user.lastName, companyData?.fullName || 'Company');
log('✅ Token generated for user:', user._id);
res.json({
tokens: {
accessToken: token,
refreshToken: token
},
user: {
id: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
position: user.position,
companyId: user.companyId.toString()
},
company: companyData ? {
id: companyData._id.toString(),
name: companyData.fullName,
inn: companyData.inn
} : null
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: `LOGIN_ERROR: ${error.message}` });
}
});
// Смена пароля
router.post('/change-password', verifyToken, async (req, res) => {
try {
const { currentPassword, newPassword } = req.body || {};
const result = await changePasswordFlow(req.userId, currentPassword, newPassword);
res.status(result.status).json(result.body);
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({ error: error.message });
}
});
// Удаление аккаунта
router.delete('/account', verifyToken, async (req, res) => {
try {
const { password } = req.body || {};
const result = await deleteAccountFlow(req.userId, password);
res.status(result.status).json(result.body);
} catch (error) {
console.error('Delete account error:', error);
res.status(500).json({ error: error.message });
}
});
// Обновить профиль / универсальные действия
router.patch('/profile', verifyToken, async (req, res) => {
try {
const rawAction = req.body?.action || req.query?.action || req.body?.type;
const payload = req.body?.payload || req.body || {};
const action = typeof rawAction === 'string' ? rawAction : '';
if (action === 'changePassword') {
const result = await changePasswordFlow(req.userId, payload.currentPassword, payload.newPassword);
return res.status(result.status).json(result.body);
}
if (action === 'deleteAccount') {
const result = await deleteAccountFlow(req.userId, payload.password);
return res.status(result.status).json(result.body);
}
if (action === 'updateProfile') {
await waitForDatabaseConnection();
const { firstName, lastName, position, phone } = payload;
if (!firstName && !lastName && !position && !phone) {
return res.status(400).json({ error: 'At least one field must be provided' });
}
const user = await User.findById(req.userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
if (firstName) user.firstName = firstName;
if (lastName) user.lastName = lastName;
if (position !== undefined) user.position = position;
if (phone !== undefined) user.phone = phone;
user.updatedAt = new Date();
await user.save();
const company = user.companyId ? await Company.findById(user.companyId) : null;
return res.json({
message: 'Profile updated successfully',
user: {
id: user._id.toString(),
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
position: user.position,
phone: user.phone,
companyId: user.companyId?.toString()
},
company: company ? {
id: company._id.toString(),
name: company.fullName,
inn: company.inn
} : null
});
}
res.json({ message: 'Profile endpoint' });
} catch (error) {
console.error('Profile update error:', error);
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,220 +0,0 @@
const express = require('express')
const fs = require('fs')
const path = require('path')
const router = express.Router()
const BuyDocument = require('../models/BuyDocument')
// Create remote-assets/docs directory if it doesn't exist
const docsDir = 'server/routers/remote-assets/docs'
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir, { recursive: true })
}
function generateId() {
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`
}
// GET /buy/docs?ownerCompanyId=...
router.get('/docs', async (req, res) => {
try {
const { ownerCompanyId } = req.query
console.log('[BUY API] GET /docs', { ownerCompanyId })
let query = {}
if (ownerCompanyId) {
query.ownerCompanyId = ownerCompanyId
}
const docs = await BuyDocument.find(query).sort({ createdAt: -1 })
const result = docs.map(doc => ({
...doc.toObject(),
url: `/api/buy/docs/${doc.id}/file`
}))
res.json(result)
} catch (error) {
console.error('[BUY API] Error fetching docs:', error)
res.status(500).json({ error: 'Failed to fetch documents' })
}
})
// POST /buy/docs
router.post('/docs', async (req, res) => {
try {
const { ownerCompanyId, name, type, fileData } = req.body || {}
console.log('[BUY API] POST /docs', { ownerCompanyId, name, type })
if (!ownerCompanyId || !name || !type) {
return res.status(400).json({ error: 'ownerCompanyId, name and type are required' })
}
if (!fileData) {
return res.status(400).json({ error: 'fileData is required' })
}
const id = generateId()
// Save file to disk
const binaryData = Buffer.from(fileData, 'base64')
const filePath = `${docsDir}/${id}.${type}`
fs.writeFileSync(filePath, binaryData)
console.log(`[BUY API] File saved to ${filePath}, size: ${binaryData.length} bytes`)
const size = binaryData.length
const doc = await BuyDocument.create({
id,
ownerCompanyId,
name,
type,
size,
filePath,
acceptedBy: []
})
console.log('[BUY API] Document created:', id)
res.status(201).json({
...doc.toObject(),
url: `/api/buy/docs/${doc.id}/file`
})
} catch (e) {
console.error(`[BUY API] Error saving file: ${e.message}`)
res.status(500).json({ error: 'Failed to save file' })
}
})
router.post('/docs/:id/accept', async (req, res) => {
try {
const { id } = req.params
const { companyId } = req.body || {}
console.log('[BUY API] POST /docs/:id/accept', { id, companyId })
if (!companyId) {
return res.status(400).json({ error: 'companyId is required' })
}
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found:', id)
return res.status(404).json({ error: 'Document not found' })
}
if (!doc.acceptedBy.includes(companyId)) {
doc.acceptedBy.push(companyId)
await doc.save()
}
res.json({ id: doc.id, acceptedBy: doc.acceptedBy })
} catch (error) {
console.error('[BUY API] Error accepting document:', error)
res.status(500).json({ error: 'Failed to accept document' })
}
})
router.get('/docs/:id/delete', async (req, res) => {
try {
const { id } = req.params
console.log('[BUY API] GET /docs/:id/delete', { id })
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found for deletion:', id)
return res.status(404).json({ error: 'Document not found' })
}
// Delete file from disk
if (doc.filePath && fs.existsSync(doc.filePath)) {
try {
fs.unlinkSync(doc.filePath)
console.log(`[BUY API] File deleted: ${doc.filePath}`)
} catch (e) {
console.error(`[BUY API] Error deleting file: ${e.message}`)
}
}
await BuyDocument.deleteOne({ id })
console.log('[BUY API] Document deleted via GET:', id)
res.json({ id: doc.id, success: true })
} catch (error) {
console.error('[BUY API] Error deleting document:', error)
res.status(500).json({ error: 'Failed to delete document' })
}
})
router.delete('/docs/:id', async (req, res) => {
try {
const { id } = req.params
console.log('[BUY API] DELETE /docs/:id', { id })
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found for deletion:', id)
return res.status(404).json({ error: 'Document not found' })
}
// Delete file from disk
if (doc.filePath && fs.existsSync(doc.filePath)) {
try {
fs.unlinkSync(doc.filePath)
console.log(`[BUY API] File deleted: ${doc.filePath}`)
} catch (e) {
console.error(`[BUY API] Error deleting file: ${e.message}`)
}
}
await BuyDocument.deleteOne({ id })
console.log('[BUY API] Document deleted:', id)
res.json({ id: doc.id, success: true })
} catch (error) {
console.error('[BUY API] Error deleting document:', error)
res.status(500).json({ error: 'Failed to delete document' })
}
})
// GET /buy/docs/:id/file - Serve the file
router.get('/docs/:id/file', async (req, res) => {
try {
const { id } = req.params
console.log('[BUY API] GET /docs/:id/file', { id })
const doc = await BuyDocument.findOne({ id })
if (!doc) {
console.log('[BUY API] Document not found:', id)
return res.status(404).json({ error: 'Document not found' })
}
const filePath = `${docsDir}/${id}.${doc.type}`
if (!fs.existsSync(filePath)) {
console.log('[BUY API] File not found on disk:', filePath)
return res.status(404).json({ error: 'File not found on disk' })
}
const fileBuffer = fs.readFileSync(filePath)
const mimeTypes = {
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pdf': 'application/pdf'
}
const mimeType = mimeTypes[doc.type] || 'application/octet-stream'
const sanitizedName = doc.name.replace(/[^\w\s\-\.]/g, '_')
res.setHeader('Content-Type', mimeType)
const encodedFilename = encodeURIComponent(`${doc.name}.${doc.type}`)
res.setHeader('Content-Disposition', `attachment; filename="${sanitizedName}.${doc.type}"; filename*=UTF-8''${encodedFilename}`)
res.setHeader('Content-Length', fileBuffer.length)
console.log(`[BUY API] Serving file ${id} from ${filePath} (${fileBuffer.length} bytes)`)
res.send(fileBuffer)
} catch (e) {
console.error(`[BUY API] Error serving file: ${e.message}`)
res.status(500).json({ error: 'Error serving file' })
}
})
module.exports = router

View File

@@ -1,502 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const BuyProduct = require('../models/BuyProduct');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const UPLOADS_ROOT = 'server/routers/remote-assets/uploads/buy-products';
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
ensureDirectory(UPLOADS_ROOT);
const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
const ALLOWED_MIME_TYPES = new Set([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
]);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const productId = req.params.id || 'common';
const productDir = `${UPLOADS_ROOT}/${productId}`;
ensureDirectory(productDir);
cb(null, productDir);
},
filename: (req, file, cb) => {
// Исправляем кодировку имени файла из Latin1 в UTF-8
const fixedName = Buffer.from(file.originalname, 'latin1').toString('utf8');
const originalExtension = path.extname(fixedName) || '';
const baseName = path
.basename(fixedName, originalExtension)
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '_'); // Убираем только недопустимые символы Windows, оставляем кириллицу
cb(null, `${Date.now()}_${baseName}${originalExtension}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: MAX_FILE_SIZE,
},
fileFilter: (req, file, cb) => {
if (ALLOWED_MIME_TYPES.has(file.mimetype)) {
cb(null, true);
return;
}
req.fileValidationError = 'UNSUPPORTED_FILE_TYPE';
cb(null, false);
},
});
const handleSingleFileUpload = (req, res, next) => {
upload.single('file')(req, res, (err) => {
if (err) {
console.error('[BuyProducts] Multer error:', err.message);
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File is too large. Maximum size is 15MB.' });
}
return res.status(400).json({ error: err.message });
}
next();
});
};
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
// GET /buy-products/company/:companyId - получить товары компании
router.get('/company/:companyId', verifyToken, async (req, res) => {
try {
const { companyId } = req.params;
log('[BuyProducts] Fetching products for company:', companyId);
const products = await BuyProduct.find({ companyId })
.sort({ createdAt: -1 })
.exec();
log('[BuyProducts] Found', products.length, 'products for company', companyId);
log('[BuyProducts] Products:', products);
res.json(products);
} catch (error) {
console.error('[BuyProducts] Error fetching products:', error.message);
console.error('[BuyProducts] Error stack:', error.stack);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// POST /buy-products - создать новый товар
router.post('/', verifyToken, async (req, res) => {
try {
const { name, description, quantity, unit, status } = req.body;
log('[BuyProducts] Creating new product:', { name, description, quantity, companyId: req.companyId });
if (!name || !description || !quantity) {
return res.status(400).json({
error: 'name, description, and quantity are required',
});
}
if (description.trim().length < 10) {
return res.status(400).json({
error: 'Description must be at least 10 characters',
});
}
const newProduct = new BuyProduct({
companyId: req.companyId,
name: name.trim(),
description: description.trim(),
quantity: quantity.trim(),
unit: unit || 'шт',
status: status || 'published',
files: [],
});
log('[BuyProducts] Attempting to save product to DB...');
const savedProduct = await newProduct.save();
log('[BuyProducts] New product created successfully:', savedProduct._id);
log('[BuyProducts] Product data:', savedProduct);
res.status(201).json(savedProduct);
} catch (error) {
console.error('[BuyProducts] Error creating product:', error.message);
console.error('[BuyProducts] Error stack:', error.stack);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// PUT /buy-products/:id - обновить товар
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { name, description, quantity, unit, status } = req.body;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// Проверить, что товар принадлежит текущей компании
if (product.companyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
// Обновить поля
if (name) product.name = name.trim();
if (description) product.description = description.trim();
if (quantity) product.quantity = quantity.trim();
if (unit) product.unit = unit;
if (status) product.status = status;
product.updatedAt = new Date();
const updatedProduct = await product.save();
log('[BuyProducts] Product updated:', id);
res.json(updatedProduct);
} catch (error) {
console.error('[BuyProducts] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// DELETE /buy-products/:id - удалить товар
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId.toString() !== req.companyId.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
await BuyProduct.findByIdAndDelete(id);
log('[BuyProducts] Product deleted:', id);
res.json({ message: 'Product deleted successfully' });
} catch (error) {
console.error('[BuyProducts] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// POST /buy-products/:id/files - добавить файл к товару
router.post('/:id/files', verifyToken, handleSingleFileUpload, async (req, res) => {
try {
const { id } = req.params;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// Только владелец товара может добавить файл
const productCompanyId = product.companyId?.toString() || product.companyId;
const requestCompanyId = req.companyId?.toString() || req.companyId;
console.log('[BuyProducts] Comparing company IDs:', {
productCompanyId,
requestCompanyId,
match: productCompanyId === requestCompanyId
});
if (productCompanyId !== requestCompanyId) {
return res.status(403).json({ error: 'Not authorized' });
}
if (req.fileValidationError) {
return res.status(400).json({ error: 'Unsupported file type. Use PDF, DOC, DOCX, XLS, XLSX or CSV.' });
}
if (!req.file) {
return res.status(400).json({ error: 'File is required' });
}
// Исправляем кодировку имени файла из Latin1 в UTF-8
const fixedFileName = Buffer.from(req.file.originalname, 'latin1').toString('utf8');
// Извлекаем timestamp из имени файла, созданного multer (формат: {timestamp}_{name}.ext)
const fileTimestamp = req.file.filename.split('_')[0];
// storagePath относительно UPLOADS_ROOT (который уже включает 'buy-products')
const relativePath = `${id}/${req.file.filename}`;
const file = {
id: `file-${fileTimestamp}`, // Используем тот же timestamp, что и в имени файла
name: fixedFileName,
url: `/uploads/buy-products/${relativePath}`,
type: req.file.mimetype,
size: req.file.size,
uploadedAt: new Date(),
storagePath: relativePath,
};
console.log('[BuyProducts] Adding file to product:', {
productId: id,
fileName: file.name,
fileSize: file.size,
filePath: relativePath
});
console.log('[BuyProducts] File object:', JSON.stringify(file, null, 2));
// Используем findByIdAndUpdate вместо save() для избежания проблем с валидацией
let updatedProduct;
try {
console.log('[BuyProducts] Calling findByIdAndUpdate with id:', id);
updatedProduct = await BuyProduct.findByIdAndUpdate(
id,
{
$push: { files: file },
$set: { updatedAt: new Date() }
},
{ new: true, runValidators: false }
);
console.log('[BuyProducts] findByIdAndUpdate completed');
} catch (updateError) {
console.error('[BuyProducts] findByIdAndUpdate error:', {
message: updateError.message,
name: updateError.name,
code: updateError.code
});
throw updateError;
}
if (!updatedProduct) {
throw new Error('Failed to update product with file');
}
console.log('[BuyProducts] File added successfully to product:', id);
log('[BuyProducts] File added to product:', id, file.name);
res.json(updatedProduct);
} catch (error) {
console.error('[BuyProducts] Error adding file:', error.message);
console.error('[BuyProducts] Error stack:', error.stack);
console.error('[BuyProducts] Error name:', error.name);
if (error.errors) {
console.error('[BuyProducts] Validation errors:', JSON.stringify(error.errors, null, 2));
}
res.status(500).json({
error: 'Internal server error',
message: error.message,
details: error.errors || {},
});
}
});
// DELETE /buy-products/:id/files/:fileId - удалить файл
router.delete('/:id/files/:fileId', verifyToken, async (req, res) => {
try {
const { id, fileId } = req.params;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId.toString() !== req.companyId.toString()) {
return res.status(403).json({ error: 'Not authorized' });
}
const fileToRemove = product.files.find((f) => f.id === fileId);
if (!fileToRemove) {
return res.status(404).json({ error: 'File not found' });
}
product.files = product.files.filter(f => f.id !== fileId);
await product.save();
const storedPath = fileToRemove.storagePath || fileToRemove.url.replace(/^\/uploads\//, '');
const absolutePath = `server/routers/remote-assets/uploads/${storedPath}`;
fs.promises.unlink(absolutePath).catch((unlinkError) => {
if (unlinkError && unlinkError.code !== 'ENOENT') {
console.error('[BuyProducts] Failed to remove file from disk:', unlinkError.message);
}
});
log('[BuyProducts] File deleted from product:', id, fileId);
res.json(product);
} catch (error) {
console.error('[BuyProducts] Error deleting file:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// POST /buy-products/:id/accept - акцептировать товар
router.post('/:id/accept', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const companyId = req.companyId;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// Не можем акцептировать собственный товар
if (product.companyId.toString() === companyId.toString()) {
return res.status(403).json({ error: 'Cannot accept own product' });
}
// Проверить, не акцептировал ли уже
const alreadyAccepted = product.acceptedBy.some(
a => a.companyId.toString() === companyId.toString()
);
if (alreadyAccepted) {
return res.status(400).json({ error: 'Already accepted' });
}
product.acceptedBy.push({
companyId,
acceptedAt: new Date()
});
await product.save();
log('[BuyProducts] Product accepted by company:', companyId);
res.json(product);
} catch (error) {
console.error('[BuyProducts] Error accepting product:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// GET /buy-products/:id/acceptances - получить компании которые акцептовали
router.get('/:id/acceptances', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const product = await BuyProduct.findById(id).populate('acceptedBy.companyId', 'shortName fullName');
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
log('[BuyProducts] Returned acceptances for product:', id);
res.json(product.acceptedBy);
} catch (error) {
console.error('[BuyProducts] Error fetching acceptances:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// GET /buy-products/download/:id/:fileId - скачать файл
router.get('/download/:id/:fileId', verifyToken, async (req, res) => {
try {
console.log('[BuyProducts] Download request received:', {
productId: req.params.id,
fileId: req.params.fileId,
userId: req.userId,
companyId: req.companyId,
headers: req.headers.authorization
});
const { id, fileId } = req.params;
const product = await BuyProduct.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
const file = product.files.find((f) => f.id === fileId);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Создаем абсолютный путь к файлу
const filePath = path.resolve(UPLOADS_ROOT, file.storagePath);
console.log('[BuyProducts] Trying to download file:', {
fileId: file.id,
fileName: file.name,
storagePath: file.storagePath,
absolutePath: filePath,
exists: fs.existsSync(filePath)
});
// Проверяем существование файла
if (!fs.existsSync(filePath)) {
console.error('[BuyProducts] File not found on disk:', filePath);
return res.status(404).json({ error: 'File not found on disk' });
}
// Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы
const encodedFileName = encodeURIComponent(file.name);
res.setHeader('Content-Type', file.type || 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
res.setHeader('Content-Length', file.size);
// Отправляем файл
res.sendFile(filePath, (err) => {
if (err) {
console.error('[BuyProducts] Error sending file:', err.message);
if (!res.headersSent) {
res.status(500).json({ error: 'Error downloading file' });
}
}
});
} catch (error) {
console.error('[BuyProducts] Error downloading file:', error.message);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@@ -1,336 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Company = require('../models/Company');
const Experience = require('../models/Experience');
const Request = require('../models/Request');
const Message = require('../models/Message');
const mongoose = require('../../../utils/mongoose');
const { Types } = mongoose;
// GET /my/info - получить мою компанию (требует авторизации) - ДОЛЖНО быть ПЕРЕД /:id
router.get('/my/info', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const user = await require('../models/User').findById(userId);
if (!user || !user.companyId) {
return res.status(404).json({ error: 'Company not found' });
}
const company = await Company.findById(user.companyId);
if (!company) {
return res.status(404).json({ error: 'Company not found' });
}
res.json({
...company.toObject(),
id: company._id
});
} catch (error) {
console.error('Get my company error:', error);
res.status(500).json({ error: error.message });
}
});
// GET /my/stats - получить статистику компании - ДОЛЖНО быть ПЕРЕД /:id
router.get('/my/stats', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
let companyId = user.companyId;
if (!companyId) {
const fallbackCompany = await Company.create({
fullName: 'Компания пользователя',
shortName: 'Компания пользователя',
verified: false,
partnerGeography: [],
});
user.companyId = fallbackCompany._id;
user.updatedAt = new Date();
await user.save();
companyId = fallbackCompany._id;
}
let company = await Company.findById(companyId);
if (!company) {
company = await Company.create({
_id: companyId,
fullName: 'Компания пользователя',
verified: false,
partnerGeography: [],
});
}
const companyIdString = company._id.toString();
const companyObjectId = Types.ObjectId.isValid(companyIdString)
? new Types.ObjectId(companyIdString)
: null;
const [sentRequests, receivedRequests, unreadMessages] = await Promise.all([
Request.countDocuments({ senderCompanyId: companyIdString }),
Request.countDocuments({ recipientCompanyId: companyIdString }),
companyObjectId
? Message.countDocuments({ recipientCompanyId: companyObjectId, read: false })
: Promise.resolve(0),
]);
// Подсчитываем просмотры профиля из запросов к профилю компании
const profileViews = company?.metrics?.profileViews || 0;
// Получаем статистику за последнюю неделю для изменений
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const sentRequestsLastWeek = await Request.countDocuments({
senderCompanyId: companyIdString,
createdAt: { $gte: weekAgo }
});
const receivedRequestsLastWeek = await Request.countDocuments({
recipientCompanyId: companyIdString,
createdAt: { $gte: weekAgo }
});
const stats = {
profileViews: profileViews,
profileViewsChange: 0, // Можно добавить отслеживание просмотров, если нужно
sentRequests,
sentRequestsChange: sentRequestsLastWeek,
receivedRequests,
receivedRequestsChange: receivedRequestsLastWeek,
newMessages: unreadMessages,
rating: Number.isFinite(company?.rating) ? Number(company.rating) : 0,
};
res.json(stats);
} catch (error) {
console.error('Get company stats error:', error);
res.status(500).json({ error: error.message });
}
});
// GET /:id/experience - получить опыт компании
router.get('/:id/experience', verifyToken, async (req, res) => {
try {
const { id } = req.params;
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const experience = await Experience.find({ companyId: new Types.ObjectId(id) })
.sort({ createdAt: -1 });
res.json(experience.map(exp => ({
...exp.toObject(),
id: exp._id
})));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /:id/experience - добавить опыт компании
router.post('/:id/experience', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { confirmed, customer, subject, volume, contact, comment } = req.body;
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const newExp = await Experience.create({
companyId: new Types.ObjectId(id),
confirmed: confirmed || false,
customer: customer || '',
subject: subject || '',
volume: volume || '',
contact: contact || '',
comment: comment || ''
});
res.status(201).json({
...newExp.toObject(),
id: newExp._id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /:id/experience/:expId - обновить опыт
router.put('/:id/experience/:expId', verifyToken, async (req, res) => {
try {
const { id, expId } = req.params;
if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) {
return res.status(400).json({ error: 'Invalid IDs' });
}
const experience = await Experience.findByIdAndUpdate(
new Types.ObjectId(expId),
{
...req.body,
updatedAt: new Date()
},
{ new: true }
);
if (!experience || experience.companyId.toString() !== id) {
return res.status(404).json({ error: 'Experience not found' });
}
res.json({
...experience.toObject(),
id: experience._id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /:id/experience/:expId - удалить опыт
router.delete('/:id/experience/:expId', verifyToken, async (req, res) => {
try {
const { id, expId } = req.params;
if (!Types.ObjectId.isValid(id) || !Types.ObjectId.isValid(expId)) {
return res.status(400).json({ error: 'Invalid IDs' });
}
const experience = await Experience.findById(new Types.ObjectId(expId));
if (!experience || experience.companyId.toString() !== id) {
return res.status(404).json({ error: 'Experience not found' });
}
await Experience.findByIdAndDelete(new Types.ObjectId(expId));
res.json({ message: 'Experience deleted' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Получить компанию по ID (ДОЛЖНО быть ПОСЛЕ специфичных маршрутов)
router.get('/:id', async (req, res) => {
try {
const company = await Company.findById(req.params.id);
if (!company) {
if (!Types.ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Company not found' });
}
const placeholder = await Company.create({
_id: new Types.ObjectId(req.params.id),
fullName: 'Новая компания',
shortName: 'Новая компания',
verified: false,
partnerGeography: [],
industry: '',
companySize: '',
});
return res.json({
...placeholder.toObject(),
id: placeholder._id,
});
}
// Отслеживаем просмотр профиля (если это не владелец компании)
const userId = req.userId;
if (userId) {
const User = require('../models/User');
const user = await User.findById(userId);
if (user && user.companyId && user.companyId.toString() !== company._id.toString()) {
// Инкрементируем просмотры профиля
if (!company.metrics) {
company.metrics = {};
}
if (!company.metrics.profileViews) {
company.metrics.profileViews = 0;
}
company.metrics.profileViews = (company.metrics.profileViews || 0) + 1;
await company.save();
}
}
res.json({
...company.toObject(),
id: company._id
});
} catch (error) {
console.error('Get company error:', error);
res.status(500).json({ error: error.message });
}
});
// Обновить компанию (требует авторизации)
const updateCompanyHandler = async (req, res) => {
try {
const company = await Company.findByIdAndUpdate(
req.params.id,
{ ...req.body, updatedAt: new Date() },
{ new: true }
);
if (!company) {
return res.status(404).json({ error: 'Company not found' });
}
res.json({
...company.toObject(),
id: company._id
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};
router.put('/:id', verifyToken, updateCompanyHandler);
router.patch('/:id', verifyToken, updateCompanyHandler);
// Поиск с AI анализом
router.post('/ai-search', async (req, res) => {
try {
const { query } = req.body;
if (!query) {
return res.status(400).json({ error: 'Query required' });
}
const q = query.toLowerCase();
const result = await Company.find({
$or: [
{ fullName: { $regex: q, $options: 'i' } },
{ shortName: { $regex: q, $options: 'i' } },
{ industry: { $regex: q, $options: 'i' } }
]
});
res.json({
companies: result.map(c => ({
...c.toObject(),
id: c._id
})),
total: result.length,
aiSuggestion: `Found ${result.length} companies matching "${query}"`
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,134 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Experience = require('../models/Experience');
const mongoose = require('../../../utils/mongoose');
const { Types } = mongoose;
// GET /experience - Получить список опыта работы компании
router.get('/', verifyToken, async (req, res) => {
try {
const { companyId } = req.query;
if (!companyId) {
return res.status(400).json({ error: 'companyId is required' });
}
if (!Types.ObjectId.isValid(companyId)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const companyExperiences = await Experience.find({
companyId: new Types.ObjectId(companyId)
}).sort({ createdAt: -1 });
res.json(companyExperiences.map(exp => ({
...exp.toObject(),
id: exp._id
})));
} catch (error) {
console.error('Get experience error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// POST /experience - Создать запись опыта работы
router.post('/', verifyToken, async (req, res) => {
try {
const { companyId, data } = req.body;
if (!companyId || !data) {
return res.status(400).json({ error: 'companyId and data are required' });
}
if (!Types.ObjectId.isValid(companyId)) {
return res.status(400).json({ error: 'Invalid company ID' });
}
const { confirmed, customer, subject, volume, contact, comment } = data;
if (!customer || !subject) {
return res.status(400).json({ error: 'customer and subject are required' });
}
const newExperience = await Experience.create({
companyId: new Types.ObjectId(companyId),
confirmed: confirmed || false,
customer,
subject,
volume: volume || '',
contact: contact || '',
comment: comment || ''
});
res.status(201).json({
...newExperience.toObject(),
id: newExperience._id
});
} catch (error) {
console.error('Create experience error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// PUT /experience/:id - Обновить запись опыта работы
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const { data } = req.body;
if (!data) {
return res.status(400).json({ error: 'data is required' });
}
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid experience ID' });
}
const updatedExperience = await Experience.findByIdAndUpdate(
new Types.ObjectId(id),
{
...data,
updatedAt: new Date()
},
{ new: true }
);
if (!updatedExperience) {
return res.status(404).json({ error: 'Experience not found' });
}
res.json({
...updatedExperience.toObject(),
id: updatedExperience._id
});
} catch (error) {
console.error('Update experience error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE /experience/:id - Удалить запись опыта работы
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
if (!Types.ObjectId.isValid(id)) {
return res.status(400).json({ error: 'Invalid experience ID' });
}
const deletedExperience = await Experience.findByIdAndDelete(new Types.ObjectId(id));
if (!deletedExperience) {
return res.status(404).json({ error: 'Experience not found' });
}
res.json({ message: 'Experience deleted successfully' });
} catch (error) {
console.error('Delete experience error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;

View File

@@ -1,137 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const BuyProduct = require('../models/BuyProduct');
const Request = require('../models/Request');
// Получить агрегированные данные для главной страницы
router.get('/aggregates', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({
docsCount: 0,
acceptsCount: 0,
requestsCount: 0
});
}
const companyId = user.companyId.toString();
// Получить все BuyProduct для подсчета файлов и акцептов
const buyProducts = await BuyProduct.find({ companyId });
// Подсчет документов - сумма всех файлов во всех BuyProduct
const docsCount = buyProducts.reduce((total, product) => {
return total + (product.files ? product.files.length : 0);
}, 0);
// Подсчет акцептов - сумма всех acceptedBy во всех BuyProduct
const acceptsCount = buyProducts.reduce((total, product) => {
return total + (product.acceptedBy ? product.acceptedBy.length : 0);
}, 0);
// Подсчет исходящих запросов (только отправленные этой компанией)
const requestsCount = await Request.countDocuments({
senderCompanyId: companyId
});
res.json({
docsCount,
acceptsCount,
requestsCount
});
} catch (error) {
console.error('Error getting aggregates:', error);
res.status(500).json({ error: error.message });
}
});
// Получить статистику компании
router.get('/stats', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const Company = require('../models/Company');
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({
profileViews: 0,
profileViewsChange: 0,
sentRequests: 0,
sentRequestsChange: 0,
receivedRequests: 0,
receivedRequestsChange: 0,
newMessages: 0,
rating: 0
});
}
const companyId = user.companyId.toString();
const company = await Company.findById(user.companyId);
const sentRequests = await Request.countDocuments({ senderCompanyId: companyId });
const receivedRequests = await Request.countDocuments({ recipientCompanyId: companyId });
res.json({
profileViews: company?.metrics?.profileViews || 0,
profileViewsChange: 0,
sentRequests,
sentRequestsChange: 0,
receivedRequests,
receivedRequestsChange: 0,
newMessages: 0,
rating: company?.rating || 0
});
} catch (error) {
console.error('Error getting stats:', error);
res.status(500).json({ error: error.message });
}
});
// Получить рекомендации партнеров (AI)
router.get('/recommendations', verifyToken, async (req, res) => {
try {
const userId = req.userId;
const User = require('../models/User');
const Company = require('../models/Company');
const user = await User.findById(userId);
if (!user || !user.companyId) {
return res.json({
recommendations: [],
message: 'No recommendations available'
});
}
// Получить компании кроме текущей
const companies = await Company.find({
_id: { $ne: user.companyId }
})
.sort({ rating: -1 })
.limit(5);
const recommendations = companies.map(company => ({
id: company._id.toString(),
name: company.fullName || company.shortName,
industry: company.industry,
logo: company.logo,
matchScore: company.rating ? Math.min(100, Math.round(company.rating * 20)) : 50,
reason: 'Matches your industry'
}));
res.json({
recommendations,
message: recommendations.length > 0 ? 'Recommendations available' : 'No recommendations available'
});
} catch (error) {
console.error('Error getting recommendations:', error);
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,263 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Message = require('../models/Message');
const mongoose = require('../../../utils/mongoose');
const { ObjectId } = mongoose.Types;
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
// GET /messages/threads - получить все потоки для компании
router.get('/threads', verifyToken, async (req, res) => {
try {
const companyId = req.companyId;
log('[Messages] Fetching threads for companyId:', companyId, 'type:', typeof companyId);
// Преобразовать в ObjectId если это строка
let companyObjectId = companyId;
let companyIdString = companyId.toString ? companyId.toString() : companyId;
try {
if (typeof companyId === 'string' && ObjectId.isValid(companyId)) {
companyObjectId = new ObjectId(companyId);
}
} catch (e) {
log('[Messages] Could not convert to ObjectId:', e.message);
}
log('[Messages] Using companyObjectId:', companyObjectId, 'companyIdString:', companyIdString);
// Получить все сообщения где текущая компания отправитель или получатель
// Поддерживаем оба формата - ObjectId и строки
const allMessages = await Message.find({
$or: [
{ senderCompanyId: companyObjectId },
{ senderCompanyId: companyIdString },
{ recipientCompanyId: companyObjectId },
{ recipientCompanyId: companyIdString },
// Также ищем по threadId который может содержать ID компании
{ threadId: { $regex: companyIdString } }
]
})
.sort({ timestamp: -1 })
.limit(500);
log('[Messages] Found', allMessages.length, 'messages for company');
if (allMessages.length === 0) {
log('[Messages] No messages found');
res.json([]);
return;
}
// Группируем по потокам и берем последнее сообщение каждого потока
const threadsMap = new Map();
allMessages.forEach(msg => {
const threadId = msg.threadId;
if (!threadsMap.has(threadId)) {
threadsMap.set(threadId, {
threadId,
lastMessage: msg.text,
lastMessageAt: msg.timestamp,
senderCompanyId: msg.senderCompanyId,
recipientCompanyId: msg.recipientCompanyId
});
}
});
const threads = Array.from(threadsMap.values()).sort((a, b) =>
new Date(b.lastMessageAt) - new Date(a.lastMessageAt)
);
log('[Messages] Returned', threads.length, 'unique threads');
res.json(threads);
} catch (error) {
console.error('[Messages] Error fetching threads:', error.message, error.stack);
res.status(500).json({ error: error.message });
}
});
// GET /messages/:threadId - получить сообщения потока
router.get('/:threadId', verifyToken, async (req, res) => {
try {
const { threadId } = req.params;
const companyId = req.companyId;
// Получить все сообщения потока
const threadMessages = await Message.find({ threadId })
.sort({ timestamp: 1 })
.exec();
// Отметить сообщения как прочитанные для текущей компании
await Message.updateMany(
{ threadId, recipientCompanyId: companyId, read: false },
{ read: true }
);
log('[Messages] Returned', threadMessages.length, 'messages for thread', threadId);
res.json(threadMessages);
} catch (error) {
console.error('[Messages] Error fetching messages:', error.message);
res.status(500).json({ error: error.message });
}
});
// POST /messages/:threadId - добавить сообщение в поток
router.post('/:threadId', verifyToken, async (req, res) => {
try {
const { threadId } = req.params;
const { text, senderCompanyId } = req.body;
if (!text || !threadId) {
return res.status(400).json({ error: 'Text and threadId required' });
}
// Определить получателя на основе threadId
// threadId формат: "thread-id1-id2"
const threadParts = threadId.replace('thread-', '').split('-');
let recipientCompanyId = null;
const currentSender = senderCompanyId || req.companyId;
const currentSenderString = currentSender.toString ? currentSender.toString() : currentSender;
if (threadParts.length >= 2) {
const companyId1 = threadParts[0];
const companyId2 = threadParts[1];
// Получатель - это другая сторона
recipientCompanyId = currentSenderString === companyId1 ? companyId2 : companyId1;
}
log('[Messages] POST /messages/:threadId');
log('[Messages] threadId:', threadId);
log('[Messages] Sender:', currentSender);
log('[Messages] SenderString:', currentSenderString);
log('[Messages] Recipient:', recipientCompanyId);
// Найти recipientCompanyId по ObjectId если нужно
let recipientObjectId = recipientCompanyId;
try {
if (typeof recipientCompanyId === 'string' && ObjectId.isValid(recipientCompanyId)) {
recipientObjectId = new ObjectId(recipientCompanyId);
}
} catch (e) {
log('[Messages] Could not convert recipientId to ObjectId');
}
const message = new Message({
threadId,
senderCompanyId: currentSender,
recipientCompanyId: recipientObjectId,
text: text.trim(),
read: false,
timestamp: new Date()
});
const savedMessage = await message.save();
log('[Messages] New message created:', savedMessage._id);
log('[Messages] Message data:', {
threadId: savedMessage.threadId,
senderCompanyId: savedMessage.senderCompanyId,
recipientCompanyId: savedMessage.recipientCompanyId
});
res.status(201).json(savedMessage);
} catch (error) {
console.error('[Messages] Error creating message:', error.message, error.stack);
res.status(500).json({ error: error.message });
}
});
// MIGRATION ENDPOINT - Fix recipientCompanyId for all messages
router.post('/admin/migrate-fix-recipients', async (req, res) => {
try {
const allMessages = await Message.find().exec();
log('[Messages] Migrating', allMessages.length, 'messages...');
let fixedCount = 0;
let errorCount = 0;
for (const message of allMessages) {
try {
const threadId = message.threadId;
if (!threadId) continue;
// Parse threadId формат "thread-id1-id2" или "id1-id2"
const ids = threadId.replace('thread-', '').split('-');
if (ids.length < 2) {
errorCount++;
continue;
}
const companyId1 = ids[0];
const companyId2 = ids[1];
// Compare with senderCompanyId
const senderIdString = message.senderCompanyId.toString ? message.senderCompanyId.toString() : message.senderCompanyId;
const expectedRecipient = senderIdString === companyId1 ? companyId2 : companyId1;
// If recipientCompanyId is not set or wrong - fix it
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
let recipientObjectId = expectedRecipient;
try {
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
recipientObjectId = new ObjectId(expectedRecipient);
}
} catch (e) {
// continue
}
await Message.updateOne(
{ _id: message._id },
{ recipientCompanyId: recipientObjectId }
);
fixedCount++;
}
} catch (err) {
console.error('[Messages] Migration error:', err.message);
errorCount++;
}
}
log('[Messages] Migration completed! Fixed:', fixedCount, 'Errors:', errorCount);
res.json({ success: true, fixed: fixedCount, errors: errorCount, total: allMessages.length });
} catch (error) {
console.error('[Messages] Migration error:', error.message);
res.status(500).json({ error: error.message });
}
});
// DEBUG ENDPOINT
router.get('/debug/all-messages', async (req, res) => {
try {
const allMessages = await Message.find().limit(10).exec();
log('[Debug] Total messages in DB:', allMessages.length);
const info = allMessages.map(m => ({
_id: m._id,
threadId: m.threadId,
senderCompanyId: m.senderCompanyId?.toString ? m.senderCompanyId.toString() : m.senderCompanyId,
recipientCompanyId: m.recipientCompanyId?.toString ? m.recipientCompanyId.toString() : m.recipientCompanyId,
text: m.text.substring(0, 30)
}));
res.json({ totalCount: allMessages.length, messages: info });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,175 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Product = require('../models/Product');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
// Helper to transform _id to id
const transformProduct = (doc) => {
if (!doc) return null;
const obj = doc.toObject ? doc.toObject() : doc;
return {
...obj,
id: obj._id,
_id: undefined
};
};
// GET /products - Получить список продуктов/услуг компании (текущего пользователя)
router.get('/', verifyToken, async (req, res) => {
try {
const companyId = req.companyId;
log('[Products] GET Fetching products for companyId:', companyId);
const products = await Product.find({ companyId })
.sort({ createdAt: -1 })
.exec();
log('[Products] Found', products.length, 'products');
res.json(products.map(transformProduct));
} catch (error) {
console.error('[Products] Get error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// POST /products - Создать продукт/услугу
router.post('/', verifyToken, async (req, res) => {
// try {
const { name, category, description, type, productUrl, price, unit, minOrder } = req.body;
const companyId = req.companyId;
log('[Products] POST Creating product:', { name, category, type });
// // Валидация
// if (!name || !category || !description || !type) {
// return res.status(400).json({ error: 'name, category, description, and type are required' });
// }
// if (description.length < 20) {
// return res.status(400).json({ error: 'Description must be at least 20 characters' });
// }
const newProduct = new Product({
name: name.trim(),
category: category.trim(),
description: description.trim(),
type,
productUrl: productUrl || '',
companyId,
price: price || '',
unit: unit || '',
minOrder: minOrder || ''
});
const savedProduct = await newProduct.save();
log('[Products] Product created with ID:', savedProduct._id);
res.status(201).json(transformProduct(savedProduct));
// } catch (error) {
// console.error('[Products] Create error:', error.message);
// res.status(500).json({ error: 'Internal server error', message: error.message });
// }
});
// PUT /products/:id - Обновить продукт/услугу
router.put('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const companyId = req.companyId;
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
// Проверить, что продукт принадлежит текущей компании
if (product.companyId !== companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
const updatedProduct = await Product.findByIdAndUpdate(
id,
{ ...updates, updatedAt: new Date() },
{ new: true, runValidators: true }
);
log('[Products] Product updated:', id);
res.json(transformProduct(updatedProduct));
} catch (error) {
console.error('[Products] Update error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// PATCH /products/:id - Частичное обновление продукта/услуги
router.patch('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const companyId = req.companyId;
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId !== companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
const updatedProduct = await Product.findByIdAndUpdate(
id,
{ ...updates, updatedAt: new Date() },
{ new: true, runValidators: true }
);
log('[Products] Product patched:', id);
res.json(transformProduct(updatedProduct));
} catch (error) {
console.error('[Products] Patch error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// DELETE /products/:id - Удалить продукт/услугу
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const companyId = req.companyId;
const product = await Product.findById(id);
if (!product) {
return res.status(404).json({ error: 'Product not found' });
}
if (product.companyId !== companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
await Product.findByIdAndDelete(id);
log('[Products] Product deleted:', id);
res.json({ message: 'Product deleted successfully' });
} catch (error) {
console.error('[Products] Delete error:', error.message);
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
module.exports = router;

View File

@@ -1,563 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Request = require('../models/Request');
const BuyProduct = require('../models/BuyProduct');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const mongoose = require('../../../utils/mongoose');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
const REQUESTS_UPLOAD_ROOT = 'server/routers/remote-assets/uploads/requests';
const ensureDirectory = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
ensureDirectory(REQUESTS_UPLOAD_ROOT);
const MAX_REQUEST_FILE_SIZE = 20 * 1024 * 1024; // 20MB
const ALLOWED_REQUEST_MIME_TYPES = new Set([
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv',
]);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const subfolder = req.requestUploadSubfolder || '';
const destinationDir = `${REQUESTS_UPLOAD_ROOT}/${subfolder}`;
ensureDirectory(destinationDir);
cb(null, destinationDir);
},
filename: (req, file, cb) => {
const extension = path.extname(file.originalname) || '';
const baseName = path
.basename(file.originalname, extension)
.replace(/[^a-zA-Z0-9-_]+/g, '_')
.toLowerCase();
cb(null, `${Date.now()}_${baseName}${extension}`);
},
});
const upload = multer({
storage,
limits: {
fileSize: MAX_REQUEST_FILE_SIZE,
},
fileFilter: (req, file, cb) => {
if (ALLOWED_REQUEST_MIME_TYPES.has(file.mimetype)) {
cb(null, true);
return;
}
if (!req.invalidFiles) {
req.invalidFiles = [];
}
req.invalidFiles.push(file.originalname);
cb(null, false);
},
});
const handleFilesUpload = (fieldName, subfolderResolver, maxCount = 10) => (req, res, next) => {
req.invalidFiles = [];
req.requestUploadSubfolder = subfolderResolver(req);
upload.array(fieldName, maxCount)(req, res, (err) => {
if (err) {
console.error('[Requests] Multer error:', err.message);
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File is too large. Maximum size is 20MB.' });
}
return res.status(400).json({ error: err.message });
}
next();
});
};
const cleanupUploadedFiles = async (req) => {
if (!Array.isArray(req.files) || req.files.length === 0) {
return;
}
const subfolder = req.requestUploadSubfolder || '';
const removalTasks = req.files.map((file) => {
const filePath = `${REQUESTS_UPLOAD_ROOT}/${subfolder}/${file.filename}`;
return fs.promises.unlink(filePath).catch((error) => {
if (error.code !== 'ENOENT') {
console.error('[Requests] Failed to cleanup uploaded file:', error.message);
}
});
});
await Promise.all(removalTasks);
};
const mapFilesToMetadata = (req) => {
if (!Array.isArray(req.files) || req.files.length === 0) {
return [];
}
const subfolder = req.requestUploadSubfolder || '';
return req.files.map((file) => {
const relativePath = `requests/${subfolder}/${file.filename}`;
return {
id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.originalname,
url: `/uploads/${relativePath}`,
type: file.mimetype,
size: file.size,
uploadedAt: new Date(),
storagePath: relativePath,
};
});
};
const normalizeToArray = (value) => {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
} catch (error) {
// ignore JSON parse errors
}
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean);
};
const removeStoredFiles = async (files = []) => {
if (!files || files.length === 0) {
return;
}
const tasks = files
.filter((file) => file && file.storagePath)
.map((file) => {
const absolutePath = `server/routers/remote-assets/uploads/${file.storagePath}`;
return fs.promises.unlink(absolutePath).catch((error) => {
if (error.code !== 'ENOENT') {
console.error('[Requests] Failed to remove stored file:', error.message);
}
});
});
await Promise.all(tasks);
};
// GET /requests/sent - получить отправленные запросы
router.get('/sent', verifyToken, async (req, res) => {
try {
const companyId = req.companyId;
if (!companyId) {
return res.status(400).json({ error: 'Company ID is required' });
}
const requests = await Request.find({ senderCompanyId: companyId })
.sort({ createdAt: -1 })
.exec();
log('[Requests] Returned', requests.length, 'sent requests for company', companyId);
res.json(requests);
} catch (error) {
console.error('[Requests] Error fetching sent requests:', error.message);
res.status(500).json({ error: error.message });
}
});
// GET /requests/received - получить полученные запросы
router.get('/received', verifyToken, async (req, res) => {
try {
const companyId = req.companyId;
if (!companyId) {
return res.status(400).json({ error: 'Company ID is required' });
}
const requests = await Request.find({ recipientCompanyId: companyId })
.sort({ createdAt: -1 })
.exec();
log('[Requests] Returned', requests.length, 'received requests for company', companyId);
res.json(requests);
} catch (error) {
console.error('[Requests] Error fetching received requests:', error.message);
res.status(500).json({ error: error.message });
}
});
// POST /requests - создать запрос
router.post(
'/',
verifyToken,
handleFilesUpload('files', (req) => `sent/${(req.companyId || 'unknown').toString()}`, 10),
async (req, res) => {
try {
const senderCompanyId = req.companyId;
const recipients = normalizeToArray(req.body.recipientCompanyIds);
const text = (req.body.text || '').trim();
const productId = req.body.productId ? String(req.body.productId) : null;
let subject = (req.body.subject || '').trim();
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
return res.status(400).json({
error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.',
details: req.invalidFiles,
});
}
if (!text) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Request text is required' });
}
if (!recipients.length) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'At least one recipient is required' });
}
let uploadedFiles = mapFilesToMetadata(req);
console.log('========================');
console.log('[Requests] Initial uploadedFiles:', uploadedFiles.length);
console.log('[Requests] ProductId:', productId);
// Если есть productId, получаем данные товара
if (productId) {
try {
const product = await BuyProduct.findById(productId);
console.log('[Requests] Product found:', product ? product.name : 'null');
console.log('[Requests] Product files count:', product?.files?.length || 0);
if (product && product.files) {
console.log('[Requests] Product files:', JSON.stringify(product.files, null, 2));
}
if (product) {
// Берем subject из товара, если не указан
if (!subject) {
subject = product.name;
}
// Если файлы не загружены вручную, используем файлы из товара
if (uploadedFiles.length === 0 && product.files && product.files.length > 0) {
console.log('[Requests] ✅ Copying files from product...');
// Копируем файлы из товара, изменяя путь для запроса
uploadedFiles = product.files.map(file => ({
id: file.id || `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
name: file.name,
url: file.url,
type: file.type,
size: file.size,
uploadedAt: file.uploadedAt || new Date(),
storagePath: file.storagePath || file.url.replace('/uploads/', ''),
}));
console.log('[Requests] ✅ Using', uploadedFiles.length, 'files from product:', productId);
console.log('[Requests] ✅ Copied files:', JSON.stringify(uploadedFiles, null, 2));
} else {
console.log('[Requests] ❌ NOT copying files. uploadedFiles.length:', uploadedFiles.length, 'product.files.length:', product.files?.length || 0);
}
}
} catch (lookupError) {
console.error('[Requests] ❌ Failed to lookup product:', lookupError.message);
console.error(lookupError.stack);
}
}
console.log('[Requests] Final uploadedFiles for saving:', JSON.stringify(uploadedFiles, null, 2));
console.log('========================');
if (!subject) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Subject is required' });
}
const results = [];
for (const recipientCompanyId of recipients) {
try {
const request = new Request({
senderCompanyId,
recipientCompanyId,
text,
productId,
subject,
files: uploadedFiles,
responseFiles: [],
status: 'pending',
});
await request.save();
results.push({
companyId: recipientCompanyId,
success: true,
message: 'Request sent successfully',
});
log('[Requests] Request sent to company:', recipientCompanyId);
} catch (err) {
console.error('[Requests] Error storing request for company:', recipientCompanyId, err.message);
results.push({
companyId: recipientCompanyId,
success: false,
message: err.message,
});
}
}
const createdAt = new Date();
res.status(201).json({
id: 'bulk-' + Date.now(),
text,
subject,
productId,
files: uploadedFiles,
result: results,
createdAt,
});
} catch (error) {
console.error('[Requests] Error creating request:', error.message);
res.status(500).json({ error: error.message });
}
}
);
// PUT /requests/:id - ответить на запрос
router.put(
'/:id',
verifyToken,
handleFilesUpload('responseFiles', (req) => `responses/${req.params.id || 'unknown'}`, 5),
async (req, res) => {
try {
const { id } = req.params;
console.log('[Requests] PUT /requests/:id called with id:', id);
console.log('[Requests] Request body:', req.body);
console.log('[Requests] Files:', req.files);
console.log('[Requests] CompanyId:', req.companyId);
const responseText = (req.body.response || '').trim();
const statusRaw = (req.body.status || 'accepted').toLowerCase();
const status = statusRaw === 'rejected' ? 'rejected' : 'accepted';
console.log('[Requests] Response text:', responseText);
console.log('[Requests] Status:', status);
if (req.invalidFiles && req.invalidFiles.length > 0) {
await cleanupUploadedFiles(req);
return res.status(400).json({
error: 'Unsupported file type. Allowed formats: PDF, DOC, DOCX, XLS, XLSX, CSV.',
details: req.invalidFiles,
});
}
if (!responseText) {
await cleanupUploadedFiles(req);
return res.status(400).json({ error: 'Response text is required' });
}
const request = await Request.findById(id);
if (!request) {
await cleanupUploadedFiles(req);
return res.status(404).json({ error: 'Request not found' });
}
if (request.recipientCompanyId !== req.companyId) {
await cleanupUploadedFiles(req);
return res.status(403).json({ error: 'Not authorized' });
}
const uploadedResponseFiles = mapFilesToMetadata(req);
console.log('[Requests] Uploaded response files count:', uploadedResponseFiles.length);
console.log('[Requests] Uploaded response files:', JSON.stringify(uploadedResponseFiles, null, 2));
if (uploadedResponseFiles.length > 0) {
await removeStoredFiles(request.responseFiles || []);
request.responseFiles = uploadedResponseFiles;
}
request.response = responseText;
request.status = status;
request.respondedAt = new Date();
request.updatedAt = new Date();
let savedRequest;
try {
savedRequest = await request.save();
log('[Requests] Request responded:', id);
} catch (saveError) {
console.error('[Requests] Mongoose save failed, trying direct MongoDB update:', saveError.message);
// Fallback: использовать MongoDB драйвер напрямую
const updateData = {
response: responseText,
status: status,
respondedAt: new Date(),
updatedAt: new Date()
};
if (uploadedResponseFiles.length > 0) {
updateData.responseFiles = uploadedResponseFiles;
}
const result = await mongoose.connection.collection('requests').findOneAndUpdate(
{ _id: new mongoose.Types.ObjectId(id) },
{ $set: updateData },
{ returnDocument: 'after' }
);
if (!result) {
throw new Error('Failed to update request');
}
savedRequest = result;
log('[Requests] Request responded via direct MongoDB update:', id);
}
res.json(savedRequest);
} catch (error) {
console.error('[Requests] Error responding to request:', error.message);
console.error('[Requests] Error stack:', error.stack);
if (error.name === 'ValidationError') {
console.error('[Requests] Validation errors:', JSON.stringify(error.errors, null, 2));
}
res.status(500).json({ error: error.message });
}
}
);
// GET /requests/download/:id/:fileId - скачать файл ответа
router.get('/download/:id/:fileId', verifyToken, async (req, res) => {
try {
console.log('[Requests] Download request received:', {
requestId: req.params.id,
fileId: req.params.fileId,
userId: req.userId,
companyId: req.companyId,
});
const { id, fileId } = req.params;
const request = await Request.findById(id);
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Проверяем, что пользователь имеет доступ к запросу (отправитель или получатель)
if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
// Ищем файл в responseFiles или в обычных files
let file = request.responseFiles?.find((f) => f.id === fileId);
if (!file) {
file = request.files?.find((f) => f.id === fileId);
}
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Создаем абсолютный путь к файлу
// Если storagePath не начинается с 'requests/', значит это файл из buy-products
let fullPath = file.storagePath;
if (!fullPath.startsWith('requests/')) {
fullPath = `buy-products/${fullPath}`;
}
const filePath = path.resolve(`server/routers/remote-assets/uploads/${fullPath}`);
console.log('[Requests] Trying to download file:', {
fileId: file.id,
fileName: file.name,
storagePath: file.storagePath,
absolutePath: filePath,
exists: fs.existsSync(filePath),
});
// Проверяем существование файла
if (!fs.existsSync(filePath)) {
console.error('[Requests] File not found on disk:', filePath);
return res.status(404).json({ error: 'File not found on disk' });
}
// Устанавливаем правильные заголовки для скачивания с поддержкой кириллицы
const encodedFileName = encodeURIComponent(file.name);
res.setHeader('Content-Type', file.type || 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`);
res.setHeader('Content-Length', file.size);
// Отправляем файл
res.sendFile(filePath, (err) => {
if (err) {
console.error('[Requests] Error sending file:', err.message);
if (!res.headersSent) {
res.status(500).json({ error: 'Error sending file' });
}
} else {
log('[Requests] File downloaded:', file.name);
}
});
} catch (error) {
console.error('[Requests] Error downloading file:', error.message);
if (!res.headersSent) {
res.status(500).json({ error: error.message });
}
}
});
// DELETE /requests/:id - удалить запрос
router.delete('/:id', verifyToken, async (req, res) => {
try {
const { id } = req.params;
const request = await Request.findById(id);
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Может удалить отправитель или получатель
if (request.senderCompanyId !== req.companyId && request.recipientCompanyId !== req.companyId) {
return res.status(403).json({ error: 'Not authorized' });
}
await removeStoredFiles(request.files || []);
await removeStoredFiles(request.responseFiles || []);
await Request.findByIdAndDelete(id);
log('[Requests] Request deleted:', id);
res.json({ message: 'Request deleted successfully' });
} catch (error) {
console.error('[Requests] Error deleting request:', error.message);
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -1,145 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Review = require('../models/Review');
const Company = require('../models/Company');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
// Функция для пересчета рейтинга компании
const updateCompanyRating = async (companyId) => {
try {
const reviews = await Review.find({ companyId });
if (reviews.length === 0) {
await Company.findByIdAndUpdate(companyId, {
rating: 0,
reviews: 0,
updatedAt: new Date()
});
return;
}
const totalRating = reviews.reduce((sum, review) => sum + review.rating, 0);
const averageRating = totalRating / reviews.length;
await Company.findByIdAndUpdate(companyId, {
rating: averageRating,
reviews: reviews.length,
updatedAt: new Date()
});
log('[Reviews] Updated company rating:', companyId, 'New rating:', averageRating);
} catch (error) {
console.error('[Reviews] Error updating company rating:', error.message);
}
};
// GET /reviews/company/:companyId - получить отзывы компании
router.get('/company/:companyId', verifyToken, async (req, res) => {
try {
const { companyId } = req.params;
const companyReviews = await Review.find({ companyId })
.sort({ createdAt: -1 })
.exec();
log('[Reviews] Returned', companyReviews.length, 'reviews for company', companyId);
res.json(companyReviews);
} catch (error) {
console.error('[Reviews] Error fetching reviews:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message,
});
}
});
// POST /reviews - создать новый отзыв
router.post('/', verifyToken, async (req, res) => {
try {
const { companyId, rating, comment } = req.body;
if (!companyId || !rating || !comment) {
return res.status(400).json({
error: 'Заполните все обязательные поля: компания, рейтинг и комментарий',
});
}
if (rating < 1 || rating > 5) {
return res.status(400).json({
error: 'Рейтинг должен быть от 1 до 5',
});
}
const trimmedComment = comment.trim();
if (trimmedComment.length < 10) {
return res.status(400).json({
error: 'Отзыв должен содержать минимум 10 символов',
});
}
if (trimmedComment.length > 1000) {
return res.status(400).json({
error: 'Отзыв не должен превышать 1000 символов',
});
}
// Получить данные пользователя из БД для актуальной информации
const User = require('../models/User');
const Company = require('../models/Company');
const user = await User.findById(req.userId);
const userCompany = user && user.companyId ? await Company.findById(user.companyId) : null;
if (!user) {
return res.status(404).json({
error: 'Пользователь не найден',
});
}
// Создать новый отзыв
const newReview = new Review({
companyId,
authorCompanyId: user.companyId || req.companyId,
authorName: user.firstName && user.lastName
? `${user.firstName} ${user.lastName}`
: req.user?.firstName && req.user?.lastName
? `${req.user.firstName} ${req.user.lastName}`
: 'Аноним',
authorCompany: userCompany?.fullName || userCompany?.shortName || req.user?.companyName || 'Компания',
rating: parseInt(rating),
comment: trimmedComment,
verified: true,
createdAt: new Date(),
updatedAt: new Date()
});
const savedReview = await newReview.save();
log('[Reviews] New review created:', savedReview._id);
// Пересчитываем рейтинг компании
await updateCompanyRating(companyId);
res.status(201).json(savedReview);
} catch (error) {
console.error('[Reviews] Error creating review:', error.message);
res.status(500).json({
error: 'Ошибка при сохранении отзыва',
message: error.message,
});
}
});
module.exports = router;

View File

@@ -1,337 +0,0 @@
const express = require('express');
const router = express.Router();
const { verifyToken } = require('../middleware/auth');
const Company = require('../models/Company');
// Функция для логирования с проверкой DEV переменной
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
// GET /search/recommendations - получить рекомендации компаний (ДОЛЖЕН быть ПЕРЕД /*)
router.get('/recommendations', verifyToken, async (req, res) => {
try {
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
const user = await User.findById(req.userId);
let filter = {};
if (user && user.companyId) {
filter._id = { $ne: user.companyId };
}
const companies = await Company.find(filter)
.sort({ rating: -1 })
.limit(5);
const recommendations = companies.map(company => ({
id: company._id.toString(),
name: company.fullName || company.shortName,
industry: company.industry,
logo: company.logo,
matchScore: Math.floor(Math.random() * 30 + 70), // 70-100
reason: 'Matches your search criteria'
}));
log('[Search] Returned recommendations:', recommendations.length);
res.json(recommendations);
} catch (error) {
console.error('[Search] Recommendations error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
});
// GET /search - Поиск компаний
router.get('/', verifyToken, async (req, res) => {
try {
console.log('[Search] === NEW VERSION WITH FIXED SIZE FILTER ===');
const {
query = '',
page = 1,
limit = 10,
offset, // Добавляем поддержку offset для точной пагинации
industries,
companySize,
geography,
minRating = 0,
hasReviews,
hasAcceptedDocs,
sortBy = 'relevance',
sortOrder = 'desc',
minEmployees, // Кастомный фильтр: минимум сотрудников
maxEmployees // Кастомный фильтр: максимум сотрудников
} = req.query;
console.log('[Search] Filters:', { minEmployees, maxEmployees, companySize });
// Получить компанию пользователя, чтобы исключить её из результатов
const User = require('../models/User');
const user = await User.findById(req.userId);
log('[Search] Request params:', { query, industries, companySize, geography, minRating, hasReviews, hasAcceptedDocs, sortBy, sortOrder });
// Маппинг кодов фильтров на значения в БД
const industryMap = {
'it': 'IT',
'finance': 'Финансы',
'manufacturing': 'Производство',
'construction': 'Строительство',
'retail': 'Розничная торговля',
'wholesale': 'Оптовая торговля',
'logistics': 'Логистика',
'healthcare': 'Здравоохранение',
'education': 'Образование',
'consulting': 'Консалтинг',
'marketing': 'Маркетинг',
'realestate': 'Недвижимость',
'food': 'Пищевая промышленность',
'agriculture': 'Сельское хозяйство',
'energy': 'Энергетика',
'telecom': 'Телекоммуникации',
'media': 'Медиа'
};
// Начальный фильтр: исключить собственную компанию
let filters = [];
if (user && user.companyId) {
filters.push({ _id: { $ne: user.companyId } });
}
// Текстовый поиск
if (query && query.trim()) {
const q = query.toLowerCase();
filters.push({
$or: [
{ fullName: { $regex: q, $options: 'i' } },
{ shortName: { $regex: q, $options: 'i' } },
{ slogan: { $regex: q, $options: 'i' } },
{ industry: { $regex: q, $options: 'i' } }
]
});
}
// Фильтр по отраслям - преобразуем коды в значения БД
if (industries) {
const industryList = Array.isArray(industries) ? industries : [industries];
if (industryList.length > 0) {
const dbIndustries = industryList
.map(code => industryMap[code])
.filter(val => val !== undefined);
log('[Search] Raw industries param:', industries);
log('[Search] Industry codes:', industryList, 'Mapped to:', dbIndustries);
if (dbIndustries.length > 0) {
filters.push({ industry: { $in: dbIndustries } });
log('[Search] Added industry filter:', { industry: { $in: dbIndustries } });
} else {
log('[Search] No industries mapped! Codes were:', industryList);
}
}
}
// Функция для парсинга диапазона из строки вида "51-250" или "500+"
const parseEmployeeRange = (sizeStr) => {
if (sizeStr.includes('+')) {
const min = parseInt(sizeStr.replace('+', ''));
return { min, max: Infinity };
}
const parts = sizeStr.split('-');
return {
min: parseInt(parts[0]),
max: parts[1] ? parseInt(parts[1]) : parseInt(parts[0])
};
};
// Функция для проверки пересечения двух диапазонов
const rangesOverlap = (range1, range2) => {
return range1.min <= range2.max && range1.max >= range2.min;
};
// Фильтр по размеру компании (чекбоксы) или кастомный диапазон
// Важно: этот фильтр должен получить все компании для корректной работы пересечения диапазонов
let sizeFilteredIds = null;
if ((companySize && companySize.length > 0) || minEmployees || maxEmployees) {
// Получаем все компании (без других фильтров, так как размер компании - это property-based фильтр)
const allCompanies = await Company.find({});
log('[Search] Employee size filter - checking companies:', allCompanies.length);
let matchingIds = [];
// Если есть кастомный диапазон - используем его
if (minEmployees || maxEmployees) {
const customRange = {
min: minEmployees ? parseInt(minEmployees, 10) : 0,
max: maxEmployees ? parseInt(maxEmployees, 10) : Infinity
};
log('[Search] Custom employee range filter:', customRange);
matchingIds = allCompanies
.filter(company => {
if (!company.companySize) {
log('[Search] Company has no size:', company.fullName);
return false;
}
const companyRange = parseEmployeeRange(company.companySize);
const overlaps = rangesOverlap(companyRange, customRange);
log('[Search] Checking overlap:', {
company: company.fullName,
companyRange,
customRange,
overlaps
});
return overlaps;
})
.map(c => c._id);
log('[Search] Matching companies by custom range:', matchingIds.length);
}
// Иначе используем чекбоксы
else if (companySize && companySize.length > 0) {
const sizeList = Array.isArray(companySize) ? companySize : [companySize];
log('[Search] Company size checkboxes filter:', sizeList);
matchingIds = allCompanies
.filter(company => {
if (!company.companySize) {
return false;
}
const companyRange = parseEmployeeRange(company.companySize);
// Проверяем пересечение с любым из выбранных диапазонов
const matches = sizeList.some(selectedSize => {
const filterRange = parseEmployeeRange(selectedSize);
const overlaps = rangesOverlap(companyRange, filterRange);
log('[Search] Check:', company.fullName, companyRange, 'vs', filterRange, '=', overlaps);
return overlaps;
});
return matches;
})
.map(c => c._id);
log('[Search] Matching companies by size checkboxes:', matchingIds.length);
}
// Сохраняем ID для дальнейшей фильтрации
sizeFilteredIds = matchingIds;
log('[Search] Size filtered IDs count:', sizeFilteredIds.length);
}
// Фильтр по географии
if (geography) {
const geoList = Array.isArray(geography) ? geography : [geography];
if (geoList.length > 0) {
filters.push({ partnerGeography: { $in: geoList } });
log('[Search] Geography filter:', { partnerGeography: { $in: geoList } });
}
}
// Фильтр по рейтингу
if (minRating) {
const rating = parseFloat(minRating);
if (rating > 0) {
filters.push({ rating: { $gte: rating } });
}
}
// Фильтр по отзывам
if (hasReviews === 'true') {
filters.push({ verified: true });
}
// Фильтр по акцептам
if (hasAcceptedDocs === 'true') {
filters.push({ verified: true });
}
// Применяем фильтр по размеру компании (если был задан)
if (sizeFilteredIds !== null) {
if (sizeFilteredIds.length > 0) {
filters.push({ _id: { $in: sizeFilteredIds } });
log('[Search] Applied size filter, IDs:', sizeFilteredIds.length);
} else {
// Если нет подходящих компаний по размеру, возвращаем пустой результат
filters.push({ _id: null });
log('[Search] No companies match size criteria');
}
}
// Комбинировать все фильтры
let filter = filters.length > 0 ? { $and: filters } : {};
// Пагинация - используем offset если передан, иначе вычисляем из page
const limitNum = parseInt(limit) || 10;
const skip = offset !== undefined ? parseInt(offset) : ((parseInt(page) || 1) - 1) * limitNum;
const pageNum = offset !== undefined ? Math.floor(skip / limitNum) + 1 : parseInt(page) || 1;
// Сортировка
let sortOptions = {};
if (sortBy === 'name') {
sortOptions.fullName = sortOrder === 'asc' ? 1 : -1;
} else {
sortOptions.rating = sortOrder === 'asc' ? 1 : -1;
}
log('[Search] Final MongoDB filter:', JSON.stringify(filter, null, 2));
let filterDebug = filters.length > 0 ? { $and: filters } : {};
const allCompanies = await Company.find({});
log('[Search] All companies in DB:', allCompanies.map(c => ({ name: c.fullName, geography: c.partnerGeography, industry: c.industry })));
const total = await Company.countDocuments(filter);
const companies = await Company.find(filter)
.sort(sortOptions)
.skip(skip)
.limit(limitNum);
const paginatedResults = companies.map(c => ({
...c.toObject(),
id: c._id
}));
log('[Search] Query:', query, 'Industries:', industries, 'Size:', companySize, 'Geo:', geography);
log('[Search] Total found:', total, 'Returning:', paginatedResults.length, 'companies');
log('[Search] Company details:', paginatedResults.map(c => ({ name: c.fullName, industry: c.industry })));
res.json({
companies: paginatedResults,
total,
page: pageNum,
totalPages: Math.ceil(total / limitNum),
_debug: {
filter: JSON.stringify(filter),
industriesReceived: industries
}
});
} catch (error) {
console.error('[Search] Error:', error.message);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
});
module.exports = router;

View File

@@ -1,92 +0,0 @@
const mongoose = require('../../../utils/mongoose');
const { ObjectId } = mongoose.Types;
const Message = require('../models/Message');
require('dotenv').config();
async function migrateMessages() {
try {
// Подключение к MongoDB происходит через server/utils/mongoose.ts
console.log('[Migration] Checking MongoDB connection...');
if (mongoose.connection.readyState !== 1) {
console.log('[Migration] Waiting for MongoDB connection...');
await new Promise((resolve) => {
mongoose.connection.once('connected', resolve);
});
}
console.log('[Migration] Connected to MongoDB');
// Найти все сообщения
const allMessages = await Message.find().exec();
console.log('[Migration] Found', allMessages.length, 'total messages');
let fixedCount = 0;
let errorCount = 0;
// Проходим по каждому сообщению
for (const message of allMessages) {
try {
const threadId = message.threadId;
if (!threadId) {
console.log('[Migration] Skipping message', message._id, '- no threadId');
continue;
}
// Парсим threadId формата "thread-id1-id2" или "id1-id2"
let ids = threadId.replace('thread-', '').split('-');
if (ids.length < 2) {
console.log('[Migration] Invalid threadId format:', threadId);
errorCount++;
continue;
}
const companyId1 = ids[0];
const companyId2 = ids[1];
// Сравниваем с senderCompanyId
const senderIdString = message.senderCompanyId.toString ? message.senderCompanyId.toString() : message.senderCompanyId;
const expectedRecipient = senderIdString === companyId1 ? companyId2 : companyId1;
// Если recipientCompanyId не установлена или неправильная - исправляем
if (!message.recipientCompanyId || message.recipientCompanyId.toString() !== expectedRecipient) {
console.log('[Migration] Fixing message', message._id);
console.log(' Old recipientCompanyId:', message.recipientCompanyId);
console.log(' Expected:', expectedRecipient);
// Конвертируем в ObjectId если нужно
let recipientObjectId = expectedRecipient;
try {
if (typeof expectedRecipient === 'string' && ObjectId.isValid(expectedRecipient)) {
recipientObjectId = new ObjectId(expectedRecipient);
}
} catch (e) {
console.log(' Could not convert to ObjectId');
}
await Message.updateOne(
{ _id: message._id },
{ recipientCompanyId: recipientObjectId }
);
fixedCount++;
console.log(' ✅ Fixed');
}
} catch (err) {
console.error('[Migration] Error processing message', message._id, ':', err.message);
errorCount++;
}
}
console.log('[Migration] ✅ Migration completed!');
console.log('[Migration] Fixed:', fixedCount, 'messages');
console.log('[Migration] Errors:', errorCount);
await mongoose.connection.close();
console.log('[Migration] Disconnected from MongoDB');
} catch (err) {
console.error('[Migration] ❌ Error:', err.message);
process.exit(1);
}
}
migrateMessages();

View File

@@ -1,382 +0,0 @@
const mongoose = require('../../../utils/mongoose');
require('dotenv').config();
// Импорт моделей
const User = require('../models/User');
const Company = require('../models/Company');
const Request = require('../models/Request');
// Подключение к MongoDB происходит через server/utils/mongoose.ts
// Проверяем, подключено ли уже
const ensureConnection = async () => {
if (mongoose.connection.readyState === 1) {
console.log('✅ MongoDB уже подключено');
return;
}
console.log('⏳ Ожидание подключения к MongoDB...');
await new Promise((resolve) => {
if (mongoose.connection.readyState === 1) {
resolve();
} else {
mongoose.connection.once('connected', resolve);
}
});
console.log('✅ Подключено к MongoDB');
};
const recreateTestUser = async () => {
try {
await ensureConnection();
const presetCompanyId = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06796');
const presetUserEmail = 'admin@test-company.ru';
const presetCompanyId2 = new mongoose.Types.ObjectId('68fe2ccda3526c303ca06797');
const presetUserEmail2 = 'manager@partner-company.ru';
// Удалить старых тестовых пользователей
console.log('🗑️ Удаление старых тестовых пользователей...');
const testEmails = [presetUserEmail, presetUserEmail2];
for (const email of testEmails) {
const oldUser = await User.findOne({ email });
if (oldUser) {
// Удалить связанную компанию
if (oldUser.companyId) {
await Company.findByIdAndDelete(oldUser.companyId);
console.log(` ✓ Старая компания для ${email} удалена`);
}
await User.findByIdAndDelete(oldUser._id);
console.log(` ✓ Старый пользователь ${email} удален`);
} else {
console.log(` Пользователь ${email} не найден`);
}
}
// Создать новую компанию с правильной кодировкой UTF-8
console.log('\n🏢 Создание тестовой компании...');
const company = await Company.create({
_id: presetCompanyId,
fullName: 'ООО "Тестовая Компания"',
shortName: 'Тестовая Компания',
inn: '1234567890',
ogrn: '1234567890123',
legalForm: 'ООО',
industry: 'IT',
companySize: '51-250',
website: 'https://test-company.ru',
phone: '+7 (999) 123-45-67',
email: 'info@test-company.ru',
description: 'Тестовая компания для разработки',
legalAddress: 'г. Москва, ул. Тестовая, д. 1',
actualAddress: 'г. Москва, ул. Тестовая, д. 1',
foundedYear: 2015,
employeeCount: '51-250',
revenue: 'До 120 млн ₽',
rating: 4.5,
reviews: 10,
verified: true,
partnerGeography: ['moscow', 'russia_all'],
slogan: 'Ваш надежный партнер в IT',
});
console.log(' ✓ Компания создана:', company.fullName);
// Создать первого пользователя с правильной кодировкой UTF-8
console.log('\n👤 Создание первого тестового пользователя...');
const user = await User.create({
email: presetUserEmail,
password: 'SecurePass123!',
firstName: 'Иван',
lastName: 'Иванов',
position: 'Директор',
phone: '+7 (999) 123-45-67',
companyId: company._id,
});
console.log(' ✓ Пользователь создан:', user.firstName, user.lastName);
// Создать вторую компанию
console.log('\n🏢 Создание второй тестовой компании...');
const company2 = await Company.create({
_id: presetCompanyId2,
fullName: 'ООО "Партнер"',
shortName: 'Партнер',
inn: '9876543210',
ogrn: '1089876543210',
legalForm: 'ООО',
industry: 'Торговля',
companySize: '11-50',
website: 'https://partner-company.ru',
phone: '+7 (495) 987-65-43',
email: 'info@partner-company.ru',
description: 'Надежный партнер для бизнеса',
legalAddress: 'г. Санкт-Петербург, пр. Невский, д. 100',
actualAddress: 'г. Санкт-Петербург, пр. Невский, д. 100',
foundedYear: 2018,
employeeCount: '11-50',
revenue: 'До 60 млн ₽',
rating: 4.3,
reviews: 5,
verified: true,
partnerGeography: ['spb', 'russia_all'],
slogan: 'Качество и надежность',
});
console.log(' ✓ Компания создана:', company2.fullName);
// Создать второго пользователя
console.log('\n👤 Создание второго тестового пользователя...');
const user2 = await User.create({
email: presetUserEmail2,
password: 'SecurePass123!',
firstName: 'Петр',
lastName: 'Петров',
position: 'Менеджер',
phone: '+7 (495) 987-65-43',
companyId: company2._id,
});
console.log(' ✓ Пользователь создан:', user2.firstName, user2.lastName);
// Проверка что данные сохранены правильно
console.log('\n✅ Проверка данных:');
console.log('\n Пользователь 1:');
console.log(' Email:', user.email);
console.log(' Имя:', user.firstName);
console.log(' Фамилия:', user.lastName);
console.log(' Компания:', company.fullName);
console.log(' Должность:', user.position);
console.log('\n Пользователь 2:');
console.log(' Email:', user2.email);
console.log(' Имя:', user2.firstName);
console.log(' Фамилия:', user2.lastName);
console.log(' Компания:', company2.fullName);
console.log(' Должность:', user2.position);
console.log('\n✅ ГОТОВО! Тестовые пользователи созданы с правильной кодировкой UTF-8');
console.log('\n📋 Данные для входа:');
console.log('\n Пользователь 1:');
console.log(' Email: admin@test-company.ru');
console.log(' Пароль: SecurePass123!');
console.log('\n Пользователь 2:');
console.log(' Email: manager@partner-company.ru');
console.log(' Пароль: SecurePass123!');
console.log('');
// Создать дополнительные тестовые компании для поиска
console.log('\n🏢 Создание дополнительных тестовых компаний...');
const testCompanies = [
{
fullName: 'ООО "ТехноСтрой"',
shortName: 'ТехноСтрой',
inn: '7707083894',
ogrn: '1077707083894',
legalForm: 'ООО',
industry: 'Строительство',
companySize: '51-250',
website: 'https://technostroy.ru',
phone: '+7 (495) 111-22-33',
email: 'info@technostroy.ru',
description: 'Строительство промышленных объектов',
foundedYear: 2010,
employeeCount: '51-250',
revenue: 'До 2 млрд ₽',
rating: 4.2,
reviews: 15,
verified: true,
partnerGeography: ['moscow', 'russia_all'],
slogan: 'Строим будущее вместе',
},
{
fullName: 'АО "ФинансГрупп"',
shortName: 'ФинансГрупп',
inn: '7707083895',
ogrn: '1077707083895',
legalForm: 'АО',
industry: 'Финансы',
companySize: '500+',
website: 'https://finansgrupp.ru',
phone: '+7 (495) 222-33-44',
email: 'contact@finansgrupp.ru',
description: 'Финансовые услуги для бизнеса',
foundedYear: 2005,
employeeCount: '500+',
revenue: 'Более 2 млрд ₽',
rating: 4.8,
reviews: 50,
verified: true,
partnerGeography: ['moscow', 'russia_all', 'international'],
slogan: 'Финансовая стабильность',
},
{
fullName: 'ООО "ИТ Решения"',
shortName: 'ИТ Решения',
inn: '7707083896',
ogrn: '1077707083896',
legalForm: 'ООО',
industry: 'IT',
companySize: '11-50',
website: 'https://it-solutions.ru',
phone: '+7 (495) 333-44-55',
email: 'hello@it-solutions.ru',
description: 'Разработка программного обеспечения',
foundedYear: 2018,
employeeCount: '11-50',
revenue: 'До 60 млн ₽',
rating: 4.5,
reviews: 8,
verified: true,
partnerGeography: ['moscow', 'spb', 'russia_all'],
slogan: 'Инновации для вашего бизнеса',
},
{
fullName: 'ООО "ЛогистикПро"',
shortName: 'ЛогистикПро',
inn: '7707083897',
ogrn: '1077707083897',
legalForm: 'ООО',
industry: 'Логистика',
companySize: '51-250',
website: 'https://logistikpro.ru',
phone: '+7 (495) 444-55-66',
email: 'info@logistikpro.ru',
description: 'Транспортные и логистические услуги',
foundedYear: 2012,
employeeCount: '51-250',
revenue: 'До 120 млн ₽',
rating: 4.3,
reviews: 20,
verified: true,
partnerGeography: ['russia_all', 'cis'],
slogan: 'Доставим в срок',
},
{
fullName: 'ООО "ПродуктТрейд"',
shortName: 'ПродуктТрейд',
inn: '7707083898',
ogrn: '1077707083898',
legalForm: 'ООО',
industry: 'Оптовая торговля',
companySize: '251-500',
website: 'https://produkttrade.ru',
phone: '+7 (495) 555-66-77',
email: 'sales@produkttrade.ru',
description: 'Оптовая торговля продуктами питания',
foundedYear: 2008,
employeeCount: '251-500',
revenue: 'До 2 млрд ₽',
rating: 4.1,
reviews: 30,
verified: true,
partnerGeography: ['moscow', 'russia_all'],
slogan: 'Качество и надежность',
},
{
fullName: 'ООО "МедСервис"',
shortName: 'МедСервис',
inn: '7707083899',
ogrn: '1077707083899',
legalForm: 'ООО',
industry: 'Здравоохранение',
companySize: '11-50',
website: 'https://medservice.ru',
phone: '+7 (495) 666-77-88',
email: 'info@medservice.ru',
description: 'Медицинские услуги и оборудование',
foundedYear: 2016,
employeeCount: '11-50',
revenue: 'До 60 млн ₽',
rating: 4.6,
reviews: 12,
verified: true,
partnerGeography: ['moscow', 'central'],
slogan: 'Забота о вашем здоровье',
},
];
for (const companyData of testCompanies) {
await Company.updateOne(
{ inn: companyData.inn },
{ $set: companyData },
{ upsert: true }
);
console.log(` ✓ Компания создана/обновлена: ${companyData.shortName}`);
}
// Создать тестовые запросы
console.log('\n📨 Создание тестовых запросов...');
await Request.deleteMany({});
const companies = await Company.find().limit(10).exec();
const testCompanyId = company._id.toString();
const requests = [];
const now = new Date();
// Создаем отправленные запросы (от тестовой компании)
for (let i = 0; i < 5; i++) {
const recipientCompany = companies[i % companies.length];
if (recipientCompany._id.toString() === testCompanyId) {
continue;
}
const createdAt = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
requests.push({
senderCompanyId: testCompanyId,
recipientCompanyId: recipientCompany._id.toString(),
subject: `Запрос на поставку ${i + 1}`,
text: `Здравствуйте! Интересует поставка товаров/услуг. Запрос ${i + 1}. Прошу предоставить коммерческое предложение.`,
files: [],
responseFiles: [],
status: i % 3 === 0 ? 'accepted' : i % 3 === 1 ? 'rejected' : 'pending',
response: i % 3 === 0
? 'Благодарим за запрос! Готовы предоставить услуги. Отправили КП на почту.'
: i % 3 === 1
? 'К сожалению, в данный момент не можем предоставить эти услуги.'
: null,
respondedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : null,
createdAt,
updatedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : createdAt,
});
}
// Создаем полученные запросы (к тестовой компании)
for (let i = 0; i < 3; i++) {
const senderCompany = companies[(i + 2) % companies.length];
if (senderCompany._id.toString() === testCompanyId) {
continue;
}
const createdAt = new Date(now.getTime() - (i + 1) * 12 * 60 * 60 * 1000);
requests.push({
senderCompanyId: senderCompany._id.toString(),
recipientCompanyId: testCompanyId,
subject: `Предложение о сотрудничестве ${i + 1}`,
text: `Добрый день! Предлагаем сотрудничество. Запрос ${i + 1}. Заинтересованы в вашей продукции.`,
files: [],
responseFiles: [],
status: 'pending',
response: null,
respondedAt: null,
createdAt,
updatedAt: createdAt,
});
}
if (requests.length > 0) {
await Request.insertMany(requests);
console.log(` ✓ Создано ${requests.length} тестовых запросов`);
}
await mongoose.connection.close();
process.exit(0);
} catch (error) {
console.error('\n❌ Ошибка:', error.message);
console.error(error);
process.exit(1);
}
};
// Запуск
recreateTestUser();

View File

@@ -1,126 +0,0 @@
const mongoose = require('../../../utils/mongoose');
require('dotenv').config();
// Подключение моделей
const Activity = require('../models/Activity');
const User = require('../models/User');
const Company = require('../models/Company');
const activityTemplates = [
{
type: 'request_received',
title: 'Получен новый запрос',
description: 'Компания отправила вам запрос на поставку товаров',
},
{
type: 'request_sent',
title: 'Запрос отправлен',
description: 'Ваш запрос был отправлен компании',
},
{
type: 'request_response',
title: 'Получен ответ на запрос',
description: 'Компания ответила на ваш запрос',
},
{
type: 'product_accepted',
title: 'Товар акцептован',
description: 'Ваш товар был акцептован компанией',
},
{
type: 'message_received',
title: 'Новое сообщение',
description: 'Вы получили новое сообщение от компании',
},
{
type: 'review_received',
title: 'Новый отзыв',
description: 'Компания оставила отзыв о сотрудничестве',
},
{
type: 'profile_updated',
title: 'Профиль обновлен',
description: 'Информация о вашей компании была обновлена',
},
{
type: 'buy_product_added',
title: 'Добавлен товар для закупки',
description: 'В раздел "Я покупаю" добавлен новый товар',
},
];
async function seedActivities() {
try {
// Подключение к MongoDB происходит через server/utils/mongoose.ts
console.log('🌱 Checking MongoDB connection...');
if (mongoose.connection.readyState !== 1) {
console.log('⏳ Waiting for MongoDB connection...');
await new Promise((resolve) => {
mongoose.connection.once('connected', resolve);
});
}
console.log('✅ Connected to MongoDB');
// Найти тестового пользователя
const testUser = await User.findOne({ email: 'admin@test-company.ru' });
if (!testUser) {
console.log('❌ Test user not found. Please run recreate-test-user.js first.');
process.exit(1);
}
const company = await Company.findById(testUser.companyId);
if (!company) {
console.log('❌ Company not found');
process.exit(1);
}
// Найти другие компании для связанных активностей
const otherCompanies = await Company.find({
_id: { $ne: company._id }
}).limit(3);
console.log('🗑️ Clearing existing activities...');
await Activity.deleteMany({ companyId: company._id.toString() });
console.log(' Creating activities...');
const activities = [];
for (let i = 0; i < 8; i++) {
const template = activityTemplates[i % activityTemplates.length];
const relatedCompany = otherCompanies[i % otherCompanies.length];
const activity = {
companyId: company._id.toString(),
userId: testUser._id.toString(),
type: template.type,
title: template.title,
description: template.description,
relatedCompanyId: relatedCompany?._id.toString(),
relatedCompanyName: relatedCompany?.shortName || relatedCompany?.fullName,
read: i >= 5, // Первые 5 непрочитанные
createdAt: new Date(Date.now() - i * 3600000), // Каждый час назад
};
activities.push(activity);
}
await Activity.insertMany(activities);
console.log(`✅ Created ${activities.length} activities`);
console.log('✨ Activities seeded successfully!');
await mongoose.connection.close();
console.log('👋 Database connection closed');
} catch (error) {
console.error('❌ Error seeding activities:', error);
process.exit(1);
}
}
// Запуск
if (require.main === module) {
seedActivities();
}
module.exports = { seedActivities };

View File

@@ -1,118 +0,0 @@
const mongoose = require('../../../utils/mongoose');
const Request = require('../models/Request');
const Company = require('../models/Company');
const User = require('../models/User');
async function seedRequests() {
try {
// Подключение к MongoDB происходит через server/utils/mongoose.ts
if (mongoose.connection.readyState !== 1) {
console.log('⏳ Waiting for MongoDB connection...');
await new Promise((resolve) => {
mongoose.connection.once('connected', resolve);
});
}
console.log('✅ Connected to MongoDB');
// Получаем все компании
const companies = await Company.find().limit(10).exec();
if (companies.length < 2) {
console.error('❌ Need at least 2 companies in database');
process.exit(1);
}
// Получаем тестового пользователя
const testUser = await User.findOne({ email: 'admin@test-company.ru' }).exec();
if (!testUser) {
console.error('❌ Test user not found');
process.exit(1);
}
const testCompanyId = testUser.companyId.toString();
console.log('📋 Test company ID:', testCompanyId);
console.log('📋 Found', companies.length, 'companies');
// Удаляем старые запросы
await Request.deleteMany({});
console.log('🗑️ Cleared old requests');
const requests = [];
const now = new Date();
// Создаем отправленные запросы (от тестовой компании)
for (let i = 0; i < 5; i++) {
const recipientCompany = companies[i % companies.length];
if (recipientCompany._id.toString() === testCompanyId) {
continue;
}
const createdAt = new Date(now.getTime() - i * 24 * 60 * 60 * 1000); // За последние 5 дней
requests.push({
senderCompanyId: testCompanyId,
recipientCompanyId: recipientCompany._id.toString(),
subject: `Запрос на поставку ${i + 1}`,
text: `Здравствуйте! Интересует поставка товаров/услуг. Запрос ${i + 1}. Прошу предоставить коммерческое предложение.`,
files: [],
responseFiles: [],
status: i % 3 === 0 ? 'accepted' : i % 3 === 1 ? 'rejected' : 'pending',
response: i % 3 === 0
? 'Благодарим за запрос! Готовы предоставить услуги. Отправили КП на почту.'
: i % 3 === 1
? 'К сожалению, в данный момент не можем предоставить эти услуги.'
: null,
respondedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : null,
createdAt,
updatedAt: i % 3 !== 2 ? new Date(createdAt.getTime() + 2 * 60 * 60 * 1000) : createdAt,
});
}
// Создаем полученные запросы (к тестовой компании)
for (let i = 0; i < 3; i++) {
const senderCompany = companies[(i + 2) % companies.length];
if (senderCompany._id.toString() === testCompanyId) {
continue;
}
const createdAt = new Date(now.getTime() - (i + 1) * 12 * 60 * 60 * 1000); // За последние 1.5 дня
requests.push({
senderCompanyId: senderCompany._id.toString(),
recipientCompanyId: testCompanyId,
subject: `Предложение о сотрудничестве ${i + 1}`,
text: `Добрый день! Предлагаем сотрудничество. Запрос ${i + 1}. Заинтересованы в вашей продукции.`,
files: [],
responseFiles: [],
status: 'pending',
response: null,
respondedAt: null,
createdAt,
updatedAt: createdAt,
});
}
// Сохраняем все запросы
const savedRequests = await Request.insertMany(requests);
console.log('✅ Created', savedRequests.length, 'test requests');
// Статистика
const sentCount = await Request.countDocuments({ senderCompanyId: testCompanyId });
const receivedCount = await Request.countDocuments({ recipientCompanyId: testCompanyId });
const withResponses = await Request.countDocuments({ senderCompanyId: testCompanyId, response: { $ne: null } });
console.log('📊 Statistics:');
console.log(' - Sent requests:', sentCount);
console.log(' - Received requests:', receivedCount);
console.log(' - With responses:', withResponses);
} catch (error) {
console.error('❌ Error:', error);
process.exit(1);
} finally {
await mongoose.connection.close();
console.log('👋 Disconnected from MongoDB');
}
}
seedRequests();

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env node
/**
* Скрипт для тестирования логирования
*
* Использование:
* node stubs/scripts/test-logging.js # Логи скрыты (DEV не установлена)
* DEV=true node stubs/scripts/test-logging.js # Логи видны
*/
// Функция логирования из маршрутов
const log = (message, data = '') => {
if (process.env.DEV === 'true') {
if (data) {
console.log(message, data);
} else {
console.log(message);
}
}
};
console.log('');
console.log('='.repeat(60));
console.log('TEST: Логирование с переменной окружения DEV');
console.log('='.repeat(60));
console.log('');
console.log('Значение DEV:', process.env.DEV || '(не установлена)');
console.log('');
// Тестируем различные логи
log('[Auth] Token verified - userId: 68fe2ccda3526c303ca06799 companyId: 68fe2ccda3526c303ca06796');
log('[Auth] Generating token for userId:', '68fe2ccda3526c303ca06799');
log('[BuyProducts] Found', 0, 'products for company 68fe2ccda3526c303ca06796');
log('[Products] GET Fetching products for companyId:', '68fe2ccda3526c303ca06799');
log('[Products] Found', 1, 'products');
log('[Reviews] Returned', 0, 'reviews for company 68fe2ccda3526c303ca06796');
log('[Messages] Fetching threads for companyId:', '68fe2ccda3526c303ca06796');
log('[Messages] Found', 4, 'messages for company');
log('[Messages] Returned', 3, 'unique threads');
log('[Search] Request params:', { query: '', page: 1 });
console.log('');
console.log('='.repeat(60));
console.log('РЕЗУЛЬТАТ:');
console.log('='.repeat(60));
if (process.env.DEV === 'true') {
console.log('✅ DEV=true - логи ВИДНЫ выше');
} else {
console.log('❌ DEV не установлена или != "true" - логи СКРЫТЫ');
console.log('');
console.log('Для включения логов запустите:');
console.log(' export DEV=true && npm start (Linux/Mac)');
console.log(' $env:DEV = "true"; npm start (PowerShell)');
console.log(' set DEV=true && npm start (CMD)');
}
console.log('');
console.log('='.repeat(60));
console.log('');

View File

@@ -1,626 +0,0 @@
# Smoke Tracker API — Документация для Frontend
## Базовый URL
```
http://localhost:8044/smoke-tracker
```
В production окружении замените на соответствующий домен.
---
## Оглавление
1. [Авторизация](#авторизация)
- [Регистрация](#post-authsignup)
- [Вход](#post-authsignin)
2. [Логирование сигарет](#логирование-сигарет)
- [Записать сигарету](#post-cigarettes)
- [Получить список сигарет](#get-cigarettes)
3. [Статистика](#статистика)
- [Дневная статистика](#get-statsdaily)
---
## Авторизация
Все эндпоинты, кроме `/auth/signup` и `/auth/signin`, требуют JWT-токен в заголовке:
```
Authorization: Bearer <token>
```
Токен возвращается при успешном входе (`/auth/signin`) и действителен **12 часов**.
---
### `POST /auth/signup`
**Описание**: Регистрация нового пользователя
**Требуется авторизация**: ❌ Нет
**Тело запроса** (JSON):
```json
{
"login": "string", // обязательно, уникальный логин
"password": "string" // обязательно
}
```
**Пример запроса**:
```bash
curl -X POST http://localhost:8044/smoke-tracker/auth/signup \
-H "Content-Type: application/json" \
-d '{
"login": "user123",
"password": "mySecurePassword"
}'
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"ok": true
}
}
```
**Возможные ошибки**:
- **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля
- **500 Internal Server Error**: `"Пользователь с таким логином уже существует"` — логин занят
---
### `POST /auth/signin`
**Описание**: Вход в систему (получение JWT-токена)
**Требуется авторизация**: ❌ Нет
**Тело запроса** (JSON):
```json
{
"login": "string", // обязательно
"password": "string" // обязательно
}
```
**Пример запроса**:
```bash
curl -X POST http://localhost:8044/smoke-tracker/auth/signin \
-H "Content-Type: application/json" \
-d '{
"login": "user123",
"password": "mySecurePassword"
}'
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"user": {
"id": "507f1f77bcf86cd799439011",
"login": "user123",
"created": "2024-01-15T10:30:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
**Поля ответа**:
- `user.id` — уникальный идентификатор пользователя
- `user.login` — логин пользователя
- `user.created` — дата создания аккаунта (ISO 8601)
- `token` — JWT-токен для авторизации (действителен 12 часов)
**Возможные ошибки**:
- **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля
- **500 Internal Server Error**: `"Неверный логин или пароль"` — неправильные учётные данные
**Использование токена**:
Сохраните токен в localStorage/sessionStorage/cookie и передавайте в заголовке всех последующих запросов:
```javascript
// Пример для fetch API
const token = localStorage.getItem('smokeToken');
fetch('http://localhost:8044/smoke-tracker/cigarettes', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
```
---
## Логирование сигарет
### `POST /cigarettes`
**Описание**: Записать факт выкуренной сигареты
**Требуется авторизация**: ✅ Да (Bearer token)
**Тело запроса** (JSON):
```json
{
"smokedAt": "string (ISO 8601)", // необязательно, по умолчанию — текущее время
"note": "string" // необязательно, заметка/комментарий
}
```
**Пример запроса**:
```bash
curl -X POST http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"smokedAt": "2024-01-15T14:30:00.000Z",
"note": "После обеда"
}'
```
**Пример без указания времени** (будет текущее время):
```bash
curl -X POST http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{}'
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"id": "507f1f77bcf86cd799439012",
"userId": "507f1f77bcf86cd799439011",
"smokedAt": "2024-01-15T14:30:00.000Z",
"note": "После обеда",
"created": "2024-01-15T14:30:05.123Z"
}
}
```
**Поля ответа**:
- `id` — уникальный идентификатор записи
- `userId` — ID пользователя
- `smokedAt` — дата и время курения (ISO 8601)
- `note` — заметка (если была указана)
- `created` — дата создания записи в БД
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
- **400 Bad Request**: `"Некорректный формат даты smokedAt"` — неверный формат даты
---
### `GET /cigarettes`
**Описание**: Получить список всех выкуренных сигарет текущего пользователя
**Требуется авторизация**: ✅ Да (Bearer token)
**Query-параметры** (все необязательные):
| Параметр | Тип | Описание | Пример |
|----------|-----|----------|--------|
| `from` | string (ISO 8601) | Начало периода (включительно) | `2024-01-01T00:00:00.000Z` |
| `to` | string (ISO 8601) | Конец периода (включительно) | `2024-01-31T23:59:59.999Z` |
**Пример запроса** (все сигареты):
```bash
curl -X GET http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Пример запроса** (с фильтрацией по датам):
```bash
curl -X GET "http://localhost:8044/smoke-tracker/cigarettes?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": [
{
"id": "507f1f77bcf86cd799439012",
"userId": "507f1f77bcf86cd799439011",
"smokedAt": "2024-01-15T10:30:00.000Z",
"note": "Утренняя",
"created": "2024-01-15T10:30:05.123Z"
},
{
"id": "507f1f77bcf86cd799439013",
"userId": "507f1f77bcf86cd799439011",
"smokedAt": "2024-01-15T14:30:00.000Z",
"note": "После обеда",
"created": "2024-01-15T14:30:05.456Z"
}
]
}
```
**Особенности**:
- Записи отсортированы по `smokedAt` (от старых к новым)
- Если указаны `from` и/или `to`, будет применена фильтрация
- Пустой массив возвращается, если сигарет в периоде нет
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
---
## Статистика
### `GET /stats/daily`
**Описание**: Получить дневную статистику по количеству сигарет для построения графика
**Требуется авторизация**: ✅ Да (Bearer token)
**Query-параметры** (все необязательные):
| Параметр | Тип | Описание | Пример | По умолчанию |
|----------|-----|----------|--------|--------------|
| `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты |
| `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время |
**Пример запроса** (последние 30 дней):
```bash
curl -X GET http://localhost:8044/smoke-tracker/stats/daily \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Пример запроса** (с указанием периода):
```bash
curl -X GET "http://localhost:8044/smoke-tracker/stats/daily?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": [
{
"date": "2024-01-15",
"count": 8
},
{
"date": "2024-01-16",
"count": 12
},
{
"date": "2024-01-17",
"count": 5
}
]
}
```
**Поля ответа**:
- `date` — дата в формате `YYYY-MM-DD`
- `count` — количество сигарет, выкуренных в этот день
**Особенности**:
- Данные отсортированы по дате (от старых к новым)
- Дни без сигарет **не включаются** в ответ (фронтенду нужно самостоятельно заполнить пропуски нулями при построении графика)
- Агрегация происходит по дате из поля `smokedAt` (не `created`)
**Пример использования для графика** (Chart.js):
```javascript
const response = await fetch('http://localhost:8044/smoke-tracker/stats/daily', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const { body } = await response.json();
// Заполнение пропущенных дней нулями
const fillMissingDates = (data, from, to) => {
const result = [];
const current = new Date(from);
const end = new Date(to);
while (current <= end) {
const dateStr = current.toISOString().split('T')[0];
const existing = data.find(d => d.date === dateStr);
result.push({
date: dateStr,
count: existing ? existing.count : 0
});
current.setDate(current.getDate() + 1);
}
return result;
};
const filledData = fillMissingDates(body, '2024-01-01', '2024-01-31');
// Данные для графика
const chartData = {
labels: filledData.map(d => d.date),
datasets: [{
label: 'Количество сигарет',
data: filledData.map(d => d.count),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
}]
};
```
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
---
## Общая структура ответов
Все эндпоинты возвращают JSON в следующем формате:
**Успешный ответ**:
```json
{
"success": true,
"body": { /* данные */ }
}
```
**Ответ с ошибкой**:
```json
{
"success": false,
"errors": "Описание ошибки"
}
```
или (при использовании глобального обработчика ошибок):
```json
{
"message": "Описание ошибки"
}
```
---
## Коды состояния HTTP
| Код | Описание |
|-----|----------|
| **200 OK** | Запрос выполнен успешно |
| **400 Bad Request** | Некорректные данные в запросе |
| **401 Unauthorized** | Требуется авторизация или токен невалидный |
| **500 Internal Server Error** | Внутренняя ошибка сервера |
---
## Примеры интеграции
### React + Axios
```javascript
import axios from 'axios';
const API_BASE_URL = 'http://localhost:8044/smoke-tracker';
// Создание экземпляра axios с базовыми настройками
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Интерцептор для добавления токена
api.interceptors.request.use(config => {
const token = localStorage.getItem('smokeToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Регистрация
export const signup = async (login, password) => {
const { data } = await api.post('/auth/signup', { login, password });
return data;
};
// Вход
export const signin = async (login, password) => {
const { data } = await api.post('/auth/signin', { login, password });
if (data.success) {
localStorage.setItem('smokeToken', data.body.token);
}
return data;
};
// Выход
export const signout = () => {
localStorage.removeItem('smokeToken');
};
// Записать сигарету
export const logCigarette = async (smokedAt = null, note = '') => {
const { data } = await api.post('/cigarettes', { smokedAt, note });
return data;
};
// Получить список сигарет
export const getCigarettes = async (from = null, to = null) => {
const params = {};
if (from) params.from = from;
if (to) params.to = to;
const { data } = await api.get('/cigarettes', { params });
return data;
};
// Получить дневную статистику
export const getDailyStats = async (from = null, to = null) => {
const params = {};
if (from) params.from = from;
if (to) params.to = to;
const { data } = await api.get('/stats/daily', { params });
return data;
};
```
### Vanilla JavaScript + Fetch
```javascript
const API_BASE_URL = 'http://localhost:8044/smoke-tracker';
// Получение токена
const getToken = () => localStorage.getItem('smokeToken');
// Базовый запрос
const apiRequest = async (endpoint, options = {}) => {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || error.errors || 'Ошибка запроса');
}
return response.json();
};
// Регистрация
async function signup(login, password) {
return apiRequest('/auth/signup', {
method: 'POST',
body: JSON.stringify({ login, password })
});
}
// Вход
async function signin(login, password) {
const data = await apiRequest('/auth/signin', {
method: 'POST',
body: JSON.stringify({ login, password })
});
if (data.success) {
localStorage.setItem('smokeToken', data.body.token);
}
return data;
}
// Записать сигарету
async function logCigarette(note = '') {
return apiRequest('/cigarettes', {
method: 'POST',
body: JSON.stringify({ note })
});
}
// Получить дневную статистику
async function getDailyStats() {
return apiRequest('/stats/daily');
}
```
---
## Рекомендации по безопасности
1. **Хранение токена**:
- Для веб-приложений: используйте `httpOnly` cookies или `sessionStorage`
- Избегайте `localStorage` при работе с чувствительными данными
- Для мобильных приложений: используйте безопасное хранилище (Keychain/Keystore)
2. **HTTPS**: В production всегда используйте HTTPS для защиты токена при передаче
3. **Обработка истечения токена**:
- Токен действителен 12 часов
- При получении ошибки 401 перенаправляйте пользователя на страницу входа
- Реализуйте механизм refresh token для бесшовного обновления
4. **Валидация на фронтенде**:
- Проверяйте корректность email/логина перед отправкой
- Требуйте минимальную длину пароля (8+ символов)
- Показывайте индикатор силы пароля
---
## Postman-коллекция
Готовая коллекция для тестирования доступна в файле:
```
server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json
```
Импортируйте её в Postman для быстрого тестирования всех эндпоинтов.
---
## Поддержка
При возникновении вопросов или обнаружении проблем обращайтесь к разработчикам backend-команды.

View File

@@ -1,89 +0,0 @@
const { Router } = require('express')
const hash = require('pbkdf2-password')()
const { promisify } = require('node:util')
const jwt = require('jsonwebtoken')
const { getAnswer } = require('../../utils/common')
const { SmokeAuthModel } = require('./model/auth')
const { SmokeUserModel } = require('./model/user')
const { SMOKE_TRACKER_TOKEN_KEY } = require('./const')
const { requiredValidate } = require('./utils')
const router = Router()
router.post(
'/signup',
requiredValidate('login', 'password'),
async (req, res, next) => {
const { login, password } = req.body
const existing = await SmokeAuthModel.findOne({ login })
if (existing) {
throw new Error('Пользователь с таким логином уже существует')
}
hash({ password }, async function (err, pass, salt, hashValue) {
if (err) return next(err)
const user = await SmokeUserModel.create({ login })
await SmokeAuthModel.create({ login, hash: hashValue, salt, userId: user.id })
res.json(getAnswer(null, { ok: true }))
})
}
)
function authenticate(login, pass, cb) {
SmokeAuthModel.findOne({ login })
.populate('userId')
.exec()
.then((user) => {
if (!user) return cb(null, null)
hash({ password: pass, salt: user.salt }, function (err, pass, salt, hashValue) {
if (err) return cb(err)
if (hashValue === user.hash) return cb(null, user)
cb(null, null)
})
})
.catch((err) => cb(err))
}
const auth = promisify(authenticate)
router.post(
'/signin',
requiredValidate('login', 'password'),
async (req, res) => {
const { login, password } = req.body
const user = await auth(login, password)
if (!user) {
throw new Error('Неверный логин или пароль')
}
const accessToken = jwt.sign(
{
...JSON.parse(JSON.stringify(user.userId)),
},
SMOKE_TRACKER_TOKEN_KEY,
{
expiresIn: '12h',
}
)
res.json(
getAnswer(null, {
user: user.userId,
token: accessToken,
})
)
}
)
module.exports = router

View File

@@ -1,76 +0,0 @@
const { Router } = require('express')
const mongoose = require('mongoose')
const { getAnswer } = require('../../utils/common')
const { CigaretteModel } = require('./model/cigarette')
const { authMiddleware } = require('./middleware/auth')
const router = Router()
// Все эндпоинты ниже требуют авторизации
router.use(authMiddleware)
// Логирование одной сигареты
router.post('/', async (req, res, next) => {
try {
const { smokedAt, note } = req.body || {}
const user = req.user
let date
if (smokedAt) {
const parsed = new Date(smokedAt)
if (Number.isNaN(parsed.getTime())) {
throw new Error('Некорректный формат даты smokedAt')
}
date = parsed
} else {
date = new Date()
}
const item = await CigaretteModel.create({
userId: new mongoose.Types.ObjectId(user.id),
smokedAt: date,
note,
})
res.json(getAnswer(null, item))
} catch (err) {
next(err)
}
})
// Получение списка сигарет пользователя (для отладки и таблиц)
router.get('/', async (req, res, next) => {
try {
const user = req.user
const { from, to } = req.query
const filter = { userId: new mongoose.Types.ObjectId(user.id) }
if (from || to) {
filter.smokedAt = {}
if (from) {
const fromDate = new Date(from)
if (!Number.isNaN(fromDate.getTime())) {
filter.smokedAt.$gte = fromDate
}
}
if (to) {
const toDate = new Date(to)
if (!Number.isNaN(toDate.getTime())) {
filter.smokedAt.$lte = toDate
}
}
}
const items = await CigaretteModel.find(filter).sort({ smokedAt: 1 })
res.json(getAnswer(null, items))
} catch (err) {
next(err)
}
})
module.exports = router

View File

@@ -1,9 +0,0 @@
exports.SMOKE_TRACKER_USER_MODEL_NAME = 'SMOKE_TRACKER_USER'
exports.SMOKE_TRACKER_AUTH_MODEL_NAME = 'SMOKE_TRACKER_AUTH'
exports.SMOKE_TRACKER_CIGARETTE_MODEL_NAME = 'SMOKE_TRACKER_CIGARETTE'
exports.SMOKE_TRACKER_TOKEN_KEY =
process.env.SMOKE_TRACKER_TOKEN_KEY ||
'smoke-tracker-secret-key-change-me'

View File

@@ -1,13 +0,0 @@
const router = require('express').Router()
const authRouter = require('./auth')
const cigarettesRouter = require('./cigarettes')
const statsRouter = require('./stats')
router.use('/auth', authRouter)
router.use('/cigarettes', cigarettesRouter)
router.use('/stats', statsRouter)
module.exports = router

View File

@@ -1,26 +0,0 @@
const jwt = require('jsonwebtoken')
const { SMOKE_TRACKER_TOKEN_KEY } = require('../const')
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization || ''
const token = authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: null
if (!token) {
throw new Error('Требуется авторизация')
}
try {
const decoded = jwt.verify(token, SMOKE_TRACKER_TOKEN_KEY)
req.user = decoded
next()
} catch (e) {
throw new Error('Неверный или истекший токен авторизации')
}
}
module.exports.authMiddleware = authMiddleware

View File

@@ -1,33 +0,0 @@
const { Schema, model } = require('mongoose')
const {
SMOKE_TRACKER_AUTH_MODEL_NAME,
SMOKE_TRACKER_USER_MODEL_NAME,
} = require('../const')
const schema = new Schema({
login: { type: String, required: true, unique: true },
hash: { type: String, required: true },
salt: { type: String, required: true },
userId: { type: Schema.Types.ObjectId, ref: SMOKE_TRACKER_USER_MODEL_NAME },
created: {
type: Date,
default: () => new Date().toISOString(),
},
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id
},
})
schema.virtual('id').get(function () {
return this._id.toHexString()
})
exports.SmokeAuthModel = model(SMOKE_TRACKER_AUTH_MODEL_NAME, schema)

View File

@@ -1,38 +0,0 @@
const { Schema, model } = require('mongoose')
const {
SMOKE_TRACKER_CIGARETTE_MODEL_NAME,
SMOKE_TRACKER_USER_MODEL_NAME,
} = require('../const')
const schema = new Schema({
userId: { type: Schema.Types.ObjectId, ref: SMOKE_TRACKER_USER_MODEL_NAME, required: true },
smokedAt: {
type: Date,
required: true,
default: () => new Date().toISOString(),
},
note: {
type: String,
},
created: {
type: Date,
default: () => new Date().toISOString(),
},
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id
},
})
schema.virtual('id').get(function () {
return this._id.toHexString()
})
exports.CigaretteModel = model(SMOKE_TRACKER_CIGARETTE_MODEL_NAME, schema)

View File

@@ -1,27 +0,0 @@
const { Schema, model } = require('mongoose')
const { SMOKE_TRACKER_USER_MODEL_NAME } = require('../const')
const schema = new Schema({
login: { type: String, required: true, unique: true },
created: {
type: Date,
default: () => new Date().toISOString(),
},
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id
},
})
schema.virtual('id').get(function () {
return this._id.toHexString()
})
exports.SmokeUserModel = model(SMOKE_TRACKER_USER_MODEL_NAME, schema)

View File

@@ -1,207 +0,0 @@
{
"info": {
"_postman_id": "9d74101d-f788-4dbf-83b3-11c8f9789b73",
"name": "Smoke Tracker",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "smoke-tracker"
},
"item": [
{
"name": "Auth • Signup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"login\": \"smoker-demo\",\n \"password\": \"secret123\"\n}"
},
"url": {
"raw": "{{baseUrl}}/smoke-tracker/auth/signup",
"host": [
"{{baseUrl}}"
],
"path": [
"smoke-tracker",
"auth",
"signup"
]
},
"description": "Регистрация нового пользователя. Повторный вызов с тем же логином вернёт ошибку."
},
"response": []
},
{
"name": "Auth • Signin",
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"if (json && json.body && json.body.token) {",
" pm.environment.set('smokeToken', json.body.token);",
"}"
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"login\": \"smoker-demo\",\n \"password\": \"secret123\"\n}"
},
"url": {
"raw": "{{baseUrl}}/smoke-tracker/auth/signin",
"host": [
"{{baseUrl}}"
],
"path": [
"smoke-tracker",
"auth",
"signin"
]
},
"description": "Авторизация пользователя. Скрипт тестов сохранит JWT в переменную окружения smokeToken."
},
"response": []
},
{
"name": "Cigarettes • Log entry",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
},
{
"key": "Authorization",
"name": "Authorization",
"value": "Bearer {{smokeToken}}",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"smokedAt\": \"2025-01-01T09:30:00.000Z\",\n \"note\": \"Первая сигарета за день\"\n}"
},
"url": {
"raw": "{{baseUrl}}/smoke-tracker/cigarettes",
"host": [
"{{baseUrl}}"
],
"path": [
"smoke-tracker",
"cigarettes"
]
},
"description": "Создать запись о выкуренной сигарете. Если smokedAt не указан, сервер использует текущее время."
},
"response": []
},
{
"name": "Cigarettes • List",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"name": "Authorization",
"value": "Bearer {{smokeToken}}",
"type": "text"
}
],
"url": {
"raw": "{{baseUrl}}/smoke-tracker/cigarettes?from=2025-01-01T00:00:00.000Z&to=2025-01-07T23:59:59.999Z",
"host": [
"{{baseUrl}}"
],
"path": [
"smoke-tracker",
"cigarettes"
],
"query": [
{
"key": "from",
"value": "2025-01-01T00:00:00.000Z"
},
{
"key": "to",
"value": "2025-01-07T23:59:59.999Z"
}
]
},
"description": "Список сигарет текущего пользователя. Параметры from/to необязательны."
},
"response": []
},
{
"name": "Stats • Daily",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"name": "Authorization",
"value": "Bearer {{smokeToken}}",
"type": "text"
}
],
"url": {
"raw": "{{baseUrl}}/smoke-tracker/stats/daily?from=2025-01-01&to=2025-01-31",
"host": [
"{{baseUrl}}"
],
"path": [
"smoke-tracker",
"stats",
"daily"
],
"query": [
{
"key": "from",
"value": "2025-01-01"
},
{
"key": "to",
"value": "2025-01-31"
}
]
},
"description": "Агрегация по дням для графиков. Если from/to не заданы, используется последний месяц."
},
"response": []
}
],
"event": [],
"variable": [
{
"key": "baseUrl",
"value": "http://localhost:8044"
},
{
"key": "smokeToken",
"value": ""
}
]
}

View File

@@ -1,67 +0,0 @@
const { Router } = require('express')
const mongoose = require('mongoose')
const { getAnswer } = require('../../utils/common')
const { CigaretteModel } = require('./model/cigarette')
const { authMiddleware } = require('./middleware/auth')
const router = Router()
// Все эндпоинты статистики требуют авторизации
router.use(authMiddleware)
// Агрегация по дням: количество сигарет в день для построения графика
router.get('/daily', async (req, res, next) => {
try {
const user = req.user
const { from, to } = req.query
const now = new Date()
const defaultFrom = new Date(now)
defaultFrom.setDate(defaultFrom.getDate() - 30)
const fromDate = from ? new Date(from) : defaultFrom
const toDate = to ? new Date(to) : now
const match = {
userId: new mongoose.Types.ObjectId(user.id),
smokedAt: {
$gte: fromDate,
$lte: toDate,
},
}
// Отладка: проверяем, сколько записей попадает в фильтр
const totalCount = await CigaretteModel.countDocuments(match)
console.log('[STATS] Match filter:', JSON.stringify(match, null, 2))
console.log('[STATS] Total cigarettes in range:', totalCount)
const data = await CigaretteModel.aggregate([
{ $match: match },
{
$group: {
_id: {
$dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' },
},
count: { $sum: 1 },
},
},
{ $sort: { _id: 1 } },
])
console.log('[STATS] Aggregation result:', data)
const result = data.map((item) => ({
date: item._id,
count: item.count,
}))
res.json(getAnswer(null, result))
} catch (err) {
next(err)
}
})
module.exports = router

View File

@@ -1,21 +0,0 @@
const requiredValidate =
(...fields) =>
(req, res, next) => {
const errors = []
fields.forEach((field) => {
if (!req.body[field]) {
errors.push(field)
}
})
if (errors.length) {
throw new Error(`Не все поля заполнены: ${errors.join(', ')}`)
} else {
next()
}
}
module.exports.requiredValidate = requiredValidate

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
export const getAnswer = (errors, data, success = true) => {
exports.getAnswer = (errors, data, success = true) => {
if (errors) {
return {
success: false,
@@ -12,7 +12,7 @@ export const getAnswer = (errors, data, success = true) => {
}
}
export const getResponse = (errors, data, success = true) => {
exports.getResponse = (errors, data, success = true) => {
if (errors.length) {
return {
success: false,

4
server/utils/const.js Normal file
View File

@@ -0,0 +1,4 @@
const rc = require('../../.serverrc')
// Connection URL
exports.mongoUrl = `mongodb://${rc.mongoAddr}:${rc.mongoPort}`

View File

@@ -1,4 +0,0 @@
import 'dotenv/config';
// Connection URL
export const mongoUrl = process.env.MONGO_ADDR || 'mongodb://admin:password@localhost:27017/procurement_db?authSource=admin';

View File

@@ -1,18 +1,17 @@
import { MongoClient as MDBClient } from 'mongodb'
const MDBClient = require('mongodb').MongoClient
import { mongoUrl } from './const'
const { mongoUrl } = require('./const')
const dbInstanses = {
}
const mongoDBConnect = async () => {
try {
const MongoClient = new MDBClient(mongoUrl)
const client = await MongoClient.connect()
console.log('Подключение к MongoDB успешно')
return client
const MongoClient = new MDBClient(mongoUrl, {
useUnifiedTopology: true,
})
return await MongoClient.connect()
} catch (error) {
console.log('Неудачная попытка подключения к MongoDB')
console.error(error)
}
}
@@ -28,6 +27,6 @@ const getDB = async (dbName) => {
}
}
export {
module.exports = {
getDB,
}

5
server/utils/mongoose.js Normal file
View File

@@ -0,0 +1,5 @@
const mongoose = require('mongoose')
const { mongoUrl } = require('./const')
mongoose.connect(`${mongoUrl}/mongoose`)

View File

@@ -1,16 +0,0 @@
import mongoose from 'mongoose'
import { mongoUrl } from './const'
mongoose.connect(mongoUrl).then(() => {
console.log('Подключение к MongoDB успешно')
}).catch((err) => {
console.log('Неудачная попытка подключения к MongoDB')
console.error(err)
})
export default mongoose
// Для совместимости с CommonJS
module.exports = mongoose
module.exports.default = mongoose

View File

@@ -1,112 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "NodeNext", /* Specify what module code is generated. */
"rootDir": ".", /* Specify the root folder within your source files. */
"moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
"resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"node_modules",
"legacy/**/*.ts"
]
}