Compare commits
271 Commits
connectme-
...
dd75c54b32
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd75c54b32 | ||
|
|
f6f9163c3f | ||
|
|
4c166a8d33 | ||
| 284be82e1e | |||
| 41b5cb6fae | |||
| c4664edd7e | |||
| 69eddf47db | |||
| 71f3f353ab | |||
| 0d1dcf21c1 | |||
| 35493a09b5 | |||
| 390d97e6d5 | |||
| eca5cba858 | |||
| 6c190b80fb | |||
| a6065dd95c | |||
| 99127c42e2 | |||
| 599ccd1582 | |||
| 2b5e5564c8 | |||
| 7937be469b | |||
| 9f72d5885e | |||
| f65fd175ca | |||
|
|
d049c29f93 | ||
|
|
351ea75072 | ||
|
|
34163788f3 | ||
|
|
4ef4dd3c1b | ||
|
|
80498a0ff0 | ||
|
|
00386cc135 | ||
|
|
f5faae7907 | ||
|
|
659f9fd684 | ||
|
|
256de78e64 | ||
|
|
1500486cd8 | ||
|
|
63a825f153 | ||
|
|
1383e360a1 | ||
|
|
ca01d1c538 | ||
|
|
a315c8d4ef | ||
|
|
5ac9559b8f | ||
|
|
7b9e7d0a99 | ||
|
|
63b25928ff | ||
|
|
7d3b563759 | ||
|
|
baba20c028 | ||
|
|
87a9b8b02d | ||
|
|
cc41fa73cd | ||
|
|
ba923b9f91 | ||
|
|
cede47157e | ||
|
|
279c4fc86d | ||
|
|
c2f8d6ecee | ||
|
|
b4858efa73 | ||
|
|
6154932d9e | ||
|
|
d4cc85f644 | ||
|
|
82e8b785c4 | ||
|
|
5785e50cc5 | ||
|
|
de101348fc | ||
|
|
f442544912 | ||
|
|
d09dbcb697 | ||
|
|
f25bae1a08 | ||
|
|
800b60fb6d | ||
|
|
36558dfb85 | ||
|
|
c11bcd5d26 | ||
|
|
8450cc2d4d | ||
|
|
b1a9ee1403 | ||
|
|
80b9d9c8c8 | ||
|
|
db6665736a | ||
|
|
81980fa011 | ||
|
|
ac5f3eee96 | ||
|
|
9d87f7479c | ||
|
|
3639524fc7 | ||
|
|
f66114b22f | ||
|
|
8090de8031 | ||
|
|
081d663711 | ||
|
|
4fe16e5aa8 | ||
|
|
1fd5495570 | ||
|
|
9d68ee735a | ||
|
|
076e51c53a | ||
|
|
409a315a25 | ||
|
|
7a3264d43d | ||
|
|
effa320fa8 | ||
|
|
cc2a66367d | ||
|
|
989b5b010e | ||
|
|
f0e7ba94d2 | ||
|
|
3739fc8449 | ||
|
|
a74d191b30 | ||
|
|
a391cc88c9 | ||
|
|
12f8e63390 | ||
|
|
37238a1385 | ||
|
|
48cd044131 | ||
|
|
5665c4bf1e | ||
|
|
ad35d47ff5 | ||
|
|
f13cdd82df | ||
|
|
d6ebe10421 | ||
|
|
6e59e801b0 | ||
|
|
5dafd60299 | ||
|
|
825d7f1dd2 | ||
|
|
a3ea53c2f0 | ||
|
|
f37f34d803 | ||
|
|
bd0b11dc4a | ||
|
|
b36106cc8c | ||
|
|
07d35c4516 | ||
|
|
471cbacb66 | ||
|
|
229b181972 | ||
|
|
72615c7b98 | ||
|
|
45cafbee91 | ||
|
|
580651094f | ||
|
|
0ee92e98b2 | ||
|
|
3d8d9ee171 | ||
|
|
bde67dc7c3 | ||
|
|
a7be793608 | ||
|
|
ca81e19d14 | ||
|
|
7bd82fedce | ||
|
|
1aeb62d490 | ||
|
|
5886270e29 | ||
|
|
8f544d5c99 | ||
|
|
8dd8ec8930 | ||
|
|
3af82f7478 | ||
|
|
39a62818e9 | ||
|
|
24ff712306 | ||
|
|
ec6b30e220 | ||
|
|
548dbfcc9d | ||
|
|
09174abaa4 | ||
|
|
7ecb73ac6e | ||
|
|
8ade320440 | ||
|
|
bffa3fa2a3 | ||
|
|
4cf29c97b9 | ||
|
|
9377771531 | ||
|
|
0a96a87f94 | ||
|
|
5c14212429 | ||
|
|
e49d38657d | ||
|
|
1c7d1fc1ae | ||
|
|
7503d076e8 | ||
|
|
04f70aaa45 | ||
|
|
7b2b7b477f | ||
|
|
da7e25d339 | ||
|
|
b9f6e4d7aa | ||
|
|
396633932b | ||
|
|
46ad6ea9f3 | ||
|
|
1fa09ecac3 | ||
|
|
18b33ae10a | ||
|
|
e4e00184a5 | ||
|
|
9177765e8c | ||
|
|
0c0c62fe1b | ||
|
|
a0c9c5bab1 | ||
|
|
01b6e4ae72 | ||
|
|
2e36ee6e8b | ||
|
|
18cfa427d2 | ||
|
|
904a227adb | ||
|
|
23e532b770 | ||
|
|
f658e1f828 | ||
|
|
0500497fc1 | ||
|
|
ea691536ac | ||
|
|
c251a640b6 | ||
|
|
8031938b2f | ||
|
|
ca4bfdade4 | ||
|
|
b5f6f6d30f | ||
|
|
36107afbc2 | ||
|
|
539b1d2277 | ||
|
|
a9490da5a6 | ||
|
|
845e57d688 | ||
|
|
6835c84cc4 | ||
|
|
337e3ee2bf | ||
|
|
72d298ef2f | ||
|
|
d410164941 | ||
|
|
6b5ae7bce1 | ||
|
|
d80c4efb49 | ||
|
|
ddcf27b022 | ||
|
|
26c53e7455 | ||
|
|
0fbbe33e8a | ||
|
|
687508d26f | ||
|
|
f89729dbeb | ||
|
|
d90fee82d5 | ||
|
|
bde6ab4c7a | ||
|
|
2d0b97be44 | ||
|
|
3c22354130 | ||
|
|
ab555cd70e | ||
|
|
95bcaf3c5e | ||
|
|
48167530fd | ||
|
|
f909d90b6f | ||
|
|
e7d114a9d9 | ||
|
|
b83e0d603c | ||
|
|
7f57b2a4d3 | ||
|
|
c8f7e47181 | ||
|
|
e5d6b7cecd | ||
|
|
8a1868482c | ||
|
|
1bf68cea08 | ||
|
|
110e8300a1 | ||
| f3566361fb | |||
| a63a229b64 | |||
| 8944508308 | |||
| 775f24cffa | |||
|
|
78b72b0edc | ||
|
|
333fe79c8b | ||
|
|
9d10c8501a | ||
|
|
d64ece382a | ||
|
|
f91f821f86 | ||
|
|
b5301f948a | ||
|
|
dd589790c2 | ||
|
|
1fcc5ed70d | ||
|
|
41dbe81001 | ||
|
|
7b685ad99e | ||
| 2f1e1dc040 | |||
|
|
70e8a6877c | ||
|
|
87fd3121f9 | ||
|
|
4f9434163e | ||
|
|
350d452a7b | ||
| 9a0669df13 | |||
|
|
c0883fc2bc | ||
| 566bce4663 | |||
| c828718498 | |||
|
|
69c280b266 | ||
| 6794b01ac8 | |||
| 1cb586f55a | |||
|
|
df21879c0d | ||
|
|
30c9c86c93 | ||
|
|
2925d0f17b | ||
|
|
752dabd015 | ||
|
|
815f11d5bc | ||
| 02eb0e60b7 | |||
| a64ac93935 | |||
| 66a48d1c7e | |||
| 26c66f16b4 | |||
| 02e50bb2f9 | |||
| fadc62c8f0 | |||
| 4759f6f7ee | |||
| 14f2164a82 | |||
| 14ef1f9bad | |||
| dc99318ff0 | |||
| d2fc5f4d5c | |||
| 938bd48fff | |||
| 96f819dc91 | |||
| 25eee8adf5 | |||
| d2b2a29d3d | |||
| 1cf71261d1 | |||
| 88552eb04f | |||
| ab92c99321 | |||
| 02963de893 | |||
| 48550416d9 | |||
| 878c5ffd68 | |||
|
|
6e37fe93f7 | ||
|
|
72a2667549 | ||
|
|
39db7b4d26 | ||
| ff25c0ecb9 | |||
|
|
f1a93bffb5 | ||
| aa231d4f43 | |||
|
|
f254d57db4 | ||
| 106f835934 | |||
| f9b30a4cfd | |||
| 5e4a99529d | |||
| 4d585002d7 | |||
| b073fe3fdf | |||
| 312cc229d8 | |||
| 11b1d670d0 | |||
|
|
522ea36bb9 | ||
|
|
8be391c8e1 | ||
|
|
ea80304c21 | ||
| 771f75ef08 | |||
|
|
edf9b2c82b | ||
|
|
8c3bf8a8ed | ||
| a88d3657bf | |||
|
|
1656ce8690 | ||
|
|
7cdbec53ee | ||
| eee00f0797 | |||
|
|
33845b743d | ||
|
|
059139e213 | ||
|
|
005e7a0ac9 | ||
| 9ee59256a9 | |||
| c2784dcf45 | |||
| 64ed9b8eda | |||
| e9814f36bf | |||
| 0bd883df59 | |||
| 1657b0c5e9 | |||
| a9673b260f | |||
|
|
801f9ac1e3 | ||
|
|
cbbb376fd6 | ||
|
|
faaec7c718 |
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Application settings
|
||||||
|
TZ=Europe/Moscow
|
||||||
|
APP_PORT=8044
|
||||||
|
|
||||||
|
MONGO_INITDB_ROOT_USERNAME=qqq
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD=qqq
|
||||||
|
|
||||||
|
# MongoDB connection string
|
||||||
|
MONGO_ADDR=mongodb://qqq:qqq@127.0.0.1:27018
|
||||||
34
Dockerfile
34
Dockerfile
@@ -1,16 +1,38 @@
|
|||||||
FROM node:20
|
FROM node:22 AS builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app/
|
||||||
|
|
||||||
|
# Сначала копируем только файлы, необходимые для установки зависимостей
|
||||||
|
COPY ./package.json /usr/src/app/package.json
|
||||||
|
COPY ./package-lock.json /usr/src/app/package-lock.json
|
||||||
|
|
||||||
|
# Устанавливаем все зависимости
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Затем копируем исходный код проекта и файлы конфигурации
|
||||||
|
COPY ./tsconfig.json /usr/src/app/tsconfig.json
|
||||||
|
COPY ./server /usr/src/app/server
|
||||||
|
|
||||||
|
# Сборка проекта
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Вторая стадия - рабочий образ
|
||||||
|
FROM node:22
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app/server/log/
|
RUN mkdir -p /usr/src/app/server/log/
|
||||||
WORKDIR /usr/src/app/
|
WORKDIR /usr/src/app/
|
||||||
|
|
||||||
COPY ./server /usr/src/app/server
|
# Копирование только package.json/package-lock.json для продакшн зависимостей
|
||||||
COPY ./package.json /usr/src/app/package.json
|
COPY ./package.json /usr/src/app/package.json
|
||||||
COPY ./package-lock.json /usr/src/app/package-lock.json
|
COPY ./package-lock.json /usr/src/app/package-lock.json
|
||||||
COPY ./.serverrc.js /usr/src/app/.serverrc.js
|
|
||||||
# COPY ./.env /usr/src/app/.env
|
|
||||||
|
|
||||||
# RUN npm i --omit=dev
|
# Установка только продакшн зависимостей
|
||||||
RUN npm ci
|
RUN npm ci --production
|
||||||
|
|
||||||
|
# Копирование собранного приложения из билдера
|
||||||
|
COPY --from=builder /usr/src/app/dist /usr/src/app/dist
|
||||||
|
COPY --from=builder /usr/src/app/server /usr/src/app/server
|
||||||
|
|
||||||
EXPOSE 8044
|
EXPOSE 8044
|
||||||
|
|
||||||
CMD ["npm", "run", "up:prod"]
|
CMD ["npm", "run", "up:prod"]
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
docker stop ms-mongo
|
docker stop ms-mongo
|
||||||
docker volume remove ms_volume
|
docker volume remove ms_volume8
|
||||||
docker volume create ms_volume
|
docker volume create ms_volume8
|
||||||
docker run --rm -v ms_volume:/data/db --name ms-mongo -p 27017:27017 -d mongo:8.0.3
|
docker run --rm \
|
||||||
|
-v ms_volume8:/data/db \
|
||||||
|
--name ms-mongo \
|
||||||
|
-p 27018:27017 \
|
||||||
|
-e MONGO_INITDB_ROOT_USERNAME=qqq \
|
||||||
|
-e MONGO_INITDB_ROOT_PASSWORD=qqq \
|
||||||
|
-d mongo:8.0.3
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
ms_volume8:
|
|
||||||
ms_logs:
|
|
||||||
|
|
||||||
services:
|
|
||||||
mongoDb:
|
|
||||||
image: mongo:8.0.3
|
|
||||||
volumes:
|
|
||||||
- ms_volume8:/data/db
|
|
||||||
restart: always
|
|
||||||
# ports:
|
|
||||||
# - 27017:27017
|
|
||||||
multy-stubs:
|
|
||||||
# build: .
|
|
||||||
image: bro.js/ms/bh:$TAG
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ms_logs:/usr/src/app/server/log
|
|
||||||
ports:
|
|
||||||
- 8044:8044
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Moscow
|
|
||||||
- MONGO_ADDR=mongodb
|
|
||||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ms_volume8:
|
||||||
|
ms_logs:
|
||||||
|
|
||||||
|
services:
|
||||||
|
multy-stubs:
|
||||||
|
image: bro.js/ms/bh:$TAG
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ms_logs:/usr/src/app/server/log
|
||||||
|
ports:
|
||||||
|
- 8044:8044
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Moscow
|
||||||
|
- MONGO_ADDR=${MONGO_ADDR}
|
||||||
|
# depends_on:
|
||||||
|
# mongoDb:
|
||||||
|
# condition: service_started
|
||||||
|
# mongoDb:
|
||||||
|
# image: mongo:8.0.3
|
||||||
|
# volumes:
|
||||||
|
# - ms_volume8:/data/db
|
||||||
|
# restart: always
|
||||||
|
# environment:
|
||||||
|
# - MONGO_INITDB_ROOT_USERNAME=${MONGO_INITDB_ROOT_USERNAME}
|
||||||
|
# - MONGO_INITDB_ROOT_PASSWORD=${MONGO_INITDB_ROOT_PASSWORD}
|
||||||
|
# ports:
|
||||||
|
# - 27018:27017
|
||||||
@@ -4,7 +4,7 @@ import pluginJs from "@eslint/js";
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ ignores: ['server/routers/old/*'] },
|
{ ignores: ['server/routers/old/*'] },
|
||||||
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
|
{ files: ["**/*.js"], languageOptions: { } },
|
||||||
{ languageOptions: { globals: globals.node } },
|
{ languageOptions: { globals: globals.node } },
|
||||||
pluginJs.configs.recommended,
|
pluginJs.configs.recommended,
|
||||||
{
|
{
|
||||||
|
|||||||
2996
package-lock.json
generated
2996
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -1,15 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-stub",
|
"name": "multi-stub",
|
||||||
"version": "1.2.0",
|
"version": "2.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "server/index.ts",
|
||||||
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env PORT=8033 npx nodemon ./server",
|
"start": "cross-env NODE_ENV=\"development\" ts-node-dev .",
|
||||||
"up:prod": "cross-env NODE_ENV=\"production\" node ./server",
|
"build": "tsc",
|
||||||
"deploy:d:stop": "docker compose down",
|
"up:prod": "node dist/server/index.js",
|
||||||
"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": "npx eslint ./server",
|
||||||
"eslint:fix": "npx eslint ./server --fix",
|
"eslint:fix": "npx eslint ./server --fix",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
@@ -23,9 +21,13 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
|
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@langchain/community": "^0.3.56",
|
||||||
|
"@langchain/core": "^0.3.77",
|
||||||
|
"@langchain/langgraph": "^0.4.9",
|
||||||
"ai": "^4.1.13",
|
"ai": "^4.1.13",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -35,15 +37,20 @@
|
|||||||
"express": "5.0.1",
|
"express": "5.0.1",
|
||||||
"express-jwt": "^8.5.1",
|
"express-jwt": "^8.5.1",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
|
"gigachat": "^0.0.16",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mongodb": "^6.12.0",
|
"langchain": "^0.3.34",
|
||||||
"mongoose": "^8.9.2",
|
"langchain-gigachat": "^0.0.14",
|
||||||
"morgan": "^1.10.0",
|
"mongodb": "^6.20.0",
|
||||||
|
"mongoose": "^8.18.2",
|
||||||
|
"mongoose-sequence": "^6.0.1",
|
||||||
|
"morgan": "^1.10.1",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"pbkdf2-password": "^1.2.1",
|
"pbkdf2-password": "^1.2.1",
|
||||||
"rotating-file-stream": "^3.2.5",
|
"rotating-file-stream": "^3.2.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"uuid": "^11.0.3"
|
"zod": "^3.24.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -53,6 +60,8 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"mockingoose": "^2.16.2",
|
"mockingoose": "^2.16.2",
|
||||||
"nodemon": "3.1.9",
|
"nodemon": "3.1.9",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0",
|
||||||
|
"ts-node-dev": "2.0.0",
|
||||||
|
"typescript": "5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
rules.md
Normal file
87
rules.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
## Правила оформления студенческих бэкендов в `multi-stub`
|
||||||
|
|
||||||
|
Этот документ описывает, как подключать новый студенческий бэкенд к общему серверу и как работать с JSON‑заглушками. Правила написаны так, чтобы их мог автоматически выполнять помощник Cursor.
|
||||||
|
|
||||||
|
### 1. Общая структура проекта студента
|
||||||
|
|
||||||
|
- **Размещение проекта**
|
||||||
|
- Каждый студенческий бэкенд живёт в своей подпапке в `server/routers/<project-name>`.
|
||||||
|
- В корне подпапки должен быть основной файл роутера `index.js` (или `index.ts`), который экспортирует `express.Router()`.
|
||||||
|
- Подключение к общему серверу выполняется в `server/index.ts` через импорт и `app.use(<mountPath>, <router>)`.
|
||||||
|
|
||||||
|
- **Использование JSON‑заглушек**
|
||||||
|
- Если проект переносится из фронтенд‑репозитория и должен только отдавать данные, то в подпапке проекта должна быть папка `json/` со всеми нужными `.json` файлами.
|
||||||
|
- HTTP‑обработчики в роутере могут просто читать и возвращать содержимое этих файлов (например, через `require('./json/...')` или `import data from './json/...json'` с включённым `resolveJsonModule` / соответствующей конфигурацией bundler'а).
|
||||||
|
|
||||||
|
### 2. Правила для Cursor при указании директории заглушек
|
||||||
|
|
||||||
|
Когда пользователь явно указывает директорию с заглушками (например: `server/routers/<project-name>/json`), помощник Cursor должен последовательно выполнить следующие шаги.
|
||||||
|
|
||||||
|
- **2.1. Проверка валидности импортов JSON‑файлов**
|
||||||
|
- Найти все `.js` / `.ts` файлы внутри подпапки проекта.
|
||||||
|
- В каждом таком файле найти импорты/require, которые ссылаются на `.json` файлы (относительные пути вроде `'./json/.../file.json'`).
|
||||||
|
- Для каждого такого импорта:
|
||||||
|
- **Проверить, что файл реально существует** по указанному пути относительно файла-импортёра.
|
||||||
|
- **Проверить расширение**: путь должен заканчиваться на `.json` (без опечаток).
|
||||||
|
- **Проверить регистр и точное совпадение имени файла** (важно для кросс‑платформенности, даже если локально используется Windows).
|
||||||
|
- Если найдены ошибки (файл не существует, опечатка в имени, неправильный относительный путь и т.п.):
|
||||||
|
- Сформировать понятный список проблем: в каком файле, какая строка/импорт и что именно не так.
|
||||||
|
- Предложить автоматически исправить пути (если по контексту можно однозначно угадать нужный `*.json` файл).
|
||||||
|
|
||||||
|
- **2.2. Проверка подключения основного роутера проекта**
|
||||||
|
- Определить основной файл роутера проекта:
|
||||||
|
- По умолчанию это `server/routers/<project-name>/index.js` (или `index.ts`).
|
||||||
|
- Открыть `server/index.ts` и убедиться, что:
|
||||||
|
- Есть импорт роутера из соответствующей подпапки, например:
|
||||||
|
- `import <SomeUniqueName>Router from './routers/<project-name>'`
|
||||||
|
- или `const <SomeUniqueName>Router = require('./routers/<project-name>')`
|
||||||
|
- Имя переменной роутера **уникально** среди всех импортов роутеров (нет другого импорта с таким же именем).
|
||||||
|
- Есть вызов `app.use('<mount-path>', <SomeUniqueName>Router)`:
|
||||||
|
- `<mount-path>` должен быть осмысленным, совпадать с названием проекта или оговариваться пользователем.
|
||||||
|
- Если импорт или `app.use` отсутствуют:
|
||||||
|
- Сформировать предложение по добавлению корректного импорта и `app.use(...)`.
|
||||||
|
- Убедиться, что используемое имя роутера не конфликтует с уже существующими.
|
||||||
|
- Если обнаружен конфликт имён:
|
||||||
|
- Предложить переименовать новый роутер в уникальное имя и обновить соответствующие места в `server/index.ts`.
|
||||||
|
|
||||||
|
### 3. Предложение «оживить» JSON‑заглушки
|
||||||
|
|
||||||
|
После того как проверка импортов и подключения роутера завершена, помощник Cursor должен **задать пользователю вопрос**, не хочет ли он превратить заглушки в полноценный бэкенд.
|
||||||
|
|
||||||
|
- **3.1. Формулировка предложения**
|
||||||
|
- Спросить у пользователя примерно так:
|
||||||
|
- «Обнаружены JSON‑заглушки в директории `<указанная-папка>`. Хотите, чтобы я попытался автоматически:
|
||||||
|
1) построить модели данных (mongoose‑схемы) на основе структуры JSON;
|
||||||
|
2) создать CRUD‑эндпоинты и/или более сложные маршруты, опираясь на существующие данные;
|
||||||
|
3) заменить прямую отдачу `*.json` файлов на работу через базу данных?»
|
||||||
|
|
||||||
|
- **3.2. Поведение при согласии пользователя**
|
||||||
|
- Проанализировать структуру JSON‑файлов:
|
||||||
|
- Определить основные сущности и поля.
|
||||||
|
- Выделить типы полей (строки, числа, даты, массивы, вложенные объекты и т.п.).
|
||||||
|
- На основе анализа предложить:
|
||||||
|
- Набор `mongoose`‑схем (`models`) с аккуратной сериализацией (виртуальное поле `id`, скрытие `_id` и `__v`).
|
||||||
|
- Набор маршрутов `express` для работы с этими моделями (минимум: чтение списков и элементов; по возможности — создание/обновление/удаление).
|
||||||
|
- Перед внесением изменений:
|
||||||
|
- Показать пользователю краткий план того, какие файлы будут созданы/изменены.
|
||||||
|
- Выполнить изменения только после явного подтверждения пользователя.
|
||||||
|
|
||||||
|
### 4. Минимальные требования к новому студенческому бэкенду
|
||||||
|
|
||||||
|
- **Обязательные элементы**
|
||||||
|
- Подпапка в `server/routers/<project-name>`.
|
||||||
|
- Основной роутер `index.js` / `index.ts`, экспортирующий `express.Router()`.
|
||||||
|
- Подключение к общему серверу в `server/index.ts` (импорт + `app.use()` с уникальным именем роутера).
|
||||||
|
|
||||||
|
- **Если используются JSON‑заглушки**
|
||||||
|
- Папка `json/` внутри проекта.
|
||||||
|
- Все пути в импортирующих файлах должны указывать на реально существующие `*.json` файлы.
|
||||||
|
- Не должно быть «магических» абсолютных путей; только относительные пути от файла до нужного JSON.
|
||||||
|
|
||||||
|
- **Если проект «оживлён»**
|
||||||
|
- Папка `model/` с моделью(ями) данных (например, через `mongoose`).
|
||||||
|
- Роуты, которые вместо прямой отдачи файлов работают с моделями и, при необходимости, с внешними сервисами.
|
||||||
|
|
||||||
|
Следуя этим правилам, можно подключать новые студенческие проекты в единый бэкенд, минимизировать типичные ошибки с путями к JSON и упростить автоматическое развитие заглушек до полноценного API.
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const noToken = 'No authorization token was found'
|
|
||||||
|
|
||||||
module.exports = (err, req, res, next) => {
|
|
||||||
if (err.message === noToken) {
|
|
||||||
res.status(400).send({
|
|
||||||
success: false, error: 'Токен авторизации не найден',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(400).send({
|
|
||||||
success: false, error: err.message || 'Что-то пошло не так',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
28
server/error.ts
Normal file
28
server/error.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { ErrorLog } from './models/ErrorLog'
|
||||||
|
|
||||||
|
const noToken = 'No authorization token was found'
|
||||||
|
|
||||||
|
export const errorHandler = (err, req, res, next) => {
|
||||||
|
// Сохраняем ошибку в базу данных
|
||||||
|
const errorLog = new ErrorLog({
|
||||||
|
message: err.message || 'Неизвестная ошибка',
|
||||||
|
stack: err.stack,
|
||||||
|
path: req.path,
|
||||||
|
method: req.method,
|
||||||
|
query: req.query,
|
||||||
|
body: req.body
|
||||||
|
})
|
||||||
|
|
||||||
|
errorLog.save()
|
||||||
|
.catch(saveErr => console.error('Ошибка при сохранении лога ошибки:', saveErr))
|
||||||
|
|
||||||
|
if (err.message === noToken) {
|
||||||
|
res.status(400).send({
|
||||||
|
success: false, error: 'Токен авторизации не найден',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(400).send({
|
||||||
|
success: false, error: err.message || 'Что-то пошло не так',
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
const express = require("express")
|
|
||||||
const bodyParser = require("body-parser")
|
|
||||||
const cookieParser = require("cookie-parser")
|
|
||||||
const session = require("express-session")
|
|
||||||
const morgan = require("morgan")
|
|
||||||
const path = require("path")
|
|
||||||
const rfs = require("rotating-file-stream")
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
require("dotenv").config()
|
|
||||||
exports.app = app
|
|
||||||
|
|
||||||
const accessLogStream = rfs.createStream("access.log", {
|
|
||||||
size: "10M",
|
|
||||||
interval: "1d",
|
|
||||||
compress: "gzip",
|
|
||||||
path: path.join(__dirname, "log"),
|
|
||||||
})
|
|
||||||
|
|
||||||
const errorLogStream = rfs.createStream("error.log", {
|
|
||||||
size: "10M",
|
|
||||||
interval: "1d",
|
|
||||||
compress: "gzip",
|
|
||||||
path: path.join(__dirname, "log"),
|
|
||||||
})
|
|
||||||
|
|
||||||
const config = require("../.serverrc")
|
|
||||||
const { setIo } = require("./io")
|
|
||||||
|
|
||||||
app.use(cookieParser())
|
|
||||||
app.use(
|
|
||||||
morgan("combined", {
|
|
||||||
stream: accessLogStream,
|
|
||||||
skip: function (req, res) {
|
|
||||||
return res.statusCode >= 400
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// log all requests to access.log
|
|
||||||
app.use(
|
|
||||||
morgan("combined", {
|
|
||||||
stream: errorLogStream,
|
|
||||||
skip: function (req, res) {
|
|
||||||
console.log('statusCode', res.statusCode, res.statusCode <= 400)
|
|
||||||
return res.statusCode < 400
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const server = setIo(app)
|
|
||||||
|
|
||||||
const sess = {
|
|
||||||
secret: "super-secret-key",
|
|
||||||
resave: true,
|
|
||||||
saveUninitialized: true,
|
|
||||||
cookie: {},
|
|
||||||
}
|
|
||||||
if (app.get("env") === "production") {
|
|
||||||
app.set("trust proxy", 1)
|
|
||||||
sess.cookie.secure = true
|
|
||||||
}
|
|
||||||
app.use(session(sess))
|
|
||||||
|
|
||||||
app.use(
|
|
||||||
bodyParser.json({
|
|
||||||
limit: "50mb",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
app.use(
|
|
||||||
bodyParser.urlencoded({
|
|
||||||
limit: "50mb",
|
|
||||||
extended: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
app.use(require("./root"))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Добавляйте сюда свои routers.
|
|
||||||
*/
|
|
||||||
app.use("/kfu-m-24-1", require("./routers/kfu-m-24-1"))
|
|
||||||
app.use("/epja-2024-1", require("./routers/epja-2024-1"))
|
|
||||||
app.use("/v1/todo", require("./routers/todo"))
|
|
||||||
app.use("/dogsitters-finder", require("./routers/dogsitters-finder"))
|
|
||||||
app.use("/kazan-explore", require("./routers/kazan-explore"))
|
|
||||||
app.use("/edateam", require("./routers/edateam-legacy"))
|
|
||||||
app.use("/dry-wash", require("./routers/dry-wash"))
|
|
||||||
app.use("/freetracker", require("./routers/freetracker"))
|
|
||||||
app.use("/dhs-testing", require("./routers/dhs-testing"))
|
|
||||||
app.use("/gamehub", require("./routers/gamehub"))
|
|
||||||
app.use("/esc", require("./routers/esc"))
|
|
||||||
app.use('/connectme', require('./routers/connectme'))
|
|
||||||
|
|
||||||
app.use(require("./error"))
|
|
||||||
|
|
||||||
server.listen(config.port, () =>
|
|
||||||
console.log(`Listening on http://localhost:${config.port}`)
|
|
||||||
)
|
|
||||||
155
server/index.ts
Normal file
155
server/index.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
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
13
server/io.js
@@ -1,13 +0,0 @@
|
|||||||
const { Server } = require('socket.io')
|
|
||||||
const { createServer } = require('http')
|
|
||||||
|
|
||||||
let io = null
|
|
||||||
|
|
||||||
module.exports.setIo = (app) => {
|
|
||||||
const server = createServer(app)
|
|
||||||
io = new Server(server, {})
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.getIo = () => io
|
|
||||||
13
server/io.ts
Normal file
13
server/io.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { createServer } from 'http'
|
||||||
|
|
||||||
|
let io = null
|
||||||
|
|
||||||
|
export const setIo = (app) => {
|
||||||
|
const server = createServer(app)
|
||||||
|
io = new Server(server, {})
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIo = () => io
|
||||||
16
server/models/ErrorLog.ts
Normal file
16
server/models/ErrorLog.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const ErrorLogSchema = new mongoose.Schema({
|
||||||
|
message: { type: String, required: true },
|
||||||
|
stack: { type: String },
|
||||||
|
path: { type: String },
|
||||||
|
method: { type: String },
|
||||||
|
query: { type: Object },
|
||||||
|
body: { type: Object },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Индекс для быстрого поиска по дате создания
|
||||||
|
ErrorLogSchema.index({ createdAt: 1 })
|
||||||
|
|
||||||
|
export const ErrorLog = mongoose.model('ErrorLog', ErrorLogSchema)
|
||||||
55
server/models/questionnaire.ts
Normal file
55
server/models/questionnaire.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
// Типы вопросов
|
||||||
|
export const QUESTION_TYPES = {
|
||||||
|
SINGLE_CHOICE: 'single_choice', // Один вариант
|
||||||
|
MULTIPLE_CHOICE: 'multiple_choice', // Несколько вариантов
|
||||||
|
TEXT: 'text', // Текстовый ответ
|
||||||
|
RATING: 'rating', // Оценка по шкале
|
||||||
|
TAG_CLOUD: 'tag_cloud' // Облако тегов
|
||||||
|
};
|
||||||
|
|
||||||
|
// Типы отображения
|
||||||
|
export const DISPLAY_TYPES = {
|
||||||
|
DEFAULT: 'default',
|
||||||
|
TAG_CLOUD: 'tag_cloud',
|
||||||
|
VOTING: 'voting',
|
||||||
|
POLL: 'poll'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Схема варианта ответа
|
||||||
|
const optionSchema = new mongoose.Schema({
|
||||||
|
text: { type: String, required: true },
|
||||||
|
count: { type: Number, default: 0 } // счетчик голосов
|
||||||
|
});
|
||||||
|
|
||||||
|
// Схема вопроса
|
||||||
|
const questionSchema = new mongoose.Schema({
|
||||||
|
text: { type: String, required: true },
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: Object.values(QUESTION_TYPES),
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
options: [optionSchema],
|
||||||
|
required: { type: Boolean, default: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Схема опроса
|
||||||
|
const questionnaireSchema = new mongoose.Schema({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
description: { type: String },
|
||||||
|
questions: [questionSchema],
|
||||||
|
displayType: {
|
||||||
|
type: String,
|
||||||
|
enum: Object.values(DISPLAY_TYPES),
|
||||||
|
default: DISPLAY_TYPES.DEFAULT
|
||||||
|
},
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
updatedAt: { type: Date, default: Date.now },
|
||||||
|
adminLink: { type: String, required: true }, // ссылка для редактирования
|
||||||
|
publicLink: { type: String, required: true } // ссылка для голосования
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema);
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const router = require('express').Router()
|
|
||||||
const mongoose = require('mongoose')
|
|
||||||
|
|
||||||
const pkg = require('../package.json')
|
|
||||||
|
|
||||||
require('./utils/mongoose')
|
|
||||||
const folderPath = path.resolve(__dirname, './routers')
|
|
||||||
const folders = fs.readdirSync(folderPath)
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
// throw new Error('check error message')
|
|
||||||
res.send(`
|
|
||||||
<h1>multy stub is working v${pkg.version}</h1>
|
|
||||||
<ul>
|
|
||||||
${folders.map((f) => `<li>${f}</li>`).join('')}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>models</h2>
|
|
||||||
<ul>${
|
|
||||||
(await Promise.all(
|
|
||||||
(await mongoose.modelNames()).map(async (name) => {
|
|
||||||
const count = await mongoose.model(name).countDocuments()
|
|
||||||
return `<li>${name} - ${count}</li>`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)).map(t => t).join(' ')
|
|
||||||
}</ul>
|
|
||||||
`)
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
2
server/routers/dogsitters-finder/const.js
Normal file
2
server/routers/dogsitters-finder/const.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
exports.DSF_AUTH_USER_MODEL_NAME = 'DSF_AUTH_USER'
|
||||||
|
exports.DSF_INTERACTION_MODEL_NAME = 'DSF_INTERACTION'
|
||||||
@@ -7,29 +7,29 @@ router.get("/users", (request, response) => {
|
|||||||
router.post("/auth", (request, response) => {
|
router.post("/auth", (request, response) => {
|
||||||
const { phoneNumber, password } = request.body;
|
const { phoneNumber, password } = request.body;
|
||||||
console.log(phoneNumber, password);
|
console.log(phoneNumber, password);
|
||||||
if (phoneNumber === "89999999999") {
|
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
|
||||||
response.send(require("./json/auth/dogsitter.success.json"));
|
response.send(require("./json/auth/success.json"));
|
||||||
} else if (phoneNumber === "89555555555") {
|
|
||||||
response.status(400).send(require("./json/auth/error.json"));
|
|
||||||
} else {
|
} else {
|
||||||
response.send(require("./json/auth/owner.success.json"));
|
response.status(401).send(require("./json/auth/error.json"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/auth/2fa", (request, response) => {
|
router.post("/auth/2fa", (request, response) => {
|
||||||
const { code } = request.body;
|
const { phoneNumber, code } = request.body;
|
||||||
if (code === "0000") {
|
if (code === "0000" && phoneNumber === "89999999999") {
|
||||||
response.send(require("./json/2fa/success.json"));
|
response.send(require("./json/2fa/dogsitter.success.json"));
|
||||||
|
} else if (code === "0000" && phoneNumber === "89559999999") {
|
||||||
|
response.send(require("./json/2fa/owner.success.json"));
|
||||||
} else {
|
} else {
|
||||||
response.status(400).send(require("./json/2fa/error.json"));
|
response.status(401).send(require("./json/2fa/error.json"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/register", (request, response) => {
|
router.post("/register", (request, response) => {
|
||||||
const { firstName, secondName, phoneNumber, password, role } = request.body;
|
const { firstName, secondName, phoneNumber, password, role } = request.body;
|
||||||
console.log(phoneNumber, password, role);
|
console.log(phoneNumber, password, role);
|
||||||
if (phoneNumber === "89283244141" || phoneNumber === "89872855893") {
|
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
|
||||||
response.status(400).send(require("./json/register/error.json"));
|
response.status(401).send(require("./json/register/error.json"));
|
||||||
} else if (role === "dogsitter") {
|
} else if (role === "dogsitter") {
|
||||||
response.send(require("./json/register/dogsitter.success.json"));
|
response.send(require("./json/register/dogsitter.success.json"));
|
||||||
} else {
|
} else {
|
||||||
@@ -37,4 +37,192 @@ router.post("/register", (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
router.get("/auth/session", (request, response) => {
|
||||||
|
const authHeader = request.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
return response.status(401).json({ error: "Authorization header missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Берём сам токен из заголовка
|
||||||
|
const token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return response.status(401).json({ error: "Bearer token missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const secretKey = "secret";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, secretKey);
|
||||||
|
|
||||||
|
if (decoded.role === "dogsitter") {
|
||||||
|
response.send(require("./json/role/dogsitter.success.json"));
|
||||||
|
} else {
|
||||||
|
response.send(require("./json/role/owner.success.json"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("token e:", e);
|
||||||
|
return response.status(403).json({ error: "Invalid token" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Проверка взаимодействия между пользователем и догситтером
|
||||||
|
router.get("/interactions/check", (req, res) => {
|
||||||
|
const { owner_id, dogsitter_id } = req.query;
|
||||||
|
|
||||||
|
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||||
|
|
||||||
|
delete require.cache[require.resolve(usersFilePath)];
|
||||||
|
const usersFile = require(usersFilePath);
|
||||||
|
|
||||||
|
const interactions = usersFile.interactions || [];
|
||||||
|
|
||||||
|
const exists = interactions.some(
|
||||||
|
(interaction) =>
|
||||||
|
interaction.owner_id === Number(owner_id) &&
|
||||||
|
interaction.dogsitter_id === Number(dogsitter_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ exists });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавление нового взаимодействия
|
||||||
|
router.post("/interactions", (req, res) => {
|
||||||
|
const { owner_id, dogsitter_id, interaction_type } = req.body;
|
||||||
|
|
||||||
|
if (!owner_id || !dogsitter_id || !interaction_type) {
|
||||||
|
return res.status(400).json({ error: "Missing required fields" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||||
|
|
||||||
|
delete require.cache[require.resolve(usersFilePath)];
|
||||||
|
const usersFile = require(usersFilePath);
|
||||||
|
|
||||||
|
if (!usersFile.interactions) {
|
||||||
|
usersFile.interactions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, существует ли уже такое взаимодействие
|
||||||
|
const exists = usersFile.interactions.some(
|
||||||
|
(interaction) =>
|
||||||
|
interaction.owner_id === Number(owner_id) &&
|
||||||
|
interaction.dogsitter_id === Number(dogsitter_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
usersFile.interactions.push({
|
||||||
|
owner_id: Number(owner_id),
|
||||||
|
dogsitter_id: Number(dogsitter_id),
|
||||||
|
interaction_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
usersFilePath,
|
||||||
|
JSON.stringify(usersFile, null, 2),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Добавлено взаимодействие: owner_id=${owner_id}, dogsitter_id=${dogsitter_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/dogsitter-viewing", (req, res) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
console.log(`Получен запрос для dogsitter с ID: ${id}`);
|
||||||
|
|
||||||
|
const usersFile = require("./json/users/users.json");
|
||||||
|
const users = usersFile.data; // Извлекаем массив из свойства "data"
|
||||||
|
|
||||||
|
const user = users.find((user) => user.id === Number(id));
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
res.json(user); // Возвращаем найденного пользователя
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: "User not found" }); // Если пользователь не найден
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/dogsitter-viewing/rating/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { rating } = req.body;
|
||||||
|
|
||||||
|
if (!rating || rating < 1 || rating > 5) {
|
||||||
|
return res.status(400).json({ error: 'Некорректная оценка' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||||
|
|
||||||
|
delete require.cache[require.resolve(usersFilePath)];
|
||||||
|
const usersFile = require(usersFilePath);
|
||||||
|
const users = usersFile.data;
|
||||||
|
|
||||||
|
const userIndex = users.findIndex(user => user.id === Number(id));
|
||||||
|
if (userIndex === -1) {
|
||||||
|
return res.status(404).json({ error: 'Догситтер не найден' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!users[userIndex].ratings) {
|
||||||
|
users[userIndex].ratings = [];
|
||||||
|
}
|
||||||
|
users[userIndex].ratings.push(rating);
|
||||||
|
|
||||||
|
if (users[userIndex].ratings.length > 100) {
|
||||||
|
users[userIndex].ratings.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = users[userIndex].ratings.reduce((sum, r) => sum + r, 0);
|
||||||
|
users[userIndex].rating = parseFloat((total / users[userIndex].ratings.length).toFixed(2));
|
||||||
|
|
||||||
|
fs.writeFileSync(usersFilePath, JSON.stringify({ data: users }, null, 2), 'utf8');
|
||||||
|
|
||||||
|
console.log(`Обновлен рейтинг догситтера ${id}: ${users[userIndex].rating}`);
|
||||||
|
|
||||||
|
res.json({ rating: users[userIndex].rating, ratings: users[userIndex].ratings });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.patch('/users/:id', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const updateData = req.body;
|
||||||
|
|
||||||
|
console.log('Полученные данные для обновления:', updateData);
|
||||||
|
|
||||||
|
|
||||||
|
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
|
||||||
|
|
||||||
|
delete require.cache[require.resolve(usersFilePath)];
|
||||||
|
const usersFile = require(usersFilePath);
|
||||||
|
const users = usersFile.data;
|
||||||
|
|
||||||
|
const userIndex = users.findIndex((user) => user.id === Number(id));
|
||||||
|
if (userIndex === -1) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
users[userIndex] = { ...users[userIndex], ...updateData };
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
usersFilePath,
|
||||||
|
JSON.stringify({ data: users }, null, 2),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Обновлённые данные пользователя:', users[userIndex]);
|
||||||
|
|
||||||
|
res.json(users[userIndex]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.7q66wTNyLZp3TGFYF_JdU-yhlWViJulTxP_PCQzO4OI"
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": "Invalid code."
|
"message": "Invalid code",
|
||||||
|
"statusCode": 401
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mywicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.sI9839YXveTpEWhdpr5QbCYllt6hHYO7NsrQDcrXZIQ"
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Two-factor authentication passed."
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"phoneNumber": 89283244141,
|
|
||||||
"firstName": "Вася",
|
|
||||||
"secondName": "Пупкин",
|
|
||||||
"role": "dogsitter",
|
|
||||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
|
||||||
"price": 1500,
|
|
||||||
"aboutMe": "Я люблю собак"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"error": "Пользователь не найден"
|
"message": "Неверный логин или пароль",
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"statusCode": 401
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"data": {
|
|
||||||
"id": 3,
|
|
||||||
"phoneNumber": 89872855893,
|
|
||||||
"firstName": "Гадий",
|
|
||||||
"secondName": "Петрович",
|
|
||||||
"role": "owner"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
server/routers/dogsitters-finder/json/auth/success.json
Normal file
5
server/routers/dogsitters-finder/json/auth/success.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"message": "Первый фактор аутентификации пройден",
|
||||||
|
"statusCode": 200
|
||||||
|
}
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwicm9sZSI6ImRvZ3NpdHRlciIsImlhdCI6MTUxNjIzOTAyMn0.T9V3-f3rD1deA5a2J-tYNw0cACEpzKHbhMPkc7gh8c0"
|
||||||
"id": 5,
|
|
||||||
"phoneNumber": 89555555555,
|
|
||||||
"firstName": "Масяня",
|
|
||||||
"secondName": "Карлова",
|
|
||||||
"role": "dogsitter",
|
|
||||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
|
||||||
"price": 100,
|
|
||||||
"aboutMe": "Все на свете - собаки"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"error": "Пользователь с таким номером телефона уже существует"
|
"message": "Такой пользователь уже был зарегистрирован",
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"statusCode": 401
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
{
|
{
|
||||||
"data": {
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Niwicm9sZSI6Im93bmVyIiwiaWF0IjoxNTE2MjM5MDIyfQ.qgOhk9tNcaMRbarRWISTgvGx5Eq_X8fcA5lhdVs2tQI"
|
||||||
"id": 6,
|
|
||||||
"phoneNumber": 89888888888,
|
|
||||||
"firstName": "Генадий",
|
|
||||||
"secondName": "Паровозов",
|
|
||||||
"role": "owner"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"role": "dogsitter"
|
||||||
|
}
|
||||||
5
server/routers/dogsitters-finder/json/role/error.json
Normal file
5
server/routers/dogsitters-finder/json/role/error.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"message": "Неверный jwt token",
|
||||||
|
"error": "Forbidden",
|
||||||
|
"statusCode": 403
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"role": "owner"
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
[
|
{
|
||||||
|
"data": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"phone_number": 89283244141,
|
"phone_number": "89999999999",
|
||||||
"first_name": "Вася",
|
"first_name": "Вася",
|
||||||
"second_name": "Пупкин",
|
"second_name": "Пупкин",
|
||||||
"role": "dogsitter",
|
"role": "dogsitter",
|
||||||
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
|
"location": "Россия, республика Татарстан, Казань, Пушкина, 12",
|
||||||
"price": 1500,
|
"price": "1500",
|
||||||
"about_me": "Я люблю собак"
|
"about_me": "Я люблю собак!",
|
||||||
|
"rating": 5,
|
||||||
|
"ratings": [
|
||||||
|
5,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"tg": "jullllllie"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
@@ -16,12 +23,18 @@
|
|||||||
"second_name": "Пуськин",
|
"second_name": "Пуськин",
|
||||||
"role": "dogsitter",
|
"role": "dogsitter",
|
||||||
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
|
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
|
||||||
"price": 1000000,
|
"price": 2000,
|
||||||
"about_me": "Я не люблю собак. И вообще я котоман."
|
"about_me": "Я не люблю собак. И вообще я котоман.",
|
||||||
|
"rating": 4,
|
||||||
|
"ratings": [
|
||||||
|
4,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"tg": "vanya006"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"phone_number": 89872855893,
|
"phone_number": 89559999999,
|
||||||
"first_name": "Гадий",
|
"first_name": "Гадий",
|
||||||
"second_name": "Петрович",
|
"second_name": "Петрович",
|
||||||
"role": "owner"
|
"role": "owner"
|
||||||
@@ -33,7 +46,24 @@
|
|||||||
"second_name": "Максим",
|
"second_name": "Максим",
|
||||||
"role": "dogsitter",
|
"role": "dogsitter",
|
||||||
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
|
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
|
||||||
"price": 1000000,
|
"price": 1750,
|
||||||
"about_me": "Миллион алых роз"
|
"about_me": "Миллион алых роз",
|
||||||
|
"rating": 4.5,
|
||||||
|
"ratings": [
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"tg": "maks100500"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"interactions": [
|
||||||
|
{
|
||||||
|
"owner_id": 3,
|
||||||
|
"dogsitter_id": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner_id": 1,
|
||||||
|
"dogsitter_id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
server/routers/dogsitters-finder/model/interaction.js
Normal file
24
server/routers/dogsitters-finder/model/interaction.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { Schema, model } = require("mongoose");
|
||||||
|
|
||||||
|
const { DSF_AUTH_USER_MODEL_NAME, DSF_INTERACTION_MODEL_NAME } = require("../../const");
|
||||||
|
|
||||||
|
const interactionSchema = new Schema({
|
||||||
|
owner_id: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: DSF_AUTH_USER_MODEL_NAME,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
dogsitter_id: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: DSF_AUTH_USER_MODEL_NAME,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interactionSchema.index({ owner_id: 1, dogsitter_id: 1 });
|
||||||
|
|
||||||
|
module.exports.Interaction = model(DSF_INTERACTION_MODEL_NAME, interactionSchema);
|
||||||
83
server/routers/dogsitters-finder/model/user.js
Normal file
83
server/routers/dogsitters-finder/model/user.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
const { Schema, model } = require("mongoose");
|
||||||
|
|
||||||
|
const { DSF_AUTH_USER_MODEL_NAME } = require("../../const");
|
||||||
|
|
||||||
|
const userSchema = new Schema({
|
||||||
|
phone_number: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
match: /^\+?\d{10,15}$/
|
||||||
|
},
|
||||||
|
first_name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
second_name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: String,
|
||||||
|
enum: ["dogsitter", "owner"],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: String,
|
||||||
|
required: function() {
|
||||||
|
return this.role === "dogsitter";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
min: 0,
|
||||||
|
required: function() {
|
||||||
|
return this.role === "dogsitter";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
about_me: {
|
||||||
|
type: String,
|
||||||
|
maxlength: 500
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
type: Number,
|
||||||
|
min: 0,
|
||||||
|
max: 5,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
ratings: {
|
||||||
|
type: [Number],
|
||||||
|
default: [],
|
||||||
|
validate: {
|
||||||
|
validator: function(arr) {
|
||||||
|
return arr.every(v => v >= 0 && v <= 5);
|
||||||
|
},
|
||||||
|
message: "Рейтинг должен быть в диапазоне от 0 до 5!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tg: {
|
||||||
|
type: String,
|
||||||
|
match: /^[a-zA-Z0-9_]{5,32}$/
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
userSchema.virtual("id").get(function() {
|
||||||
|
return this._id.toHexString();
|
||||||
|
});
|
||||||
|
|
||||||
|
userSchema.set("toJSON", {
|
||||||
|
virtuals: true,
|
||||||
|
versionKey: false,
|
||||||
|
transform: function(doc, ret) {
|
||||||
|
delete ret._id;
|
||||||
|
delete ret.__v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports.User = model(DSF_AUTH_USER_MODEL_NAME, userSchema);
|
||||||
149
server/routers/dogsitters-finder/routes.js
Normal file
149
server/routers/dogsitters-finder/routes.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const { Router } = require('express')
|
||||||
|
const { expressjwt } = require('express-jwt')
|
||||||
|
|
||||||
|
const { getAnswer } = require('../../utils/common')
|
||||||
|
const { User, Interaction } = require('./model')
|
||||||
|
const { TOKEN_KEY } = require('./const')
|
||||||
|
const { requiredValidate } = require('./utils')
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
// Получение списка пользователей
|
||||||
|
router.get('/users', async (req, res) => {
|
||||||
|
|
||||||
|
const users = await User.find()
|
||||||
|
.select('-__v -ratings -phone_number')
|
||||||
|
.lean()
|
||||||
|
|
||||||
|
console.log('get users successfull')
|
||||||
|
|
||||||
|
res.send(getAnswer(null, users))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Получение конкретного пользователя
|
||||||
|
router.get('/dogsitter-viewing', async (req, res) => {
|
||||||
|
const { userId } = req.params
|
||||||
|
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
.select('-__v -ratings')
|
||||||
|
.lean()
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).send(getAnswer(new Error('Пользователь не найден')))
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(getAnswer(null, user))
|
||||||
|
})
|
||||||
|
|
||||||
|
router.use(expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }))
|
||||||
|
|
||||||
|
// Добавление оценки пользователю
|
||||||
|
router.post('/dogsitter-viewing/rating', requiredValidate('value'), async (req, res) => {
|
||||||
|
const { userId } = req.params
|
||||||
|
const { value } = req.body
|
||||||
|
const authUserId = req.auth.id
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await User.findById(userId)
|
||||||
|
if (!user) throw new Error('Пользователь не найден')
|
||||||
|
if (user.role !== 'dogsitter') throw new Error('Нельзя оценивать этого пользователя')
|
||||||
|
if (user.id === authUserId) throw new Error('Нельзя оценивать самого себя')
|
||||||
|
|
||||||
|
user.ratings.push(Number(value))
|
||||||
|
user.rating = user.ratings.reduce((a, b) => a + b, 0) / user.ratings.length
|
||||||
|
|
||||||
|
const updatedUser = await user.save()
|
||||||
|
|
||||||
|
res.send(getAnswer(null, {
|
||||||
|
id: updatedUser.id,
|
||||||
|
rating: updatedUser.rating.toFixed(1),
|
||||||
|
totalRatings: updatedUser.ratings.length
|
||||||
|
}))
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send(getAnswer(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Обновление информации пользователя
|
||||||
|
router.patch('/users', async (req, res) => {
|
||||||
|
const { userId } = req.params
|
||||||
|
const updates = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await User.findByIdAndUpdate(userId, updates, { new: true })
|
||||||
|
.select('-__v -ratings')
|
||||||
|
|
||||||
|
if (!user) throw new Error('Пользователь не найден')
|
||||||
|
res.send(getAnswer(null, user))
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send(getAnswer(error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Создание объекта взаимодействия
|
||||||
|
router.post('/interactions',
|
||||||
|
expressjwt({ secret: TOKEN_KEY, algorithms: ['HS256'] }),
|
||||||
|
requiredValidate('dogsitter_id'),
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dogsitter_id } = req.body
|
||||||
|
const owner_id = req.auth.id // ID из JWT токена
|
||||||
|
|
||||||
|
// Проверка существования пользователей
|
||||||
|
const [owner, dogsitter] = await Promise.all([
|
||||||
|
User.findById(owner_id),
|
||||||
|
User.findById(dogsitter_id)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!owner || owner.role !== 'owner') {
|
||||||
|
throw new Error('Владелец не найден или имеет неверную роль')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dogsitter || dogsitter.role !== 'dogsitter') {
|
||||||
|
throw new Error('Догситтер не найден или имеет неверную роль')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание взаимодействия
|
||||||
|
const interaction = await Interaction.create({
|
||||||
|
owner_id,
|
||||||
|
dogsitter_id
|
||||||
|
})
|
||||||
|
|
||||||
|
res.send(getAnswer(null, {
|
||||||
|
id: interaction.id,
|
||||||
|
timestamp: interaction.timestamp
|
||||||
|
}))
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send(getAnswer(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get('/interactions/check', async (req, res) => {
|
||||||
|
const { owner_id, dogsitter_id } = req.query;
|
||||||
|
|
||||||
|
if (!owner_id || !dogsitter_id) {
|
||||||
|
return res.status(400).send(getAnswer('Missing owner_id or dogsitter_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Поиск взаимодействий по owner_id и dogsitter_id
|
||||||
|
const interactions = await Interaction.find({ owner_id, dogsitter_id })
|
||||||
|
.select('-__v') // Выбираем только нужные поля
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
if (interactions.length === 0) {
|
||||||
|
return res.status(404).send(getAnswer('No interactions found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(getAnswer(null, interactions));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking interactions:', error);
|
||||||
|
res.status(500).send(getAnswer('Internal Server Error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
const router = require('express').Router()
|
const router = require("express").Router();
|
||||||
const {MasterModel} = require('./model/master')
|
const { MasterModel } = require("./model/master");
|
||||||
const mongoose = require("mongoose")
|
const mongoose = require("mongoose");
|
||||||
const {OrderModel} = require("./model/order")
|
const { OrderModel } = require("./model/order");
|
||||||
|
|
||||||
|
router.post("/masters/list", async (req, res, next) => {
|
||||||
router.get("/masters", async (req, res, next) => {
|
|
||||||
try {
|
try {
|
||||||
|
const { startDate, endDate } = req.body;
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
throw new Error("Missing startDate or endDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const end = new Date(endDate);
|
||||||
const masters = await MasterModel.find({});
|
const masters = await MasterModel.find({});
|
||||||
const orders = await OrderModel.find({});
|
|
||||||
|
const orders = await OrderModel.find({
|
||||||
|
$or: [
|
||||||
|
{ startWashTime: { $lt: end }, endWashTime: { $gt: start } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
const mastersWithOrders = masters.map((master) => {
|
const mastersWithOrders = masters.map((master) => {
|
||||||
const masterOrders = orders.filter((order) => {
|
const masterOrders = orders.filter((order) => {
|
||||||
@@ -36,11 +48,11 @@ router.get("/masters", async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/masters/:id', async (req, res,next) => {
|
router.delete("/masters/:id", async (req, res, next) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!mongoose.Types.ObjectId.isValid(id)){
|
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||||
throw new Error('ID is required')
|
throw new Error("ID is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -48,58 +60,52 @@ router.delete('/masters/:id', async (req, res,next) => {
|
|||||||
new: true,
|
new: true,
|
||||||
});
|
});
|
||||||
if (!master) {
|
if (!master) {
|
||||||
throw new Error('master not found')
|
throw new Error("master not found");
|
||||||
}
|
}
|
||||||
res.status(200).send({success: true, body: master})
|
res.status(200).send({ success: true, body: master });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error)
|
next(error);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
router.post("/masters", async (req, res, next) => {
|
||||||
|
const { name, phone } = req.body;
|
||||||
|
|
||||||
router.post('/masters', async (req, res,next) => {
|
if (!name || !phone) {
|
||||||
|
throw new Error("Enter name and phone");
|
||||||
const {name, phone} = req.body
|
|
||||||
|
|
||||||
if (!name || !phone ){
|
|
||||||
throw new Error('Enter name and phone')
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const master = await MasterModel.create({name, phone})
|
const master = await MasterModel.create({ name, phone });
|
||||||
res.status(200).send({success: true, body: master})
|
res.status(200).send({ success: true, body: master });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error)
|
next(error);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
router.patch("/masters/:id", async (req, res, next) => {
|
||||||
router.patch('/masters/:id', async (req, res, next) => {
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||||
throw new Error('ID is required')
|
throw new Error("ID is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, phone } = req.body;
|
const { name, phone } = req.body;
|
||||||
|
|
||||||
if (!name && !phone) {
|
if (!name && !phone) {
|
||||||
throw new Error('Enter name and phone')
|
throw new Error("Enter name and phone");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateData = {};
|
const updateData = {};
|
||||||
if (name) updateData.name = name;
|
if (name) updateData.name = name;
|
||||||
if (phone) updateData.phone = phone;
|
if (phone) updateData.phone = phone;
|
||||||
|
|
||||||
const master = await MasterModel.findByIdAndUpdate(
|
const master = await MasterModel.findByIdAndUpdate(id, updateData, {
|
||||||
id,
|
new: true,
|
||||||
updateData,
|
});
|
||||||
{ new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!master) {
|
if (!master) {
|
||||||
throw new Error('master not found')
|
throw new Error("master not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send({ success: true, body: master });
|
res.status(200).send({ success: true, body: master });
|
||||||
@@ -108,4 +114,4 @@ router.patch('/masters/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router;
|
||||||
|
|||||||
23
server/routers/dry-wash/get-token.js
Normal file
23
server/routers/dry-wash/get-token.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const getGigaToken = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||||
|
const data = await response.json()
|
||||||
|
return data.features['dry-wash-bh'].GIGA_TOKEN.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSystemPrompt = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||||
|
const data = await response.json()
|
||||||
|
return data.features['dry-wash-bh'].SYSTEM_PROMPT.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGigaChatModel = async () => {
|
||||||
|
const response = await fetch('https://admin.bro-js.ru/api/config/v1/dev')
|
||||||
|
const data = await response.json()
|
||||||
|
return data.features['dry-wash-bh'].GIGA_CHAT_MODEL.value
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getGigaToken,
|
||||||
|
getSystemPrompt,
|
||||||
|
getGigaChatModel
|
||||||
|
}
|
||||||
29
server/routers/dry-wash/model/order.car-img.js
Normal file
29
server/routers/dry-wash/model/order.car-img.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const { Schema, model } = require('mongoose')
|
||||||
|
|
||||||
|
const schema = new Schema({
|
||||||
|
image: String,
|
||||||
|
imageRating: String,
|
||||||
|
imageDescription: String,
|
||||||
|
orderId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'dry-wash-order'
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
type: Date,
|
||||||
|
default: () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
schema.set('toJSON', {
|
||||||
|
virtuals: true,
|
||||||
|
versionKey: false,
|
||||||
|
transform(_doc, ret) {
|
||||||
|
delete ret._id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
schema.virtual('id').get(function () {
|
||||||
|
return this._id.toHexString()
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.OrderCarImgModel = model('dry-wash-order-car-image', schema)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const { Schema, model } = require('mongoose')
|
const { Schema, model } = require('mongoose')
|
||||||
const { orderStatus } = require('./const')
|
const { orderStatus } = require('./const')
|
||||||
|
const { OrderNumberModel } = require('./order.number')
|
||||||
|
|
||||||
const schema = new Schema({
|
const schema = new Schema({
|
||||||
phone: {
|
phone: {
|
||||||
@@ -14,7 +15,7 @@ const schema = new Schema({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
carColor: String,
|
carColor: Schema.Types.Mixed,
|
||||||
startWashTime: {
|
startWashTime: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: true
|
required: true
|
||||||
@@ -27,6 +28,10 @@ const schema = new Schema({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
orderNumber: {
|
||||||
|
type: String,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -47,6 +52,18 @@ const schema = new Schema({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
schema.pre('save', async function (next) {
|
||||||
|
if (this.isNew) {
|
||||||
|
const counter = await OrderNumberModel.findOneAndUpdate(
|
||||||
|
{ _id: 'orderNumber' },
|
||||||
|
{ $inc: { sequenceValue: 1 } },
|
||||||
|
{ new: true, upsert: true }
|
||||||
|
)
|
||||||
|
this.orderNumber = counter.sequenceValue.toString()
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
schema.set('toJSON', {
|
schema.set('toJSON', {
|
||||||
virtuals: true,
|
virtuals: true,
|
||||||
versionKey: false,
|
versionKey: false,
|
||||||
|
|||||||
14
server/routers/dry-wash/model/order.number.js
Normal file
14
server/routers/dry-wash/model/order.number.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const { Schema, model } = require('mongoose')
|
||||||
|
|
||||||
|
const schema = new Schema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sequenceValue: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.OrderNumberModel = model('dry-wash-order-number', schema)
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
const mongoose = require("mongoose")
|
const mongoose = require("mongoose")
|
||||||
const router = require('express').Router()
|
const router = require('express').Router()
|
||||||
|
const multer = require('multer')
|
||||||
const { MasterModel } = require('./model/master')
|
const { MasterModel } = require('./model/master')
|
||||||
const { OrderModel } = require('./model/order')
|
const { OrderModel } = require('./model/order')
|
||||||
|
const { OrderCarImgModel } = require('./model/order.car-img')
|
||||||
const { orderStatus } = require('./model/const')
|
const { orderStatus } = require('./model/const')
|
||||||
|
const { getGigaToken, getSystemPrompt, getGigaChatModel } = require('./get-token')
|
||||||
|
|
||||||
const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value)
|
const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value)
|
||||||
const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value)
|
const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value)
|
||||||
const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100
|
const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100
|
||||||
const isValidCarColor = (value) => value.length < 50 && /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
|
const isValidCarColor = (value) => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value >= 0 && value <= 7
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
return /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value)
|
const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value)
|
||||||
|
|
||||||
const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/
|
const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/
|
||||||
@@ -26,6 +36,9 @@ const isValidLocation = (value) => {
|
|||||||
const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value)
|
const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value)
|
||||||
const isValidOrderNotes = (value) => value.length < 500
|
const isValidOrderNotes = (value) => value.length < 500
|
||||||
|
|
||||||
|
const allowedMimeTypes = ['image/jpeg', 'image/png']
|
||||||
|
const sizeLimitInMegaBytes = 15
|
||||||
|
|
||||||
const VALIDATION_MESSAGES = {
|
const VALIDATION_MESSAGES = {
|
||||||
order: {
|
order: {
|
||||||
notFound: 'Order not found'
|
notFound: 'Order not found'
|
||||||
@@ -60,6 +73,13 @@ const VALIDATION_MESSAGES = {
|
|||||||
carColor: {
|
carColor: {
|
||||||
invalid: 'Invalid car color'
|
invalid: 'Invalid car color'
|
||||||
},
|
},
|
||||||
|
carImg: {
|
||||||
|
required: 'Car image file is required',
|
||||||
|
invalid: {
|
||||||
|
type: `Invalid car image file type. Allowed types: ${allowedMimeTypes}`,
|
||||||
|
size: `Invalid car image file size. Limit is ${sizeLimitInMegaBytes}MB`
|
||||||
|
}
|
||||||
|
},
|
||||||
washingBegin: {
|
washingBegin: {
|
||||||
required: 'Begin time of washing is required',
|
required: 'Begin time of washing is required',
|
||||||
invalid: 'Invalid begin time of washing'
|
invalid: 'Invalid begin time of washing'
|
||||||
@@ -143,17 +163,21 @@ router.post('/create', async (req, res, next) => {
|
|||||||
|
|
||||||
router.get('/:id', async (req, res, next) => {
|
router.get('/:id', async (req, res, next) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
|
||||||
if (!mongoose.Types.ObjectId.isValid(id)) {
|
if (!mongoose.Types.ObjectId.isValid(id)) {
|
||||||
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const order = await OrderModel.findById(id)
|
const order = await OrderModel.findById(id)
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).send({ success: true, body: order })
|
const imgProps = await OrderCarImgModel.findOne({ orderId: order.id })
|
||||||
|
|
||||||
|
res.status(200).send({ success: true, body: { ...order.toObject(), ...imgProps?.toObject() } })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
@@ -248,4 +272,191 @@ router.delete('/:id', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const storage = multer.memoryStorage()
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: { fileSize: sizeLimitInMegaBytes * 1024 * 1024 },
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (allowedMimeTypes.includes(file.mimetype)) {
|
||||||
|
cb(null, true)
|
||||||
|
} else {
|
||||||
|
cb(new Error(VALIDATION_MESSAGES.carImg.invalid.type), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
const GIGA_CHAT_OAUTH = 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth'
|
||||||
|
const GIGA_CHAT_API = 'https://gigachat.devices.sberbank.ru/api/v1'
|
||||||
|
|
||||||
|
const getToken = async (req, res) => {
|
||||||
|
const gigaToken = await getGigaToken()
|
||||||
|
|
||||||
|
const rqUID = uuidv4()
|
||||||
|
const body = new URLSearchParams({
|
||||||
|
scope: "GIGACHAT_API_PERS",
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(GIGA_CHAT_OAUTH, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${gigaToken}`,
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
Accept: "application/json",
|
||||||
|
RqUID: rqUID,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json()
|
||||||
|
console.error("Ошибка запроса: ", errorData)
|
||||||
|
return res.status(response.status).json(errorData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadImage = async (file, accessToken) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
const blob = new Blob([file.buffer], { type: file.mimetype })
|
||||||
|
formData.append('file', blob, file.originalname)
|
||||||
|
formData.append('purpose', 'general')
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
maxBodyLength: Infinity,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${GIGA_CHAT_API}/files`, formData, config)
|
||||||
|
return response.data.id
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS_MAP = ['white', 'black', 'silver', 'gray', 'beige-brown', 'red', 'blue', 'green']
|
||||||
|
|
||||||
|
const getColorName = (colorKey) => {
|
||||||
|
if (typeof colorKey === 'number' && COLORS_MAP[colorKey]) {
|
||||||
|
return COLORS_MAP[colorKey]
|
||||||
|
}
|
||||||
|
return colorKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzeImage = async (fileId, token, imgProps) => {
|
||||||
|
const response = await fetch(`${GIGA_CHAT_API}/chat/completions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: (await getGigaChatModel()) ?? "GigaChat-Max",
|
||||||
|
stream: false,
|
||||||
|
update_interval: 0,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: (await getSystemPrompt()) ?? `Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения.
|
||||||
|
Тебе предоставляют фотографию, где явно выделяется одна машина (например, она расположена в центре и имеет больший размер в кадре по сравнению с остальными).
|
||||||
|
ВАЖНО: Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля:
|
||||||
|
{
|
||||||
|
"value": число от 0 до 10 (целое или с одним знаком после запятой),
|
||||||
|
"description": "текстовое описание на русском языке"
|
||||||
|
}.
|
||||||
|
Правила:
|
||||||
|
1. Поле "value":
|
||||||
|
- Должно быть числом от 0 до 10 (0 = машина абсолютно чистая, 10 = машина максимально грязная) ИЛИ undefined (если не удалось оценить);
|
||||||
|
2. Поле "description":
|
||||||
|
- Должно содержать 2-3 предложения на русском языке;
|
||||||
|
- Обязательно указать конкретные признаки загрязнения;
|
||||||
|
- Объяснить, почему выставлен именно такой балл.
|
||||||
|
- Должно быть связано только с автомобилем.
|
||||||
|
НЕ ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры. НЕ ИСПОЛЬЗУЙ markdown форматирование. ОТВЕТ ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON. Если на фотографии нельзя явно выделить одну машину, то ОЦЕНКА ДОЛЖНА ИМЕТЬ ЗНАЧЕНИЕ undefined и в описании должно быть указано, что по фотографии не удалось оценить степень загрязнения автомобиля, при этом НЕ ОПИСЫВАЙ НИЧЕГО ДРУГОГО КРОМЕ АВТОМОБИЛЯ`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке. Учти, что владелец указал, что исходный цвет машины: ${getColorName(imgProps.color)}`,
|
||||||
|
attachments: [fileId],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(data.choices[0].message.content)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return { description: data.choices[0].message.content }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertFileToBase64 = (file) => {
|
||||||
|
const base64Image = file.buffer.toString('base64')
|
||||||
|
return `data:${file.mimetype};base64,${base64Image}`
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
|
||||||
|
|
||||||
|
router.post('/:id/upload-car-img', upload.single('file'), async (req, res) => {
|
||||||
|
const { id: orderId } = req.params
|
||||||
|
if (!mongoose.Types.ObjectId.isValid(orderId)) {
|
||||||
|
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
|
||||||
|
}
|
||||||
|
const order = await OrderModel.findById(orderId)
|
||||||
|
if (!order) {
|
||||||
|
throw new Error(VALIDATION_MESSAGES.order.notFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.file) {
|
||||||
|
throw new Error(VALIDATION_MESSAGES.carImg.required)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await OrderCarImgModel.deleteMany({ orderId })
|
||||||
|
|
||||||
|
const { access_token } = await getToken(req, res)
|
||||||
|
|
||||||
|
const fileId = await uploadImage(req.file, access_token)
|
||||||
|
const { value, description } = await analyzeImage(fileId, access_token, { carColor: order.carColor }) ?? {}
|
||||||
|
|
||||||
|
const orderCarImg = await OrderCarImgModel.create({
|
||||||
|
image: convertFileToBase64(req.file),
|
||||||
|
imageRating: value,
|
||||||
|
imageDescription: description,
|
||||||
|
orderId: order.id,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).send({ success: true, body: orderCarImg })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.use((err, req, res, next) => {
|
||||||
|
if (err instanceof multer.MulterError) {
|
||||||
|
switch (err.message) {
|
||||||
|
case 'File too large':
|
||||||
|
return res.status(400).json({ success: false, error: VALIDATION_MESSAGES.carImg.invalid.size })
|
||||||
|
default:
|
||||||
|
return res.status(400).json({ success: false, error: err.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(err.message)
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const router = require('express').Router();
|
|
||||||
|
|
||||||
router.get('/recipe-data', (request, response) => {
|
|
||||||
response.send(require('./json/recipe-data/success.json'))
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/userpage-data', (req, res)=>{
|
|
||||||
res.send(require('./json/userpage-data/success.json'))
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/homepage-data', (req, res)=>{
|
|
||||||
res.send(require('./json/homepage-data/success.json'))
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
21
server/routers/edateam-legacy/index.ts
Normal file
21
server/routers/edateam-legacy/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import recipeData from './json/recipe-data/success.json';
|
||||||
|
import userpageData from './json/userpage-data/success.json';
|
||||||
|
import homepageData from './json/homepage-data/success.json';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/recipe-data', (request, response) => {
|
||||||
|
response.send(recipeData)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/userpage-data', (req, res)=>{
|
||||||
|
res.send(userpageData)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/homepage-data', (req, res)=>{
|
||||||
|
res.send(homepageData)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -8,22 +8,44 @@ router.get("/update-like", (request, response) => {
|
|||||||
response.send(require("./json/gamepage/success.json"));
|
response.send(require("./json/gamepage/success.json"));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/categories", (request, response) => {
|
router.get("/add-to-cart", (request, response) => {
|
||||||
response.send(require("./json/categories/success.json"));
|
response.send(require("./json/home-page-data/games-in-cart.json"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/categories", (request, response) => {
|
||||||
|
response.send(require("./json/home-page-data/all-games.json"));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/favourites", (request, response) => {
|
||||||
|
response.send(require("./json/home-page-data/all-games.json"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// router.get("/shopping-cart", (request, response) => {
|
||||||
|
// response.send(require("./json/shopping-cart/success.json"));
|
||||||
|
// });
|
||||||
|
|
||||||
router.get("/shopping-cart", (request, response) => {
|
router.get("/shopping-cart", (request, response) => {
|
||||||
response.send(require("./json/shopping-cart/success.json"));
|
response.send(require("./json/home-page-data/games-in-cart.json"));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/home", (request, response) => {
|
// Добавляем поддержку разных ответов для /home
|
||||||
response.send(require("./json/home-page-data/success.json"));
|
router.get("/home", (req, res) => {
|
||||||
|
if (stubs.home === "success") {
|
||||||
|
res.send(require("./json/home-page-data/success.json"));
|
||||||
|
} else if (stubs.home === "empty") {
|
||||||
|
res.send({ data: [] }); // Отправляем пустой массив
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ success: false, message: "Server error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/all-games", (request, response) => {
|
router.get("/all-games", (request, response) => {
|
||||||
response.send(require("./json/home-page-data/all-games.json"));
|
response.send(require("./json/home-page-data/all-games.json"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
home: "success",
|
||||||
|
};
|
||||||
|
|
||||||
// // Маршрут для обновления лайков
|
// // Маршрут для обновления лайков
|
||||||
// router.post("/update-like", (request, response) => {
|
// router.post("/update-like", (request, response) => {
|
||||||
@@ -38,7 +60,6 @@ router.get("/all-games", (request, response) => {
|
|||||||
// });
|
// });
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
const fs = require("fs").promises;
|
const fs = require("fs").promises;
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
@@ -88,5 +109,149 @@ router.post("/update-like", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Путь к JSON-файлу с корзиной
|
||||||
|
const cartFilePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"./json/home-page-data/games-in-cart.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Функция для чтения JSON-файла
|
||||||
|
async function readCart() {
|
||||||
|
const data = await fs.readFile(cartFilePath, "utf-8");
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для записи в JSON-файл
|
||||||
|
async function writeCart(data) {
|
||||||
|
await fs.writeFile(cartFilePath, JSON.stringify(data, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Маршрут для добавления/удаления товара в корзине
|
||||||
|
router.post("/add-to-cart", async (req, res) => {
|
||||||
|
const { id, action } = req.body;
|
||||||
|
|
||||||
|
// Проверка наличия id и action
|
||||||
|
if (id === undefined || action === undefined) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, message: "Invalid id or action" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cartData = await readCart();
|
||||||
|
let ids = cartData.data.ids;
|
||||||
|
|
||||||
|
if (action === "add") {
|
||||||
|
// Если action "add", добавляем товар, если его нет в корзине
|
||||||
|
if (!ids?.includes(id)) {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
} else if (action === "remove") {
|
||||||
|
// Если action "remove", удаляем товар, если он есть в корзине
|
||||||
|
if (ids?.includes(id)) {
|
||||||
|
ids = ids.filter((item) => item !== id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если action невалиден
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, message: "Invalid action" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем обновленные данные обратно в файл
|
||||||
|
cartData.data.ids = ids;
|
||||||
|
await writeCart(cartData);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Cart updated successfully",
|
||||||
|
data: cartData.data, // Возвращаем обновленные данные
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cart:", error);
|
||||||
|
res.status(500).json({ success: false, message: "Server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
const createElement = (key, value, buttonTitle, basePath) => `
|
||||||
|
<label>
|
||||||
|
<input name="${key}" type="radio" ${
|
||||||
|
stubs[key] === value ? "checked" : ""
|
||||||
|
} onclick="fetch('${basePath}/admin/set/${key}/${value}')"/>
|
||||||
|
${buttonTitle || value}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
|
||||||
|
router.get("/admin/home", (request, response) => {
|
||||||
|
const basePath = request.baseUrl; // Получаем базовый путь маршрутизатора
|
||||||
|
response.send(`
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Настройка данных для /home</legend>
|
||||||
|
${createElement("home", "success", "Отдать успешный ответ", basePath)}
|
||||||
|
${createElement("home", "empty", "Отдать пустой массив", basePath)}
|
||||||
|
${createElement("home", "error", "Отдать ошибку", basePath)}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/admin/game-page", (request, response) => {
|
||||||
|
response.send(`
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Настройка данных для /game-page</legend>
|
||||||
|
${createElement(
|
||||||
|
"game-page",
|
||||||
|
"success",
|
||||||
|
"Отдать успешный ответ"
|
||||||
|
)}
|
||||||
|
${createElement("game-page", "empty", "Отдать пустой массив")}
|
||||||
|
${createElement("game-page", "error", "Отдать ошибку")}
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/admin/categories", (request, response) => {
|
||||||
|
response.send(`
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Настройка данных для /categories</legend>
|
||||||
|
${createElement(
|
||||||
|
"categories",
|
||||||
|
"success",
|
||||||
|
"Отдать успешный ответ"
|
||||||
|
)}
|
||||||
|
${createElement("categories", "empty", "Отдать пустой массив")}
|
||||||
|
${createElement("categories", "error", "Отдать ошибку")}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/admin/favourites", (request, response) => {
|
||||||
|
response.send(`
|
||||||
|
<div>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Настройка данных для /favourites</legend>
|
||||||
|
${createElement(
|
||||||
|
"favourites",
|
||||||
|
"success",
|
||||||
|
"Отдать успешный ответ"
|
||||||
|
)}
|
||||||
|
${createElement("favourites", "empty", "Отдать пустой массив")}
|
||||||
|
${createElement("favourites", "error", "Отдать ошибку")}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/admin/set/:key/:value", (request, response) => {
|
||||||
|
const { key, value } = request.params;
|
||||||
|
stubs[key] = value;
|
||||||
|
response.send("Настройки обновлены!");
|
||||||
|
});
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"games1": [
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"title": "How to Survive",
|
|
||||||
"price": 259,
|
|
||||||
"old_price": 500,
|
|
||||||
"image": "sales_game1",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"title": "Red Solstice 2 Survivors",
|
|
||||||
"price": 561,
|
|
||||||
"image": "sales_game2",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"title": "Sons Of The Forests",
|
|
||||||
"price": 820,
|
|
||||||
"old_price": 1100,
|
|
||||||
"image": "new_game2",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "The Witcher 3: Wild Hunt",
|
|
||||||
"price": 990,
|
|
||||||
"old_price": 1200,
|
|
||||||
"image": "leaders_game4",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Atomic Heart",
|
|
||||||
"price": 1200,
|
|
||||||
"old_price": 2500,
|
|
||||||
"image": "leaders_game5",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"title": "Crab Game",
|
|
||||||
"price": 600,
|
|
||||||
"old_price": 890,
|
|
||||||
"image": "leaders_game6",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"games2": [
|
|
||||||
{
|
|
||||||
"id": 7,
|
|
||||||
"title": "Alpha League",
|
|
||||||
"price": 299,
|
|
||||||
"image": "new_game1",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 8,
|
|
||||||
"title": "Sons Of The Forests",
|
|
||||||
"price": 820,
|
|
||||||
"old_price": 1100,
|
|
||||||
"image": "new_game2",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9,
|
|
||||||
"title": "Pacific Drives",
|
|
||||||
"price": 1799,
|
|
||||||
"image": "new_game3",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "The Witcher 3: Wild Hunt",
|
|
||||||
"price": 990,
|
|
||||||
"old_price": 1200,
|
|
||||||
"image": "leaders_game4",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Atomic Heart",
|
|
||||||
"price": 1200,
|
|
||||||
"old_price": 2500,
|
|
||||||
"image": "leaders_game5",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"title": "Crab Game",
|
|
||||||
"price": 600,
|
|
||||||
"old_price": 890,
|
|
||||||
"image": "leaders_game6",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"games3": [
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"title": "Elden Ring",
|
|
||||||
"price": 3295,
|
|
||||||
"old_price": 3599,
|
|
||||||
"image": "leaders_game2",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 11,
|
|
||||||
"title": "Counter-Strike 2",
|
|
||||||
"price": 479,
|
|
||||||
"image": "leaders_game1",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 12,
|
|
||||||
"title": "PUBG: BATTLEGROUNDS",
|
|
||||||
"price": 199,
|
|
||||||
"image": "leaders_game3",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"title": "The Witcher 3: Wild Hunt",
|
|
||||||
"price": 990,
|
|
||||||
"old_price": 1200,
|
|
||||||
"image": "leaders_game4",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"title": "Atomic Heart",
|
|
||||||
"price": 1200,
|
|
||||||
"old_price": 2500,
|
|
||||||
"image": "leaders_game5",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"title": "Crab Game",
|
|
||||||
"price": 600,
|
|
||||||
"old_price": 890,
|
|
||||||
"image": "leaders_game6",
|
|
||||||
"os": "windows",
|
|
||||||
"fav1": "star1",
|
|
||||||
"fav2": "star2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,28 +5,28 @@
|
|||||||
{
|
{
|
||||||
"username": "Пользователь1",
|
"username": "Пользователь1",
|
||||||
"text": "Текст комментария 1",
|
"text": "Текст комментария 1",
|
||||||
"likes": 9,
|
"likes": 13,
|
||||||
"rating": 8,
|
"rating": 8,
|
||||||
"date": "2025-03-01T10:00:00Z"
|
"date": "2025-03-01T10:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Пользователь2",
|
"username": "Пользователь2",
|
||||||
"text": "Текст комментария 2",
|
"text": "Текст комментария 2",
|
||||||
"likes": 7,
|
"likes": 10,
|
||||||
"rating": 7,
|
"rating": 7,
|
||||||
"date": "2025-01-01T10:00:00Z"
|
"date": "2025-01-01T10:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Пользователь3",
|
"username": "Пользователь3",
|
||||||
"text": "Текст комментария 3",
|
"text": "Текст комментария 3",
|
||||||
"likes": 5,
|
"likes": 4,
|
||||||
"rating": 3,
|
"rating": 3,
|
||||||
"date": "2025-02-01T10:00:00Z"
|
"date": "2025-02-01T10:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "Пользователь4",
|
"username": "Пользователь4",
|
||||||
"text": "Текст комментария 4",
|
"text": "Текст комментария 4",
|
||||||
"likes": 15,
|
"likes": 18,
|
||||||
"rating": 2,
|
"rating": 2,
|
||||||
"date": "2025-12-01T10:00:00Z"
|
"date": "2025-12-01T10:00:00Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,144 +3,183 @@
|
|||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "The Witcher 3: Wild Hunt",
|
"title": "Elden Ring",
|
||||||
"image": "game1",
|
"image": "game17",
|
||||||
"text": "$10",
|
"price": 3295,
|
||||||
"imgPath": "img_top_1",
|
"old_price": 3599,
|
||||||
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
|
"imgPath": "img_top_17",
|
||||||
|
"description": "Крупномасштабная RPG, действие которой происходит в обширном открытом мире c богатой мифологией и множеством опасных врагов.",
|
||||||
"category": "RPG"
|
"category": "RPG"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "Red Dead Redemption 2",
|
"title": "The Witcher 3: Wild Hunt",
|
||||||
|
"image": "game1",
|
||||||
|
"price": 990,
|
||||||
|
"old_price": 1200,
|
||||||
|
"imgPath": "img_top_1",
|
||||||
|
"description": "Эпическая RPG с открытым миром, в которой Геральт из Ривии охотится на монстров и раскрывает политические заговоры.",
|
||||||
|
"category": "RPG"
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"title": "Red Dead Redemption 2",
|
||||||
"image": "game2",
|
"image": "game2",
|
||||||
"text": "$10",
|
"price": 980,
|
||||||
|
"old_price": 3800,
|
||||||
"imgPath": "img_top_2",
|
"imgPath": "img_top_2",
|
||||||
"description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.",
|
"description": "Приключенческая игра с открытым миром на Диком Западе, рассказывающая историю Артура Моргана.",
|
||||||
"category": "Adventures"
|
"category": "Adventures"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "Forza Horizon 5",
|
"title": "Forza Horizon 5",
|
||||||
"image": "game3",
|
"image": "game3",
|
||||||
"text": "$10",
|
"price": 1900,
|
||||||
"imgPath": "img_top_3",
|
"imgPath": "img_top_3",
|
||||||
"description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.",
|
"description": "Гоночная игра с огромным открытым миром, действие которой происходит в Мексике.",
|
||||||
"category": "Race"
|
"category": "Race"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"name": "Atomic Heart",
|
"title": "Atomic Heart",
|
||||||
"image": "game4",
|
"image": "game4",
|
||||||
"text": "$10",
|
"price": 1200,
|
||||||
|
"old_price": 2500,
|
||||||
|
|
||||||
"imgPath": "img_top_4",
|
"imgPath": "img_top_4",
|
||||||
"description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.",
|
"description": "Экшен-шутер с элементами RPG, разворачивающийся в альтернативной Советской России.",
|
||||||
"category": "Shooters"
|
"category": "Shooters"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"name": "Counter-Strike 2",
|
"title": "Counter-Strike 2",
|
||||||
"image": "game5",
|
"image": "game5",
|
||||||
"text": "$10",
|
"price": 479,
|
||||||
|
|
||||||
"imgPath": "img_top_5",
|
"imgPath": "img_top_5",
|
||||||
"description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.",
|
"description": "Популярный онлайн-шутер с соревновательным геймплеем и тактическими элементами.",
|
||||||
"category": "Shooters"
|
"category": "Shooters"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"name": "Grand Theft Auto V",
|
"title": "Grand Theft Auto V",
|
||||||
"image": "game6",
|
"image": "game6",
|
||||||
"text": "$10",
|
"price": 700,
|
||||||
|
|
||||||
"imgPath": "img_top_6",
|
"imgPath": "img_top_6",
|
||||||
"description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.",
|
"description": "Игра с открытым миром, где можно погрузиться в криминальный мир Лос-Сантоса.",
|
||||||
"category": "Adventures"
|
"category": "Adventures"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"name": "Assassin’s Creed IV: Black Flag",
|
"title": "Assassin’s Creed IV: Black Flag",
|
||||||
"image": "game7",
|
"image": "game7",
|
||||||
"text": "$10",
|
"price": 1100,
|
||||||
|
|
||||||
"imgPath": "img_top_7",
|
"imgPath": "img_top_7",
|
||||||
"description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.",
|
"description": "Приключенческая игра о пиратах и морских сражениях в эпоху золотого века пиратства.",
|
||||||
"category": "Adventures"
|
"category": "Adventures"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 8,
|
"id": 8,
|
||||||
"name": "Spider-Man",
|
"title": "Spider-Man",
|
||||||
"image": "game8",
|
"image": "game8",
|
||||||
"text": "$10",
|
"price": 3800,
|
||||||
|
|
||||||
"imgPath": "img_top_8",
|
"imgPath": "img_top_8",
|
||||||
"description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.",
|
"description": "Игра о супергерое Человеке-пауке с захватывающими битвами и паркуром по Нью-Йорку.",
|
||||||
"category": "Action"
|
"category": "Action"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"name": "Assassin’s Creed Mirage",
|
"title": "Assassin’s Creed Mirage",
|
||||||
"image": "game9",
|
"image": "game9",
|
||||||
"text": "$10",
|
"price": 1600,
|
||||||
|
|
||||||
"imgPath": "img_top_9",
|
"imgPath": "img_top_9",
|
||||||
"description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.",
|
"description": "Приключенческая игра с упором на скрытность, вдохновленная классическими частями серии.",
|
||||||
"category": "Action"
|
"category": "Action"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 10,
|
"id": 10,
|
||||||
"name": "Assassin’s Creed Valhalla",
|
"title": "Assassin’s Creed Valhalla",
|
||||||
"image": "game10",
|
"image": "game10",
|
||||||
"text": "$10",
|
"price": 800,
|
||||||
|
"old_price": 2200,
|
||||||
|
|
||||||
"imgPath": "img_top_10",
|
"imgPath": "img_top_10",
|
||||||
"description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.",
|
"description": "RPG с открытым миром о викингах, включающая битвы, исследования и строительство поселений.",
|
||||||
"category": "RPG"
|
"category": "RPG"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 11,
|
"id": 11,
|
||||||
"name": "ARK: Survival Evolved",
|
"title": "ARK: Survival Evolved",
|
||||||
"image": "game11",
|
"image": "game11",
|
||||||
"text": "$10",
|
"price": 790,
|
||||||
|
|
||||||
"imgPath": "img_top_11",
|
"imgPath": "img_top_11",
|
||||||
"description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.",
|
"description": "Выживание в открытом мире с динозаврами, строительством и многопользовательскими элементами.",
|
||||||
"category": "Simulators"
|
"category": "Simulators"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 12,
|
"id": 12,
|
||||||
"name": "FIFA 23",
|
"title": "FIFA 23",
|
||||||
"image": "game12",
|
"image": "game12",
|
||||||
"text": "$10",
|
"price": 3900,
|
||||||
|
|
||||||
"imgPath": "img_top_12",
|
"imgPath": "img_top_12",
|
||||||
"description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.",
|
"description": "Популярный футбольный симулятор с улучшенной графикой и реалистичным геймплеем.",
|
||||||
"category": "Sports"
|
"category": "Sports"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 13,
|
"id": 13,
|
||||||
"name": "Dirt 5",
|
"title": "Dirt 5",
|
||||||
"image": "game13",
|
"image": "game13",
|
||||||
"text": "$10",
|
"price": 2300,
|
||||||
|
|
||||||
"imgPath": "img_top_13",
|
"imgPath": "img_top_13",
|
||||||
"description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.",
|
"description": "Аркадная гоночная игра с фокусом на ралли и внедорожных соревнованиях.",
|
||||||
"category": "Race"
|
"category": "Race"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 14,
|
"id": 14,
|
||||||
"name": "Cyberpunk 2077",
|
"title": "Cyberpunk 2077",
|
||||||
"image": "game14",
|
"image": "game14",
|
||||||
"text": "$10",
|
"price": 3400,
|
||||||
|
|
||||||
"imgPath": "img_top_14",
|
"imgPath": "img_top_14",
|
||||||
"description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.",
|
"description": "RPG в киберпанк-сеттинге с нелинейным сюжетом и детализированным открытым миром.",
|
||||||
"category": "RPG"
|
"category": "RPG"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 15,
|
||||||
"name": "Age of Empires IV",
|
"title": "Age of Empires IV",
|
||||||
"image": "game15",
|
"image": "game15",
|
||||||
"text": "$10",
|
"price": 3200,
|
||||||
|
|
||||||
"imgPath": "img_top_15",
|
"imgPath": "img_top_15",
|
||||||
"description": "Классическая стратегия в реальном времени с историческими кампаниями.",
|
"description": "Классическая стратегия в реальном времени с историческими кампаниями.",
|
||||||
"category": "Strategies"
|
"category": "Strategies"
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 16,
|
"id": 16,
|
||||||
"name": "Civilization VI",
|
"title": "Civilization VI",
|
||||||
"image": "game16",
|
"image": "game16",
|
||||||
"text": "$10",
|
"price": 4200,
|
||||||
"imgPath": "img_top_16",
|
"imgPath": "img_top_16",
|
||||||
"description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.",
|
"description": "Глобальная пошаговая стратегия, в которой игроки строят и развивают цивилизации.",
|
||||||
"category": "Strategies"
|
"category": "Strategies"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"ids": [
|
||||||
|
3,
|
||||||
|
13,
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
4,
|
||||||
|
9,
|
||||||
|
15,
|
||||||
|
6,
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,43 +3,51 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"topSail": [
|
"topSail": [
|
||||||
{
|
{
|
||||||
|
"id": 1,
|
||||||
"image": "game1",
|
"image": "game1",
|
||||||
"text": "$10",
|
"price": 1500,
|
||||||
"imgPath": "img_top_1"
|
"imgPath": "img_top_1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 2,
|
||||||
"image": "game2",
|
"image": "game2",
|
||||||
"text": "$10",
|
"price": 980,
|
||||||
"imgPath": "img_top_2"
|
"imgPath": "img_top_2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 3,
|
||||||
"image": "game3",
|
"image": "game3",
|
||||||
"text": "$10",
|
"price": 1900,
|
||||||
"imgPath": "img_top_3"
|
"imgPath": "img_top_3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 4,
|
||||||
"image": "game4",
|
"image": "game4",
|
||||||
"text": "$10",
|
"price": 1200,
|
||||||
"imgPath": "img_top_4"
|
"imgPath": "img_top_4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 5,
|
||||||
"image": "game5",
|
"image": "game5",
|
||||||
"text": "$10",
|
"price": 479,
|
||||||
"imgPath": "img_top_5"
|
"imgPath": "img_top_5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 6,
|
||||||
"image": "game6",
|
"image": "game6",
|
||||||
"text": "$10",
|
"price": 700,
|
||||||
"imgPath": "img_top_6"
|
"imgPath": "img_top_6"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 7,
|
||||||
"image": "game7",
|
"image": "game7",
|
||||||
"text": "$10",
|
"price": 1100,
|
||||||
"imgPath": "img_top_7"
|
"imgPath": "img_top_7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"id": 8,
|
||||||
"image": "game8",
|
"image": "game8",
|
||||||
"text": "$10",
|
"price": 3800,
|
||||||
"imgPath": "img_top_8"
|
"imgPath": "img_top_8"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -97,22 +105,26 @@
|
|||||||
{
|
{
|
||||||
"image": "news1",
|
"image": "news1",
|
||||||
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
|
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
|
||||||
"imgPath": "img_news_1"
|
"imgPath": "img_news_1",
|
||||||
|
"link": "https://gamemag.ru/news/185583/delta-force-hawk-ops-gameplay-showcase-havoc-warfare"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "news2",
|
"image": "news2",
|
||||||
"text": "Первый трейлер Assassin’s Creed Shadows — с темнокожим самураем в феодальной Японии",
|
"text": "Первый трейлер Assassin’s Creed Shadows — с темнокожим самураем в феодальной Японии",
|
||||||
"imgPath": "img_news_2"
|
"imgPath": "img_news_2",
|
||||||
|
"link": "https://stopgame.ru/newsdata/62686/pervyy_trailer_assassin_s_creed_shadows_s_temnokozhim_samuraem_v_feodalnoy_yaponii"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "news3",
|
"image": "news3",
|
||||||
"text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony",
|
"text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony",
|
||||||
"imgPath": "img_news_3"
|
"imgPath": "img_news_3",
|
||||||
|
"link": "https://stopgame.ru/newsdata/62706/prizrak_cusimy_vyshel_na_pk_i_uzhe_stavit_rekordy_dlya_sony"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "news4",
|
"image": "news4",
|
||||||
"text": "Авторы Skull and Bones расширяют планы на второй сезо",
|
"text": "Авторы Skull and Bones расширяют планы на второй сезон",
|
||||||
"imgPath": "img_news_4"
|
"imgPath": "img_news_4",
|
||||||
|
"link": "https://stopgame.ru/newsdata/62711/avtory_skull_and_bones_rasshiryayut_plany_na_vtoroy_sezon"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -788,5 +788,121 @@
|
|||||||
"image_url": "w_six"
|
"image_url": "w_six"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"intro_text": "Let's see how well you know UNICS!",
|
||||||
|
"intro_image": "culture",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "When was Kazan founded?",
|
||||||
|
"options": [
|
||||||
|
"1005",
|
||||||
|
"1156",
|
||||||
|
"1230",
|
||||||
|
"1323"
|
||||||
|
],
|
||||||
|
"correct_answer": "1005",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "What is the main river flowing through Kazan?",
|
||||||
|
"options": [
|
||||||
|
"Volga",
|
||||||
|
"Kazanka",
|
||||||
|
"Kama",
|
||||||
|
"Izh"
|
||||||
|
],
|
||||||
|
"correct_answer": "Kazanka",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Who was the first khan of Kazan?",
|
||||||
|
"options": [
|
||||||
|
"Ulugh Muhammad",
|
||||||
|
"Akhmat",
|
||||||
|
"Shah Ali",
|
||||||
|
"Mamuka"
|
||||||
|
],
|
||||||
|
"correct_answer": "Ulugh Muhammad",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "What is the name of Kazan's main sports complex where the 2013 Universiade was held?",
|
||||||
|
"options": [
|
||||||
|
"Kazan Arena",
|
||||||
|
"Tatneft Arena",
|
||||||
|
"Bugulma Arena",
|
||||||
|
"Ak Bars Sports Palace"
|
||||||
|
],
|
||||||
|
"correct_answer": "Kazan Arena",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Which mosque in Kazan is considered one of the largest in Russia?",
|
||||||
|
"options": [
|
||||||
|
"Kul Sharif Mosque",
|
||||||
|
"Mari El Mosque",
|
||||||
|
"Aisha Mosque",
|
||||||
|
"Imam Muhammad Mosque"
|
||||||
|
],
|
||||||
|
"correct_answer": "Kul Sharif Mosque",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "What is the name of the square where the Kazan Kremlin and Kul Sharif Mosque are located?",
|
||||||
|
"options": [
|
||||||
|
"Vakhitov Square",
|
||||||
|
"Freedom Square",
|
||||||
|
"Kremlin Square",
|
||||||
|
"Revolution Square"
|
||||||
|
],
|
||||||
|
"correct_answer": "Kremlin Square",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "What symbol of Kazan is depicted on the city's coat of arms?",
|
||||||
|
"options": [
|
||||||
|
"Dragon",
|
||||||
|
"Tiger",
|
||||||
|
"Lion",
|
||||||
|
"Eagle"
|
||||||
|
],
|
||||||
|
"correct_answer": "Dragon",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Who was the architect of the Kazan Kremlin?",
|
||||||
|
"options": [
|
||||||
|
"Ivan Zarudny",
|
||||||
|
"Fyodor Benjamin",
|
||||||
|
"Andrey Ushakov",
|
||||||
|
"Yury Dashevsky"
|
||||||
|
],
|
||||||
|
"correct_answer": "Andrey Ushakov",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Which of these universities is located in Kazan?",
|
||||||
|
"options": [
|
||||||
|
"Moscow State University",
|
||||||
|
"Kazan Federal University",
|
||||||
|
"St. Petersburg Polytechnic University",
|
||||||
|
"Novosibirsk State University"
|
||||||
|
],
|
||||||
|
"correct_answer": "Kazan Federal University",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "What is the name of the largest street in Kazan, which is also the center of the city's nightlife?",
|
||||||
|
"options": [
|
||||||
|
"Kremlin Street",
|
||||||
|
"Bauman Street",
|
||||||
|
"Pushkin Street",
|
||||||
|
"Kayum Nasyri Street"
|
||||||
|
],
|
||||||
|
"correct_answer": "Bauman Street",
|
||||||
|
"image_url": "culture"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -788,5 +788,121 @@
|
|||||||
"image_url": "w_six"
|
"image_url": "w_six"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"intro_text": "Давайте узнаем, насколько вы хорошо знаете историю Казани!",
|
||||||
|
"intro_image": "culture",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Когда Казань была основана?",
|
||||||
|
"options": [
|
||||||
|
"1000 год",
|
||||||
|
"1156 год",
|
||||||
|
"1230 год",
|
||||||
|
"1323 год"
|
||||||
|
],
|
||||||
|
"correct_answer": "1000 год",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Как называется главная река, протекающая через Казань?",
|
||||||
|
"options": [
|
||||||
|
"Волга",
|
||||||
|
"Казанка",
|
||||||
|
"Кама",
|
||||||
|
"Иж"
|
||||||
|
],
|
||||||
|
"correct_answer": "Казанка",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Кто был первым казанским ханом?",
|
||||||
|
"options": [
|
||||||
|
"Улу-Мухаммед",
|
||||||
|
"Ахмат",
|
||||||
|
"Шах-Али",
|
||||||
|
"Мамука"
|
||||||
|
],
|
||||||
|
"correct_answer": "Улу-Мухаммед",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Как называется главный спортивный комплекс Казани, где проводились Универсиада 2013 года?",
|
||||||
|
"options": [
|
||||||
|
"Казан Арена",
|
||||||
|
"Татнефть Арена",
|
||||||
|
"Бугульминская арена",
|
||||||
|
"Дворец спорта \"Ак Барс\""
|
||||||
|
],
|
||||||
|
"correct_answer": "Казан Арена",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Какая мечеть в Казани считается одной из самых больших в России?",
|
||||||
|
"options": [
|
||||||
|
"Мечеть Кул Шариф",
|
||||||
|
"Мечеть Марий Эл",
|
||||||
|
"Мечеть Айша",
|
||||||
|
"Мечеть имама Мухаммада"
|
||||||
|
],
|
||||||
|
"correct_answer": "Мечеть Кул Шариф",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Как называется площадь, на которой расположены Казанский Кремль и мечеть Кул Шариф?",
|
||||||
|
"options": [
|
||||||
|
"Площадь Вахитова",
|
||||||
|
"Площадь Свободы",
|
||||||
|
"Площадь Кремля",
|
||||||
|
"Площадь Революции"
|
||||||
|
],
|
||||||
|
"correct_answer": "Площадь Кремля",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Какой символ Казани изображён на гербе города?",
|
||||||
|
"options": [
|
||||||
|
"Дракон",
|
||||||
|
"Тигр",
|
||||||
|
"Лев",
|
||||||
|
"Орел"
|
||||||
|
],
|
||||||
|
"correct_answer": "Дракон",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Какой архитектор спроектировал Казанский Кремль?",
|
||||||
|
"options": [
|
||||||
|
"Иван Зарудный",
|
||||||
|
"Фёдор Бенжамин",
|
||||||
|
"Андрей Ушаков",
|
||||||
|
"Юрий Дашевский"
|
||||||
|
],
|
||||||
|
"correct_answer": "Андрей Ушаков",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Какой из этих вузов находится в Казани?",
|
||||||
|
"options": [
|
||||||
|
"Московский государственный университет",
|
||||||
|
"Казанский федеральный университет",
|
||||||
|
"Санкт-Петербургский политехнический университет",
|
||||||
|
"Новосибирский государственный университет"
|
||||||
|
],
|
||||||
|
"correct_answer": "Казанский федеральный университет",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Как называется крупнейшая в Казани улица, которая также является центром ночной жизни города?",
|
||||||
|
"options": [
|
||||||
|
"Кремлевская улица",
|
||||||
|
"Баумана улица",
|
||||||
|
"Пушкина улица",
|
||||||
|
"Каюма Насыри улица"
|
||||||
|
],
|
||||||
|
"correct_answer": "Баумана улица",
|
||||||
|
"image_url": "culture"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -788,5 +788,121 @@
|
|||||||
"image_url": "w_six"
|
"image_url": "w_six"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"intro_text": "Әйдәгез, Казан тарихын белүегезне ачыклыйк!",
|
||||||
|
"intro_image": "culture",
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"question": "Казан кайчан нигезләнгән?",
|
||||||
|
"options": [
|
||||||
|
"1000 ел",
|
||||||
|
"1156 ел",
|
||||||
|
"1230 ел",
|
||||||
|
"1323 ел"
|
||||||
|
],
|
||||||
|
"correct_answer": "1000 ел",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казан аша агучы төп елга ничек атала?",
|
||||||
|
"options": [
|
||||||
|
"Идел",
|
||||||
|
"Казанка",
|
||||||
|
"Кама",
|
||||||
|
"Иж"
|
||||||
|
],
|
||||||
|
"correct_answer": "Казанка",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казанның беренче ханы кем булган?",
|
||||||
|
"options": [
|
||||||
|
"Олуг Мөхәммәт",
|
||||||
|
"Әхмәт",
|
||||||
|
"Шаһ-Әли",
|
||||||
|
"Мамука"
|
||||||
|
],
|
||||||
|
"correct_answer": "Олуг Мөхәммәт",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "2013 елда Универсиада узган Казанның төп спорт комплексы ничек атала?",
|
||||||
|
"options": [
|
||||||
|
"Казан Арена",
|
||||||
|
"Татнефть Арена",
|
||||||
|
"Бөгелмә Арена",
|
||||||
|
"Ак Барс спорт сарае"
|
||||||
|
],
|
||||||
|
"correct_answer": "Казан Арена",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казандагы иң зур мәчетләрнең берсе Россиядәге кайсысы?",
|
||||||
|
"options": [
|
||||||
|
"Кол Шәриф мәчете",
|
||||||
|
"Марий Эл мәчете",
|
||||||
|
"Айша мәчете",
|
||||||
|
"Имам Мөхәммәт мәчете"
|
||||||
|
],
|
||||||
|
"correct_answer": "Кол Шәриф мәчете",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казан Кремле һәм Кол Шәриф мәчете урнашкан мәйдан ничек атала?",
|
||||||
|
"options": [
|
||||||
|
"Вахитов мәйданы",
|
||||||
|
"Ирек мәйданы",
|
||||||
|
"Кремль мәйданы",
|
||||||
|
"Революция мәйданы"
|
||||||
|
],
|
||||||
|
"correct_answer": "Кремль мәйданы",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казан гербында кайсы символ сурәтләнгән?",
|
||||||
|
"options": [
|
||||||
|
"Аждаһа",
|
||||||
|
"Юлбарыс",
|
||||||
|
"Арслан",
|
||||||
|
"Бүре"
|
||||||
|
],
|
||||||
|
"correct_answer": "Аждаһа",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казан Кремленең архитекторын атагыз.",
|
||||||
|
"options": [
|
||||||
|
"Иван Зарудный",
|
||||||
|
"Фёдор Бенжамин",
|
||||||
|
"Андрей Ушаков",
|
||||||
|
"Юрий Дашевский"
|
||||||
|
],
|
||||||
|
"correct_answer": "Андрей Ушаков",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Бу югары уку йортларының кайсысы Казанда урнашкан?",
|
||||||
|
"options": [
|
||||||
|
"Мәскәү дәүләт университеты",
|
||||||
|
"Казан федераль университеты",
|
||||||
|
"Санкт-Петербург политехник университеты",
|
||||||
|
"Новосибирск дәүләт университеты"
|
||||||
|
],
|
||||||
|
"correct_answer": "Казан федераль университеты",
|
||||||
|
"image_url": "culture"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"question": "Казандагы иң зур урам ничек атала? Ул шулай ук шәһәрнең төнге тормыш үзәге булып тора.",
|
||||||
|
"options": [
|
||||||
|
"Кремль урамы",
|
||||||
|
"Бауман урамы",
|
||||||
|
"Пушкин урамы",
|
||||||
|
"Каюм Насыйри урамы"
|
||||||
|
],
|
||||||
|
"correct_answer": "Бауман урамы",
|
||||||
|
"image_url": "culture"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
server/routers/kfu-m-24-1/back-new/.env
Normal file
1
server/routers/kfu-m-24-1/back-new/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
GIGACHAT_API_KEY=NzgzNTkxMjMtNDQ0Ny00ODFhLTkwMjgtODYxZjUzYjI0ZWQxOjA5NDEwMzEwLTM5YjItNDUzOS1hYWYzLWE4ZDA1MDExNmQ4Nw==
|
||||||
2
server/routers/kfu-m-24-1/back-new/.gitignore
vendored
Normal file
2
server/routers/kfu-m-24-1/back-new/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
21
server/routers/kfu-m-24-1/back-new/README.md
Normal file
21
server/routers/kfu-m-24-1/back-new/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# back-new
|
||||||
|
|
||||||
|
非Python实现的后端(Node.js + Express)
|
||||||
|
|
||||||
|
## 启动方法
|
||||||
|
|
||||||
|
1. 安装依赖:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
2. 启动服务:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
默认端口:`3002`
|
||||||
|
|
||||||
|
## 支持接口
|
||||||
|
- POST `/api/auth/login` 用户登录
|
||||||
|
- POST `/api/auth/register` 用户注册
|
||||||
|
- GET `/gigachat/prompt?prompt=xxx` 生成图片(返回模拟图片链接)
|
||||||
24
server/routers/kfu-m-24-1/back-new/app.js
Normal file
24
server/routers/kfu-m-24-1/back-new/app.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const featuresConfig = require('./features.config');
|
||||||
|
const imageRoutes = require('./features/image/image.routes');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
if (featuresConfig.auth) {
|
||||||
|
app.use('/api/auth', require('./features/auth/auth.routes'));
|
||||||
|
}
|
||||||
|
if (featuresConfig.user) {
|
||||||
|
app.use('/api/user', require('./features/user/user.routes'));
|
||||||
|
}
|
||||||
|
if (featuresConfig.image) {
|
||||||
|
app.use('/gigachat', imageRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/api/', (req, res) => {
|
||||||
|
res.json({ message: 'API root' });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = app;
|
||||||
5
server/routers/kfu-m-24-1/back-new/features.config.js
Normal file
5
server/routers/kfu-m-24-1/back-new/features.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
auth: true,
|
||||||
|
user: true,
|
||||||
|
image: true, // 关闭为 false
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
const usersDb = require('../../shared/usersDb');
|
||||||
|
const makeLinks = require('../../shared/hateoas');
|
||||||
|
|
||||||
|
exports.login = (req, res) => {
|
||||||
|
const { username, password, email } = req.body;
|
||||||
|
const user = usersDb.findUser(username, email, password);
|
||||||
|
if (user) {
|
||||||
|
res.json({
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName
|
||||||
|
},
|
||||||
|
token: 'token-' + user.id,
|
||||||
|
message: 'Login successful'
|
||||||
|
},
|
||||||
|
_links: makeLinks('/api/auth', {
|
||||||
|
self: '/login',
|
||||||
|
profile: '/profile/',
|
||||||
|
logout: '/logout'
|
||||||
|
}),
|
||||||
|
_meta: {}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.register = (req, res) => {
|
||||||
|
const { username, password, email, firstName, lastName } = req.body;
|
||||||
|
if (usersDb.exists(username, email)) {
|
||||||
|
return res.status(409).json({ error: 'User already exists' });
|
||||||
|
}
|
||||||
|
const newUser = usersDb.addUser({ username, password, email, firstName, lastName });
|
||||||
|
res.json({
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: newUser.id,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
},
|
||||||
|
token: 'token-' + newUser.id,
|
||||||
|
message: 'Register successful'
|
||||||
|
},
|
||||||
|
_links: makeLinks('/api/auth', {
|
||||||
|
self: '/register',
|
||||||
|
login: '/login',
|
||||||
|
profile: '/profile/'
|
||||||
|
}),
|
||||||
|
_meta: {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.profile = (req, res) => {
|
||||||
|
const auth = req.headers.authorization;
|
||||||
|
if (!auth || !auth.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ error: 'No token provided' });
|
||||||
|
}
|
||||||
|
const token = auth.replace('Bearer ', '');
|
||||||
|
const id = parseInt(token.replace('token-', ''));
|
||||||
|
const user = usersDb.findById(id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
data: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName
|
||||||
|
},
|
||||||
|
_links: makeLinks('/api/auth', {
|
||||||
|
self: '/profile/',
|
||||||
|
logout: '/logout'
|
||||||
|
}),
|
||||||
|
_meta: {}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.logout = (req, res) => {
|
||||||
|
res.json({
|
||||||
|
message: 'Logout successful',
|
||||||
|
_links: makeLinks('/api/auth', {
|
||||||
|
self: '/logout',
|
||||||
|
login: '/login'
|
||||||
|
}),
|
||||||
|
_meta: {}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const ctrl = require('./auth.controller');
|
||||||
|
|
||||||
|
router.post('/login', ctrl.login);
|
||||||
|
router.post('/register', ctrl.register);
|
||||||
|
router.get('/profile/', ctrl.profile);
|
||||||
|
router.post('/logout', ctrl.logout);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const makeLinks = require('../../shared/hateoas');
|
||||||
|
const path = require('path');
|
||||||
|
const qs = require('qs');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||||
|
|
||||||
|
exports.generate = async (req, res) => {
|
||||||
|
const { prompt } = req.query;
|
||||||
|
if (!prompt) {
|
||||||
|
return res.status(400).json({ error: 'Prompt parameter is required' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const apiKey = process.env.GIGACHAT_API_KEY;
|
||||||
|
const tokenResp = await axios.post(
|
||||||
|
'https://ngw.devices.sberbank.ru:9443/api/v2/oauth',
|
||||||
|
{
|
||||||
|
'scope':' GIGACHAT_API_PERS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Basic ${apiKey}`,
|
||||||
|
'RqUID':'6f0b1291-c7f3-43c6-bb2e-9f3efb2dc98e'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const accessToken = tokenResp.data.access_token;
|
||||||
|
const chatResp = await axios.post(
|
||||||
|
'https://gigachat.devices.sberbank.ru/api/v1/chat/completions',
|
||||||
|
{
|
||||||
|
model: "GigaChat",
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: "Ты — Василий Кандинский" },
|
||||||
|
{ role: "user", content: prompt }
|
||||||
|
],
|
||||||
|
stream: false,
|
||||||
|
function_call: 'auto'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'RqUID': uuidv4(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const content = chatResp.data.choices[0].message.content;
|
||||||
|
const match = content.match(/<img src=\"(.*?)\"/);
|
||||||
|
if (!match) {
|
||||||
|
return res.status(500).json({ error: 'No image generated' });
|
||||||
|
}
|
||||||
|
const imageId = match[1];
|
||||||
|
const imageResp = await axios.get(
|
||||||
|
`https://gigachat.devices.sberbank.ru/api/v1/files/${imageId}/content`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'RqUID': uuidv4(),
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res.set('Content-Type', 'image/jpeg');
|
||||||
|
res.set('X-HATEOAS', JSON.stringify(makeLinks('/gigachat', { self: '/prompt' })));
|
||||||
|
res.send(imageResp.data);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response) {
|
||||||
|
console.error('AI生成图片出错:');
|
||||||
|
console.error('status:', err.response.status);
|
||||||
|
console.error('headers:', err.response.headers);
|
||||||
|
console.error('data:', err.response.data);
|
||||||
|
console.error('config:', err.config);
|
||||||
|
} else {
|
||||||
|
console.error('AI生成图片出错:', err.message);
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const ctrl = require('./image.controller');
|
||||||
|
|
||||||
|
router.get('/prompt', ctrl.generate);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const usersDb = require('../../shared/usersDb');
|
||||||
|
const makeLinks = require('../../shared/hateoas');
|
||||||
|
|
||||||
|
exports.list = (req, res) => {
|
||||||
|
res.json({
|
||||||
|
data: usersDb.getAll(),
|
||||||
|
_links: makeLinks('/api/user', {
|
||||||
|
self: '/list',
|
||||||
|
}),
|
||||||
|
_meta: {}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const ctrl = require('./user.controller');
|
||||||
|
|
||||||
|
router.get('/list', ctrl.list);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
5455
server/routers/kfu-m-24-1/back-new/package-lock.json
generated
Normal file
5455
server/routers/kfu-m-24-1/back-new/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
server/routers/kfu-m-24-1/back-new/package.json
Normal file
21
server/routers/kfu-m-24-1/back-new/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "back-new",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "非Python实现的后端,兼容前端接口",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.10.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.0.0",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^30.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
server/routers/kfu-m-24-1/back-new/server.js
Normal file
5
server/routers/kfu-m-24-1/back-new/server.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const app = require('./app');
|
||||||
|
const PORT = process.env.PORT || 3002;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Mock backend running on https://dev.bro.js.ru/ms/back-new/${PORT}`);
|
||||||
|
});
|
||||||
8
server/routers/kfu-m-24-1/back-new/shared/hateoas.js
Normal file
8
server/routers/kfu-m-24-1/back-new/shared/hateoas.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
function makeLinks(base, links) {
|
||||||
|
const result = {};
|
||||||
|
for (const [rel, path] of Object.entries(links)) {
|
||||||
|
result[rel] = { href: base + path };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
module.exports = makeLinks;
|
||||||
20
server/routers/kfu-m-24-1/back-new/shared/usersDb.js
Normal file
20
server/routers/kfu-m-24-1/back-new/shared/usersDb.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
let users = [
|
||||||
|
{ id: 1, username: 'test', password: '123456', email: 'test@example.com', firstName: 'Test', lastName: 'User' }
|
||||||
|
];
|
||||||
|
let nextId = 2;
|
||||||
|
|
||||||
|
exports.findUser = (username, email, password) =>
|
||||||
|
users.find(u => (u.username === username || u.email === email) && u.password === password);
|
||||||
|
|
||||||
|
exports.findById = (id) => users.find(u => u.id === id);
|
||||||
|
|
||||||
|
exports.addUser = ({ username, password, email, firstName, lastName }) => {
|
||||||
|
const newUser = { id: nextId++, username, password, email, firstName, lastName };
|
||||||
|
users.push(newUser);
|
||||||
|
return newUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.exists = (username, email) =>
|
||||||
|
users.some(u => u.username === username || u.email === email);
|
||||||
|
|
||||||
|
exports.getAll = () => users;
|
||||||
@@ -1 +1,26 @@
|
|||||||
[{"id":1,"description":"10 слов в Data Science","imageFilename":"kart1.jpg","words":[2,3,4,5,6,7,8,9,10,11,12]}]
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"description": "10 слов в Data Science",
|
||||||
|
"imageFilename": "kart1.jpg",
|
||||||
|
"words": [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"description": "Job Interview",
|
||||||
|
"imageFilename": "kart1.jpg",
|
||||||
|
"words": [13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"description": "ReactJS",
|
||||||
|
"imageFilename": "kart1.jpg",
|
||||||
|
"words": [23, 24, 25, 26, 27, 28, 29, 30, 31, 32]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"description": "NodeJS",
|
||||||
|
"imageFilename": "kart1.jpg",
|
||||||
|
"words": [33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -607,8 +607,7 @@ function createGigachat(options = {}) {
|
|||||||
}
|
}
|
||||||
var gigachat = createGigachat();
|
var gigachat = createGigachat();
|
||||||
// Annotate the CommonJS export names for ESM import in node:
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
0 && (module.exports = {
|
module.exports = {
|
||||||
createGigachat,
|
createGigachat,
|
||||||
gigachat
|
gigachat
|
||||||
});
|
}
|
||||||
//# sourceMappingURL=index.js.map
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ router.use(async (req, res, next) => {
|
|||||||
process.env.GIGACHAT_ACCESS_TOKEN = json.access_token;
|
process.env.GIGACHAT_ACCESS_TOKEN = json.access_token;
|
||||||
process.env.GIGACHAT_EXPIRES_AT = json.expires_at;
|
process.env.GIGACHAT_EXPIRES_AT = json.expires_at;
|
||||||
console.log(JSON.stringify(response.data));
|
console.log(JSON.stringify(response.data));
|
||||||
} catch {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
|
|
||||||
|
const wordsRouter = require('./words');
|
||||||
const dictionariesRouter = require('./dictionaries');
|
const dictionariesRouter = require('./dictionaries');
|
||||||
const unitsRouter = require('./units');
|
const unitsRouter = require('./units');
|
||||||
const gigachatRouter = require('./gigachat');
|
const gigachatRouter = require('./gigachat');
|
||||||
|
const usersRouter = require('./users');
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
const delay =
|
const delay =
|
||||||
@@ -12,6 +14,8 @@ const delay =
|
|||||||
};
|
};
|
||||||
|
|
||||||
router.use(delay());
|
router.use(delay());
|
||||||
|
router.use('/words', wordsRouter);
|
||||||
router.use('/dictionaries', dictionariesRouter);
|
router.use('/dictionaries', dictionariesRouter);
|
||||||
router.use('/units', unitsRouter);
|
router.use('/units', unitsRouter);
|
||||||
router.use('/gigachat', gigachatRouter);
|
router.use('/gigachat', gigachatRouter);
|
||||||
|
router.use('/users', usersRouter);
|
||||||
|
|||||||
@@ -4,9 +4,45 @@ const router = require('express').Router();
|
|||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
const data = require('./data/units.json');
|
const data = require('./units.json');
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
res.send(data);
|
// for every data set author from users and save it to authoredData variable
|
||||||
|
const users = require('../users/users.json');
|
||||||
|
const authoredData = data.map((unit) => {
|
||||||
|
const user = users.find((user) => user.public_id == unit.author);
|
||||||
|
let authoredUnit = undefined;
|
||||||
|
if (user) {
|
||||||
|
authoredUnit = { ...unit, author: user };
|
||||||
|
}
|
||||||
|
return authoredUnit;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(authoredData);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:id', (req, res) => {
|
||||||
|
const id = parseInt(req.params.id);
|
||||||
|
const updatedUnit = req.body;
|
||||||
|
|
||||||
|
if (!updatedUnit) {
|
||||||
|
return res.status(400).send('No unit to be added');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return res.status(500).send('No data to be updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = data.findIndex((unit) => unit.id === id);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return res.status(404).send('Not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
data.splice(index, 1);
|
||||||
|
|
||||||
|
data.push({...updatedUnit, author: updatedUnit.author.public_id});
|
||||||
|
|
||||||
|
res.status(200).send(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/', (req, res) => {
|
router.put('/', (req, res) => {
|
||||||
@@ -16,17 +52,17 @@ router.put('/', (req, res) => {
|
|||||||
return res.status(400).send('No new unit to be added');
|
return res.status(400).send('No new unit to be added');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newUnit.author) {
|
||||||
|
return res.status(400).send('User is not logged in!');
|
||||||
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return res.status(500).send('No data to be updated');
|
return res.status(500).send('No data to be updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
const newId = data.length + 1;
|
const newId = data.length + 1;
|
||||||
const filename = newUnit.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
data.push({ ...newUnit, id: newId });
|
||||||
fs.writeFileSync(path.join(__dirname, 'data', `${filename}.md`), newUnit.content);
|
|
||||||
|
|
||||||
data.push({ id: newId, filename: filename, name: newUnit.name });
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(__dirname, 'data', 'units.json'), JSON.stringify(data));
|
|
||||||
res.status(200).send(data);
|
res.status(200).send(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,24 +75,19 @@ router.delete('/:id', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data.splice(index, 1);
|
data.splice(index, 1);
|
||||||
fs.writeFileSync(path.join(__dirname, 'data', 'units.json'), JSON.stringify(data));
|
|
||||||
res.send({ message: `Unit with ID ${id} deleted` });
|
res.send({ message: `Unit with ID ${id} deleted` });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/:id', (req, res) => {
|
router.get('/:id', (req, res) => {
|
||||||
|
const users = require('../users/users.json');
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(req.params.id);
|
||||||
const unit = data.find((unit) => unit.id === id);
|
const unit = data.find((unit) => unit.id === id);
|
||||||
|
|
||||||
if (!unit) {
|
if (!unit) {
|
||||||
return res.status(404).send('Not found');
|
return res.status(404).send('Unit not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const unitFilepath = path.join(__dirname, 'data', `${unit.filename}.md`);
|
const user = users.find((user) => user.public_id == unit.author);
|
||||||
const unitContent = fs.readFileSync(unitFilepath, 'utf-8');
|
|
||||||
|
|
||||||
if (!unitContent) {
|
res.send({...unit, author: user});
|
||||||
return res.status(404).send('Not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({ ...unit, content: unitContent });
|
|
||||||
});
|
});
|
||||||
|
|||||||
20
server/routers/kfu-m-24-1/eng-it-lean/units/units.json
Normal file
20
server/routers/kfu-m-24-1/eng-it-lean/units/units.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"content": "# Цель урока\n\nИзучение структуры документации программы с блоком кода.\n\n## Лексика\n\n### Базовая лексика:\n\n- Documentation – документация\n- Code block – блок кода\n- Description – описание\n- Function – функция\n- Variable – переменная\n- Comment – комментарий\n\n### Расширенная лексика:\n\n- API – интерфейс прикладного программирования\n- Method – метод\n- Class – класс\n- Library – библиотека\n- Framework – фреймворк\n\n## Грамматический фокус\n\nПравило: Структура документации программы должна включать краткое описание, блок кода и примеры использования.\n\nПример:\n\nDocumentation for a program typically includes the following sections:\n\n1. **Description**: A brief overview of what the program does and its purpose.\n2. **Code Block**: The actual code that implements the functionality described in the first section.\n3. **Examples**: One or more examples demonstrating how to use the features described in the documentation.\n\nТипичные ошибки и как их избежать: Ошибки могут возникнуть из-за недостаточного описания функционала или неправильного форматирования кода. Чтобы избежать этого, важно тщательно проработать каждый раздел документации и убедиться, что все примеры корректны и понятны.\n\n## Контекстуализация\n\nТекст для анализа:\n\n**Description**: This is a simple Python script that calculates the average value of a list of numbers.\n\n**Code Block**: \n```python\ndef calculate_average(numbers):\n \"\"\"Calculate the average value of a list of numbers\"\"\"\n return sum(numbers)/len(numbers)\n```\n\nПримеры использования:\n\n```python\n# Example usage\nnumbers = [10, 20, 30]\naverage = calculate_average(numbers)\nprint(\"The average value of the list\", numbers, \"is\", average)\n```\n\n## Упражнения\n\nПисьменное задание: Написать документацию для простой функции на языке Python, которая принимает список чисел и возвращает среднее значение. Включить описание, код блока и пример использования.\n\nУстная практика: Ролевой диалог между разработчиком и техническим писателем о структуре и содержании документации программы.\n\nАналитическое задание: Проанализировать существующую документацию программы и найти ошибки или неясности. Предложить улучшения.\n\n## Домашнее задание\n\nТекстовые задачи:\n\n- Написать документацию для другой функции на языке Python, используя правильную структуру.\n- Исправить ошибки в существующей документации программы.\n- Перевести фрагмент документации на русский язык, сохраняя точность и стиль.\n",
|
||||||
|
"id": 2,
|
||||||
|
"author": "1738707541324",
|
||||||
|
"name": "Документация программы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "### Цель урока:\nИзучение ключевых слов и фраз, связанных с процессом трудоустройства, а также освоение базовой структуры диалога на собеседовании.\n\n### Лексика:\n**Базовая лексика:**\n1. **Job interview** – собеседование при приеме на работу\n2. **Resume / CV** – резюме\n3. **Cover letter** – сопроводительное письмо\n4. **Interviewer** – интервьюер\n5. **Application form** – анкета при приеме на работу\n6. **Salary** – зарплата\n7. **Benefits** – льготы\n\n**Расширенная лексика:**\n1. **To apply for a job** – подавать заявку на работу\n2. **To be offered the job** – получить предложение о работе\n3. **To negotiate salary** – вести переговоры о зарплате\n4. **To accept the offer** – принять предложение\n5. **To decline the offer** – отклонить предложение\n6. **To resign from your current position** – подать заявление об уходе с текущей работы\n7. **To start working at the company** – начать работать в компании\n8. **Probation period** – испытательный срок\n9. **References** – рекомендации\n10. **Work experience** – опыт работы\n\n### Грамматический фокус:\n**Правило:**\nСтруктура простого вопроса на английском языке:\n- Общий вопрос: \"Do you have any questions?\"\n- Специальный вопрос: \"What are your strengths and weaknesses?\"\n\n**Пример:**\nОбщий вопрос: \"How do you feel about this job opportunity?\"\nСпециальный вопрос: \"Can you tell me about your previous work experience?\"\n\n**Типичные ошибки и как их избежать:**\nОшибка: Неправильное использование порядка слов в вопросах.\nРешение: Практиковать построение вопросов до автоматизма.\n\n### Контекстуализация:\n**Текст для анализа:**\n\"I'm applying for the position of a marketing manager at XYZ Company. Here is my resume.\"\n\"Thank you for considering me. Can you please tell me more about the responsibilities of this role?\"\n\"Sure, let me give you an overview.\"\n\n### Упражнения:\n**Письменное задание:**\nСоставьте список из 5 вопросов, которые вы бы задали на собеседовании. Используйте простые вопросы и специальные вопросы.\n\n**Устная практика:**\nРолевая игра: один студент играет роль интервьюера, другой – кандидата на должность. Меняйтесь ролями.\n\n**Аналитическое задание:**\nНайдите и исправьте ошибки в следующем письме:\n\"Dear HR Manager,\n\nMy name is John Smith and I am writing to apply for the position of Sales Representative at ABC Inc. I enclose my resume for your review.\n\nI believe that my skills and experiences make me an ideal candidate for this position. In my current role as a sales representative at XYZ Corp, I have consistently met or exceeded my sales targets. Additionally, I possess strong communication and negotiation skills which will enable me to effectively represent your products and services.\n\nIf you would like to schedule an interview, please contact me at your convenience. Thank you for your time and consideration.\n\nBest regards,\nJohn Smith\"\n\n### Домашнее задание:\n**Текстовые задачи:**\n1. Написать сопроводительное письмо для конкретной вакансии, используя расширенную лексику.\n2. Составить резюме для воображаемой должности, включая все необходимые разделы.\n3. Перевести текст собеседования на английский язык, сохраняя структуру и смысл.",
|
||||||
|
"id": 3,
|
||||||
|
"author": "1738707541324",
|
||||||
|
"name": "Job Interview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "# Multifunctional Verbs\n\n## Overview\n\nThis unit focuses on the use of multifunctional verbs in English. These verbs are able to express multiple meanings depending on their use in a sentence.\n\n## Learning Objectives\n\nBy the end of this unit, you will be able to:\n\n* Identify the different forms of the main multifunctional verb.\n* Explain how these forms can be used interchangeably in sentences.\n* Demonstrate the correct usage of the three forms of the multifunctional verb by providing sentences and examples.\n\n## Vocabulary Review\n\n| Term | Definition |\n| ---- | -------------------------------------------------------- |\n| Be | To express a present or ongoing state of being. |\n| Have | To express ownership or possession. |\n| Do | To express an action to be done, future action or habit. |\n\n## Activities\n\n### Activity 1: Identify the Different Forms of the Main Multifunctional Verb\n\n* Read through each sentence and identify if the verb is used in its present tense (is), past tense (was/were), or future tense (will, would).\n* Discuss how this usage can vary depending on context.\n* Write down sentences that use different forms to illustrate your points.\n\n1. **Sentence 1**\n\n : \"The cat is sleeping.\"\n * Present tense: The cat is sleeping.\n * Past tense: The cat slept.\n * Future tense: The cat will sleep.\n2. **Sentence 2**\n\n : \"I have a dog at home.\"\n * Present tense: I have a dog.\n * Past tense: I had a dog.\n * Future tense: I will have a dog.\n3. **Sentence 3**\n\n : \"We are going on a hike tomorrow.\"\n * Present tense: We are going on a hike.\n * Past tense: We went on a hike.\n * Future tense: We will go on a hike.\n4. **Sentence 4**\n\n : \"He has been studying all day.\"\n * Present tense: He is studying.\n * Past tense: He studied.\n * Future tense: He will study.\n5. **Sentence 5**\n\n : \"We are going to buy some groceries later today.\"\n * Present tense: We are going to buy some groceries.\n * Past tense: We bought some groceries.\n * Future tense: We will buy some groceries.\n\n### Activity 2: Explain How These Forms Can Be Used Interchangeably in Sentences\n\n* Read through a sentence and identify the present, past, and future tense uses.\n* In pairs, explain why these forms are used interchangeably.\n* Provide examples of sentences that demonstrate this usage.\n* Highlight how the context changes the meaning.\n\n### Activity 3: Correct Usage of the Three Forms of the Multifunctional Verb\n\n* Read through a sentence and identify which form is being used.\n* In pairs, discuss why these forms are used in certain situations.\n* Provide sentences that demonstrate the correct usage of the three forms.",
|
||||||
|
"id": 1,
|
||||||
|
"author": "1738707541324",
|
||||||
|
"name": "Multifunctional Verbs"
|
||||||
|
}
|
||||||
|
]
|
||||||
53
server/routers/kfu-m-24-1/eng-it-lean/users/index.js
Normal file
53
server/routers/kfu-m-24-1/eng-it-lean/users/index.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const router = require('express').Router();
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
let data = require('./users.json');
|
||||||
|
const path = require('path');
|
||||||
|
router.get('/', (req, res) => {
|
||||||
|
res.send(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
const newUser = req.body;
|
||||||
|
|
||||||
|
data.push(newUser);
|
||||||
|
fs.writeFileSync(path.join(__dirname, 'users.json'), JSON.stringify(data));
|
||||||
|
res.send(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/login', (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
const user = data.find((user) => user.email === email && user.password === password);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).send('Пользователь не найден');
|
||||||
|
}
|
||||||
|
res.json({ public_id: user.public_id });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/account', (req, res) => {
|
||||||
|
const { public_id } = req.query;
|
||||||
|
const user = data.find((user) => user.public_id == public_id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).send('Пользователь не найден');
|
||||||
|
}
|
||||||
|
res.send({ ...user, id: -1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/account/save', (req, res) => {
|
||||||
|
const updatedUser = req.body;
|
||||||
|
const { public_id } = updatedUser;
|
||||||
|
const index = data.findIndex((user) => user.public_id == public_id);
|
||||||
|
|
||||||
|
if (!index || index === -1) {
|
||||||
|
res.status(404).send('Пользователь не найден');
|
||||||
|
}
|
||||||
|
|
||||||
|
data[index] = { ...data[index], ...updatedUser, id: data[index].id, password: data[index].password };
|
||||||
|
fs.writeFileSync(path.join(__dirname, 'users.json'), JSON.stringify(data));
|
||||||
|
|
||||||
|
res.status(200);
|
||||||
|
});
|
||||||
1
server/routers/kfu-m-24-1/eng-it-lean/users/users.json
Normal file
1
server/routers/kfu-m-24-1/eng-it-lean/users/users.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"id":1738707541324,"public_id":1738707541324,"email":"1@gmail.com","password":"1","age":"22","nickname":"324324","about":"Чиловый "}]
|
||||||
@@ -132,5 +132,306 @@
|
|||||||
"During model validation, its ability to make accurate predictions on new data is checked.",
|
"During model validation, its ability to make accurate predictions on new data is checked.",
|
||||||
"Validation showed that the model is robust against changes in data and has low generalization error."
|
"Validation showed that the model is robust against changes in data and has low generalization error."
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"word": "resume",
|
||||||
|
"translation": "резюме",
|
||||||
|
"definition": "a document containing a summary of your work experience, education, and skills that you submit when applying for a job",
|
||||||
|
"synonyms": ["CV", "curriculum vitae"],
|
||||||
|
"examples": [
|
||||||
|
"Make sure to update your resume before the interview."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"word": "interviewer",
|
||||||
|
"translation": "интервьюер",
|
||||||
|
"definition": "the person who conducts an interview, typically a representative of the company or organization offering the job",
|
||||||
|
"synonyms": ["questioner", "examiner"],
|
||||||
|
"examples": [
|
||||||
|
"The interviewer asked about my previous work experiences."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"word": "qualification",
|
||||||
|
"translation": "квалификация",
|
||||||
|
"definition": "a quality, skill, or attribute that makes someone suitable for a particular job or activity",
|
||||||
|
"synonyms": ["credential", "competence"],
|
||||||
|
"examples": [
|
||||||
|
"Do you have any qualifications in project management?"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"word": "experience",
|
||||||
|
"translation": "опыт",
|
||||||
|
"definition": "practical contact with and observation of facts or events, especially those gained through employment",
|
||||||
|
"synonyms": ["background", "track record"],
|
||||||
|
"examples": [
|
||||||
|
"She has five years of experience in marketing."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"word": "skillset",
|
||||||
|
"translation": "набор навыков",
|
||||||
|
"definition": "the range of skills and abilities possessed by an individual",
|
||||||
|
"synonyms": ["abilities", "talents"],
|
||||||
|
"examples": [
|
||||||
|
"Her skillset includes proficiency in several programming languages."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"word": "cover letter",
|
||||||
|
"translation": "сопроводительное письмо",
|
||||||
|
"definition": "a document sent with your resume that provides additional information on why you're qualified for the position",
|
||||||
|
"synonyms": ["application letter", "letter of introduction"],
|
||||||
|
"examples": [
|
||||||
|
"Always include a well-written cover letter with your application."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"word": "hiring manager",
|
||||||
|
"translation": "менеджер по найму",
|
||||||
|
"definition": "the person responsible for making hiring decisions within a company or department",
|
||||||
|
"synonyms": ["recruiter", "HR manager"],
|
||||||
|
"examples": [
|
||||||
|
"The hiring manager will review all applications and select candidates for interviews."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"word": "job description",
|
||||||
|
"translation": "описание вакансии",
|
||||||
|
"definition": "a detailed account of the responsibilities, duties, required skills, and working conditions associated with a specific job",
|
||||||
|
"synonyms": ["position profile", "role specification"],
|
||||||
|
"examples": [
|
||||||
|
"Read the job description carefully to understand what the employer is looking for."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"word": "salary negotiation",
|
||||||
|
"translation": "переговоры о зарплате",
|
||||||
|
"definition": "the process of discussing and agreeing upon the compensation for a job, including salary, benefits, and other forms of remuneration",
|
||||||
|
"synonyms": ["compensation discussion", "pay bargaining"],
|
||||||
|
"examples": [
|
||||||
|
"It's important to prepare for salary negotiations during the final stages of the interview process."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"word": "reference",
|
||||||
|
"translation": "рекомендация",
|
||||||
|
"definition": "a person who can vouch for your qualifications, character, and work ethic, often contacted by potential employers",
|
||||||
|
"synonyms": ["endorsement", "testimonial"],
|
||||||
|
"examples": [
|
||||||
|
"Be prepared to provide references from former supervisors or colleagues."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"word": "component",
|
||||||
|
"translation": "компонент",
|
||||||
|
"definition": "A reusable piece of code that renders part of the user interface.",
|
||||||
|
"synonyms": ["module", "widget"],
|
||||||
|
"examples": [
|
||||||
|
"In React, components are the building blocks of the UI."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"word": "props",
|
||||||
|
"translation": "пропсы",
|
||||||
|
"definition": "Short for 'properties', these are read-only components passed down from parent components to child components.",
|
||||||
|
"synonyms": ["attributes", "parameters"],
|
||||||
|
"examples": [
|
||||||
|
"Props allow you to pass data between components."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"word": "state",
|
||||||
|
"translation": "состояние",
|
||||||
|
"definition": "An object that holds data specific to a component which may change over time.",
|
||||||
|
"synonyms": ["data", "context"],
|
||||||
|
"examples": [
|
||||||
|
"Managing state is crucial for dynamic web applications."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"word": "render",
|
||||||
|
"translation": "рендеринг",
|
||||||
|
"definition": "The process of updating the DOM to match the current state of a component.",
|
||||||
|
"synonyms": ["update", "refresh"],
|
||||||
|
"examples": [
|
||||||
|
"React efficiently handles rendering to ensure smooth updates."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"word": "virtual DOM",
|
||||||
|
"translation": "виртуальный DOM",
|
||||||
|
"definition": "A lightweight copy of the actual DOM maintained by React to improve performance by minimizing updates to the real DOM.",
|
||||||
|
"synonyms": ["shadow tree", "virtual representation"],
|
||||||
|
"examples": [
|
||||||
|
"Virtual DOM allows React to update only necessary parts of the UI."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"word": "JSX",
|
||||||
|
"translation": "JSX",
|
||||||
|
"definition": "A syntax extension to JavaScript used in React to describe what the user interface should look like.",
|
||||||
|
"synonyms": ["template language", "syntax extension"],
|
||||||
|
"examples": [
|
||||||
|
"JSX makes it easier to write and understand React components."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"word": "hooks",
|
||||||
|
"translation": "хуки",
|
||||||
|
"definition": "Functions that let you use state and other React features without writing a class.",
|
||||||
|
"synonyms": ["functionalities", "utilities"],
|
||||||
|
"examples": [
|
||||||
|
"Hooks make functional components more powerful."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"word": "event handling",
|
||||||
|
"translation": "обработка событий",
|
||||||
|
"definition": "The mechanism by which React components respond to user actions such as clicks, key presses, etc.",
|
||||||
|
"synonyms": ["interaction management", "action response"],
|
||||||
|
"examples": [
|
||||||
|
"Event handlers in React allow you to define how components react to user interactions."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"word": "lifecycle methods",
|
||||||
|
"translation": "методы жизненного цикла",
|
||||||
|
"definition": "Methods called at different stages of a component's existence, allowing developers to perform tasks at each stage.",
|
||||||
|
"synonyms": ["phase callbacks", "stage handlers"],
|
||||||
|
"examples": [
|
||||||
|
"Lifecycle methods help manage the behavior of components throughout their lifecycle."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"word": "routing",
|
||||||
|
"translation": "маршрутизация",
|
||||||
|
"definition": "The process of defining and managing navigation paths within a single-page application.",
|
||||||
|
"synonyms": ["navigation control", "path management"],
|
||||||
|
"examples": [
|
||||||
|
"React Router is commonly used for routing in React apps."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"word": "server-side",
|
||||||
|
"translation": "серверная сторона",
|
||||||
|
"definition": "Refers to operations performed by the server in contrast to client-side operations.",
|
||||||
|
"synonyms": ["backend", "back-end"],
|
||||||
|
"examples": [
|
||||||
|
"Node.js is primarily used for server-side development."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"word": "asynchronous",
|
||||||
|
"translation": "асинхронный",
|
||||||
|
"definition": "Programming model where operations execute independently of other operations, allowing efficient handling of multiple requests simultaneously.",
|
||||||
|
"synonyms": ["non-blocking", "concurrent"],
|
||||||
|
"examples": [
|
||||||
|
"Node.js uses asynchronous I/O to handle many concurrent connections efficiently."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"word": "callback",
|
||||||
|
"translation": "коллбек",
|
||||||
|
"definition": "A function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.",
|
||||||
|
"synonyms": ["handler", "continuation"],
|
||||||
|
"examples": [
|
||||||
|
"Callbacks are widely used in Node.js for handling asynchronous operations."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"word": "event loop",
|
||||||
|
"translation": "цикл событий",
|
||||||
|
"definition": "Mechanism in Node.js that handles asynchronous operations and ensures non-blocking I/O by offloading operations to the system kernel whenever possible.",
|
||||||
|
"synonyms": ["event-driven architecture", "runtime environment"],
|
||||||
|
"examples": [
|
||||||
|
"The event loop is fundamental to understanding how Node.js works."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"word": "package manager",
|
||||||
|
"translation": "менеджер пакетов",
|
||||||
|
"definition": "Tool used to install, update, configure, and remove packages in Node.js projects.",
|
||||||
|
"synonyms": ["dependency manager", "library manager"],
|
||||||
|
"examples": [
|
||||||
|
"npm and yarn are popular package managers for Node.js."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"word": "middleware",
|
||||||
|
"translation": "промежуточное ПО",
|
||||||
|
"definition": "Software that sits between an application and the backend infrastructure, providing additional functionality to the request-response cycle.",
|
||||||
|
"synonyms": ["interceptor", "filter"],
|
||||||
|
"examples": [
|
||||||
|
"Express.js uses middleware to handle common tasks like logging, authentication, and error handling."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"word": "REST API",
|
||||||
|
"translation": "REST API",
|
||||||
|
"definition": "Architectural style for building APIs using HTTP methods to perform CRUD operations on resources.",
|
||||||
|
"synonyms": ["web service", "API design pattern"],
|
||||||
|
"examples": [
|
||||||
|
"Many Node.js applications implement RESTful APIs to communicate with clients."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"word": "streams",
|
||||||
|
"translation": "потоки",
|
||||||
|
"definition": "Data structures that facilitate continuous transfer of data in chunks rather than loading everything into memory at once.",
|
||||||
|
"synonyms": ["data flow", "pipeline"],
|
||||||
|
"examples": [
|
||||||
|
"Streams are useful for handling large files or continuous data in Node.js."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"word": "cluster module",
|
||||||
|
"translation": "модуль кластера",
|
||||||
|
"definition": "Built-in module in Node.js that allows an application to be split into multiple processes running on separate CPU cores, improving performance and scalability.",
|
||||||
|
"synonyms": ["multi-processing", "parallel execution"],
|
||||||
|
"examples": [
|
||||||
|
"Using the cluster module can significantly enhance the throughput of a Node.js application."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"word": "event emitter",
|
||||||
|
"translation": "излучатель событий",
|
||||||
|
"definition": "Class in Node.js that facilitates communication between objects in an application by emitting named events that other objects can listen to.",
|
||||||
|
"synonyms": ["publisher-subscriber", "observer pattern"],
|
||||||
|
"examples": [
|
||||||
|
"Event emitters are useful for implementing custom event-based systems in Node.js."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ router.post('/:user_id/:action/:id', (req, res) => {
|
|||||||
const user_id = parseInt(req.params.user_id);
|
const user_id = parseInt(req.params.user_id);
|
||||||
const id = parseInt(req.params.id);
|
const id = parseInt(req.params.id);
|
||||||
const action = req.params.action;
|
const action = req.params.action;
|
||||||
|
/*
|
||||||
if (users_data.findIndex((item) => item.id === user_id) === -1 || data.findIndex((item) => item.id === id) === -1) {
|
if (users_data.findIndex((item) => item.id === user_id) === -1 || data.findIndex((item) => item.id === id) === -1) {
|
||||||
res.status(404).send();
|
res.status(404).send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if (action !== 'participate' && action !== 'refuse') {
|
if (action !== 'participate' && action !== 'refuse') {
|
||||||
res.status(400).send({ error: 'Invalid action' });
|
res.status(400).send({ error: 'Invalid action' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
"Музыка"
|
"Музыка"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": 1252744945,
|
||||||
|
"username": "Моряков Сергей",
|
||||||
|
"photo": "",
|
||||||
|
"about": "Люблю путешествия и фотографию, обожаю изучать новые культуры.",
|
||||||
|
"email": "sergey.moryakov@example.com",
|
||||||
|
"interests": [
|
||||||
|
"Путешествия и туризм",
|
||||||
|
"Искусство, фотография и дизайн"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"username": "Мария Смирнова",
|
"username": "Мария Смирнова",
|
||||||
@@ -218,16 +229,5 @@
|
|||||||
"Медицина и здоровье",
|
"Медицина и здоровье",
|
||||||
"Спорт, фитнес и ЗОЖ"
|
"Спорт, фитнес и ЗОЖ"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 1252744945,
|
|
||||||
"username": "Моряков Сергей",
|
|
||||||
"photo": "https://i.pravatar.cc/150?img=50",
|
|
||||||
"about": "Люблю путешествия и фотографию, обожаю изучать новые культуры.",
|
|
||||||
"email": "maria.smirnova@example.com",
|
|
||||||
"interests": [
|
|
||||||
"Путешествия и туризм",
|
|
||||||
"Искусство, фотография и дизайн"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -27,10 +27,12 @@ router.post('/:to_id/:action/:from_id', (req, res) => {
|
|||||||
const to_id = parseInt(req.params.to_id);
|
const to_id = parseInt(req.params.to_id);
|
||||||
const from_id = parseInt(req.params.from_id);
|
const from_id = parseInt(req.params.from_id);
|
||||||
const action = req.params.action;
|
const action = req.params.action;
|
||||||
|
/*
|
||||||
if (data.findIndex((item) => item.id === to_id) === -1 || data.findIndex((item) => item.id === from_id) === -1) {
|
if (data.findIndex((item) => item.id === to_id) === -1 || data.findIndex((item) => item.id === from_id) === -1) {
|
||||||
res.status(404).send();
|
res.status(404).send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if (action !== 'like' && action !== 'dislike') {
|
if (action !== 'like' && action !== 'dislike') {
|
||||||
res.status(400).send({ error: 'Invalid action' });
|
res.status(400).send({ error: 'Invalid action' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
113
server/routers/procurement/index.js
Normal file
113
server/routers/procurement/index.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const dotenv = require('dotenv');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts)
|
||||||
|
const mongoose = require('../../utils/mongoose');
|
||||||
|
|
||||||
|
// Загрузить переменные окружения
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// Включить логирование при разработке: установите DEV=true в .env или при запуске
|
||||||
|
// export DEV=true && npm start (для Linux/Mac)
|
||||||
|
// set DEV=true && npm start (для Windows)
|
||||||
|
// По умолчанию логи отключены. Все console.log функции отключаются если DEV !== 'true'
|
||||||
|
if (process.env.DEV === 'true') {
|
||||||
|
console.log('ℹ️ DEBUG MODE ENABLED - All logs are visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Импортировать маршруты - прямые пути без path.join и __dirname
|
||||||
|
const authRoutes = require('./routes/auth');
|
||||||
|
const companiesRoutes = require('./routes/companies');
|
||||||
|
const messagesRoutes = require('./routes/messages');
|
||||||
|
const searchRoutes = require('./routes/search');
|
||||||
|
const buyRoutes = require('./routes/buy');
|
||||||
|
const experienceRoutes = require('./routes/experience');
|
||||||
|
const productsRoutes = require('./routes/products');
|
||||||
|
const reviewsRoutes = require('./routes/reviews');
|
||||||
|
const buyProductsRoutes = require('./routes/buyProducts');
|
||||||
|
const requestsRoutes = require('./routes/requests');
|
||||||
|
const homeRoutes = require('./routes/home');
|
||||||
|
const activityRoutes = require('./routes/activity');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Проверить подключение к MongoDB (подключение происходит в server/utils/mongoose.ts)
|
||||||
|
const dbConnected = mongoose.connection.readyState === 1;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json({ charset: 'utf-8' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, charset: 'utf-8' }));
|
||||||
|
|
||||||
|
// Set UTF-8 encoding for all responses
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// CORS headers
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Задержка для имитации сети (опционально)
|
||||||
|
const delay = (ms = 300) => (req, res, next) => setTimeout(next, ms);
|
||||||
|
app.use(delay());
|
||||||
|
|
||||||
|
// Статика для загруженных файлов
|
||||||
|
const uploadsRoot = 'server/remote-assets/uploads';
|
||||||
|
if (!fs.existsSync(uploadsRoot)) {
|
||||||
|
fs.mkdirSync(uploadsRoot, { recursive: true });
|
||||||
|
}
|
||||||
|
app.use('/uploads', express.static(uploadsRoot));
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
api: 'running',
|
||||||
|
database: dbConnected ? 'mongodb' : 'mock',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршруты
|
||||||
|
app.use('/auth', authRoutes);
|
||||||
|
app.use('/companies', companiesRoutes);
|
||||||
|
app.use('/messages', messagesRoutes);
|
||||||
|
app.use('/search', searchRoutes);
|
||||||
|
app.use('/buy', buyRoutes);
|
||||||
|
app.use('/buy-products', buyProductsRoutes);
|
||||||
|
app.use('/experience', experienceRoutes);
|
||||||
|
app.use('/products', productsRoutes);
|
||||||
|
app.use('/reviews', reviewsRoutes);
|
||||||
|
app.use('/requests', requestsRoutes);
|
||||||
|
app.use('/home', homeRoutes);
|
||||||
|
app.use('/activities', activityRoutes);
|
||||||
|
|
||||||
|
// Обработка ошибок
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error('API Error:', err);
|
||||||
|
res.status(err.status || 500).json({
|
||||||
|
error: err.message || 'Internal server error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).json({
|
||||||
|
error: 'Not found'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Экспортировать для использования в brojs
|
||||||
|
module.exports = app;
|
||||||
42
server/routers/procurement/middleware/auth.js
Normal file
42
server/routers/procurement/middleware/auth.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
const log = (message, data = '') => {
|
||||||
|
if (process.env.DEV === 'true') {
|
||||||
|
if (data) {
|
||||||
|
console.log(message, data);
|
||||||
|
} else {
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = (req, res, next) => {
|
||||||
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: 'No token provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');
|
||||||
|
req.userId = decoded.userId;
|
||||||
|
req.companyId = decoded.companyId;
|
||||||
|
req.user = decoded;
|
||||||
|
log('[Auth] Token verified - userId:', decoded.userId, 'companyId:', decoded.companyId);
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Token verification failed:', error.message);
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateToken = (userId, companyId, firstName = '', lastName = '', companyName = '') => {
|
||||||
|
log('[Auth] Generating token for userId:', userId, 'companyId:', companyId);
|
||||||
|
return jwt.sign(
|
||||||
|
{ userId, companyId, firstName, lastName, companyName },
|
||||||
|
process.env.JWT_SECRET || 'your-secret-key',
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { verifyToken, generateToken };
|
||||||
61
server/routers/procurement/models/Activity.js
Normal file
61
server/routers/procurement/models/Activity.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const activitySchema = new mongoose.Schema({
|
||||||
|
companyId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
'message_received',
|
||||||
|
'message_sent',
|
||||||
|
'request_received',
|
||||||
|
'request_sent',
|
||||||
|
'request_response',
|
||||||
|
'product_accepted',
|
||||||
|
'review_received',
|
||||||
|
'profile_updated',
|
||||||
|
'product_added',
|
||||||
|
'buy_product_added'
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
relatedCompanyId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
relatedCompanyName: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
type: mongoose.Schema.Types.Mixed
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индексы для оптимизации
|
||||||
|
activitySchema.index({ companyId: 1, createdAt: -1 });
|
||||||
|
activitySchema.index({ companyId: 1, read: 1, createdAt: -1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Activity', activitySchema);
|
||||||
|
|
||||||
43
server/routers/procurement/models/BuyDocument.js
Normal file
43
server/routers/procurement/models/BuyDocument.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const buyDocumentSchema = new mongoose.Schema({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
ownerCompanyId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
filePath: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
acceptedBy: {
|
||||||
|
type: [String],
|
||||||
|
default: []
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('BuyDocument', buyDocumentSchema);
|
||||||
|
|
||||||
87
server/routers/procurement/models/BuyProduct.js
Normal file
87
server/routers/procurement/models/BuyProduct.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
// Явно определяем схему для файлов
|
||||||
|
const fileSchema = new mongoose.Schema({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
storagePath: String,
|
||||||
|
uploadedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}, { _id: false });
|
||||||
|
|
||||||
|
const buyProductSchema = new mongoose.Schema({
|
||||||
|
companyId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
minlength: 10,
|
||||||
|
maxlength: 1000
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
unit: {
|
||||||
|
type: String,
|
||||||
|
default: 'шт'
|
||||||
|
},
|
||||||
|
files: [fileSchema],
|
||||||
|
acceptedBy: [{
|
||||||
|
companyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company'
|
||||||
|
},
|
||||||
|
acceptedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['draft', 'published'],
|
||||||
|
default: 'published'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индексы для оптимизации поиска
|
||||||
|
buyProductSchema.index({ companyId: 1, createdAt: -1 });
|
||||||
|
buyProductSchema.index({ name: 'text', description: 'text' });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('BuyProduct', buyProductSchema);
|
||||||
76
server/routers/procurement/models/Company.js
Normal file
76
server/routers/procurement/models/Company.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const companySchema = new mongoose.Schema({
|
||||||
|
fullName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
shortName: String,
|
||||||
|
inn: {
|
||||||
|
type: String,
|
||||||
|
sparse: true
|
||||||
|
},
|
||||||
|
ogrn: String,
|
||||||
|
legalForm: String,
|
||||||
|
industry: String,
|
||||||
|
companySize: String,
|
||||||
|
website: String,
|
||||||
|
phone: String,
|
||||||
|
email: String,
|
||||||
|
slogan: String,
|
||||||
|
description: String,
|
||||||
|
foundedYear: Number,
|
||||||
|
employeeCount: String,
|
||||||
|
revenue: String,
|
||||||
|
legalAddress: String,
|
||||||
|
actualAddress: String,
|
||||||
|
bankDetails: String,
|
||||||
|
logo: String,
|
||||||
|
rating: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
min: 0,
|
||||||
|
max: 5
|
||||||
|
},
|
||||||
|
reviews: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
ownerId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
platformGoals: [String],
|
||||||
|
productsOffered: String,
|
||||||
|
productsNeeded: String,
|
||||||
|
partnerIndustries: [String],
|
||||||
|
partnerGeography: [String],
|
||||||
|
verified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
type: {
|
||||||
|
profileViews: { type: Number, default: 0 }
|
||||||
|
},
|
||||||
|
default: {}
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
collection: 'companies',
|
||||||
|
minimize: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индексы для поиска
|
||||||
|
companySchema.index({ fullName: 'text', shortName: 'text', description: 'text' });
|
||||||
|
companySchema.index({ industry: 1 });
|
||||||
|
companySchema.index({ rating: -1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Company', companySchema);
|
||||||
46
server/routers/procurement/models/Experience.js
Normal file
46
server/routers/procurement/models/Experience.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const experienceSchema = new mongoose.Schema({
|
||||||
|
companyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
confirmed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
volume: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индексы для оптимизации поиска
|
||||||
|
experienceSchema.index({ companyId: 1, createdAt: -1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Experience', experienceSchema);
|
||||||
|
|
||||||
37
server/routers/procurement/models/Message.js
Normal file
37
server/routers/procurement/models/Message.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const messageSchema = new mongoose.Schema({
|
||||||
|
threadId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
senderCompanyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
recipientCompanyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индекс для быстрого поиска сообщений потока
|
||||||
|
messageSchema.index({ threadId: 1, timestamp: -1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Message', messageSchema);
|
||||||
57
server/routers/procurement/models/Product.js
Normal file
57
server/routers/procurement/models/Product.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const productSchema = new mongoose.Schema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
minlength: 20,
|
||||||
|
maxlength: 500
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['sell', 'buy'],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
productUrl: String,
|
||||||
|
companyId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
price: String,
|
||||||
|
unit: String,
|
||||||
|
minOrder: String,
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индекс для поиска
|
||||||
|
productSchema.index({ companyId: 1, type: 1 });
|
||||||
|
productSchema.index({ name: 'text', description: 'text' });
|
||||||
|
|
||||||
|
// Transform _id to id in JSON output
|
||||||
|
productSchema.set('toJSON', {
|
||||||
|
transform: (doc, ret) => {
|
||||||
|
ret.id = ret._id;
|
||||||
|
delete ret._id;
|
||||||
|
delete ret.__v;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Product', productSchema);
|
||||||
82
server/routers/procurement/models/Request.js
Normal file
82
server/routers/procurement/models/Request.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const requestSchema = new mongoose.Schema({
|
||||||
|
senderCompanyId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
recipientCompanyId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
trim: true,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
files: [{
|
||||||
|
id: { type: String },
|
||||||
|
name: { type: String },
|
||||||
|
url: { type: String },
|
||||||
|
type: { type: String },
|
||||||
|
size: { type: Number },
|
||||||
|
storagePath: { type: String },
|
||||||
|
uploadedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
productId: {
|
||||||
|
type: String,
|
||||||
|
ref: 'BuyProduct'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['pending', 'accepted', 'rejected'],
|
||||||
|
default: 'pending'
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
responseFiles: [{
|
||||||
|
id: { type: String },
|
||||||
|
name: { type: String },
|
||||||
|
url: { type: String },
|
||||||
|
type: { type: String },
|
||||||
|
size: { type: Number },
|
||||||
|
storagePath: { type: String },
|
||||||
|
uploadedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
respondedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индексы для оптимизации поиска
|
||||||
|
requestSchema.index({ senderCompanyId: 1, createdAt: -1 });
|
||||||
|
requestSchema.index({ recipientCompanyId: 1, createdAt: -1 });
|
||||||
|
requestSchema.index({ senderCompanyId: 1, recipientCompanyId: 1 });
|
||||||
|
requestSchema.index({ subject: 1, createdAt: -1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Request', requestSchema);
|
||||||
58
server/routers/procurement/models/Review.js
Normal file
58
server/routers/procurement/models/Review.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const reviewSchema = new mongoose.Schema({
|
||||||
|
companyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
authorCompanyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
authorName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
authorCompany: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
rating: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
min: 1,
|
||||||
|
max: 5
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
minlength: 10,
|
||||||
|
maxlength: 1000
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
verified: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Индексы для оптимизации поиска
|
||||||
|
reviewSchema.index({ companyId: 1, createdAt: -1 });
|
||||||
|
reviewSchema.index({ authorCompanyId: 1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Review', reviewSchema);
|
||||||
73
server/routers/procurement/models/User.js
Normal file
73
server/routers/procurement/models/User.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const userSchema = new mongoose.Schema({
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
lowercase: true,
|
||||||
|
trim: true,
|
||||||
|
match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
minlength: 8
|
||||||
|
},
|
||||||
|
firstName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
position: String,
|
||||||
|
phone: String,
|
||||||
|
companyId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'Company'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
collection: 'users',
|
||||||
|
minimize: false,
|
||||||
|
toObject: { versionKey: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
userSchema.set('toObject', { virtuals: false, versionKey: false });
|
||||||
|
|
||||||
|
// Хешировать пароль перед сохранением
|
||||||
|
userSchema.pre('save', async function(next) {
|
||||||
|
if (!this.isModified('password')) return next();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
this.password = await bcrypt.hash(this.password, salt);
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Метод для сравнения паролей
|
||||||
|
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||||
|
return await bcrypt.compare(candidatePassword, this.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Скрыть пароль при преобразовании в JSON
|
||||||
|
userSchema.methods.toJSON = function() {
|
||||||
|
const obj = this.toObject();
|
||||||
|
delete obj.password;
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = mongoose.model('User', userSchema);
|
||||||
239
server/routers/procurement/routes/__tests__/buyProducts.test.js
Normal file
239
server/routers/procurement/routes/__tests__/buyProducts.test.js
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const request = require('supertest')
|
||||||
|
|
||||||
|
// Mock auth middleware
|
||||||
|
const mockAuthMiddleware = (req, res, next) => {
|
||||||
|
req.user = {
|
||||||
|
companyId: 'test-company-id',
|
||||||
|
id: 'test-user-id',
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Buy Products Routes', () => {
|
||||||
|
let app
|
||||||
|
let router
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
app = express()
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// Create a test router with mock middleware
|
||||||
|
router = express.Router()
|
||||||
|
|
||||||
|
// Mock endpoints for testing structure
|
||||||
|
router.get('/company/:companyId', mockAuthMiddleware, (req, res) => {
|
||||||
|
res.json([])
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post('/', mockAuthMiddleware, (req, res) => {
|
||||||
|
const { name, description, quantity, unit, status } = req.body
|
||||||
|
|
||||||
|
if (!name || !description || !quantity) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'name, description, and quantity are required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description.trim().length < 10) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Description must be at least 10 characters',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = {
|
||||||
|
_id: 'product-' + Date.now(),
|
||||||
|
companyId: req.user.companyId,
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
quantity: quantity.trim(),
|
||||||
|
unit: unit || 'шт',
|
||||||
|
status: status || 'published',
|
||||||
|
files: [],
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(product)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/buy-products', router)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /buy-products/company/:companyId', () => {
|
||||||
|
it('should return products list for a company', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/buy-products/company/test-company-id')
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(Array.isArray(res.body)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
// This test would fail without proper auth middleware
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/buy-products/company/test-company-id')
|
||||||
|
|
||||||
|
expect(res.status).toBeLessThan(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('POST /buy-products', () => {
|
||||||
|
it('should create a new product with valid data', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'This is a test product description',
|
||||||
|
quantity: '10',
|
||||||
|
unit: 'шт',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(201)
|
||||||
|
|
||||||
|
expect(res.body).toHaveProperty('_id')
|
||||||
|
expect(res.body.name).toBe('Test Product')
|
||||||
|
expect(res.body.description).toBe(productData.description)
|
||||||
|
expect(res.body.status).toBe('published')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject product without name', async () => {
|
||||||
|
const productData = {
|
||||||
|
description: 'This is a test product description',
|
||||||
|
quantity: '10',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.body.error).toContain('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject product without description', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
quantity: '10',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.body.error).toContain('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject product without quantity', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'This is a test product description',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.body.error).toContain('required')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reject product with description less than 10 characters', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'short',
|
||||||
|
quantity: '10',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(400)
|
||||||
|
|
||||||
|
expect(res.body.error).toContain('10 characters')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set default unit to "шт" if not provided', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'This is a test product description',
|
||||||
|
quantity: '10',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(201)
|
||||||
|
|
||||||
|
expect(res.body.unit).toBe('шт')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use provided unit', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'This is a test product description',
|
||||||
|
quantity: '10',
|
||||||
|
unit: 'кг',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(201)
|
||||||
|
|
||||||
|
expect(res.body.unit).toBe('кг')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set status to "published" by default', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'This is a test product description',
|
||||||
|
quantity: '10',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(201)
|
||||||
|
|
||||||
|
expect(res.body.status).toBe('published')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Data validation', () => {
|
||||||
|
it('should trim whitespace from product data', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: ' Test Product ',
|
||||||
|
description: ' This is a test product description ',
|
||||||
|
quantity: ' 10 ',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(201)
|
||||||
|
|
||||||
|
expect(res.body.name).toBe('Test Product')
|
||||||
|
expect(res.body.description).toBe('This is a test product description')
|
||||||
|
expect(res.body.quantity).toBe('10')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include companyId from auth token', async () => {
|
||||||
|
const productData = {
|
||||||
|
name: 'Test Product',
|
||||||
|
description: 'This is a test product description',
|
||||||
|
quantity: '10',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/buy-products')
|
||||||
|
.send(productData)
|
||||||
|
.expect(201)
|
||||||
|
|
||||||
|
expect(res.body.companyId).toBe('test-company-id')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
101
server/routers/procurement/routes/activity.js
Normal file
101
server/routers/procurement/routes/activity.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { verifyToken } = require('../middleware/auth');
|
||||||
|
const Activity = require('../models/Activity');
|
||||||
|
const User = require('../models/User');
|
||||||
|
|
||||||
|
// Получить последние активности компании
|
||||||
|
router.get('/', verifyToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
|
||||||
|
if (!user || !user.companyId) {
|
||||||
|
return res.json({ activities: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyId = user.companyId.toString();
|
||||||
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
|
|
||||||
|
const activities = await Activity.find({ companyId })
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(limit)
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({ activities });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting activities:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отметить активность как прочитанную
|
||||||
|
router.patch('/:id/read', verifyToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
|
||||||
|
if (!user || !user.companyId) {
|
||||||
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyId = user.companyId.toString();
|
||||||
|
const activityId = req.params.id;
|
||||||
|
|
||||||
|
const activity = await Activity.findOne({
|
||||||
|
_id: activityId,
|
||||||
|
companyId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activity) {
|
||||||
|
return res.status(404).json({ error: 'Activity not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.read = true;
|
||||||
|
await activity.save();
|
||||||
|
|
||||||
|
res.json({ success: true, activity });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating activity:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отметить все активности как прочитанные
|
||||||
|
router.post('/mark-all-read', verifyToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const user = await User.findById(userId);
|
||||||
|
|
||||||
|
if (!user || !user.companyId) {
|
||||||
|
return res.status(403).json({ error: 'Access denied' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyId = user.companyId.toString();
|
||||||
|
|
||||||
|
await Activity.updateMany(
|
||||||
|
{ companyId, read: false },
|
||||||
|
{ $set: { read: true } }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking all as read:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создать активность (вспомогательная функция)
|
||||||
|
router.createActivity = async (data) => {
|
||||||
|
try {
|
||||||
|
const activity = new Activity(data);
|
||||||
|
await activity.save();
|
||||||
|
return activity;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating activity:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
515
server/routers/procurement/routes/auth.js
Normal file
515
server/routers/procurement/routes/auth.js
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
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;
|
||||||
220
server/routers/procurement/routes/buy.js
Normal file
220
server/routers/procurement/routes/buy.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user