Compare commits

...

319 Commits

Author SHA1 Message Date
Primakov Alexandr Alexandrovich
919714089a Add smoke test workflow to check application stability and MongoDB readiness; update ESLint and test commands for better output
Some checks failed
Code Quality Checks / lint-and-typecheck (push) Successful in 10m46s
Code Quality Checks / smoke-test (push) Failing after 8m12s
2025-12-05 17:06:39 +03:00
Primakov Alexandr Alexandrovich
7066252bcb Update Jest configuration to include TypeScript support and add new code quality checks workflow; translate comments to Russian and adjust paths in test files. 2025-12-05 16:51:44 +03:00
d477a0a5f1 fix 2025-11-22 00:05:51 +03:00
4c35decfd7 обновил критерии 2025-11-21 23:47:56 +03:00
599170df2c update 2025-11-21 22:37:14 +03:00
449aef6f54 добавил импорт 2025-11-21 18:43:04 +03:00
1d4521b803 обновление логики 2025-11-21 16:53:13 +03:00
fa860921da update 2025-11-21 16:19:47 +03:00
Primakov Alexandr Alexandrovich
2480f7c376 Update smoke-tracker API documentation to reflect changes in JWT token expiration; modify auth.js to implement a permanent token without expiration. 2025-11-17 20:13:20 +03:00
Primakov Alexandr Alexandrovich
414383163e Enhance smoke-tracker API to include statistics for active users only; update documentation to reflect changes in user activity criteria and statistics calculations. 2025-11-17 14:40:37 +03:00
Primakov Alexandr Alexandrovich
f856d94596 Add summary statistics endpoint to smoke-tracker API; update documentation to include new route 2025-11-17 14:14:15 +03:00
Primakov Alexandr Alexandrovich
dd75c54b32 Refactor userId handling in cigarettes and stats routes to use mongoose ObjectId for consistency; add debug logging for stats aggregation. 2025-11-17 14:04:46 +03:00
Primakov Alexandr Alexandrovich
f6f9163c3f Update bcryptjs to version 3.0.3 and add smoke-tracker router to the server configuration. 2025-11-17 13:25:20 +03:00
Primakov Alexandr Alexandrovich
4c166a8d33 rules 2025-11-16 23:55:33 +03:00
284be82e1e Refactor file handling in BuyProduct and Request models; implement file schema for better structure. Update routes to handle file uploads and downloads with improved error handling and logging. Adjust MongoDB connection management across scripts and routes for consistency. 2025-11-05 19:06:11 +03:00
41b5cb6fae update 2025-11-04 22:39:29 +03:00
c4664edd7e fix mongo 2025-11-04 19:46:39 +03:00
69eddf47db fix auth 2025-11-04 19:32:58 +03:00
71f3f353ab update project 2025-11-04 18:20:19 +03:00
0d1dcf21c1 замечания 3 2025-11-02 12:40:42 +03:00
35493a09b5 фикс 2025-10-27 20:04:02 +03:00
390d97e6d5 исправил ошибки рантайма 2025-10-27 19:52:35 +03:00
eca5cba858 миграция 2025-10-27 19:37:21 +03:00
6c190b80fb add new back 2025-10-27 18:58:38 +03:00
a6065dd95c new procurement 2025-10-23 09:49:04 +03:00
99127c42e2 исправление поиска 2025-10-18 12:08:25 +03:00
599ccd1582 обновил бэк закупок 2025-10-18 11:30:18 +03:00
2b5e5564c8 прямые импорты 2025-10-14 13:44:18 +03:00
7937be469b фикс 2025-10-14 12:24:31 +03:00
9f72d5885e фикс 2025-10-14 11:58:10 +03:00
f65fd175ca add /procurement 2025-10-14 11:08:29 +03:00
Primakov Alexandr Alexandrovich
d049c29f93 чистка 2025-09-23 14:23:52 +03:00
xingzhe.ru
351ea75072 update server/routers/back-new/features/image/image.controller.js 2025-07-04 00:21:13 +00:00
xingzhe.ru
34163788f3 update server/routers/back-new/server.js 2025-07-03 13:29:35 +00:00
xingzhe.ru
4ef4dd3c1b upload files 2025-07-03 13:28:29 +00:00
xingzhe.ru
80498a0ff0 update server/routers/back-new/features/auth/auth.controller.js 2025-07-03 13:15:52 +00:00
xingzhe.ru
00386cc135 update server/routers/back-new/shared/usersDb.js 2025-07-03 11:13:50 +00:00
xingzhe.ru
f5faae7907 update server/routers/back-new/features/auth/auth.controller.js 2025-07-03 11:13:17 +00:00
xingzhe.ru
659f9fd684 update server/routers/back-new/features/auth/auth.routes.js 2025-07-03 11:12:28 +00:00
xingzhe.ru
256de78e64 update server/routers/back-new/features/image/image.controller.js 2025-07-02 18:24:58 +00:00
xingzhe.ru
1500486cd8 update server/routers/back-new/features/image/image.controller.js 2025-07-02 18:03:43 +00:00
xingzhe.ru
63a825f153 update server/routers/back-new/server.js 2025-07-02 10:28:05 +00:00
xingzhe.ru
1383e360a1 update server/routers/back-new/server.js 2025-07-02 10:02:36 +00:00
xingzhe.ru
ca01d1c538 update server/routers/back-new/server.js 2025-07-02 09:53:14 +00:00
xingzhe.ru
a315c8d4ef update server/routers/back-new/.env 2025-07-02 09:46:19 +00:00
xingzhe.ru
5ac9559b8f update server/routers/back-new/features/image/image.controller.js 2025-07-02 09:45:02 +00:00
xingzhe.ru
7b9e7d0a99 update server/routers/back-new/features/image/image.controller.js 2025-07-02 09:27:12 +00:00
xingzhe.ru
63b25928ff update server/routers/back-new/features/image/image.controller.js 2025-07-02 09:17:24 +00:00
xingzhe.ru
7d3b563759 update server/routers/back-new/server.js 2025-07-01 16:06:02 +00:00
xingzhe.ru
baba20c028 update server/routers/back-new/server.js 2025-07-01 16:03:09 +00:00
xingzhe.ru
87a9b8b02d update server/routers/back-new/server.js 2025-07-01 15:59:39 +00:00
xingzhe.ru
cc41fa73cd update server/routers/back-new/.env 2025-07-01 15:30:24 +00:00
xingzhe.ru
ba923b9f91 update server/routers/back-new/.env 2025-07-01 15:15:32 +00:00
xingzhe.ru
cede47157e update server/routers/back-new/features/image/image.controller.js 2025-07-01 14:53:42 +00:00
xingzhe.ru
279c4fc86d update server/routers/back-new/.env 2025-07-01 14:50:12 +00:00
xingzhe.ru
c2f8d6ecee update server/routers/back-new/server.js 2025-07-01 14:42:09 +00:00
xingzhe.ru
b4858efa73 update server/routers/back-new/features/image/image.controller.js 2025-07-01 12:47:42 +00:00
xingzhe.ru
6154932d9e update server/routers/back-new/.env 2025-07-01 12:46:40 +00:00
xingzhe.ru
d4cc85f644 create server/routers/kfu-m-24-1/back-new/.env 2025-07-01 12:17:29 +00:00
xingzhe.ru
82e8b785c4 update server/routers/back-new/.env 2025-07-01 11:10:56 +00:00
xingzhe.ru
5785e50cc5 create server/routers/back-new/.env 2025-07-01 10:32:36 +00:00
xingzhe.ru
de101348fc update server/routers/back-new/server.js 2025-07-01 10:04:03 +00:00
xingzhe.ru
f442544912 create server/routers/back-new/server.js 2025-07-01 09:59:29 +00:00
xingzhe.ru
d09dbcb697 update server/index.ts 2025-06-30 21:42:11 +00:00
xingzhe.ru
f25bae1a08 update server/routers/back-new/app.js 2025-06-30 21:37:18 +00:00
xingzhe.ru
800b60fb6d delete server/routers/back-new/server.js 2025-06-30 21:36:40 +00:00
xingzhe.ru
36558dfb85 upload files 2025-06-30 16:23:34 +00:00
xingzhe.ru
c11bcd5d26 upload files 2025-06-30 16:23:09 +00:00
xingzhe.ru
8450cc2d4d upload files 2025-06-29 22:31:53 +00:00
xingzhe.ru
b1a9ee1403 upload files 2025-06-29 22:31:00 +00:00
xingzhe.ru
80b9d9c8c8 delete server/routers/project-monday 2025-06-29 11:07:36 +00:00
xingzhe.ru
db6665736a create server/routers/project-monday 2025-06-29 10:50:45 +00:00
DmitrievMS
81980fa011 Запрос на слияние 'sber_mobile' (#45) из sber_mobile в main 2025-06-16 11:13:15 +00:00
Дмитриев Максим Сергеевич
ac5f3eee96 fix moderate.js 2025-06-16 14:12:52 +03:00
DmitrievMS
9d87f7479c Запрос на слияние 'sber_mobile' (#44) из sber_mobile в main 2025-06-16 11:05:06 +00:00
Дмитриев Максим Сергеевич
3639524fc7 remove console log 2025-06-16 14:04:48 +03:00
Дмитриев Максим Сергеевич
f66114b22f add initiatives folder 2025-06-16 14:03:13 +03:00
Дмитриев Максим Сергеевич
8090de8031 remove initiatives folder 2025-06-16 14:02:19 +03:00
DmitrievMS
081d663711 Запрос на слияние 'sber_mobile' (#43) из sber_mobile в main 2025-06-16 09:37:06 +00:00
Дмитриев Максим Сергеевич
4fe16e5aa8 remove console log 2025-06-16 12:36:41 +03:00
DmitrievMS
1fd5495570 Запрос на слияние 'sber_mobile' (#42) из sber_mobile в main 2025-06-15 20:07:46 +00:00
Дмитриев Максим Сергеевич
9d68ee735a remove console logs 2025-06-15 23:07:17 +03:00
DmitrievMS
076e51c53a Запрос на слияние 'sber_mobile' (#41) из sber_mobile в main 2025-06-15 19:54:47 +00:00
Дмитриев Максим Сергеевич
409a315a25 refactoring 2025-06-15 22:51:10 +03:00
Дмитриев Максим Сергеевич
7a3264d43d Merge branch 'main' into sber_mobile 2025-06-15 22:48:38 +03:00
Daniya15
effa320fa8 Запрос на слияние 'feature/sber_mobile/ai_initiatives' (#32) из feature/sber_mobile/ai_initiatives в sber_mobile 2025-06-15 19:23:10 +00:00
Дания
cc2a66367d votes 2025-06-15 20:39:27 +03:00
DmitrievMS
989b5b010e Запрос на слияние 'sber_mobile' (#38) из sber_mobile в main 2025-06-15 16:22:31 +00:00
DmitrievMS
f0e7ba94d2 Запрос на слияние 'feature/sber_mobile/avatars' (#37) из feature/sber_mobile/avatars в sber_mobile 2025-06-15 16:22:06 +00:00
Max
3739fc8449 add avatars 2025-06-15 18:28:53 +03:00
DmitrievMS
a74d191b30 Запрос на слияние 'sber_mobile' (#36) из sber_mobile в main 2025-06-15 14:49:15 +00:00
DmitrievMS
a391cc88c9 Запрос на слияние 'feature/sber_mobile/additional_services' (#35) из feature/sber_mobile/additional_services в sber_mobile 2025-06-15 14:48:58 +00:00
Max
12f8e63390 fix api method 2025-06-15 17:36:41 +03:00
Дания
37238a1385 change moderate and initiatives 2025-06-15 16:13:57 +03:00
DenAntonov
48cd044131 Запрос на слияние 'feature/sber_mobile/ai_chats' (#34) из feature/sber_mobile/ai_chats в main 2025-06-14 20:40:50 +00:00
DenAntonov
5665c4bf1e code refactoring and agent improvement 2025-06-14 23:35:48 +03:00
Дания
ad35d47ff5 Merge updates from main 2025-06-14 22:59:46 +03:00
DmitrievMS
f13cdd82df Запрос на слияние 'sber_mobile' (#31) из sber_mobile в main 2025-06-14 16:34:05 +00:00
DmitrievMS
d6ebe10421 Запрос на слияние 'feature/sber_mobile/support' (#30) из feature/sber_mobile/support в sber_mobile 2025-06-14 16:30:23 +00:00
Max
6e59e801b0 add tickets data 2025-06-14 19:29:48 +03:00
DmitrievMS
5dafd60299 Запрос на слияние 'sber_mobile' (#29) из sber_mobile в main 2025-06-14 15:29:52 +00:00
Дмитриев Максим Сергеевич
825d7f1dd2 remove test api 2025-06-14 18:29:20 +03:00
DmitrievMS
a3ea53c2f0 Запрос на слияние 'feature/sber_mobile/support' (#28) из feature/sber_mobile/support в sber_mobile 2025-06-14 15:27:08 +00:00
Дмитриев Максим Сергеевич
f37f34d803 fix getting giga token 2025-06-14 18:26:13 +03:00
DenAntonov
bd0b11dc4a add chat moderation 2025-06-14 16:12:03 +03:00
DmitrievMS
b36106cc8c Запрос на слияние 'sber_mobile' (#27) из sber_mobile в main 2025-06-14 11:46:18 +00:00
Дмитриев Максим Сергеевич
07d35c4516 add test endpoint 2025-06-14 14:45:31 +03:00
DenAntonov
471cbacb66 Запрос на слияние 'feature/sber_mobile/chat' (#23) из feature/sber_mobile/chat в main 2025-06-14 10:39:26 +00:00
DmitrievMS
229b181972 Запрос на слияние 'sber_mobile' (#25) из sber_mobile в main 2025-06-14 10:38:54 +00:00
DmitrievMS
72615c7b98 Запрос на слияние 'feature/sber_mobile/support' (#24) из feature/sber_mobile/support в sber_mobile 2025-06-14 10:38:19 +00:00
Дмитриев Максим Сергеевич
45cafbee91 add requirements 2025-06-14 13:37:26 +03:00
DenAntonov
580651094f remove websocket add polling 2025-06-14 13:36:06 +03:00
DmitrievMS
0ee92e98b2 Запрос на слияние 'sber_mobile' (#22) из sber_mobile в main 2025-06-14 10:10:37 +00:00
DmitrievMS
3d8d9ee171 Запрос на слияние 'feature/sber_mobile/support' (#21) из feature/sber_mobile/support в sber_mobile 2025-06-14 10:09:40 +00:00
DenAntonov
bde67dc7c3 fix socket server 2025-06-14 10:30:12 +03:00
Дания
a7be793608 change file type and fix agents 2025-06-14 02:01:19 +03:00
Max
ca81e19d14 add tickets creation 2025-06-14 00:16:02 +03:00
DenAntonov
7bd82fedce change socket settings 2025-06-13 23:52:02 +03:00
Max
1aeb62d490 add rag tool 2025-06-13 23:15:13 +03:00
Max
5886270e29 add history tool 2025-06-13 22:31:32 +03:00
DenAntonov
8f544d5c99 Запрос на слияние 'feature/sber_mobile/chat' (#20) из feature/sber_mobile/chat в main 2025-06-13 19:01:13 +00:00
Max
8dd8ec8930 add getting support chat history 2025-06-13 21:07:13 +03:00
Max
3af82f7478 fix system prompt 2025-06-13 19:44:45 +03:00
Дания
39a62818e9 fix error 2025-06-12 21:07:06 +03:00
DenAntonov
24ff712306 add sockets and change subscription 2025-06-12 21:04:12 +03:00
Max
ec6b30e220 add support ai-agent 2025-06-12 20:58:54 +03:00
Дания
548dbfcc9d fix error 2025-06-12 20:48:56 +03:00
Дания
09174abaa4 add ai_initiatives 2025-06-12 19:39:57 +03:00
Max
7ecb73ac6e add constants 2025-06-12 16:50:44 +03:00
DenAntonov
8ade320440 Запрос на слияние 'sber_mobile' (#19) из sber_mobile в main 2025-06-12 13:28:15 +00:00
DenAntonov
bffa3fa2a3 Merge branch 'sber_mobile' of ssh://gitverse.ru:2222/brojs-students/multy-stub into sber_mobile 2025-06-12 16:25:39 +03:00
DenAntonov
4cf29c97b9 add chats api 2025-06-12 16:21:46 +03:00
DmitrievMS
9377771531 Запрос на слияние 'sber_mobile' (#18) из sber_mobile в main 2025-06-11 16:06:05 +00:00
DmitrievMS
0a96a87f94 Запрос на слияние 'feature/sber_mobile/support' (#17) из feature/sber_mobile/support в sber_mobile 2025-06-11 16:04:37 +00:00
Дмитриев Максим Сергеевич
5c14212429 fix router 2025-06-11 19:03:58 +03:00
DmitrievMS
e49d38657d Запрос на слияние 'sber_mobile' (#16) из sber_mobile в main 2025-06-11 15:52:24 +00:00
DmitrievMS
1c7d1fc1ae Запрос на слияние 'feature/sber_mobile/support' (#15) из feature/sber_mobile/support в sber_mobile 2025-06-11 15:51:55 +00:00
Дмитриев Максим Сергеевич
7503d076e8 fix supabase insert 2025-06-11 18:51:32 +03:00
DmitrievMS
04f70aaa45 Запрос на слияние 'sber_mobile' (#14) из sber_mobile в main 2025-06-11 15:41:50 +00:00
DmitrievMS
7b2b7b477f Запрос на слияние 'feature/sber_mobile/support' (#13) из feature/sber_mobile/support в sber_mobile 2025-06-11 15:39:20 +00:00
Max
da7e25d339 add support table and api 2025-06-10 23:25:51 +03:00
DenAntonov
b9f6e4d7aa fix outgoing json 2025-06-10 21:45:57 +03:00
DmitrievMS
396633932b Запрос на слияние 'feature/change_services' (#12) из feature/change_services в sber_mobile 2025-06-08 19:26:25 +00:00
Max
46ad6ea9f3 change services in db 2025-06-08 22:24:19 +03:00
DenAntonov
1fa09ecac3 Запрос на слияние 'feature/sber_mobile/db_api' (#11) из feature/sber_mobile/db_api в sber_mobile 2025-06-08 18:37:13 +00:00
DenAntonov
18b33ae10a add initiatives 2025-06-08 21:36:08 +03:00
Max
e4e00184a5 change services api 2025-06-08 19:46:23 +03:00
DmitrievMS
9177765e8c Запрос на слияние 'sber_mobile' (#10) из sber_mobile в main 2025-06-07 13:14:12 +00:00
Max
0c0c62fe1b add avatar getting 2025-06-07 16:12:29 +03:00
Max
a0c9c5bab1 add db scheme 2025-06-07 15:26:22 +03:00
Max
01b6e4ae72 delete sql 2025-06-07 15:20:23 +03:00
DmitrievMS
2e36ee6e8b Запрос на слияние 'sber_mobile' (#9) из sber_mobile в main 2025-06-07 12:17:46 +00:00
Max
18cfa427d2 add sql 2025-06-07 15:13:36 +03:00
DenAntonov
904a227adb delete server/routers/kfu-m-24-1/sber_mobile/DB_Scheme.txt 2025-06-07 11:11:45 +00:00
DenAntonov
23e532b770 Запрос на слияние 'sber_mobile' (#8) из sber_mobile в main 2025-06-07 09:02:19 +00:00
DenAntonov
f658e1f828 Запрос на слияние 'feature/sber_mobile/db_api' (#7) из feature/sber_mobile/db_api в sber_mobile 2025-06-07 09:00:42 +00:00
DenAntonov
0500497fc1 fix api and add apartment info 2025-06-07 00:48:51 +03:00
Max
ea691536ac add db api 2025-06-04 18:49:25 +03:00
Дмитриев Максим Сергеевич
c251a640b6 add profile 2025-06-03 12:24:19 +03:00
Max
8031938b2f change request 2025-05-31 19:37:17 +03:00
Max
ca4bfdade4 change users 2025-05-31 19:27:45 +03:00
Max
b5f6f6d30f fix router 2025-05-31 19:19:25 +03:00
Max
36107afbc2 add router 2025-05-31 19:17:10 +03:00
Max
539b1d2277 add getting profile proto 2025-05-31 19:13:39 +03:00
Max
a9490da5a6 refactor code 2025-05-31 11:44:46 +03:00
DmitrievMS
845e57d688 Запрос на слияние 'sber_mobile' (#6) из sber_mobile в main 2025-05-24 18:13:30 +00:00
Max
6835c84cc4 fix export js object 2025-05-24 21:06:51 +03:00
Max
337e3ee2bf fix exports 2025-05-24 20:50:27 +03:00
Max
72d298ef2f fix router 2025-05-24 20:43:57 +03:00
DmitrievMS
d410164941 Запрос на слияние 'sber_mobile' (#5) из sber_mobile в main 2025-05-24 13:43:35 +00:00
Max
6b5ae7bce1 refactor code 2025-05-24 16:43:09 +03:00
DmitrievMS
d80c4efb49 Запрос на слияние 'sber_mobile' (#4) из sber_mobile в main 2025-05-24 13:27:00 +00:00
Max
ddcf27b022 add supabase refresh 2025-05-24 16:24:30 +03:00
DmitrievMS
26c53e7455 Запрос на слияние 'sber_mobile' (#3) из sber_mobile в main 2025-05-20 10:59:05 +00:00
Дмитриев Максим Сергеевич
0fbbe33e8a fix/fix supabase 2025-05-20 13:47:10 +03:00
primakov.a.a
687508d26f Запрос на слияние 'sber_mobile' (#2) из sber_mobile в main 2025-05-19 20:56:00 +00:00
Max
f89729dbeb feature/add supabase auth 2025-05-18 21:58:54 +03:00
Primakov Alexandr Alexandrovich
d90fee82d5 2.0.0 2025-05-08 18:36:25 +03:00
Primakov Alexandr Alexandrovich
bde6ab4c7a progress bars 2025-05-08 18:36:13 +03:00
Primakov Alexandr Alexandrovich
2d0b97be44 update statistics screen 2025-05-08 18:25:37 +03:00
Primakov Alexandr Alexandrovich
3c22354130 fix: обновление конфигурации docker-compose.yml и улучшение обработки URL в сервере
- Закомментированы секции mongoDb в docker-compose.yml для упрощения конфигурации.
- Добавлена функция getUrl для динамического формирования URL в server.ts, что улучшает обработку запросов в зависимости от окружения.
- Удалены лишние консольные логи из файлов mongo.ts и mongoose.ts для повышения читаемости кода.
2025-05-08 16:13:53 +03:00
Primakov Alexandr Alexandrovich
ab555cd70e fix: улучшение логирования mongoUrl в утилитах
- Обновлены консольные логи для переменной mongoUrl в файлах mongo.ts и mongoose.ts для более удобного отображения.
- Упрощена инициализация MongoClient, убрав лишние параметры.
2025-05-08 15:53:59 +03:00
Primakov Alexandr Alexandrovich
95bcaf3c5e 2 2025-05-08 15:48:02 +03:00
Primakov Alexandr Alexandrovich
48167530fd feat: добавление логирования mongoUrl в утилиты
- Добавлены консольные логи для переменной mongoUrl в файле const.ts для упрощения отладки подключения к MongoDB.
2025-05-08 15:43:11 +03:00
Primakov Alexandr Alexandrovich
f909d90b6f fix: обновление конфигурации docker-compose.yml для mongoDb
- Изменена переменная окружения MONGO_ADDR для использования значения из окружения вместо жестко закодированного адреса.
2025-05-08 15:36:52 +03:00
Primakov Alexandr Alexandrovich
e7d114a9d9 fix: исправление отступов в конфигурации docker-compose.yml для mongoDb
- Исправлены отступы в секции depends_on для корректного форматирования файла.
2025-05-08 15:23:27 +03:00
Primakov Alexandr Alexandrovich
b83e0d603c - Добавлены зависимости для корректного запуска mongoDb перед multy-stubs. 2025-05-08 15:22:49 +03:00
Primakov Alexandr Alexandrovich
7f57b2a4d3 fix: удаление скрипта postinstall из package.json
- Удален скрипт postinstall, который создавал файлы .env и .env.example.
- Обновлен файл package.json для упрощения конфигурации проекта.
2025-05-08 15:18:48 +03:00
Primakov Alexandr Alexandrovich
c8f7e47181 feat: добавление конфигурации Docker Compose для MongoDB и multy-stubs
- Создан файл docker-compose.yml для настройки сервисов MongoDB и multy-stubs.
- Определены необходимые переменные окружения и порты для взаимодействия сервисов.
2025-05-08 15:15:07 +03:00
Primakov Alexandr Alexandrovich
e5d6b7cecd feat: добавление скрипта postinstall и обновление package-lock.json
- Добавлен скрипт postinstall для автоматического создания файлов .env и .env.example.
- Обновлен package-lock.json для отражения изменений в зависимостях.
2025-05-08 14:30:39 +03:00
Primakov Alexandr Alexandrovich
8a1868482c feat: обновление конфигурации проекта с использованием TypeScript и улучшение обработки ошибок
- Переписаны основные файлы сервера с JavaScript на TypeScript.
- Добавлен новый обработчик ошибок с логированием в базу данных.
- Обновлен Dockerfile для поддержки сборки TypeScript.
- Изменены настройки окружения для MongoDB в docker-compose.
- Удалены устаревшие файлы и добавлены новые модели и утилиты для работы с MongoDB.
- Обновлены зависимости в package.json и package-lock.json.
2025-05-08 14:18:03 +03:00
RustamRu
1bf68cea08 Merge branch 'master' of ssh://85.143.175.152:222/bro-students/multy-stub
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-03-20 12:30:28 +03:00
RustamRu
110e8300a1 feat: dry-wash, use car color in ai prompt 2025-03-20 12:30:22 +03:00
f3566361fb Merge pull request 'feat: rewrite the request to receive orders from the masters' (#110) from feat/filter-master into master
Reviewed-on: #110
2025-03-17 23:44:19 +03:00
a63a229b64 feat: rewrite the request to receive orders from the masters
Some checks failed
platform/multy-stub/pipeline/pr-master There was a failure building this commit
platform/multy-stub/pipeline/head There was a failure building this commit
2025-03-17 23:41:07 +03:00
8944508308 Merge pull request 'feat: make the time of the master to be taken from the body' (#109) from feat/filter-master into master
Reviewed-on: #109
2025-03-17 21:07:21 +03:00
775f24cffa feat: make the time of the master to be taken from the body 2025-03-17 21:05:48 +03:00
RustamRu
78b72b0edc feat: add GigaChat model retrieval for enhanced image analysis 2025-03-14 08:34:16 +03:00
RustamRu
333fe79c8b fix: change carColor type to Mixed for improved flexibility 2025-03-12 17:56:46 +03:00
RustamRu
9d10c8501a Merge branch 'master' of ssh://85.143.175.152:222/bro-students/multy-stub 2025-03-12 17:41:54 +03:00
RustamRu
d64ece382a fix: update car color validation to handle both string and number types 2025-03-12 17:41:48 +03:00
Primakov Alexandr Alexandrovich
f91f821f86 fix navigation elements 2025-03-12 09:12:09 +03:00
Primakov Alexandr Alexandrovich
b5301f948a Еще правки с урлами и верстка 2025-03-12 00:37:32 +03:00
Primakov Alexandr Alexandrovich
dd589790c2 fix путей 2025-03-12 00:29:55 +03:00
Primakov Alexandr Alexandrovich
1fcc5ed70d init Questionnaire 2025-03-11 23:50:50 +03:00
RustamRu
41dbe81001 Merge branch 'master' of ssh://85.143.175.152:222/bro-students/multy-stub 2025-03-09 11:04:07 +03:00
RustamRu
7b685ad99e feat: add dynamic system prompt for car image analysis 2025-03-09 11:04:01 +03:00
2f1e1dc040 Merge pull request 'feature/master-date' (#108) from feature/master-date into master
Reviewed-on: #108
2025-03-09 09:43:26 +03:00
RustamRu
70e8a6877c change car img size limit 2025-03-04 19:04:38 +03:00
RustamRu
87fd3121f9 fix: get img value data 2025-03-03 20:41:55 +03:00
RustamRu
4f9434163e Merge branch 'master' of ssh://85.143.175.152:222/bro-students/multy-stub 2025-03-03 20:14:06 +03:00
RustamRu
350d452a7b fix file convert to base64 2025-03-03 20:13:51 +03:00
9a0669df13 feat: add find by id 2025-03-03 20:08:28 +03:00
RustamRu
c0883fc2bc add get token, fix prompt
Some checks failed
platform/multy-stub/pipeline/pr-master There was a failure building this commit
platform/multy-stub/pipeline/head There was a failure building this commit
2025-03-03 19:49:11 +03:00
566bce4663 feat: delete image
Some checks failed
platform/multy-stub/pipeline/pr-master There was a failure building this commit
platform/multy-stub/pipeline/head There was a failure building this commit
2025-03-03 19:46:10 +03:00
c828718498 feat: add today filter 2025-03-03 19:36:56 +03:00
RustamRu
69c280b266 evaluate with ai car dirtiness by image 2025-03-03 18:21:32 +03:00
6794b01ac8 feat: add fetch image
Some checks failed
ms-devops/pipeline/head There was a failure building this commit
2025-02-23 12:32:53 +03:00
1cb586f55a Merge pull request 'feat: upload car image' (#106) from feature/dry/wash-car-image-upload into master
Some checks failed
ms-devops/pipeline/head There was a failure building this commit
Reviewed-on: #106
2025-02-23 12:22:27 +03:00
RustamRu
df21879c0d feat: upload car image
Some checks failed
ms-devops/pipeline/head There was a failure building this commit
ms-devops/pipeline/pr-master There was a failure building this commit
2025-02-22 16:58:58 +03:00
Primakov Alexandr Alexandrovich
30c9c86c93 Merge branch 'master' of ssh://85.143.175.152:222/bro-students/multy-stub 2025-02-10 22:19:46 +03:00
Primakov Alexandr Alexandrovich
2925d0f17b fix eslint 2025-02-10 22:19:01 +03:00
Primakov Alexandr Alexandrovich
752dabd015 fix eslint 2025-02-10 22:13:55 +03:00
Primakov Alexandr Alexandrovich
815f11d5bc add nav router 2025-02-10 22:07:54 +03:00
02eb0e60b7 Merge pull request 'Fix paths' (#105) from dsf-fix-1 into master
Reviewed-on: #105
2025-02-08 14:55:33 +03:00
a64ac93935 Fix paths
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 14:54:57 +03:00
66a48d1c7e Merge pull request 'Add queries to dogsitter-viewing' (#104) from dsf-fix-1 into master
Reviewed-on: #104
2025-02-08 14:34:34 +03:00
26c66f16b4 Add queries to dogsitter-viewing 2025-02-08 14:33:29 +03:00
02e50bb2f9 Merge pull request 'Update users.json' (#103) from dsf-fix-1 into master
Reviewed-on: #103
2025-02-08 14:26:13 +03:00
fadc62c8f0 Update users.json 2025-02-08 14:25:29 +03:00
4759f6f7ee Merge pull request 'Uncomment dogsitters app' (#102) from dsf-fix-1 into master
Reviewed-on: #102
2025-02-08 13:45:14 +03:00
14f2164a82 Uncomment dogsitters app 2025-02-08 13:44:34 +03:00
14ef1f9bad Merge pull request 'Fix export' (#101) from dsf-fix-1 into master
Reviewed-on: #101
2025-02-08 13:28:29 +03:00
dc99318ff0 Fix export 2025-02-08 13:27:38 +03:00
d2fc5f4d5c Merge pull request 'Fix dogsitters backend' (#100) from dsf-fix-1 into master
Reviewed-on: #100
2025-02-08 13:17:00 +03:00
938bd48fff Fix dogsitters backend 2025-02-08 13:15:02 +03:00
96f819dc91 Merge pull request 'fix' (#99) from sberhubproject into master
Reviewed-on: #99
2025-02-08 13:12:46 +03:00
25eee8adf5 fix
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 13:10:58 +03:00
d2b2a29d3d Merge pull request 'Remove backend files' (#98) from dsf-fix-1 into master
Reviewed-on: #98
2025-02-08 13:10:27 +03:00
1cf71261d1 Remove backend files 2025-02-08 13:08:55 +03:00
88552eb04f Merge pull request 'Uncomment dogsitters app' (#97) from dsf-fix into master
Reviewed-on: #97
2025-02-08 12:49:30 +03:00
ab92c99321 Uncomment dogsitters app
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 12:47:43 +03:00
02963de893 Merge pull request 'Изменение путей запросов' (#96) from dogsitters-finder-3 into master
Reviewed-on: #96
2025-02-08 12:41:38 +03:00
48550416d9 Изменение путей запросов
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 12:40:39 +03:00
aa
878c5ffd68 Merge pull request 'изменение админ панели' (#95) from gamehub into master
Reviewed-on: #95
2025-02-08 11:45:19 +03:00
aaeii
6e37fe93f7 изменение админ панели
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 11:44:00 +03:00
Primakov Alexandr Alexandrovich
72a2667549 1.2.1 2025-02-08 10:39:49 +03:00
Primakov Alexandr Alexandrovich
39db7b4d26 fix 2025-02-08 10:39:45 +03:00
aa
ff25c0ecb9 Merge pull request 'fix path' (#93) from gamehub into master
Reviewed-on: #93
2025-02-08 10:34:05 +03:00
aaeii
f1a93bffb5 fix path 2025-02-08 10:33:24 +03:00
aa
aa231d4f43 Merge pull request 'upd json' (#92) from gamehub into master
Reviewed-on: #92
2025-02-08 09:59:04 +03:00
aaeii
f254d57db4 upd json 2025-02-08 09:57:34 +03:00
106f835934 Merge pull request 'dogsitters-finder' (#91) from dogsitters-finder into master
Reviewed-on: #91
2025-02-08 04:50:31 +03:00
f9b30a4cfd Merge branch 'master' into dogsitters-finder
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 04:45:56 +03:00
5e4a99529d Add backend and db settings 2025-02-08 04:44:17 +03:00
4d585002d7 Add backend and DB settings 2025-02-08 04:38:22 +03:00
b073fe3fdf Merge pull request 'Изменены запросы и добавлены новые' (#89) from dogsitters-finder-2 into master
Reviewed-on: #89
2025-02-08 02:39:15 +03:00
312cc229d8 Изменены запросы и добавлены новые
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-08 02:23:22 +03:00
11b1d670d0 Merge pull request 'small fixes in kfu-m-24-1/eng-it-lean' (#88) from kfu-m-24-1/eng-it-lean into master
Reviewed-on: #88
2025-02-07 12:26:56 +03:00
Ruslan Zagitov
522ea36bb9 fix: delete broken dicitonary
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-07 00:32:53 +03:00
Ruslan Zagitov
8be391c8e1 fix: /units 2025-02-07 00:30:46 +03:00
Ruslan Zagitov
ea80304c21 fix: /users 2025-02-07 00:08:03 +03:00
aa
771f75ef08 Merge pull request 'add new game, add link' (#87) from gamehub into master
Reviewed-on: #87
2025-02-05 22:26:15 +03:00
aaeii
edf9b2c82b add new game, add link 2025-02-05 22:24:05 +03:00
Ruslan Zagitov
8c3bf8a8ed fix 2025-02-05 19:34:45 +03:00
a88d3657bf Merge pull request 'kfu-m-24-1/eng-it-lean quick fix' (#86) from kfu-m-24-1/eng-it-lean into master
Reviewed-on: #86
2025-02-05 19:33:51 +03:00
Ruslan Zagitov
1656ce8690 Merge branch 'master' into kfu-m-24-1/eng-it-lean 2025-02-05 19:25:39 +03:00
Ruslan Zagitov
7cdbec53ee fix 2025-02-05 18:55:22 +03:00
eee00f0797 Merge pull request 'feat: kfu-m-24-1/eng-it-lean add /users endpoint; small features in /units' (#85) from kfu-m-24-1/eng-it-lean into master
Reviewed-on: #85
2025-02-05 13:54:09 +03:00
Ruslan Zagitov
33845b743d feat: add /users endpoint; update /units 2025-02-05 13:53:11 +03:00
Ruslan Zagitov
059139e213 feat: add /users endpoint; update /units 2025-02-05 13:45:49 +03:00
Ruslan Zagitov
005e7a0ac9 chore: add dictionaries 2025-02-05 13:25:11 +03:00
9ee59256a9 feat: generate order number (#69)
Добавил генерацию номера заказа при создании

Co-authored-by: RustamRu <kagapov.rustam@gmail.com>
Reviewed-on: #69
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
2025-02-02 15:11:59 +03:00
aa
c2784dcf45 Merge pull request 'gamehub' (#76) from gamehub into master
Reviewed-on: #76
2025-02-01 16:20:44 +03:00
64ed9b8eda Merge pull request 'kazan-explore culture quiz' (#84) from kazan-explore into master
Reviewed-on: #84
2025-02-01 15:18:40 +03:00
e9814f36bf kazan-explore culture quiz
All checks were successful
platform/multy-stub/pipeline/head This commit looks good
2025-02-01 15:17:54 +03:00
0bd883df59 Merge pull request 'delete verification user' (#83) from sberhubproject into master
Reviewed-on: #83
2025-02-01 14:17:43 +03:00
1657b0c5e9 Merge pull request 'feat (connectme): add request for city' (#82) from connectme-request into master
Reviewed-on: #82
2025-02-01 14:15:36 +03:00
a9673b260f delete verification user 2025-02-01 14:15:33 +03:00
khisametdinov
8ee5ca5528 feat (connectme): add request for city
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-02-01 11:53:10 +03:00
e1e335098e Merge pull request '[feat] add interaction telegram bot' (#81) from sberhubproject into master
Reviewed-on: #81
2025-02-01 11:20:30 +03:00
619975d1e0 [feat] add interaction telegram bot 2025-02-01 11:19:41 +03:00
f29bc83d56 Merge pull request '[feat][refactor] add Gigachat and edit data users' (#80) from sberhubproject into master
Reviewed-on: #80
2025-02-01 09:39:46 +03:00
61347d1aee [feat][refactor] add Gigachat and edit data users 2025-02-01 09:38:54 +03:00
ac7a99ef15 Merge pull request 'kazan-explore services data fixed' (#79) from kazan-explore into master
Reviewed-on: #79
2025-02-01 03:43:02 +03:00
eb3f1f7e3f kazan-explore services data fixed 2025-02-01 03:42:16 +03:00
3e02ea5843 Merge pull request 'kazan-explore services data added' (#78) from kazan-explore into master
Reviewed-on: #78
2025-02-01 03:27:43 +03:00
d06aeeb246 kazan-explore services data added 2025-02-01 03:19:45 +03:00
e331f69885 Merge pull request '[refactor] edit data interests' (#77) from sberhubproject into master
Reviewed-on: #77
2025-02-01 01:10:39 +03:00
23f1277edb [refactor] edit data interests 2025-02-01 01:09:22 +03:00
aaeii
801f9ac1e3 new games-in-cart 2025-01-31 20:18:27 +03:00
16bffcafa6 Merge pull request 'feat: add vercel/ai package; gigachat api; refactor dictionaries; add units put request' (#72) from kfu-m-24-1/eng-it-lean into master
Reviewed-on: #72
2025-01-31 18:37:55 +03:00
922909397d Merge pull request 'kazan-explore changes' (#75) from kazan-explore into master
Reviewed-on: #75
2025-01-31 18:03:08 +03:00
233192ba01 kazan-explore changes 2025-01-31 18:00:56 +03:00
Ruslan Zagitov
f72d5220de fix: delete broken dictionary 2025-01-31 16:02:07 +03:00
Ruslan Zagitov
30af0fb1dd Merge branch 'master' into kfu-m-24-1/eng-it-lean 2025-01-31 15:54:22 +03:00
854e249100 Merge pull request 'edit data events' (#74) from sberhubproject into master
Reviewed-on: #74
2025-01-31 15:05:41 +03:00
eddad1cc3f edit data events 2025-01-31 15:02:49 +03:00
c96e435344 Merge pull request 'delete function delay' (#73) from sberhubproject into master
Reviewed-on: #73
2025-01-31 14:44:35 +03:00
7358faef1d delete function delay 2025-01-31 14:42:36 +03:00
Ruslan Zagitov
68de877b06 refactor: /dictionaries endpoint 2025-01-31 13:59:40 +03:00
c000106ec8 Merge pull request '[feat] add chgk api' (#71) from sber_web into master
Reviewed-on: #71
2025-01-31 13:43:18 +03:00
Max
da84344a63 [feat] add chgk api 2025-01-31 13:35:11 +03:00
Ruslan Zagitov
daf5bf7970 chore: install vercel/ai package; remove gigachat/ai static file; requests from /gigachat endpoint are not require SSL 2025-01-31 13:26:34 +03:00
Ruslan Zagitov
4ef941d62f fix: unit router PUT request 2025-01-30 12:41:00 +03:00
Ruslan Zagitov
16fda2e7ed feat: add gigachat api 2025-01-30 12:24:47 +03:00
ff15a48414 Merge pull request '[feat][refactor] add events and edit users' (#70) from sberhubproject into master
Reviewed-on: #70
2025-01-29 21:25:59 +03:00
367c0de6fb [feat][refactor] add events and edit users 2025-01-29 15:50:53 +03:00
3c89d8b9a8 Merge pull request 'feat: add get master with orders' (#68) from feat/get-master into master
Reviewed-on: #68
2025-01-26 11:02:24 +03:00
bdc8d9a8e0 feat: add get master with orders
Some checks failed
platform/multy-stub/pipeline/head There was a failure building this commit
2025-01-26 00:41:48 +03:00
aaeii
cbbb376fd6 update json gamehub: fix format 2025-01-25 23:24:34 +03:00
aaeii
faaec7c718 update json gamehub: fix price 2025-01-25 23:16:03 +03:00
8814c2a64b Merge pull request 'kazan-explore multy stub changes' (#67) from kazan-explore into master
Reviewed-on: #67
2025-01-24 23:02:53 +03:00
298a82e0ae kazan-explore multy stub changes 2025-01-24 23:02:25 +03:00
a86eb0d4ef Merge pull request 'kazan-explore multy stub changes' (#66) from kazan-explore into master
Reviewed-on: #66
2025-01-24 22:42:50 +03:00
335179ad26 kazan-explore multy stub changes 2025-01-24 22:39:25 +03:00
a1d331b5b4 Merge pull request 'esc stubs fix2' (#65) from esc-stubs into master
Reviewed-on: #65
2025-01-24 16:54:00 +03:00
b36ee36e3a Merge pull request 'esc stubs fix?' (#64) from esc-stubs into master
Reviewed-on: #64
2025-01-24 16:44:45 +03:00
6e0934e585 Merge pull request 'esc stubs' (#63) from esc-stubs into master
Reviewed-on: #63
2025-01-24 16:31:13 +03:00
194 changed files with 38089 additions and 2188 deletions

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Application settings
TZ=Europe/Moscow
APP_PORT=8044
MONGO_INITDB_ROOT_USERNAME=qqq
MONGO_INITDB_ROOT_PASSWORD=qqq
# MongoDB connection string
MONGO_ADDR=mongodb://qqq:qqq@127.0.0.1:27018

138
.gitea/workflows/check.yaml Normal file
View File

@@ -0,0 +1,138 @@
name: Code Quality Checks
run-name: Проверка кода (lint & typecheck) от ${{ gitea.actor }}
on: [push]
jobs:
lint-and-typecheck:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run eslint
- name: Run TypeScript type check
run: npx tsc --noEmit
- name: Run tests
run: npm test
smoke-test:
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start MongoDB
run: |
docker run -d \
--name mongodb-smoke-test \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=password \
mongo:8.0.3
- name: Wait for MongoDB to be ready
run: |
timeout=30
elapsed=0
while ! docker exec mongodb-smoke-test mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
if [ $elapsed -ge $timeout ]; then
echo "MongoDB не запустился за $timeout секунд"
exit 1
fi
echo "Ожидание запуска MongoDB... ($elapsed/$timeout)"
sleep 2
elapsed=$((elapsed + 2))
done
echo "MongoDB готов"
- name: Start application
env:
MONGO_ADDR: mongodb://admin:password@localhost:27017/test_db?authSource=admin
PORT: 8044
NODE_ENV: test
run: |
npm start > app.log 2>&1 &
echo $! > app.pid
echo "Приложение запущено с PID: $(cat app.pid)"
- name: Wait for application to start
run: |
timeout=30
elapsed=0
while ! node -e "require('http').get('http://localhost:8044', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" 2>/dev/null; do
if [ $elapsed -ge $timeout ]; then
echo "Приложение не запустилось за $timeout секунд"
cat app.log
exit 1
fi
echo "Ожидание запуска приложения... ($elapsed/$timeout)"
sleep 2
elapsed=$((elapsed + 2))
done
echo "Приложение запущено"
- name: Check application stability (30 seconds)
run: |
duration=30
elapsed=0
while [ $elapsed -lt $duration ]; do
if ! kill -0 $(cat app.pid) 2>/dev/null; then
echo "❌ Приложение упало через $elapsed секунд"
cat app.log
exit 1
fi
if ! node -e "require('http').get('http://localhost:8044', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" 2>/dev/null; then
echo "❌ Приложение не отвечает через $elapsed секунд"
cat app.log
exit 1
fi
echo "✓ Приложение работает ($elapsed/$duration секунд)"
sleep 5
elapsed=$((elapsed + 5))
done
echo "✅ Приложение стабильно работает в течение $duration секунд"
- name: Stop application
if: always()
run: |
if [ -f app.pid ]; then
pid=$(cat app.pid)
if kill -0 $pid 2>/dev/null; then
echo "Остановка приложения (PID: $pid)"
kill -TERM $pid || true
sleep 3
if kill -0 $pid 2>/dev/null; then
echo "Принудительная остановка приложения"
kill -9 $pid 2>/dev/null || true
fi
fi
rm -f app.pid
fi
- name: Stop MongoDB
if: always()
run: |
docker stop mongodb-smoke-test || true
docker rm mongodb-smoke-test || true

View File

@@ -1,16 +1,38 @@
FROM node:20
FROM node:22 AS builder
WORKDIR /usr/src/app/
# Сначала копируем только файлы, необходимые для установки зависимостей
COPY ./package.json /usr/src/app/package.json
COPY ./package-lock.json /usr/src/app/package-lock.json
# Устанавливаем все зависимости
RUN npm ci
# Затем копируем исходный код проекта и файлы конфигурации
COPY ./tsconfig.json /usr/src/app/tsconfig.json
COPY ./server /usr/src/app/server
# Сборка проекта
RUN npm run build
# Вторая стадия - рабочий образ
FROM node:22
RUN mkdir -p /usr/src/app/server/log/
WORKDIR /usr/src/app/
COPY ./server /usr/src/app/server
# Копирование только package.json/package-lock.json для продакшн зависимостей
COPY ./package.json /usr/src/app/package.json
COPY ./package-lock.json /usr/src/app/package-lock.json
COPY ./.serverrc.js /usr/src/app/.serverrc.js
# COPY ./.env /usr/src/app/.env
# RUN npm i --omit=dev
RUN npm ci
# Установка только продакшн зависимостей
RUN npm ci --production
# Копирование собранного приложения из билдера
COPY --from=builder /usr/src/app/dist /usr/src/app/dist
COPY --from=builder /usr/src/app/server /usr/src/app/server
EXPOSE 8044
CMD ["npm", "run", "up:prod"]

View File

@@ -1,6 +1,12 @@
#!/bin/sh
docker stop ms-mongo
docker volume remove ms_volume
docker volume create ms_volume
docker run --rm -v ms_volume:/data/db --name ms-mongo -p 27017:27017 -d mongo:8.0.3
docker volume remove ms_volume8
docker volume create ms_volume8
docker run --rm \
-v ms_volume8:/data/db \
--name ms-mongo \
-p 27018:27017 \
-e MONGO_INITDB_ROOT_USERNAME=qqq \
-e MONGO_INITDB_ROOT_PASSWORD=qqq \
-d mongo:8.0.3

View File

@@ -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
View 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

View File

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

View File

@@ -1,43 +1,43 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* Для подробного объяснения каждого свойства конфигурации, посетите:
* https://jestjs.io/docs/configuration
*/
/** @type {import('jest').Config} */
const config = {
// All imported modules in your tests should be mocked automatically
// Все импортированные модули в тестах должны быть автоматически замоканы
// automock: false,
// Stop running tests after `n` failures
// Остановить выполнение тестов после `n` неудач
// bail: 0,
// The directory where Jest should store its cached dependency information
// Директория, где Jest должен хранить кэшированную информацию о зависимостях
// cacheDirectory: "C:\\Users\\alex\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls, instances, contexts and results before every test
// Автоматически очищать вызовы моков, экземпляры, контексты и результаты перед каждым тестом
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
// Указывает, должна ли собираться информация о покрытии во время выполнения тестов
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// Массив glob-паттернов, указывающих набор файлов, для которых должна собираться информация о покрытии
collectCoverageFrom: [
"<rootDir>/server/routers/**/*.js"
],
// The directory where Jest should output its coverage files
// Директория, куда Jest должен выводить файлы покрытия
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// Массив строк regexp-паттернов, используемых для пропуска сбора покрытия
coveragePathIgnorePatterns: [
"\\\\node_modules\\\\",
"<rootDir>/server/routers/old"
],
// Indicates which provider should be used to instrument code for coverage
// Указывает, какой провайдер должен использоваться для инструментирования кода для покрытия
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// Список имен репортеров, которые Jest использует при записи отчетов о покрытии
// coverageReporters: [
// "json",
// "text",
@@ -45,156 +45,159 @@ const config = {
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// Объект, который настраивает принудительное применение минимальных порогов для результатов покрытия
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// Путь к пользовательскому извлекателю зависимостей
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// Заставить вызовы устаревших API выбрасывать полезные сообщения об ошибках
// errorOnDeprecated: false,
// The default configuration for fake timers
// Конфигурация по умолчанию для поддельных таймеров
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// Принудительно собирать покрытие из игнорируемых файлов, используя массив glob-паттернов
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// Путь к модулю, который экспортирует асинхронную функцию, вызываемую один раз перед всеми наборами тестов
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// Путь к модулю, который экспортирует асинхронную функцию, вызываемую один раз после всех наборов тестов
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// Набор глобальных переменных, которые должны быть доступны во всех тестовых окружениях
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// Максимальное количество воркеров, используемых для запуска тестов. Может быть указано в % или числом. Например, maxWorkers: 10% будет использовать 10% от количества CPU + 1 в качестве максимального числа воркеров. maxWorkers: 2 будет использовать максимум 2 воркера.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// Массив имен директорий, которые должны быть рекурсивно найдены вверх от местоположения требуемого модуля
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// Массив расширений файлов, которые используют ваши модули
moduleFileExtensions: [
"js",
"mjs",
"cjs",
"jsx",
"ts",
"tsx",
"json",
"node"
],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// Карта из регулярных выражений в имена модулей или массивы имен модулей, которые позволяют заглушить ресурсы одним модулем
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// Массив строк regexp-паттернов, сопоставляемых со всеми путями модулей перед тем, как они будут считаться 'видимыми' для загрузчика модулей
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// Активирует уведомления для результатов тестов
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// Перечисление, которое указывает режим уведомлений. Требует { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Пресет, который используется в качестве основы для конфигурации Jest
preset: 'ts-jest',
// Run tests from one or more projects
// Запускать тесты из одного или нескольких проектов
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// Используйте эту опцию конфигурации для добавления пользовательских репортеров в Jest
// reporters: undefined,
// Automatically reset mock state before every test
// Автоматически сбрасывать состояние моков перед каждым тестом
// resetMocks: false,
// Reset the module registry before running each individual test
// Сбрасывать реестр модулей перед запуском каждого отдельного теста
// resetModules: false,
// A path to a custom resolver
// Путь к пользовательскому резолверу
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// Автоматически восстанавливать состояние моков и реализацию перед каждым тестом
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// Корневая директория, которую Jest должен сканировать для поиска тестов и модулей
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// Список путей к директориям, которые Jest должен использовать для поиска файлов
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// Позволяет использовать пользовательский раннер вместо стандартного тестового раннера Jest
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// Пути к модулям, которые выполняют некоторый код для настройки или подготовки тестового окружения перед каждым тестом
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// Список путей к модулям, которые выполняют некоторый код для настройки или подготовки тестового фреймворка перед каждым тестом
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// Количество секунд, после которого тест считается медленным и сообщается как таковой в результатах.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// Список путей к модулям сериализаторов снимков, которые Jest должен использовать для тестирования снимков
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Тестовое окружение, которое будет использоваться для тестирования
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// Опции, которые будут переданы в testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// Добавляет поле местоположения к результатам тестов
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// Glob-паттерны, которые Jest использует для обнаружения тестовых файлов
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// Массив строк regexp-паттернов, которые сопоставляются со всеми тестовыми путями, сопоставленные тесты пропускаются
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// Regexp-паттерн или массив паттернов, которые Jest использует для обнаружения тестовых файлов
// testRegex: [],
// This option allows the use of a custom results processor
// Эта опция позволяет использовать пользовательский процессор результатов
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// Эта опция позволяет использовать пользовательский тестовый раннер
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// Карта из регулярных выражений в пути к трансформерам
transform: {
'^.+\\.ts$': 'ts-jest',
'^.+\\.tsx$': 'ts-jest',
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// Массив строк regexp-паттернов, которые сопоставляются со всеми путями исходных файлов, сопоставленные файлы будут пропускать трансформацию
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// Массив строк regexp-паттернов, которые сопоставляются со всеми модулями перед тем, как загрузчик модулей автоматически вернет мок для них
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// Указывает, должен ли каждый отдельный тест сообщаться во время выполнения
verbose: true,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// Массив regexp-паттернов, которые сопоставляются со всеми путями исходных файлов перед повторным запуском тестов в режиме наблюдения
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// Использовать ли watchman для обхода файлов
// watchman: true,
};

5146
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,13 @@
{
"name": "multi-stub",
"version": "1.2.0",
"version": "2.0.0",
"description": "",
"main": "index.js",
"main": "server/index.ts",
"type": "commonjs",
"scripts": {
"start": "cross-env PORT=8033 npx nodemon ./server",
"up:prod": "cross-env NODE_ENV=\"production\" node ./server",
"deploy:d:stop": "docker compose down",
"deploy:d:build": "docker compose build",
"deploy:d:up": "docker compose up -d",
"redeploy": "npm run deploy:d:stop && npm run deploy:d:build && npm run deploy:d:up",
"start": "cross-env NODE_ENV=\"development\" ts-node-dev .",
"build": "tsc",
"up:prod": "node dist/server/index.js",
"eslint": "npx eslint ./server",
"eslint:fix": "npx eslint ./server --fix",
"test": "jest"
@@ -23,34 +21,49 @@
"license": "MIT",
"homepage": "https://bitbucket.org/online-mentor/multi-stub#readme",
"dependencies": {
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"@langchain/community": "^0.3.56",
"@langchain/core": "^0.3.77",
"@langchain/langgraph": "^0.4.9",
"ai": "^4.1.13",
"axios": "^1.7.7",
"bcrypt": "^5.1.0",
"bcryptjs": "^3.0.3",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.7",
"express": "5.0.1",
"express-jwt": "^8.5.1",
"express-session": "^1.18.1",
"gigachat": "^0.0.16",
"jsdom": "^25.0.1",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.12.0",
"mongoose": "^8.9.2",
"morgan": "^1.10.0",
"langchain": "^0.3.34",
"langchain-gigachat": "^0.0.14",
"mongodb": "^6.20.0",
"mongoose": "^8.18.2",
"mongoose-sequence": "^6.0.1",
"morgan": "^1.10.1",
"multer": "^1.4.5-lts.1",
"pbkdf2-password": "^1.2.1",
"rotating-file-stream": "^3.2.5",
"socket.io": "^4.8.1",
"uuid": "^11.0.3"
"zod": "^3.24.3"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/jest": "^30.0.0",
"@types/node": "22.10.2",
"eslint": "^9.17.0",
"globals": "^15.14.0",
"jest": "^29.7.0",
"mockingoose": "^2.16.2",
"nodemon": "3.1.9",
"supertest": "^7.0.0"
"supertest": "^7.0.0",
"ts-jest": "^29.4.6",
"ts-node-dev": "2.0.0",
"typescript": "5.7.3"
}
}

87
rules.md Normal file
View 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.

View File

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

View File

@@ -2,7 +2,7 @@ const { describe, it, expect } = require('@jest/globals')
const request = require('supertest')
const express = require('express')
const mockingoose = require('mockingoose')
const { ListModel } = require('../data/model/todo/list')
const { ListModel } = require('../routers/todo/model/todo/list')
const todo = require('../routers/todo/routes')

View File

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

View File

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

157
server/index.ts Normal file
View File

@@ -0,0 +1,157 @@
import express from 'express'
import cookieParser from 'cookie-parser'
import session from 'express-session'
import morgan from 'morgan'
import path from 'path'
import 'dotenv/config'
import root from './server'
import { errorHandler } from './error'
import kfuM241Router from './routers/kfu-m-24-1'
import epja20241Router from './routers/epja-2024-1'
import todoRouter from './routers/todo'
import dogsittersFinderRouter from './routers/dogsitters-finder'
import kazanExploreRouter from './routers/kazan-explore'
import edateamRouter from './routers/edateam-legacy'
import dryWashRouter from './routers/dry-wash'
import freetrackerRouter from './routers/freetracker'
import dhsTestingRouter from './routers/dhs-testing'
import gamehubRouter from './routers/gamehub'
import escRouter from './routers/esc'
import connectmeRouter from './routers/connectme'
import questioneerRouter from './routers/questioneer'
import procurementRouter from './routers/procurement'
import smokeTrackerRouter from './routers/smoke-tracker'
import assessmentToolsRouter from './routers/assessment-tools'
import { setIo } from './io'
export const app = express()
// Динамический импорт rotating-file-stream
const initServer = async () => {
const rfs = await import('rotating-file-stream')
const accessLogStream = rfs.createStream("access.log", {
size: "10M",
interval: "1d",
compress: "gzip",
path: path.join(__dirname, "log"),
})
const errorLogStream = rfs.createStream("error.log", {
size: "10M",
interval: "1d",
compress: "gzip",
path: path.join(__dirname, "log"),
})
app.use(cookieParser())
app.use(
morgan("combined", {
stream: accessLogStream,
skip: function (req, res) {
return res.statusCode >= 400
},
})
)
// log all requests to access.log
app.use(
morgan("combined", {
stream: errorLogStream,
skip: function (req, res) {
console.log('statusCode', res.statusCode, res.statusCode <= 400)
return res.statusCode < 400
},
})
)
console.log('warming up 🔥')
const sess = {
secret: "super-secret-key",
resave: true,
saveUninitialized: true,
cookie: {},
}
if (app.get("env") !== "development") {
app.set("trust proxy", 1)
}
app.use(session(sess))
app.use(
express.json({
limit: "50mb",
})
)
app.use(
express.urlencoded({
limit: "50mb",
extended: true,
})
)
app.use(root)
/**
* Добавляйте сюда свои routers.
*/
app.use("/kfu-m-24-1", kfuM241Router)
app.use("/epja-2024-1", epja20241Router)
app.use("/v1/todo", todoRouter)
app.use("/dogsitters-finder", dogsittersFinderRouter)
app.use("/kazan-explore", kazanExploreRouter)
app.use("/edateam", edateamRouter)
app.use("/dry-wash", dryWashRouter)
app.use("/freetracker", freetrackerRouter)
app.use("/dhs-testing", dhsTestingRouter)
app.use("/gamehub", gamehubRouter)
app.use("/esc", escRouter)
app.use('/connectme', connectmeRouter)
app.use('/questioneer', questioneerRouter)
app.use('/procurement', procurementRouter)
app.use('/smoke-tracker', smokeTrackerRouter)
app.use('/assessment-tools', assessmentToolsRouter)
app.use(errorHandler)
// Создаем обычный HTTP сервер
const server = app.listen(process.env.PORT ?? 8044, () => {
console.log(`🚀 Сервер запущен на http://localhost:${process.env.PORT ?? 8044}`)
})
// Обработка сигналов завершения процесса
process.on('SIGTERM', () => {
console.log('🛑 Получен сигнал SIGTERM. Выполняется корректное завершение...')
server.close(() => {
console.log('✅ Сервер успешно остановлен')
process.exit(0)
})
})
process.on('SIGINT', () => {
console.log('🛑 Получен сигнал SIGINT. Выполняется корректное завершение...')
server.close(() => {
console.log('✅ Сервер успешно остановлен')
process.exit(0)
})
})
// Обработка необработанных исключений
process.on('uncaughtException', (err) => {
console.error('❌ Необработанное исключение:', err)
server.close(() => {
process.exit(1)
})
})
// Обработка необработанных отклонений промисов
process.on('unhandledRejection', (reason, promise) => {
console.error('⚠️ Необработанное отклонение промиса:', reason)
server.close(() => {
process.exit(1)
})
})
return server
}
initServer().catch(console.error)

View File

@@ -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
View 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
View 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)

View 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);

View File

@@ -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

View File

@@ -0,0 +1,18 @@
const router = require('express').Router();
// Импортировать mongoose из общего модуля (подключение происходит в server/utils/mongoose.ts)
const mongoose = require('../../utils/mongoose');
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
router.use(timer());
// Подключение маршрутов - прямые пути без path.join и __dirname
router.use('/events', require('./routes/event'));
router.use('/teams', require('./routes/teams'));
router.use('/experts', require('./routes/experts'));
router.use('/criteria', require('./routes/criteria'));
router.use('/ratings', require('./routes/ratings'));
module.exports = router;
module.exports.default = router;

View File

@@ -0,0 +1,50 @@
const mongoose = require('mongoose');
const criterionItemSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
maxScore: {
type: Number,
default: 5,
min: 0,
max: 10
}
}, { _id: false });
const criteriaSchema = new mongoose.Schema({
eventId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Event',
required: true
},
blockName: {
type: String,
required: true
},
criteriaType: {
type: String,
enum: ['team', 'participant', 'all'],
default: 'all',
required: true
},
criteria: [criterionItemSchema],
order: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
module.exports = mongoose.model('Criteria', criteriaSchema);

View File

@@ -0,0 +1,44 @@
const mongoose = require('mongoose');
const eventSchema = new mongoose.Schema({
name: {
type: String,
required: true,
default: 'Новое мероприятие'
},
description: {
type: String,
default: ''
},
eventDate: {
type: Date,
required: true,
default: Date.now
},
location: {
type: String,
default: ''
},
status: {
type: String,
enum: ['draft', 'ready', 'active', 'completed'],
default: 'draft'
},
votingEnabled: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
module.exports = mongoose.model('Event', eventSchema);

View File

@@ -0,0 +1,43 @@
const mongoose = require('mongoose');
const crypto = require('crypto');
const expertSchema = new mongoose.Schema({
eventId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Event',
required: true
},
fullName: {
type: String,
required: true
},
token: {
type: String,
unique: true
},
qrCodeUrl: {
type: String,
default: ''
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Generate unique token before saving
expertSchema.pre('save', function(next) {
if (!this.token) {
this.token = crypto.randomBytes(16).toString('hex');
}
next();
});
module.exports = mongoose.model('Expert', expertSchema);

View File

@@ -0,0 +1,64 @@
const mongoose = require('mongoose');
const ratingItemSchema = new mongoose.Schema({
criteriaId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Criteria',
required: true
},
criterionName: {
type: String,
required: true
},
score: {
type: Number,
required: true,
min: 0,
max: 5
}
}, { _id: false });
const ratingSchema = new mongoose.Schema({
eventId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Event',
required: true
},
expertId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Expert',
required: true
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Team',
required: true
},
ratings: [ratingItemSchema],
totalScore: {
type: Number,
default: 0
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Calculate total score before saving
ratingSchema.pre('save', function(next) {
this.totalScore = this.ratings.reduce((sum, item) => sum + item.score, 0);
next();
});
// Ensure unique combination of expert and team
ratingSchema.index({ expertId: 1, teamId: 1 }, { unique: true });
module.exports = mongoose.model('Rating', ratingSchema);

View File

@@ -0,0 +1,52 @@
const mongoose = require('mongoose');
const teamSchema = new mongoose.Schema({
eventId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Event',
required: true
},
type: {
type: String,
enum: ['team', 'participant'],
required: true
},
name: {
type: String,
required: true
},
projectName: {
type: String,
default: ''
},
caseDescription: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
},
votingStatus: {
type: String,
enum: ['not_evaluated', 'evaluating', 'evaluated'],
default: 'not_evaluated'
},
isActiveForVoting: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
module.exports = mongoose.model('Team', teamSchema);

View File

@@ -0,0 +1,14 @@
const Event = require('./Event');
const Team = require('./Team');
const Expert = require('./Expert');
const Criteria = require('./Criteria');
const Rating = require('./Rating');
module.exports = {
Event,
Team,
Expert,
Criteria,
Rating
};

View File

@@ -0,0 +1,152 @@
const router = require('express').Router();
const { Criteria } = require('../models');
// Критерии по умолчанию из hack.md
const DEFAULT_CRITERIA = [
{
blockName: 'Оценка проекта команды',
criteriaType: 'team',
criteria: [
{ name: 'Соответствие решения поставленной задаче', maxScore: 5 },
{ name: 'Оригинальность - использование нестандартных технических и проектных подходов', maxScore: 5 },
{ name: 'Работоспособность решения', maxScore: 1 },
{ name: 'Технологическая сложность решения', maxScore: 2 },
{ name: 'Объем функциональных возможностей решения', maxScore: 2 },
{ name: 'Аргументация способа выбранного решения', maxScore: 5 },
{ name: 'Качество предоставления информации', maxScore: 5 },
{ name: 'Наличие удобного UX/UI', maxScore: 5 },
{ name: 'Наличие не менее 5 AI-агентов', maxScore: 5 }
],
order: 0
},
{
blockName: 'Оценка выступления участника',
criteriaType: 'participant',
criteria: [
{ name: 'Качество презентации и донесения идеи', maxScore: 5 },
{ name: 'Понимание технологии и решения', maxScore: 5 },
{ name: 'Аргументация выбранного подхода', maxScore: 5 },
{ name: 'Ответы на вопросы жюри', maxScore: 5 },
{ name: 'Коммуникативные навыки', maxScore: 5 }
],
order: 1
}
];
// GET /api/criteria - получить все блоки критериев
router.get('/', async (req, res) => {
try {
const { eventId, criteriaType } = req.query;
const filter = {};
if (eventId) filter.eventId = eventId;
if (criteriaType && criteriaType !== 'all') {
filter.$or = [
{ criteriaType: criteriaType },
{ criteriaType: 'all' }
];
}
const criteria = await Criteria.find(filter).sort({ order: 1 });
res.json(criteria);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/criteria/:id - получить блок критериев по ID
router.get('/:id', async (req, res) => {
try {
const criteria = await Criteria.findById(req.params.id);
if (!criteria) {
return res.status(404).json({ error: 'Criteria not found' });
}
res.json(criteria);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/criteria - создать блок критериев
router.post('/', async (req, res) => {
try {
const { eventId, blockName, criteriaType, criteria, order } = req.body;
if (!eventId || !blockName || !criteria || !Array.isArray(criteria)) {
return res.status(400).json({ error: 'EventId, block name and criteria array are required' });
}
const criteriaBlock = await Criteria.create({
eventId,
blockName,
criteriaType: criteriaType || 'all',
criteria,
order: order !== undefined ? order : 0
});
res.status(201).json(criteriaBlock);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/criteria/default - загрузить критерии по умолчанию из hack.md
router.post('/default', async (req, res) => {
try {
const { eventId } = req.body;
if (!eventId) {
return res.status(400).json({ error: 'EventId is required' });
}
// Удаляем все существующие критерии для этого мероприятия
await Criteria.deleteMany({ eventId });
// Создаем критерии по умолчанию с eventId
const criteriaWithEventId = DEFAULT_CRITERIA.map(c => ({
...c,
eventId
}));
const createdCriteria = await Criteria.insertMany(criteriaWithEventId);
res.status(201).json(createdCriteria);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/criteria/:id - редактировать блок
router.put('/:id', async (req, res) => {
try {
const { blockName, criteriaType, criteria, order } = req.body;
const criteriaBlock = await Criteria.findById(req.params.id);
if (!criteriaBlock) {
return res.status(404).json({ error: 'Criteria not found' });
}
if (blockName !== undefined) criteriaBlock.blockName = blockName;
if (criteriaType !== undefined) criteriaBlock.criteriaType = criteriaType;
if (criteria !== undefined) criteriaBlock.criteria = criteria;
if (order !== undefined) criteriaBlock.order = order;
await criteriaBlock.save();
res.json(criteriaBlock);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/criteria/:id - удалить блок
router.delete('/:id', async (req, res) => {
try {
const criteria = await Criteria.findByIdAndDelete(req.params.id);
if (!criteria) {
return res.status(404).json({ error: 'Criteria not found' });
}
res.json({ message: 'Criteria deleted successfully', criteria });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,108 @@
const router = require('express').Router();
const { Event } = require('../models');
// GET /api/events - получить все мероприятия
router.get('/', async (req, res) => {
try {
const events = await Event.find().sort({ eventDate: -1 });
res.json(events);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/events/:id - получить одно мероприятие
router.get('/:id', async (req, res) => {
try {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
res.json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/events - создать новое мероприятие
router.post('/', async (req, res) => {
try {
const { name, description, eventDate, location, status } = req.body;
const event = await Event.create({
name: name || 'Новое мероприятие',
description: description || '',
eventDate: eventDate || new Date(),
location: location || '',
status: status || 'draft',
votingEnabled: false
});
res.status(201).json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/events/:id - обновить мероприятие
router.put('/:id', async (req, res) => {
try {
const { name, description, eventDate, location, status } = req.body;
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
if (name !== undefined) event.name = name;
if (description !== undefined) event.description = description;
if (eventDate !== undefined) event.eventDate = eventDate;
if (location !== undefined) event.location = location;
if (status !== undefined) event.status = status;
await event.save();
res.json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/events/:id - удалить мероприятие
router.delete('/:id', async (req, res) => {
try {
const event = await Event.findByIdAndDelete(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
res.json({ message: 'Мероприятие удалено', event });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/events/:id/toggle-voting - вкл/выкл оценку
router.patch('/:id/toggle-voting', async (req, res) => {
try {
const event = await Event.findById(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Мероприятие не найдено' });
}
event.votingEnabled = !event.votingEnabled;
await event.save();
res.json(event);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,117 @@
const router = require('express').Router();
const { Expert } = require('../models');
// GET /api/experts - список экспертов
router.get('/', async (req, res) => {
try {
const { eventId } = req.query;
const filter = {};
if (eventId) filter.eventId = eventId;
const experts = await Expert.find(filter).sort({ createdAt: -1 });
res.json(experts);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/experts/by-token/:token - получить данные эксперта по токену
router.get('/by-token/:token', async (req, res) => {
try {
const expert = await Expert.findOne({ token: req.params.token });
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
res.json(expert);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/experts/:id - получить эксперта по ID
router.get('/:id', async (req, res) => {
try {
const expert = await Expert.findById(req.params.id);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
res.json(expert);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/experts - создать эксперта (с генерацией уникальной ссылки)
router.post('/', async (req, res) => {
try {
const { eventId, fullName } = req.body;
if (!eventId || !fullName) {
return res.status(400).json({ error: 'EventId and full name are required' });
}
// Создаем нового эксперта
const expert = new Expert({
eventId,
fullName
});
// Сохраняем эксперта (токен генерируется в pre-save хуке)
await expert.save();
// Формируем URL для QR кода ПОСЛЕ сохранения, когда токен уже сгенерирован
// Приоритеты:
// 1) Явная переменная окружения FRONTEND_BASE_URL (например, https://platform.brojs.ru)
// 2) Проксируемые заголовки x-forwarded-proto / x-forwarded-host
// 3) Локальные req.protocol + req.get('host')
const forwardedProto = req.get('x-forwarded-proto');
const forwardedHost = req.get('x-forwarded-host');
const protocol = forwardedProto || req.protocol;
const host = forwardedHost || req.get('host');
const baseUrl = process.env.FRONTEND_BASE_URL || `${protocol}://${host}`;
expert.qrCodeUrl = `${baseUrl}/assessment-tools/expert/${expert.token}`;
// Сохраняем еще раз с обновленным qrCodeUrl
await expert.save();
res.status(201).json(expert);
} catch (error) {
console.error('Error creating expert:', error);
res.status(500).json({ error: error.message });
}
});
// PUT /api/experts/:id - редактировать эксперта
router.put('/:id', async (req, res) => {
try {
const { fullName } = req.body;
const expert = await Expert.findById(req.params.id);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
if (fullName !== undefined) expert.fullName = fullName;
await expert.save();
res.json(expert);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/experts/:id - удалить эксперта
router.delete('/:id', async (req, res) => {
try {
const expert = await Expert.findByIdAndDelete(req.params.id);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
res.json({ message: 'Expert deleted successfully', expert });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,240 @@
const router = require('express').Router();
const { Rating, Team, Expert, Criteria } = require('../models');
// GET /api/ratings - получить все оценки (с фильтрами)
router.get('/', async (req, res) => {
try {
const { expertId, teamId, eventId } = req.query;
const filter = {};
if (expertId) filter.expertId = expertId;
if (teamId) filter.teamId = teamId;
if (eventId) filter.eventId = eventId;
const ratings = await Rating.find(filter)
.populate('expertId', 'fullName')
.populate('teamId', 'name type')
.sort({ createdAt: -1 });
res.json(ratings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/team/:teamId - оценки конкретной команды
router.get('/team/:teamId', async (req, res) => {
try {
const ratings = await Rating.find({ teamId: req.params.teamId })
.populate('expertId', 'fullName')
.populate('teamId', 'name type projectName');
res.json(ratings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/expert/:expertId - оценки конкретного эксперта
router.get('/expert/:expertId', async (req, res) => {
try {
const ratings = await Rating.find({ expertId: req.params.expertId })
.populate('teamId', 'name type projectName');
res.json(ratings);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/statistics - статистика с группировкой по командам
router.get('/statistics', async (req, res) => {
try {
const { type, eventId } = req.query;
// Получаем все команды
const teamFilter = { isActive: true };
if (type) teamFilter.type = type;
if (eventId) teamFilter.eventId = eventId;
const teams = await Team.find(teamFilter);
// Получаем все оценки
const ratingFilter = {};
if (eventId) ratingFilter.eventId = eventId;
const ratings = await Rating.find(ratingFilter)
.populate('expertId', 'fullName')
.populate('teamId', 'name type projectName');
// Группируем оценки по командам
const statistics = teams.map(team => {
const teamRatings = ratings.filter(r => r.teamId && r.teamId._id.toString() === team._id.toString());
// Считаем средние оценки по критериям
const criteriaStats = {};
teamRatings.forEach(rating => {
rating.ratings.forEach(item => {
if (!criteriaStats[item.criterionName]) {
criteriaStats[item.criterionName] = {
name: item.criterionName,
scores: [],
average: 0
};
}
criteriaStats[item.criterionName].scores.push(item.score);
});
});
// Вычисляем средние значения
Object.keys(criteriaStats).forEach(key => {
const scores = criteriaStats[key].scores;
criteriaStats[key].average = scores.reduce((sum, s) => sum + s, 0) / scores.length;
});
// Считаем общий балл команды (среднее от всех экспертов)
const totalScore = teamRatings.length > 0
? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length
: 0;
return {
team: {
_id: team._id,
name: team.name,
type: team.type,
projectName: team.projectName
},
ratings: teamRatings.map(r => ({
expert: r.expertId ? r.expertId.fullName : 'Unknown',
criteria: r.ratings,
totalScore: r.totalScore
})),
criteriaStats: Object.values(criteriaStats),
totalScore: totalScore,
ratingsCount: teamRatings.length
};
});
res.json(statistics);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/ratings/top3 - топ-3 команды и топ-3 участники отдельно
// ВАЖНО: всегда возвращаем объект вида { teams: Top3Item[], participants: Top3Item[] },
// чтобы фронтенд мог безопасно работать с data.teams / data.participants
router.get('/top3', async (req, res) => {
try {
const { type, eventId } = req.query;
// Получаем все активные команды/участников
const teamFilter = { isActive: true };
if (eventId) teamFilter.eventId = eventId;
const teams = await Team.find(teamFilter);
const ratingFilter = {};
if (eventId) ratingFilter.eventId = eventId;
const ratings = await Rating.find(ratingFilter).populate('teamId', 'name type projectName');
const calculateTop3 = (sourceTeams) => {
const teamScores = sourceTeams.map((team) => {
const teamRatings = ratings.filter(
(r) => r.teamId && r.teamId._id.toString() === team._id.toString()
);
const totalScore =
teamRatings.length > 0
? teamRatings.reduce((sum, r) => sum + r.totalScore, 0) / teamRatings.length
: 0;
return {
team: {
_id: team._id,
name: team.name,
type: team.type,
projectName: team.projectName
},
totalScore,
ratingsCount: teamRatings.length
};
});
return teamScores
.filter((t) => t.ratingsCount > 0)
.sort((a, b) => b.totalScore - a.totalScore)
.slice(0, 3);
};
const teamEntities = teams.filter((t) => t.type === 'team');
const participantEntities = teams.filter((t) => t.type === 'participant');
const teamTop3 = calculateTop3(teamEntities);
const participantTop3 = calculateTop3(participantEntities);
// Параметр type управляет только содержимым, но не форматом ответа
const response = {
teams: !type || type === 'team' ? teamTop3 : [],
participants: !type || type === 'participant' ? participantTop3 : []
};
res.json(response);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/ratings - создать/обновить оценку эксперта
router.post('/', async (req, res) => {
try {
const { eventId, expertId, teamId, ratings } = req.body;
if (!eventId || !expertId || !teamId || !ratings || !Array.isArray(ratings)) {
return res.status(400).json({ error: 'EventId, expert ID, team ID, and ratings array are required' });
}
// Проверяем существование эксперта и команды
const expert = await Expert.findById(expertId);
const team = await Team.findById(teamId);
if (!expert) {
return res.status(404).json({ error: 'Expert not found' });
}
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
// Проверяем, активна ли команда
if (!team.isActive) {
return res.status(400).json({ error: 'Team voting is disabled' });
}
// Ищем существующую оценку
let rating = await Rating.findOne({ eventId, expertId, teamId });
if (rating) {
// Обновляем существующую оценку
rating.ratings = ratings;
await rating.save();
} else {
// Создаем новую оценку
rating = await Rating.create({
eventId,
expertId,
teamId,
ratings
});
}
// Возвращаем с populate
rating = await Rating.findById(rating._id)
.populate('expertId', 'fullName')
.populate('teamId', 'name type projectName');
res.status(rating ? 200 : 201).json(rating);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,194 @@
const router = require('express').Router();
const { Team } = require('../models');
// GET /api/teams - список всех команд
router.get('/', async (req, res) => {
try {
const { type, eventId } = req.query;
const filter = {};
if (type) filter.type = type;
if (eventId) filter.eventId = eventId;
const teams = await Team.find(filter).sort({ createdAt: -1 });
res.json(teams);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/teams/active/voting - получить активную для оценки команду (ДОЛЖЕН БЫТЬ ПЕРЕД /:id)
router.get('/active/voting', async (req, res) => {
try {
const { eventId } = req.query;
const filter = { isActiveForVoting: true };
if (eventId) filter.eventId = eventId;
const team = await Team.findOne(filter);
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/stop-all-voting/global - остановить все оценивания (ДОЛЖЕН БЫТЬ ПЕРЕД /:id)
router.patch('/stop-all-voting/global', async (req, res) => {
try {
const { eventId } = req.body;
// Находим все команды, которые сейчас оцениваются
const filter = { isActiveForVoting: true };
if (eventId) filter.eventId = eventId;
const result = await Team.updateMany(
filter,
{
isActiveForVoting: false,
votingStatus: 'evaluated'
}
);
res.json({
message: 'All voting stopped successfully',
modifiedCount: result.modifiedCount
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/teams/:id - получить команду по ID
router.get('/:id', async (req, res) => {
try {
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// POST /api/teams - создать команду/участника
router.post('/', async (req, res) => {
try {
const { eventId, type, name, projectName, caseDescription } = req.body;
if (!eventId || !type || !name) {
return res.status(400).json({ error: 'EventId, type and name are required' });
}
const team = await Team.create({
eventId,
type,
name,
projectName: projectName || '',
caseDescription: caseDescription || '',
isActive: true
});
res.status(201).json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PUT /api/teams/:id - редактировать команду
router.put('/:id', async (req, res) => {
try {
const { type, name, projectName, caseDescription } = req.body;
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
if (type !== undefined) team.type = type;
if (name !== undefined) team.name = name;
if (projectName !== undefined) team.projectName = projectName;
if (caseDescription !== undefined) team.caseDescription = caseDescription;
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE /api/teams/:id - удалить команду
router.delete('/:id', async (req, res) => {
try {
const team = await Team.findByIdAndDelete(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
res.json({ message: 'Team deleted successfully', team });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/:id/activate-for-voting - активировать команду для оценки
router.patch('/:id/activate-for-voting', async (req, res) => {
try {
// Получаем команду для активации
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
// Деактивируем все команды этого мероприятия
const previouslyActive = await Team.findOne({
isActiveForVoting: true,
eventId: team.eventId
});
if (previouslyActive) {
previouslyActive.isActiveForVoting = false;
previouslyActive.votingStatus = 'evaluated';
await previouslyActive.save();
}
// Активируем выбранную команду
team.isActiveForVoting = true;
team.votingStatus = 'evaluating';
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/:id/stop-voting - остановить оценивание конкретной команды
router.patch('/:id/stop-voting', async (req, res) => {
try {
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
team.isActiveForVoting = false;
team.votingStatus = 'evaluated';
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// PATCH /api/teams/:id/toggle-active - остановить оценку команды
router.patch('/:id/toggle-active', async (req, res) => {
try {
const team = await Team.findById(req.params.id);
if (!team) {
return res.status(404).json({ error: 'Team not found' });
}
team.isActive = !team.isActive;
await team.save();
res.json(team);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

View File

@@ -0,0 +1,36 @@
// Импортировать mongoose из общего модуля (подключение происходит автоматически)
const mongoose = require('../../../utils/mongoose');
const Event = require('../models/Event');
async function recreateTestUser() {
try {
// Проверяем подключение к MongoDB
if (mongoose.connection.readyState !== 1) {
console.log('Waiting for MongoDB connection...');
await new Promise(resolve => setTimeout(resolve, 1000));
}
console.log('Connected to MongoDB');
// Создаем тестовое мероприятие если его нет
let event = await Event.findOne();
if (!event) {
event = await Event.create({
name: 'Tatar san',
status: 'draft',
votingEnabled: false
});
console.log('Test event created:', event.name);
} else {
console.log('Event already exists:', event.name);
}
console.log('Database initialized successfully');
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
recreateTestUser();

View File

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

View File

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

View File

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

View File

@@ -7,29 +7,29 @@ router.get("/users", (request, response) => {
router.post("/auth", (request, response) => {
const { phoneNumber, password } = request.body;
console.log(phoneNumber, password);
if (phoneNumber === "89999999999") {
response.send(require("./json/auth/dogsitter.success.json"));
} else if (phoneNumber === "89555555555") {
response.status(400).send(require("./json/auth/error.json"));
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
response.send(require("./json/auth/success.json"));
} else {
response.send(require("./json/auth/owner.success.json"));
response.status(401).send(require("./json/auth/error.json"));
}
});
router.post("/auth/2fa", (request, response) => {
const { code } = request.body;
if (code === "0000") {
response.send(require("./json/2fa/success.json"));
const { phoneNumber, code } = request.body;
if (code === "0000" && phoneNumber === "89999999999") {
response.send(require("./json/2fa/dogsitter.success.json"));
} else if (code === "0000" && phoneNumber === "89559999999") {
response.send(require("./json/2fa/owner.success.json"));
} else {
response.status(400).send(require("./json/2fa/error.json"));
response.status(401).send(require("./json/2fa/error.json"));
}
});
router.post("/register", (request, response) => {
const { firstName, secondName, phoneNumber, password, role } = request.body;
console.log(phoneNumber, password, role);
if (phoneNumber === "89283244141" || phoneNumber === "89872855893") {
response.status(400).send(require("./json/register/error.json"));
if (phoneNumber === "89999999999" || phoneNumber === "89559999999") {
response.status(401).send(require("./json/register/error.json"));
} else if (role === "dogsitter") {
response.send(require("./json/register/dogsitter.success.json"));
} else {
@@ -37,4 +37,192 @@ router.post("/register", (request, response) => {
}
});
module.exports = router;
router.get("/auth/session", (request, response) => {
const authHeader = request.headers.authorization;
if (!authHeader) {
return response.status(401).json({ error: "Authorization header missing" });
}
// Берём сам токен из заголовка
const token = authHeader.split(" ")[1];
if (!token) {
return response.status(401).json({ error: "Bearer token missing" });
}
const jwt = require("jsonwebtoken");
const secretKey = "secret";
try {
const decoded = jwt.verify(token, secretKey);
if (decoded.role === "dogsitter") {
response.send(require("./json/role/dogsitter.success.json"));
} else {
response.send(require("./json/role/owner.success.json"));
}
} catch (e) {
console.log("token e:", e);
return response.status(403).json({ error: "Invalid token" });
}
});
// Проверка взаимодействия между пользователем и догситтером
router.get("/interactions/check", (req, res) => {
const { owner_id, dogsitter_id } = req.query;
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
delete require.cache[require.resolve(usersFilePath)];
const usersFile = require(usersFilePath);
const interactions = usersFile.interactions || [];
const exists = interactions.some(
(interaction) =>
interaction.owner_id === Number(owner_id) &&
interaction.dogsitter_id === Number(dogsitter_id)
);
res.json({ exists });
});
// Добавление нового взаимодействия
router.post("/interactions", (req, res) => {
const { owner_id, dogsitter_id, interaction_type } = req.body;
if (!owner_id || !dogsitter_id || !interaction_type) {
return res.status(400).json({ error: "Missing required fields" });
}
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
delete require.cache[require.resolve(usersFilePath)];
const usersFile = require(usersFilePath);
if (!usersFile.interactions) {
usersFile.interactions = [];
}
// Проверяем, существует ли уже такое взаимодействие
const exists = usersFile.interactions.some(
(interaction) =>
interaction.owner_id === Number(owner_id) &&
interaction.dogsitter_id === Number(dogsitter_id)
);
if (!exists) {
usersFile.interactions.push({
owner_id: Number(owner_id),
dogsitter_id: Number(dogsitter_id),
interaction_type,
});
fs.writeFileSync(
usersFilePath,
JSON.stringify(usersFile, null, 2),
"utf8"
);
console.log(
`Добавлено взаимодействие: owner_id=${owner_id}, dogsitter_id=${dogsitter_id}`
);
}
res.json({ success: true });
});
router.get("/dogsitter-viewing", (req, res) => {
const { id } = req.query;
console.log(`Получен запрос для dogsitter с ID: ${id}`);
const usersFile = require("./json/users/users.json");
const users = usersFile.data; // Извлекаем массив из свойства "data"
const user = users.find((user) => user.id === Number(id));
if (user) {
res.json(user); // Возвращаем найденного пользователя
} else {
res.status(404).json({ error: "User not found" }); // Если пользователь не найден
}
});
const fs = require('fs');
const path = require('path');
router.post('/dogsitter-viewing/rating/:id', (req, res) => {
const { id } = req.params;
const { rating } = req.body;
if (!rating || rating < 1 || rating > 5) {
return res.status(400).json({ error: 'Некорректная оценка' });
}
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
delete require.cache[require.resolve(usersFilePath)];
const usersFile = require(usersFilePath);
const users = usersFile.data;
const userIndex = users.findIndex(user => user.id === Number(id));
if (userIndex === -1) {
return res.status(404).json({ error: 'Догситтер не найден' });
}
if (!users[userIndex].ratings) {
users[userIndex].ratings = [];
}
users[userIndex].ratings.push(rating);
if (users[userIndex].ratings.length > 100) {
users[userIndex].ratings.shift();
}
const total = users[userIndex].ratings.reduce((sum, r) => sum + r, 0);
users[userIndex].rating = parseFloat((total / users[userIndex].ratings.length).toFixed(2));
fs.writeFileSync(usersFilePath, JSON.stringify({ data: users }, null, 2), 'utf8');
console.log(`Обновлен рейтинг догситтера ${id}: ${users[userIndex].rating}`);
res.json({ rating: users[userIndex].rating, ratings: users[userIndex].ratings });
});
router.patch('/users/:id', (req, res) => {
const { id } = req.params;
const updateData = req.body;
console.log('Полученные данные для обновления:', updateData);
const usersFilePath = path.resolve(__dirname, "./json/users/users.json");
delete require.cache[require.resolve(usersFilePath)];
const usersFile = require(usersFilePath);
const users = usersFile.data;
const userIndex = users.findIndex((user) => user.id === Number(id));
if (userIndex === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[userIndex] = { ...users[userIndex], ...updateData };
fs.writeFileSync(
usersFilePath,
JSON.stringify({ data: users }, null, 2),
'utf8'
);
console.log('Обновлённые данные пользователя:', users[userIndex]);
res.json(users[userIndex]);
});
module.exports = router

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
{
"status": "success",
"message": "Two-factor authentication passed."
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,39 +1,69 @@
[
{
"id": 1,
"phone_number": 89283244141,
"first_name": "Вася",
"second_name": "Пупкин",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Пушкина, 12",
"price": 1500,
"about_me": "Я люблю собак"
},
{
"id": 2,
"phone_number": 89272844541,
"first_name": "Ваня",
"second_name": "Пуськин",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
"price": 1000000,
"about_me": "Я не люблю собак. И вообще я котоман."
},
{
"id": 3,
"phone_number": 89872855893,
"first_name": "Гадий",
"second_name": "Петрович",
"role": "owner"
},
{
"id": 4,
"phone_number": 89872844591,
"first_name": "Галкин",
"second_name": "Максим",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
"price": 1000000,
"about_me": "Миллион алых роз"
}
]
{
"data": [
{
"id": 1,
"phone_number": "89999999999",
"first_name": "Вася",
"second_name": "Пупкин",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, Пушкина, 12",
"price": "1500",
"about_me": "Я люблю собак!",
"rating": 5,
"ratings": [
5,
5
],
"tg": "jullllllie"
},
{
"id": 2,
"phone_number": 89272844541,
"first_name": "Ваня",
"second_name": "Пуськин",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, улица Абсалямова, 19",
"price": 2000,
"about_me": "Я не люблю собак. И вообще я котоман.",
"rating": 4,
"ratings": [
4,
4
],
"tg": "vanya006"
},
{
"id": 3,
"phone_number": 89559999999,
"first_name": "Гадий",
"second_name": "Петрович",
"role": "owner"
},
{
"id": 4,
"phone_number": 89872844591,
"first_name": "Галкин",
"second_name": "Максим",
"role": "dogsitter",
"location": "Россия, республика Татарстан, Казань, проспект Ямашева, 83",
"price": 1750,
"about_me": "Миллион алых роз",
"rating": 4.5,
"ratings": [
4,
5
],
"tg": "maks100500"
}
],
"interactions": [
{
"owner_id": 3,
"dogsitter_id": 4
},
{
"owner_id": 1,
"dogsitter_id": 2
}
]
}

View 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);

View 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);

View 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

View File

@@ -1,87 +1,117 @@
const router = require('express').Router()
const {MasterModel} = require('./model/master')
const mongoose = require("mongoose")
const router = require("express").Router();
const { MasterModel } = require("./model/master");
const mongoose = require("mongoose");
const { OrderModel } = require("./model/order");
router.post("/masters/list", async (req, res, next) => {
try {
const { startDate, endDate } = req.body;
router.get('/masters', async (req, res,next) => {
try {
const master = await MasterModel.find({})
res.status(200).send({success: true, body: master})
} catch (error) {
next(error)
}
})
router.delete('/masters/:id', async (req, res,next) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)){
throw new Error('ID is required')
if (!startDate || !endDate) {
throw new Error("Missing startDate or endDate");
}
try {
const master = await MasterModel.findByIdAndDelete(id, {
new: true,
});
if (!master) {
throw new Error('master not found')
}
res.status(200).send({success: true, body: master})
} catch (error) {
next(error)
}
})
const start = new Date(startDate);
const end = new Date(endDate);
const masters = await MasterModel.find({});
const orders = await OrderModel.find({
$or: [
{ startWashTime: { $lt: end }, endWashTime: { $gt: start } }
]
});
router.post('/masters', async (req, res,next) => {
const {name, phone} = req.body
if (!name || !phone ){
throw new Error('Enter name and phone')
}
try {
const master = await MasterModel.create({name, phone})
res.status(200).send({success: true, body: master})
} catch (error) {
next(error)
}
})
router.patch('/masters/:id', async (req, res, next) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error('ID is required')
}
const { name, phone } = req.body;
if (!name && !phone) {
throw new Error('Enter name and phone')
}
try {
const updateData = {};
if (name) updateData.name = name;
if (phone) updateData.phone = phone;
const master = await MasterModel.findByIdAndUpdate(
id,
updateData,
{ new: true }
const mastersWithOrders = masters.map((master) => {
const masterOrders = orders.filter((order) => {
return (
order?.master && order.master.toString() === master._id.toString()
);
});
if (!master) {
throw new Error('master not found')
}
const schedule = masterOrders.map((order) => ({
id: order._id,
startWashTime: order.startWashTime,
endWashTime: order.endWashTime,
}));
res.status(200).send({ success: true, body: master });
} catch (error) {
next(error);
}
return {
id: master._id,
name: master.name,
schedule: schedule,
phone: master.phone,
};
});
res.status(200).send({ success: true, body: mastersWithOrders });
} catch (error) {
next(error);
}
});
module.exports = router
router.delete("/masters/:id", async (req, res, next) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error("ID is required");
}
try {
const master = await MasterModel.findByIdAndDelete(id, {
new: true,
});
if (!master) {
throw new Error("master not found");
}
res.status(200).send({ success: true, body: master });
} catch (error) {
next(error);
}
});
router.post("/masters", async (req, res, next) => {
const { name, phone } = req.body;
if (!name || !phone) {
throw new Error("Enter name and phone");
}
try {
const master = await MasterModel.create({ name, phone });
res.status(200).send({ success: true, body: master });
} catch (error) {
next(error);
}
});
router.patch("/masters/:id", async (req, res, next) => {
const { id } = req.params;
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error("ID is required");
}
const { name, phone } = req.body;
if (!name && !phone) {
throw new Error("Enter name and phone");
}
try {
const updateData = {};
if (name) updateData.name = name;
if (phone) updateData.phone = phone;
const master = await MasterModel.findByIdAndUpdate(id, updateData, {
new: true,
});
if (!master) {
throw new Error("master not found");
}
res.status(200).send({ success: true, body: master });
} catch (error) {
next(error);
}
});
module.exports = router;

View 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
}

View 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)

View File

@@ -1,5 +1,6 @@
const { Schema, model } = require('mongoose')
const { orderStatus } = require('./const')
const { OrderNumberModel } = require('./order.number')
const schema = new Schema({
phone: {
@@ -14,7 +15,7 @@ const schema = new Schema({
type: Number,
required: true
},
carColor: String,
carColor: Schema.Types.Mixed,
startWashTime: {
type: Date,
required: true
@@ -27,6 +28,10 @@ const schema = new Schema({
type: String,
required: true
},
orderNumber: {
type: String,
unique: true
},
status: {
type: String,
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', {
virtuals: true,
versionKey: false,

View 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)

View File

@@ -1,13 +1,23 @@
const mongoose = require("mongoose")
const router = require('express').Router()
const multer = require('multer')
const { MasterModel } = require('./model/master')
const { OrderModel } = require('./model/order')
const { OrderCarImgModel } = require('./model/order.car-img')
const { orderStatus } = require('./model/const')
const { getGigaToken, getSystemPrompt, getGigaChatModel } = require('./get-token')
const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value)
const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value)
const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100
const isValidCarColor = (value) => value.length < 50 && /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
const isValidCarColor = (value) => {
if (typeof value === 'number') {
return value >= 0 && value <= 7
} else if (typeof value === 'string') {
return /^[#a-z0-9а-я-\s,.()]+$/i.test(value)
}
return false
}
const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value)
const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/
@@ -26,6 +36,9 @@ const isValidLocation = (value) => {
const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value)
const isValidOrderNotes = (value) => value.length < 500
const allowedMimeTypes = ['image/jpeg', 'image/png']
const sizeLimitInMegaBytes = 15
const VALIDATION_MESSAGES = {
order: {
notFound: 'Order not found'
@@ -60,6 +73,13 @@ const VALIDATION_MESSAGES = {
carColor: {
invalid: 'Invalid car color'
},
carImg: {
required: 'Car image file is required',
invalid: {
type: `Invalid car image file type. Allowed types: ${allowedMimeTypes}`,
size: `Invalid car image file size. Limit is ${sizeLimitInMegaBytes}MB`
}
},
washingBegin: {
required: 'Begin time of washing is required',
invalid: 'Invalid begin time of washing'
@@ -143,17 +163,21 @@ router.post('/create', async (req, res, next) => {
router.get('/:id', async (req, res, next) => {
const { id } = req.params
if (!mongoose.Types.ObjectId.isValid(id)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
try {
const order = await OrderModel.findById(id)
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
res.status(200).send({ success: true, body: order })
const imgProps = await OrderCarImgModel.findOne({ orderId: order.id })
res.status(200).send({ success: true, body: { ...order.toObject(), ...imgProps?.toObject() } })
} catch (error) {
next(error)
}
@@ -248,4 +272,191 @@ router.delete('/:id', async (req, res, next) => {
}
})
module.exports = router
const storage = multer.memoryStorage()
const upload = multer({
storage: storage,
limits: { fileSize: sizeLimitInMegaBytes * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error(VALIDATION_MESSAGES.carImg.invalid.type), false)
}
}
})
const { v4: uuidv4 } = require("uuid")
const axios = require('axios')
const GIGA_CHAT_OAUTH = 'https://ngw.devices.sberbank.ru:9443/api/v2/oauth'
const GIGA_CHAT_API = 'https://gigachat.devices.sberbank.ru/api/v1'
const getToken = async (req, res) => {
const gigaToken = await getGigaToken()
const rqUID = uuidv4()
const body = new URLSearchParams({
scope: "GIGACHAT_API_PERS",
})
const response = await fetch(GIGA_CHAT_OAUTH, {
method: "POST",
headers: {
Authorization: `Basic ${gigaToken}`,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
RqUID: rqUID,
},
body,
})
if (!response.ok) {
const errorData = await response.json()
console.error("Ошибка запроса: ", errorData)
return res.status(response.status).json(errorData)
}
return await response.json()
}
const uploadImage = async (file, accessToken) => {
const formData = new FormData()
const blob = new Blob([file.buffer], { type: file.mimetype })
formData.append('file', blob, file.originalname)
formData.append('purpose', 'general')
const config = {
maxBodyLength: Infinity,
headers: {
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
}
try {
const response = await axios.post(`${GIGA_CHAT_API}/files`, formData, config)
return response.data.id
} catch (error) {
console.error(error)
}
}
const COLORS_MAP = ['white', 'black', 'silver', 'gray', 'beige-brown', 'red', 'blue', 'green']
const getColorName = (colorKey) => {
if (typeof colorKey === 'number' && COLORS_MAP[colorKey]) {
return COLORS_MAP[colorKey]
}
return colorKey
}
const analyzeImage = async (fileId, token, imgProps) => {
const response = await fetch(`${GIGA_CHAT_API}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
model: (await getGigaChatModel()) ?? "GigaChat-Max",
stream: false,
update_interval: 0,
messages: [
{
role: "system",
content: (await getSystemPrompt()) ?? `Ты эксперт по оценке степени загрязнения автомобилей. Твоя задача — анализировать фотографии машин и определять степень их загрязнения.
Тебе предоставляют фотографию, где явно выделяется одна машина (например, она расположена в центре и имеет больший размер в кадре по сравнению с остальными).
ВАЖНО: Твой ответ ДОЛЖЕН быть СТРОГО в формате JSON и содержать ТОЛЬКО следующие поля:
{
"value": число от 0 до 10 (целое или с одним знаком после запятой),
"description": "текстовое описание на русском языке"
}.
Правила:
1. Поле "value":
- Должно быть числом от 0 до 10 (0 = машина абсолютно чистая, 10 = машина максимально грязная) ИЛИ undefined (если не удалось оценить);
2. Поле "description":
- Должно содержать 2-3 предложения на русском языке;
- Обязательно указать конкретные признаки загрязнения;
- Объяснить, почему выставлен именно такой балл.
- Должно быть связано только с автомобилем.
НЕ ДОБАВЛЯЙ никаких дополнительных полей или комментариев вне JSON структуры. НЕ ИСПОЛЬЗУЙ markdown форматирование. ОТВЕТ ДОЛЖЕН БЫТЬ ВАЛИДНЫМ JSON. Если на фотографии нельзя явно выделить одну машину, то ОЦЕНКА ДОЛЖНА ИМЕТЬ ЗНАЧЕНИЕ undefined и в описании должно быть указано, что по фотографии не удалось оценить степень загрязнения автомобиля, при этом НЕ ОПИСЫВАЙ НИЧЕГО ДРУГОГО КРОМЕ АВТОМОБИЛЯ`,
},
{
role: "user",
content: `Дай оценку для приложенного файла изображения согласно структуре, ответ должен быть на русском языке. Учти, что владелец указал, что исходный цвет машины: ${getColorName(imgProps.color)}`,
attachments: [fileId],
},
],
}),
})
const data = await response.json()
console.log(data)
try {
return JSON.parse(data.choices[0].message.content)
} catch (error) {
console.error(error)
return { description: data.choices[0].message.content }
}
}
const convertFileToBase64 = (file) => {
const base64Image = file.buffer.toString('base64')
return `data:${file.mimetype};base64,${base64Image}`
}
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"
router.post('/:id/upload-car-img', upload.single('file'), async (req, res) => {
const { id: orderId } = req.params
if (!mongoose.Types.ObjectId.isValid(orderId)) {
throw new Error(VALIDATION_MESSAGES.orderId.invalid)
}
const order = await OrderModel.findById(orderId)
if (!order) {
throw new Error(VALIDATION_MESSAGES.order.notFound)
}
if (!req.file) {
throw new Error(VALIDATION_MESSAGES.carImg.required)
}
try {
await OrderCarImgModel.deleteMany({ orderId })
const { access_token } = await getToken(req, res)
const fileId = await uploadImage(req.file, access_token)
const { value, description } = await analyzeImage(fileId, access_token, { carColor: order.carColor }) ?? {}
const orderCarImg = await OrderCarImgModel.create({
image: convertFileToBase64(req.file),
imageRating: value,
imageDescription: description,
orderId: order.id,
created: new Date().toISOString(),
})
res.status(200).send({ success: true, body: orderCarImg })
} catch (error) {
console.error(error)
}
})
router.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
switch (err.message) {
case 'File too large':
return res.status(400).json({ success: false, error: VALIDATION_MESSAGES.carImg.invalid.size })
default:
return res.status(400).json({ success: false, error: err.message })
}
}
throw new Error(err.message)
})
module.exports = router

View File

@@ -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;

View 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;

View File

@@ -8,22 +8,44 @@ router.get("/update-like", (request, response) => {
response.send(require("./json/gamepage/success.json"));
});
router.get("/categories", (request, response) => {
response.send(require("./json/categories/success.json"));
router.get("/add-to-cart", (request, response) => {
response.send(require("./json/home-page-data/games-in-cart.json"));
});
router.get("/categories", (request, response) => {
response.send(require("./json/home-page-data/all-games.json"));
});
router.get("/favourites", (request, response) => {
response.send(require("./json/home-page-data/all-games.json"));
});
// router.get("/shopping-cart", (request, response) => {
// response.send(require("./json/shopping-cart/success.json"));
// });
router.get("/shopping-cart", (request, response) => {
response.send(require("./json/shopping-cart/success.json"));
response.send(require("./json/home-page-data/games-in-cart.json"));
});
router.get("/home", (request, response) => {
response.send(require("./json/home-page-data/success.json"));
// Добавляем поддержку разных ответов для /home
router.get("/home", (req, res) => {
if (stubs.home === "success") {
res.send(require("./json/home-page-data/success.json"));
} else if (stubs.home === "empty") {
res.send({ data: [] }); // Отправляем пустой массив
} else {
res.status(500).json({ success: false, message: "Server error" });
}
});
router.get("/all-games", (request, response) => {
response.send(require("./json/home-page-data/all-games.json"));
});
const stubs = {
home: "success",
};
// // Маршрут для обновления лайков
// router.post("/update-like", (request, response) => {
@@ -38,7 +60,6 @@ router.get("/all-games", (request, response) => {
// });
// });
const fs = require("fs").promises;
const path = require("path");
@@ -49,7 +70,7 @@ const commentsFilePath = path.join(__dirname, "./json/gamepage/success.json");
async function readComments() {
const data = await fs.readFile(commentsFilePath, "utf-8");
const parsedData = JSON.parse(data);
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
console.log("Прочитанные данные:", parsedData); // Логируем полученные данные
return parsedData;
}
// Write to JSON file
@@ -88,5 +109,149 @@ router.post("/update-like", async (req, res) => {
}
});
// Путь к JSON-файлу с корзиной
const cartFilePath = path.join(
__dirname,
"./json/home-page-data/games-in-cart.json"
);
// Функция для чтения JSON-файла
async function readCart() {
const data = await fs.readFile(cartFilePath, "utf-8");
return JSON.parse(data);
}
// Функция для записи в JSON-файл
async function writeCart(data) {
await fs.writeFile(cartFilePath, JSON.stringify(data, null, 2), "utf-8");
}
// Маршрут для добавления/удаления товара в корзине
router.post("/add-to-cart", async (req, res) => {
const { id, action } = req.body;
// Проверка наличия id и action
if (id === undefined || action === undefined) {
return res
.status(400)
.json({ success: false, message: "Invalid id or action" });
}
try {
const cartData = await readCart();
let ids = cartData.data.ids;
if (action === "add") {
// Если action "add", добавляем товар, если его нет в корзине
if (!ids?.includes(id)) {
ids.push(id);
}
} else if (action === "remove") {
// Если action "remove", удаляем товар, если он есть в корзине
if (ids?.includes(id)) {
ids = ids.filter((item) => item !== id);
}
} else {
// Если action невалиден
return res
.status(400)
.json({ success: false, message: "Invalid action" });
}
// Записываем обновленные данные обратно в файл
cartData.data.ids = ids;
await writeCart(cartData);
res.status(200).json({
success: true,
message: "Cart updated successfully",
data: cartData.data, // Возвращаем обновленные данные
});
} catch (error) {
console.error("Error updating cart:", error);
res.status(500).json({ success: false, message: "Server error" });
}
});
module.exports = router;
const createElement = (key, value, buttonTitle, basePath) => `
<label>
<input name="${key}" type="radio" ${
stubs[key] === value ? "checked" : ""
} onclick="fetch('${basePath}/admin/set/${key}/${value}')"/>
${buttonTitle || value}
</label>
`;
router.get("/admin/home", (request, response) => {
const basePath = request.baseUrl; // Получаем базовый путь маршрутизатора
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /home</legend>
${createElement("home", "success", "Отдать успешный ответ", basePath)}
${createElement("home", "empty", "Отдать пустой массив", basePath)}
${createElement("home", "error", "Отдать ошибку", basePath)}
</fieldset>
</div>
`);
});
router.get("/admin/game-page", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /game-page</legend>
${createElement(
"game-page",
"success",
"Отдать успешный ответ"
)}
${createElement("game-page", "empty", "Отдать пустой массив")}
${createElement("game-page", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/categories", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /categories</legend>
${createElement(
"categories",
"success",
"Отдать успешный ответ"
)}
${createElement("categories", "empty", "Отдать пустой массив")}
${createElement("categories", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/favourites", (request, response) => {
response.send(`
<div>
<fieldset>
<legend>Настройка данных для /favourites</legend>
${createElement(
"favourites",
"success",
"Отдать успешный ответ"
)}
${createElement("favourites", "empty", "Отдать пустой массив")}
${createElement("favourites", "error", "Отдать ошибку")}
</fieldset>
</div>
`);
});
router.get("/admin/set/:key/:value", (request, response) => {
const { key, value } = request.params;
stubs[key] = value;
response.send("Настройки обновлены!");
});

View File

@@ -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"
}
]
}
}

View File

@@ -5,28 +5,28 @@
{
"username": ользователь1",
"text": "Текст комментария 1",
"likes": 9,
"likes": 13,
"rating": 8,
"date": "2025-03-01T10:00:00Z"
},
{
"username": ользователь2",
"text": "Текст комментария 2",
"likes": 7,
"likes": 10,
"rating": 7,
"date": "2025-01-01T10:00:00Z"
},
{
"username": ользователь3",
"text": "Текст комментария 3",
"likes": 5,
"likes": 4,
"rating": 3,
"date": "2025-02-01T10:00:00Z"
},
{
"username": ользователь4",
"text": "Текст комментария 4",
"likes": 15,
"likes": 18,
"rating": 2,
"date": "2025-12-01T10:00:00Z"
}

View File

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

View File

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

View File

@@ -3,43 +3,51 @@
"data": {
"topSail": [
{
"id": 1,
"image": "game1",
"text": "$10",
"price": 1500,
"imgPath": "img_top_1"
},
{
"id": 2,
"image": "game2",
"text": "$10",
"price": 980,
"imgPath": "img_top_2"
},
{
"id": 3,
"image": "game3",
"text": "$10",
"price": 1900,
"imgPath": "img_top_3"
},
{
"id": 4,
"image": "game4",
"text": "$10",
"price": 1200,
"imgPath": "img_top_4"
},
{
"id": 5,
"image": "game5",
"text": "$10",
"price": 479,
"imgPath": "img_top_5"
},
{
"id": 6,
"image": "game6",
"text": "$10",
"price": 700,
"imgPath": "img_top_6"
},
{
"id": 7,
"image": "game7",
"text": "$10",
"price": 1100,
"imgPath": "img_top_7"
},
{
"id": 8,
"image": "game8",
"text": "$10",
"price": 3800,
"imgPath": "img_top_8"
}
],
@@ -97,23 +105,27 @@
{
"image": "news1",
"text": "Разработчики Delta Force: Hawk Ops представили крупномасштабный режим Havoc Warfare",
"imgPath": "img_news_1"
"imgPath": "img_news_1",
"link": "https://gamemag.ru/news/185583/delta-force-hawk-ops-gameplay-showcase-havoc-warfare"
},
{
"image": "news2",
"text": "Первый трейлер Assassins Creed Shadows — с темнокожим самураем в феодальной Японии",
"imgPath": "img_news_2"
"imgPath": "img_news_2",
"link": "https://stopgame.ru/newsdata/62686/pervyy_trailer_assassin_s_creed_shadows_s_temnokozhim_samuraem_v_feodalnoy_yaponii"
},
{
"image": "news3",
"text": "Призрак Цусимы» вышел на ПК — и уже ставит рекорды для Sony",
"imgPath": "img_news_3"
"imgPath": "img_news_3",
"link": "https://stopgame.ru/newsdata/62706/prizrak_cusimy_vyshel_na_pk_i_uzhe_stavit_rekordy_dlya_sony"
},
{
"image": "news4",
"text": "Авторы Skull and Bones расширяют планы на второй сезо",
"imgPath": "img_news_4"
"text": "Авторы Skull and Bones расширяют планы на второй сезон",
"imgPath": "img_news_4",
"link": "https://stopgame.ru/newsdata/62711/avtory_skull_and_bones_rasshiryayut_plany_na_vtoroy_sezon"
}
]
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
{
"banks": [
{
"name": "Sberbank of Russia",
"description": "One of the largest and most popular banks in Russia. There are many branches and ATMs in Kazan. Sberbank offers a wide range of services, including loans, deposits, insurance, business services, and online banking."
},
{
"name": "VTB",
"description": "The second largest bank in Russia, with many offices and ATMs in Kazan. VTB offers various financial products for individuals and businesses, including loans, deposits, investment solutions, and cards."
},
{
"name": "Tinkoff Bank",
"description": "Although Tinkoff does not have traditional offices in Kazan, its products and services are available in the city through online banking and remote services. Tinkoff offers favorable terms for credit cards, deposits, and services for small and medium-sized businesses."
},
{
"name": "Alfa-Bank",
"description": "One of the largest private banks in Russia, with offices in Kazan. Alfa-Bank offers standard banking services such as loans, deposits, cards, as well as investment and insurance products."
},
{
"name": "Rosselkhozbank",
"description": "Rosselkhozbank is also present in Kazan, specializing in servicing the agro-industrial complex, but also provides services for individuals and businesses, including loans, deposits, and cards."
},
{
"name": "RBC Bank",
"description": "A Russian bank with several offices and ATMs in Kazan. It offers loans, cards, deposits, and business services."
},
{
"name": "Bank Saint Petersburg",
"description": "A local bank that also provides services in Kazan. It offers a wide range of banking products for individuals and businesses."
}
],
"hospitals": [
{
"name": "Kazan City Clinical Hospital No. 1",
"description": "One of the largest multidisciplinary hospitals in Kazan, offering services in surgery, traumatology, neurology, cardiology, and other medical fields. Modern technologies and highly qualified staff."
},
{
"name": "Republican Clinical Hospital",
"description": "The main medical organization of the Republic of Tatarstan, providing a wide range of services for adults and children, including emergency care, high-tech surgeries, and diagnostics."
},
{
"name": "City Hospital No. 7",
"description": "A hospital specializing in providing medical care in therapeutic, surgical, and resuscitation medicine. The hospital employs experienced specialists and uses modern treatment methods."
},
{
"name": "Kazan Children's Clinical Hospital",
"description": "A specialized medical facility for children, providing services for the treatment of diseases related to pediatrics, surgery, cardiology, and other fields for children of all ages."
},
{
"name": "Kazan Oncology Dispensary",
"description": "A medical institution specializing in the treatment of oncological diseases. It uses the latest methods of cancer diagnosis and treatment, including chemotherapy, radiotherapy, and surgical interventions."
},
{
"name": "City Hospital No. 18",
"description": "A multidisciplinary medical institution offering treatment in various medical fields, including traumatology, neurology, and cardiology. The hospital has a rehabilitation department for patients recovering from serious diseases."
},
{
"name": "Republican Hospital for War Veterans",
"description": "A medical institution providing specialized care for World War II veterans, disabled individuals, and elderly people. It also offers a wide range of services for citizens with chronic diseases."
}
],
"pharmacies": [
{
"name": "Apteka 36.6",
"description": "A pharmacy chain with a wide range of medications, vitamins, cosmetics, and health products. Loyalty programs and online orders for customer convenience."
},
{
"name": "Rigla",
"description": "One of the largest pharmacy chains in Russia. It offers a wide range of medicines, medical products, and cosmetics. It also provides the option to order online."
},
{
"name": "Zdorovaya Semya",
"description": "A pharmacy chain focused on the sale of medicines and health products, including medical equipment. Often runs promotions and discounts on popular items."
},
{
"name": "A5 Pharmacy Chain",
"description": "Pharmacies offering a wide range of products, including medicines, vitamins, cosmetics, and children's products. Convenient delivery and online order services."
},
{
"name": "Samson-Pharma Pharmacy",
"description": "A pharmacy chain offering customers all necessary medicines and health products. Pharmacies offer various discount and bonus programs for regular customers."
},
{
"name": "Tsvetnoy Pharmacy Chain",
"description": "Pharmacies known for their convenient locations and high-quality service. They sell medicines, vitamins, self-care products, and medical equipment."
},
{
"name": "Doctor Stoletev Pharmacy",
"description": "A pharmacy chain focused on selling pharmaceutical products, medical goods, and cosmetics. Convenient service and promotions for customers."
}
],
"airports": [
{
"name": "Kazan International Airport",
"description": "The main airport of the city of Kazan, serving international and domestic flights. The airport is equipped with modern terminals, comfortable waiting areas, shops, and restaurants. It is one of the largest in the Volga region and an important transport hub for Tatarstan."
},
{
"name": "Kazan-2 (when it was operational)",
"description": "Previously used for domestic flights and military needs. It is no longer fully operational as all passenger flights have been redirected to Kazan International Airport. The airport building is closed for commercial air traffic."
}
]
}

View File

@@ -0,0 +1,102 @@
{
"banks": [
{
"name": "Сбербанк России",
"description": "Один из крупнейших и самых популярных банков в России. В Казани есть множество отделений и банкоматов. Сбербанк предлагает широкий спектр услуг, включая кредиты, депозиты, страхование, обслуживание бизнеса и онлайн-банкинг."
},
{
"name": "ВТБ",
"description": "Второй по величине банк в России, с большим количеством офисов и банкоматов в Казани. ВТБ предлагает различные финансовые продукты для физических и юридических лиц, включая кредиты, вклады, инвестиционные решения и карты."
},
{
"name": "Тинькофф Банк",
"description": "Несмотря на то что у Тинькофф нет традиционных офисов в Казани, его продукты и услуги доступны в городе через онлайн-банкинг и удаленное обслуживание. Тинькофф предлагает выгодные условия по кредитным картам, вклады, а также услуги для малого и среднего бизнеса."
},
{
"name": "Альфа-Банк",
"description": "Один из крупных частных банков в России, с офисами в Казани. Альфа-Банк предлагает стандартные банковские услуги, такие как кредиты, депозиты, карты, а также инвестиционные и страховые продукты."
},
{
"name": "Россельхозбанк",
"description": "В Казани также присутствует Россельхозбанк, специализирующийся на обслуживании агропромышленного комплекса, но также предоставляет услуги для физических и юридических лиц, включая кредиты, депозиты и карты."
},
{
"name": "РБК Банк",
"description": "Российский банк с рядом офисов и банкоматов в Казани. Предлагает кредиты, карты, депозиты, а также обслуживание для бизнеса."
},
{
"name": "Банк Санкт-Петербург",
"description": "Местный банк, который также предоставляет услуги в Казани. Предлагает широкий выбор банковских продуктов для частных лиц и бизнеса."
}
],
"hospitals": [
{
"name": "Казанская городская клиническая больница №1",
"description": "Одна из крупнейших многопрофильных больниц Казани, предлагающая услуги в области хирургии, травматологии, неврологии, кардиологии и других медицинских направлений. Современные технологии и высококвалифицированный персонал."
},
{
"name": "Республиканская клиническая больница",
"description": "Основная медицинская организация Республики Татарстан, предоставляющая широкий спектр услуг для взрослых и детей, включая экстренную помощь, высокотехнологичные операции и диагностику."
},
{
"name": "Городская больница №7",
"description": "Больница, специализирующаяся на оказании медицинской помощи в области терапевтической, хирургической и реанимационной медицины. В больнице работают опытные специалисты, используемые современные методы лечения."
},
{
"name": "Казанская детская клиническая больница",
"description": "Профильное медицинское учреждение для детей, которое предоставляет услуги по лечению заболеваний, связанных с педиатрией, хирургией, кардиологией и другими направлениями для детей всех возрастов."
},
{
"name": "Казанский онкологический диспансер",
"description": "Медицинское учреждение, специализирующееся на лечении онкологических заболеваний. Использует новейшие методы диагностики и лечения рака, включая химиотерапию, радиотерапию и операционные вмешательства."
},
{
"name": "Городская больница №18",
"description": "Многопрофильное медицинское учреждение, предлагающее лечение в различных областях медицины, включая травматологию, неврологию и кардиологию. В больнице есть отделение для реабилитации пациентов после тяжелых заболеваний."
},
{
"name": "Республиканская больница для ветеранов войн",
"description": "Медицинское учреждение, оказывающее специализированную помощь ветеранам Великой Отечественной войны, инвалидам и пожилым людям. Также предлагает широкий спектр услуг для граждан с хроническими заболеваниями."
}
],
"pharmacies": [
{
"name": "Аптека 36,6",
"description": "Сеть аптек с большим ассортиментом лекарственных средств, витаминов, косметики и товаров для здоровья. Программы лояльности и онлайн-заказы для удобства клиентов."
},
{
"name": "Ригла",
"description": "Одна из крупнейших аптечных сетей в России. Предлагает широкий выбор лекарств, медицинских товаров и косметики. Также предоставляет возможность заказа через интернет."
},
{
"name": "Здоровая семья",
"description": "Аптечная сеть, ориентированная на продажу лекарств и товаров для здоровья, включая медицинскую технику. Часто проводятся акции и скидки на популярные товары."
},
{
"name": "Аптечная сеть 'А5'",
"description": "Аптеки, предлагающие широкий ассортимент товаров, включая лекарства, витамины, косметику и товары для детей. Удобные услуги доставки и онлайн-заказов."
},
{
"name": "Аптека 'Самсон-Фарма'",
"description": "Сеть аптек, предоставляющая клиентам все необходимые лекарства и товары для здоровья. Аптеки предлагают различные программы скидок и бонусов для постоянных клиентов."
},
{
"name": "Аптечная сеть 'Цветной'",
"description": "Аптеки, известные своими удобными местоположениями и качественным обслуживанием. В продаже лекарства, витамины, товары для ухода за собой и медтехника."
},
{
"name": "Аптека 'Доктор Столетов'",
"description": "Сеть аптек, ориентированная на продажу фармацевтической продукции, медицинских товаров и косметики. Удобный сервис и акции для клиентов."
}
],
"airports": [
{
"name": "Международный аэропорт Казань",
"description": "Главный аэропорт города Казани, обслуживающий международные и внутренние рейсы. Аэропорт оснащен современными терминалами, удобными зонами ожидания, магазинами и ресторанами. Он является одним из крупнейших в Поволжье и важным транспортным узлом для Татарстана."
},
{
"name": "Казань-2 (когда был действующим)",
"description": "Ранее используемый аэропорт для внутренних рейсов и военных нужд. В настоящее время не функционирует в полном объеме, поскольку все пассажирские рейсы перенаправлены в Международный аэропорт Казань. Здание аэропорта закрыто для коммерческих авиаперевозок."
}
]
}

View File

@@ -0,0 +1,102 @@
{
"banks": [
{
"name": "Россия Сбербанкы",
"description": "Россиядәге иң зур һәм популяр банкларның берсе. Казан шәһәрендә күпсанлы бүлекләр һәм банкоматлар бар. Сбербанк киң спектрлы хезмәтләр тәкъдим итә, шул исәптән кредитлар, депозиты, иминиятләштерү, бизнеска хезмәт күрсәтү һәм онлайн-банкчылык."
},
{
"name": "ВТБ",
"description": "Россиядә икенче зурлыктагы банк, Казан шәһәрендә күп санлы офислар һәм банкоматлар белән. ВТБ физик һәм юридик затлар өчен төрле финанс продуктларын тәкъдим итә, шул исәптән кредитлар, депозитлар, инвестицион чишелешләр һәм карталар."
},
{
"name": "Тинькофф Банк",
"description": "Тинькофф Казан шәһәрендә традицион офисларга ия булмаса да, аның продуктлары һәм хезмәтләре шәһәрдә онлайн-банкчылык һәм ерак хезмәт күрсәтү аша тәкъдим ителә. Тинькофф кредит карталары, депозитлар, шулай ук кечкенә һәм урта бизнес өчен хезмәтләр тәкъдим итә."
},
{
"name": "Альфа-Банк",
"description": "Россиядәге зур шәхси банкларның берсе, Казанда офислары белән. Альфа-Банк стандарт банк хезмәтләрен тәкъдим итә, шул исәптән кредитлар, депозитлар, карталар, шулай ук инвестицион һәм иминият продуктлары."
},
{
"name": "Россельхозбанк",
"description": "Казан шәһәрендә Россельхозбанк та бар, ул агропромышленность өлкәсендә хезмәт күрсәтүгә махсуслашкан, ләкин шулай ук физик һәм юридик затлар өчен хезмәтләр тәкъдим итә, шул исәптән кредитлар, депозитлар һәм карталар."
},
{
"name": "РБК Банк",
"description": "Казан шәһәрендә офислары һәм банкоматлары булган Россия банкы. Кредитлар, карталар, депозитлар һәм бизнеска хезмәт күрсәтү тәкъдим итә."
},
{
"name": "Санкт-Петербург Банкы",
"description": "Казан шәһәрендә дә хезмәт күрсәткән җирле банк. Ул шәхси затлар һәм бизнес өчен киң банк продуктлары сайлау тәкъдим итә."
}
],
"hospitals": [
{
"name": "Казан шәһәр клиник хастаханәсе №1",
"description": "Казанның иң зур күппрофильле хастаханәләренең берсе, хирургия, травматология, неврология, кардиология һәм башка медицина юнәлешләре буенча хезмәтләр тәкъдим итә. Замана технологияләре һәм югары квалификацияле персонал."
},
{
"name": "Республиканың клиник хастаханәсе",
"description": "Татарстан Республикасы өчен төп медицина оешмасы, зур спектрдагы хезмәтләрне тәкъдим итә, шул исәптән ашыгыч ярдәм, югары технологияле операцияләр һәм диагностика."
},
{
"name": "Шәһәр хастаханәсе №7",
"description": "Терапевтик, хирургик һәм реанимация медицинасы өлкәсендә медицина ярдәме күрсәтүгә махсуслашкан хастаханә. Хастаханәдә тәҗрибәле белгечләр эшли, заманча дәвалау ысуллары кулланыла."
},
{
"name": "Казан балалар клиник хастаханәсе",
"description": "Балалар өчен профильле медицина учреждениесе, педиатрия, хирургия, кардиология һәм башка юнәлешләр буенча хезмәтләр тәкъдим итә."
},
{
"name": "Казан онкология диспансеры",
"description": "Онкологик авыруларны дәвалауга махсуслашкан медицина учреждениесе. Рак диагнозын һәм дәвалауны үткәрүдә заманча ысуллар кулланыла, шул исәптән химиотерапия, радиотерапия һәм операцияләр."
},
{
"name": "Шәһәр хастаханәсе №18",
"description": "Күппрофильле медицина учреждениесе, төрле медицина өлкәләрендә дәвалау тәкъдим итә, шул исәптән травматология, неврология һәм кардиология. Хастаханәдә авыр авырулардан соң реабилитация бүлекләре бар."
},
{
"name": "Ветераннар өчен республика хастаханәсе",
"description": "Бөек Ватан сугышы ветераннарына, инвалидларга һәм картларга махсус медицина ярдәме күрсәтүче учреждение. Шулай ук хроник авырулары булган гражданнар өчен хезмәтләр тәкъдим итә."
}
],
"pharmacies": [
{
"name": "Аптека 36,6",
"description": "Дәреслекләр, витаминнар, косметика һәм сәламәтлек товарларының киң ассортименты булган аптека челтәре. Лояльлек программалары һәм онлайн-заказлар клиентлар өчен уңайлы."
},
{
"name": "Ригла",
"description": "Россиядәге иң зур аптекалар челтәрләренең берсе. Дәреслекләр, медицина товарлары һәм косметика тәкъдим итә. Шулай ук интернет аша заказ бирү мөмкинлеге бар."
},
{
"name": "Здоровая семья",
"description": "Дәреслекләр һәм сәламәтлек товарлары, шул исәптән медицина техникасы сатуга юнәлдерелгән аптека челтәре. Популяр товарларга акцияләр һәм ташламалар еш үткәрелә."
},
{
"name": "Аптечная сеть 'А5'",
"description": "Дәреслекләр, витаминнар, косметика һәм балалар товарларының киң ассортименты булган аптека челтәре. Уңайлы җибәрү һәм онлайн-заказлар хезмәтләре."
},
{
"name": "Аптека 'Самсон-Фарма'",
"description": "Дәреслекләр һәм сәламәтлек товарлары тәкъдим итә торган аптека челтәре. Аптекалар даими клиентлар өчен скидкалар һәм бонуслар тәкъдим итә."
},
{
"name": "Аптечная сеть 'Цветной'",
"description": "Уңайлы урнашкан һәм сыйфатлы хезмәт күрсәтү белән танылган аптекалар. Дәреслекләр, витаминнар, үз-үзеңне карау товарлары һәм медицина техникасы сатыла."
},
{
"name": "Аптека 'Доктор Столетов'",
"description": "Фармацевтик продукция, медицина товарлары һәм косметика сату белән шөгыльләнгән аптека челтәре. Уңайлы хезмәт һәм клиентлар өчен акцияләр."
}
],
"airports": [
{
"name": "Казан Халыкара Аэропорты",
"description": "Казанның төп аэропорты, халыкара һәм эчке рейсларны башкаручы. Аэропорт заманча терминаллар, уңайлы көтү зоналары, кибетләр һәм рестораннар белән җиһазландырылган. Ул Поволжье төбәгендә иң зур аэропортларның берсе һәм Татарстан өчен мөһим транспорт узелы."
},
{
"name": "Казан-2 (эшләгән вакытта)",
"description": "Элекке эчке рейслар һәм хәрби кирәклекләр өчен кулланылган аэропорт. Хәзерге вакытта тулы көченә эшләми, чөнки барлык пассажир рейслары Казан Халыкара Аэропортына күчерелгән. Аэропорт бинасы коммерция авиаперевозкалары өчен ябык."
}
]
}

View File

@@ -788,5 +788,121 @@
"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"
}
]
}
}

View File

@@ -788,5 +788,121 @@
"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"
}
]
}
}

View File

@@ -788,5 +788,121 @@
"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"
}
]
}
}

View File

@@ -0,0 +1,27 @@
const { Schema, model } = require('mongoose')
const { KAZAN_EXPLORE_RESULTS_MODEL_NAME } = require('../const')
const schema = new Schema({
userId: { type: String },
items: [
{
quizId: { type: String },
result: { type: Number }
}
]
})
schema.set('toJSON', {
virtuals: true,
versionKey: false,
transform: function (doc, ret) {
delete ret._id
}
})
schema.virtual('id').get(function () {
return this._id.toHexString()
})
exports.ResultsModel = model(KAZAN_EXPLORE_RESULTS_MODEL_NAME, schema)

View File

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

View File

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

View 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` 生成图片(返回模拟图片链接)

View 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;

View File

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

View File

@@ -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: {}
});
};

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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: {}
});
};

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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}`);
});

View 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;

View 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;

View File

@@ -1,17 +0,0 @@
[
{
"id": 0,
"description": "10 часто используемых",
"imageFilename": "kart1.jpg"
},
{
"id": 1,
"description": "10 слов в Data Science",
"imageFilename": "kart1.jpg"
},
{
"id": 2,
"description": "IT Basics Dictionary",
"imageFilename": "kart1.jpg"
}
]

View File

@@ -6,18 +6,14 @@
"id": 0,
"word": "Tech",
"definition": "short for technical, relating to the knowledge, machines, or methods used in science and industry. Tech is a whole industry, which includes IT",
"examples": [
"“As a DevOps engineer I have been working in Tech since 2020.”"
],
"examples": ["“As a DevOps engineer I have been working in Tech since 2020.”"],
"synonyms": ["IT"]
},
{
"id": 1,
"word": "career path",
"definition": "the series of jobs or roles that constitute a person's career, especially one in a particular field",
"examples": [
"“Technology is an evolving field with a variety of available career paths.”"
],
"examples": ["“Technology is an evolving field with a variety of available career paths.”"],
"synonyms": []
}
]
@@ -146,130 +142,5 @@
]
}
]
},
{
"id": 2,
"words": [
{
"id": 0,
"word": "software",
"translation": "программное обеспечение",
"definition": "A collection of computer instructions that perform a specific task, typically for use by humans or machines.",
"synonyms": ["код", "приложение", "управление программами"],
"examples":
[
"I need to update the software on my new laptop.",
"The company uses Windows as its operating system."
]
},
{
"id": 1,
"word": "hardware",
"translation": "железо",
"definition": "Physical components of a computer that process information, including processors and storage devices.",
"synonyms": ["equipment", "приборы", "оборудование"],
"examples":
[
"The keyboard is part of the hardware on this device.",
"They upgraded their router to improve internet speed."
]
},
{
"id": 2,
"word": "network",
"translation": "сети",
"definition": "A system of interconnected devices that communicate with each other through data transmission over a networked medium.",
"synonyms": ["трансляция", "коммуникации", "диалог"],
"examples":
[
"We use the internet to connect our devices in the same area.",
"The company relies on their internal network for data sharing."
]
},
{
"id": 3,
"word": "algorithm",
"translation": "алгоритм",
"definition": "A set of instructions that a computer follows to solve a problem or achieve a specific task.",
"synonyms": ["процесс", "схема", "текст"],
"examples":
[
"The algorithm for sorting numbers is easy to follow.",
"The new software includes an advanced algorithm."
]
},
{
"id": 4,
"word": "encryption",
"translation": "криптография",
"definition": "A technique that transforms information into a secure form, making it unreadable without the appropriate key.",
"synonyms": ["шифрование", "окрышение", "опциональное"],
"examples":
[
"Our data is encrypted to ensure its privacy and security.",
"I need to use an encryption program for my important documents."
]
},
{
"id": 5,
"word": "debugging",
"translation": "поиск и исправление ошибок",
"definition": "The process of identifying and correcting errors or defects in a computer program.",
"synonyms": ["исправление", "сканирование", "анализ"],
"examples":
[
"I need to debug the code for this new project.",
"We use automated tools to find bugs."
]
},
{
"id": 6,
"word": "API",
"translation": "интерфейс приложения",
"definition": "A set of rules and protocols that enables communication between software applications, typically over a network.",
"synonyms": ["серверное программирование", "функциональная структура"],
"examples":
[
"We use the API for our mobile app to access data from the backend server.",
"I need to write an API for connecting my devices to the internet."
]
},
{
"id": 7,
"word": "virtual",
"translation": "виртуальный",
"definition": "A representation of a thing that does not exist physically but exists in digital form.",
"synonyms": ["высокопроизводительный", "представление", "цифровой"],
"examples":
[
"I use virtual reality to experience different environments.",
"Our company offers virtual office spaces for remote work."
]
},
{
"id": 8,
"word": "infrastructure",
"translation": "инфраструктура",
"definition": "The underlying systems and equipment of a computer network or organization, including hardware, software, and physical connections.",
"synonyms": ["оборудование", "устройство", "системы"],
"examples":
[
"Our IT infrastructure is robust to ensure reliable operations.",
"They need to improve their internet infrastructure for better connectivity."
]
},
{
"id": 9,
"word": "hacker",
"translation": "хакер",
"definition": "A skilled individual who uses computer technology to break into and misuse a system or network.",
"synonyms": ["дезориентированный", "манипулятор", "прокурор"],
"examples":
[
"I need to avoid getting involved with hackers.",
"They were caught hacking into the company's confidential database."
]
}
]
}
]

View File

@@ -0,0 +1,26 @@
[
{
"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]
}
]

View File

@@ -1,42 +1,99 @@
const fs = require('fs');
const path = require('path');
const router = require("express").Router();
const router = require('express').Router();
module.exports = router;
const data = require("./data/dictionaries.json");
const wordsData = require("./data/dictionaryWords.json");
const dictionaries = require('./dictionaries.json');
const words = require('../words/words.json');
router.get("/", (req, res) => {
res.send(data);
router.get('/', (req, res) => {
res.send(dictionaries);
});
// Put new dictionary to the array of dictionaries
router.put('/new', (req, res) => {
if (!data || !Array.isArray(data)) {
return res.status(400).send('No array of dictionaries found`');
router.get('/:id', (req, res) => {
const id = parseInt(req.params.id);
if (!id || isNaN(id)) {
return res.status(400).send('Invalid ID'); // Bad request
}
const updatedData = req.body;
if (!updatedData) {
return res.status(400).send('No data to update'); // Bad request
}
if (!data) {
if (!dictionaries) {
return res.status(500).send('No data to update'); // Internal server error
}
const indexedUpdatedData = { id: data.length, ...updatedData }; // Add the new dictionary to the array
const dictionary = dictionaries.find((dictionary) => dictionary.id === id);
data.push(indexedUpdatedData); // Add the new dictionary to the array
if (!dictionary) {
return res.status(404).send('Not found');
}
const dictionaryWords = dictionary.words.map((wordId) => {
const word = words.find((word) => word.id === wordId);
return { ...word, ...word };
});
res.send({ ...dictionary, words: dictionaryWords });
});
fs.writeFile(path.join(__dirname, 'data/dictionaries.json'), JSON.stringify(data), (err) => {
router.post('/:id', (req, res) => {
const id = parseInt(req.params.id);
if (!id || isNaN(id)) {
return res.status(400).send('Invalid ID'); // Bad request
}
if (!dictionaries) {
return res.status(500).send('No data to update'); // Internal server error
}
const dictionary = dictionaries.find((dictionary) => dictionary.id === id);
if (!dictionary) {
return res.status(404).send('Not found');
}
const newWord = req.body;
if (!newWord) {
return res.status(400).send('No data to add'); // Bad request
}
console.log(newWord);
if (isNaN(newWord.id)) {
return res.status(400).send('Invalid word ID'); // Bad request
}
dictionary.words.push(newWord.id);
fs.writeFile(path.join(__dirname, 'dictionaries.json'), JSON.stringify(dictionaries), (err) => {
if (err) {
console.error(err); // Log the error
return res.status(500).send('Error saving data');
}
res.status(200).json(data); // Send back the updated data
res.status(200).json(dictionary); // Send back the updated data
});
});
// Put new dictionary to the array of dictionaries
router.put('/', (req, res) => {
if (!dictionaries || !Array.isArray(dictionaries)) {
return res.status(400).send('No array of dictionaries found`');
}
const newData = req.body;
if (!newData) {
return res.status(400).send('No data to add'); // Bad request
}
if (!dictionaries) {
return res.status(500).send('No data to update'); // Internal server error
}
const indexedUpdatedData = { ...newData, id: dictionaries.length + 1 }; // Add the new dictionary to the array
dictionaries.push(indexedUpdatedData); // Add the new dictionary to the array
fs.writeFile(path.join(__dirname, 'dictionaries.json'), JSON.stringify(dictionaries), (err) => {
if (err) {
console.error(err); // Log the error
return res.status(500).send('Error saving data');
}
res.status(200).json(dictionaries); // Send back the updated data
});
});
@@ -47,15 +104,15 @@ router.delete('/:id', (req, res) => {
return res.status(400).send('Invalid ID'); // Bad request
}
const index = data.findIndex((dictionary) => dictionary.id === id);
const index = dictionaries.findIndex((dictionary) => dictionary.id === id);
if (index < 0) {
return res.status(404).send('Not found'); // Not found
}
data.splice(index, 1); // Remove the dictionary from the array
dictionaries.splice(index, 1); // Remove the dictionary from the array
fs.writeFile(path.join(__dirname, 'data/dictionaries.json'), JSON.stringify(data), (err) => {
fs.writeFile(path.join(__dirname, 'dictionaries.json'), JSON.stringify(dictionaries), (err) => {
if (err) {
console.error(err); // Log the error
return res.status(500).send('Error saving data');
@@ -63,14 +120,3 @@ router.delete('/:id', (req, res) => {
res.send({ message: `Dictionary with id ${id} deleted` });
});
});
router.get("/:id", (req, res) => {
const id = parseInt(req.params.id);
const words = wordsData.find((word) => word.id === id);
if (!words) {
return res.status(404).send("Not found");
}
res.send(words);
});

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